mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge remote-tracking branch 'origin/master' into sec-143-cross-site-scripting-cwe-79
This commit is contained in:
commit
22440937bf
6
.github/workflows/e2e-reusable.yml
vendored
6
.github/workflows/e2e-reusable.yml
vendored
|
@ -41,6 +41,11 @@ on:
|
||||||
description: 'PR number to run tests for.'
|
description: 'PR number to run tests for.'
|
||||||
required: false
|
required: false
|
||||||
type: number
|
type: number
|
||||||
|
node_view_version:
|
||||||
|
description: 'Node View version to run tests with.'
|
||||||
|
required: false
|
||||||
|
default: '1'
|
||||||
|
type: string
|
||||||
secrets:
|
secrets:
|
||||||
CYPRESS_RECORD_KEY:
|
CYPRESS_RECORD_KEY:
|
||||||
description: 'Cypress record key.'
|
description: 'Cypress record key.'
|
||||||
|
@ -160,6 +165,7 @@ jobs:
|
||||||
spec: '${{ inputs.spec }}'
|
spec: '${{ inputs.spec }}'
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||||
|
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
E2E_TESTS: true
|
E2E_TESTS: true
|
||||||
|
|
6
.github/workflows/e2e-tests.yml
vendored
6
.github/workflows/e2e-tests.yml
vendored
|
@ -27,6 +27,11 @@ on:
|
||||||
description: 'URL to call after workflow is done.'
|
description: 'URL to call after workflow is done.'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
|
node_view_version:
|
||||||
|
description: 'Node View version to run tests with.'
|
||||||
|
required: false
|
||||||
|
default: '1'
|
||||||
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
calls-start-url:
|
calls-start-url:
|
||||||
|
@ -46,6 +51,7 @@ jobs:
|
||||||
branch: ${{ github.event.inputs.branch || 'master' }}
|
branch: ${{ github.event.inputs.branch || 'master' }}
|
||||||
user: ${{ github.event.inputs.user || 'PR User' }}
|
user: ${{ github.event.inputs.user || 'PR User' }}
|
||||||
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
|
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
|
||||||
|
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
|
||||||
secrets:
|
secrets:
|
||||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
|
|
||||||
|
|
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -1,3 +1,42 @@
|
||||||
|
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Adjust arrow button colors in dark mode ([#11248](https://github.com/n8n-io/n8n/issues/11248)) ([439132c](https://github.com/n8n-io/n8n/commit/439132c291a812d57702c94eaa12878394ac4c69))
|
||||||
|
* **core:** Ensure error reporter does not promote `info` to `error` messages ([#11245](https://github.com/n8n-io/n8n/issues/11245)) ([a7fc7fc](https://github.com/n8n-io/n8n/commit/a7fc7fc22997acec86dc94386c95349fd018f4ae))
|
||||||
|
* **core:** Override executions mode if `regular` during worker startup ([#11250](https://github.com/n8n-io/n8n/issues/11250)) ([c0aa28c](https://github.com/n8n-io/n8n/commit/c0aa28c6cf3f77b04e04663217c9df8e3803ed3f))
|
||||||
|
* **core:** Wrap nodes for tool use at a suitable time ([#11238](https://github.com/n8n-io/n8n/issues/11238)) ([c2fb881](https://github.com/n8n-io/n8n/commit/c2fb881d61291209802438d95892d052f5c82d43))
|
||||||
|
* Don't show pinned data tooltip for pinned nodes ([#11249](https://github.com/n8n-io/n8n/issues/11249)) ([c2ad156](https://github.com/n8n-io/n8n/commit/c2ad15646d326a8f71e314d54efe202a5bcdd296))
|
||||||
|
* **editor:** Bring back the "Forgot password" link on SigninView ([#11216](https://github.com/n8n-io/n8n/issues/11216)) ([4e78c46](https://github.com/n8n-io/n8n/commit/4e78c46a7450c7fc0694369944d4fb446cef2348))
|
||||||
|
* **editor:** Fix chat crashing when rendering output-parsed content ([#11210](https://github.com/n8n-io/n8n/issues/11210)) ([4aaebfd](https://github.com/n8n-io/n8n/commit/4aaebfd4358f590e98c453ad4e65cc2c9d0f76f8))
|
||||||
|
* **editor:** Make submit in ChangePasswordView work again ([#11227](https://github.com/n8n-io/n8n/issues/11227)) ([4f27b39](https://github.com/n8n-io/n8n/commit/4f27b39b45b58779d363980241e6e5e11b58f5da))
|
||||||
|
* Expressions display actual result of evaluating expression inside string ([#11257](https://github.com/n8n-io/n8n/issues/11257)) ([7f5f0a9](https://github.com/n8n-io/n8n/commit/7f5f0a9df3b3fae6e2f9787443ac1cf9415d5932))
|
||||||
|
* **Google Ads Node:** Update to use v17 api ([#11243](https://github.com/n8n-io/n8n/issues/11243)) ([3d97f02](https://github.com/n8n-io/n8n/commit/3d97f02a8d2b6e5bc7c97c5271bed97417ecacd2))
|
||||||
|
* **Google Calendar Node:** Fix issue with conference data types not loading ([#11185](https://github.com/n8n-io/n8n/issues/11185)) ([4012758](https://github.com/n8n-io/n8n/commit/401275884e5db0287e4eeffb3c7497dd5e024880))
|
||||||
|
* **Google Calendar Node:** Mode to add or replace attendees in event update ([#11132](https://github.com/n8n-io/n8n/issues/11132)) ([6c6a8ef](https://github.com/n8n-io/n8n/commit/6c6a8efdea83cf7194304ce089d7b72d8f6c1a9d))
|
||||||
|
* **HTTP Request Tool Node:** Respond with an error when receive binary response ([#11219](https://github.com/n8n-io/n8n/issues/11219)) ([0d23a7f](https://github.com/n8n-io/n8n/commit/0d23a7fb5ba41545f70c4848d30b90af91b1e7e6))
|
||||||
|
* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#11242](https://github.com/n8n-io/n8n/issues/11242)) ([b7ee0c4](https://github.com/n8n-io/n8n/commit/b7ee0c4087eae346bc7e5360130d6c812dbe99db))
|
||||||
|
* **n8n Trigger Node:** Merge with Workflow Trigger node ([#11174](https://github.com/n8n-io/n8n/issues/11174)) ([6ec6b51](https://github.com/n8n-io/n8n/commit/6ec6b5197ae97eb86496effd458fcc0b9b223ef3))
|
||||||
|
* **OpenAI Node:** Fix tool parameter parsing issue ([#11201](https://github.com/n8n-io/n8n/issues/11201)) ([5a1d81a](https://github.com/n8n-io/n8n/commit/5a1d81ad917fde5cd6a387fe2d4ec6aab6b71349))
|
||||||
|
* **Set Node:** Fix issue with UI properties not being hidden ([#11263](https://github.com/n8n-io/n8n/issues/11263)) ([1affc27](https://github.com/n8n-io/n8n/commit/1affc27b6bf9a559061a06f92bebe8167d938665))
|
||||||
|
* **Strava Trigger Node:** Fix issue with webhook not being deleted ([#11226](https://github.com/n8n-io/n8n/issues/11226)) ([566529c](https://github.com/n8n-io/n8n/commit/566529ca1149988a54a58b3c34bbe4d9f1add6db))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add tracking for node errors and update node graph ([#11060](https://github.com/n8n-io/n8n/issues/11060)) ([d3b05f1](https://github.com/n8n-io/n8n/commit/d3b05f1c54e62440666297d8e484ccd22168da48))
|
||||||
|
* **core:** Dedupe ([#10101](https://github.com/n8n-io/n8n/issues/10101)) ([52dd2c7](https://github.com/n8n-io/n8n/commit/52dd2c76196c6895b47145c2b85a6895ce2874d4))
|
||||||
|
* **editor:** Send workflow context to assistant store ([#11135](https://github.com/n8n-io/n8n/issues/11135)) ([fade9e4](https://github.com/n8n-io/n8n/commit/fade9e43c84a0ae1fbc80f3ee546a418970e2380))
|
||||||
|
* **Gong Node:** New node ([#10777](https://github.com/n8n-io/n8n/issues/10777)) ([785b47f](https://github.com/n8n-io/n8n/commit/785b47feb3b83cf36aaed57123f8baca2bbab307))
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **Google Sheets Node:** Don't load whole spreadsheet dataset to determine columns when appending data ([#11235](https://github.com/n8n-io/n8n/issues/11235)) ([26ad091](https://github.com/n8n-io/n8n/commit/26ad091f473bca4e5d3bdc257e0818be02e52db5))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.63.0](https://github.com/n8n-io/n8n/compare/n8n@1.62.1...n8n@1.63.0) (2024-10-09)
|
# [1.63.0](https://github.com/n8n-io/n8n/compare/n8n@1.62.1...n8n@1.63.0) (2024-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix redo connections
|
||||||
it('should undo/redo adding node in the middle', () => {
|
it('should undo/redo adding node in the middle', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -114,6 +115,7 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
|
||||||
it('should undo/redo moving nodes', () => {
|
it('should undo/redo moving nodes', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -146,18 +148,14 @@ describe('Undo/Redo', () => {
|
||||||
it('should undo/redo deleting a connection using context menu', () => {
|
it('should undo/redo deleting a connection using context menu', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().realHover();
|
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
|
||||||
cy.get('.connection-actions .delete')
|
|
||||||
.filter(':visible')
|
|
||||||
.should('be.visible')
|
|
||||||
.click({ force: true });
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
// FIXME: Canvas V2: Fix disconnecting by moving
|
||||||
it('should undo/redo deleting a connection by moving it away', () => {
|
it('should undo/redo deleting a connection by moving it away', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -206,6 +204,7 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
WorkflowPage.getters.disabledNodes().should('have.length', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix undo renaming node
|
||||||
it('should undo/redo renaming node using keyboard shortcut', () => {
|
it('should undo/redo renaming node using keyboard shortcut', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -244,6 +243,7 @@ describe('Undo/Redo', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
|
||||||
it('should undo/redo multiple steps', () => {
|
it('should undo/redo multiple steps', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
|
|
@ -16,6 +16,7 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Missing execute button if no nodes
|
||||||
it('should render canvas', () => {
|
it('should render canvas', () => {
|
||||||
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
WorkflowPage.getters.nodeViewRoot().should('be.visible');
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
|
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
WorkflowPage.getters.executeWorkflowButton().should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix changing of connection
|
||||||
it('should connect and disconnect a simple node', () => {
|
it('should connect and disconnect a simple node', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
|
||||||
cy.get('.jtk-connector').should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
|
||||||
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
|
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`)
|
.getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
|
||||||
.should('have.class', 'jtk-endpoint-connected');
|
.should('be.visible');
|
||||||
|
|
||||||
cy.get('.jtk-connector').should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
// Disconnect Set1
|
// Disconnect Set1
|
||||||
cy.drag(
|
cy.drag(
|
||||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||||
[-200, 100],
|
[-200, 100],
|
||||||
);
|
);
|
||||||
cy.get('.jtk-connector').should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add first step', () => {
|
it('should add first step', () => {
|
||||||
|
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should add a connected node using plus endpoint', () => {
|
it('should add a connected node using plus endpoint', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
cy.get('.plus-endpoint').should('be.visible').click();
|
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
|
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should add a connected node dragging from node creator', () => {
|
it('should add a connected node dragging from node creator', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
cy.get('.plus-endpoint').should('be.visible').click();
|
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||||
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
|
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
|
||||||
|
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
it('should open a category when trying to drag and drop it on the canvas', () => {
|
it('should open a category when trying to drag and drop it on the canvas', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
cy.get('.plus-endpoint').should('be.visible').click();
|
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
||||||
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
|
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
|
||||||
|
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
|
||||||
it('should add disconnected node if nothing is selected', () => {
|
it('should add disconnected node if nothing is selected', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
// Deselect nodes
|
// Deselect nodes
|
||||||
WorkflowPage.getters.nodeViewBackground().click({ force: true });
|
WorkflowPage.getters.nodeView().click({ force: true });
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
|
@ -136,10 +138,10 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
||||||
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
|
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
||||||
const httpNodeLeft = parseFloat($httpNode.css('left'));
|
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
|
||||||
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -159,10 +161,12 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
WorkflowPage.getters.nodeConnections().first().realHover();
|
||||||
cy.get('.connection-actions .delete').first().click({ force: true });
|
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
|
||||||
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
|
||||||
it('should delete a connection by moving it away from endpoint', () => {
|
it('should delete a connection by moving it away from endpoint', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
@ -216,10 +220,10 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.actions.hitSelectAll();
|
WorkflowPage.actions.hitSelectAll();
|
||||||
|
|
||||||
WorkflowPage.actions.hitCopy();
|
WorkflowPage.actions.hitCopy();
|
||||||
successToast().should('contain', 'Copied!');
|
successToast().should('contain', 'Copied to clipboard');
|
||||||
|
|
||||||
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
||||||
successToast().should('contain', 'Copied!');
|
successToast().should('contain', 'Copied to clipboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select/deselect all nodes', () => {
|
it('should select/deselect all nodes', () => {
|
||||||
|
@ -231,17 +235,31 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Selection via arrow keys is broken
|
||||||
it('should select nodes using arrow keys', () => {
|
it('should select nodes using arrow keys', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.get('body').type('{leftArrow}');
|
cy.get('body').type('{leftArrow}');
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected');
|
const selectedCanvasNodes = () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => WorkflowPage.getters.canvasNodes(),
|
||||||
|
() => WorkflowPage.getters.canvasNodes().parent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
|
||||||
|
() => selectedCanvasNodes().first().should('have.class', 'selected'),
|
||||||
|
);
|
||||||
cy.get('body').type('{rightArrow}');
|
cy.get('body').type('{rightArrow}');
|
||||||
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected');
|
cy.ifCanvasVersion(
|
||||||
|
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
|
||||||
|
() => selectedCanvasNodes().last().should('have.class', 'selected'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
|
||||||
it('should select nodes using shift and arrow keys', () => {
|
it('should select nodes using shift and arrow keys', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
@ -251,6 +269,7 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix select & deselect
|
||||||
it('should not break lasso selection when dragging node action buttons', () => {
|
it('should not break lasso selection when dragging node action buttons', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
|
@ -262,6 +281,7 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Fix select & deselect
|
||||||
it('should not break lasso selection with multiple clicks on node action buttons', () => {
|
it('should not break lasso selection with multiple clicks on node action buttons', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from './../constants';
|
} from './../constants';
|
||||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||||
|
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||||
|
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
// Make sure outputless switch was connected correctly
|
// Make sure outputless switch was connected correctly
|
||||||
cy.get(
|
WorkflowPage.getters
|
||||||
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
|
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
|
||||||
).should('be.visible');
|
.should('exist');
|
||||||
// Make sure all connections are there after reload
|
// Make sure all connections are there after reload
|
||||||
for (let i = 0; i < desiredOutputs; i++) {
|
for (let i = 0; i < desiredOutputs; i++) {
|
||||||
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeInputEndpointByName(setName)
|
.getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
|
||||||
.should('have.class', 'jtk-endpoint-connected');
|
.should('exist');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||||
.nodeViewBackground()
|
|
||||||
.click((i + 1) * 200, (i + 1) * 200, { force: true });
|
|
||||||
}
|
}
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
|
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
|
|
||||||
|
|
||||||
// Connect Set1 and Set2 to merge
|
// Connect Set1 and Set2 to merge
|
||||||
cy.draganddrop(
|
cy.draganddrop(
|
||||||
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
|
||||||
|
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
|
||||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||||
);
|
);
|
||||||
|
const checkConnections = () => {
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes(
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
|
`${EDIT_FIELDS_SET_NODE_NAME}1`,
|
||||||
|
)
|
||||||
|
.should('exist');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
|
||||||
|
.should('exist');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
|
||||||
|
.should('exist');
|
||||||
|
};
|
||||||
|
checkConnections();
|
||||||
|
|
||||||
// Make sure all connections are there after save & reload
|
// Make sure all connections are there after save & reload
|
||||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
|
checkConnections();
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
// cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||||
WorkflowPage.actions.executeWorkflow();
|
WorkflowPage.actions.executeWorkflow();
|
||||||
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
WorkflowPage.getters.stopExecutionButton().should('not.exist');
|
||||||
|
|
||||||
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
|
||||||
cy.get('[data-label="2 items"]').should('be.visible');
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('[data-label="2 items"]').should('be.visible'),
|
||||||
|
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add nodes and check execution success', () => {
|
it('should add nodes and check execution success', () => {
|
||||||
|
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.executeWorkflow();
|
WorkflowPage.actions.executeWorkflow();
|
||||||
|
|
||||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
cy.ifCanvasVersion(
|
||||||
cy.get('.data-count').should('have.length', 4);
|
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||||
cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success');
|
() => cy.get('[data-edge-status=success]').should('have.length', 3),
|
||||||
|
);
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.data-count').should('have.length', 4),
|
||||||
|
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
|
||||||
|
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
|
||||||
|
);
|
||||||
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
|
cy.ifCanvasVersion(
|
||||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
() =>
|
||||||
cy.get('.jtk-connector').should('have.length', 4);
|
cy
|
||||||
|
.get('.plus-draggable-endpoint')
|
||||||
|
.filter(':visible')
|
||||||
|
.should('not.have.class', 'ep-success'),
|
||||||
|
() =>
|
||||||
|
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.jtk-connector.success').should('have.length', 3),
|
||||||
|
// The new version of the canvas correctly shows executed data being passed to the input of the next node
|
||||||
|
() => cy.get('[data-edge-status=success]').should('have.length', 4),
|
||||||
|
);
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.data-count').should('have.length', 4),
|
||||||
|
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete node using context menu', () => {
|
it('should delete node using context menu', () => {
|
||||||
|
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
WorkflowPage.getters.canvasNodes().should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Figure out how to test moving of the node
|
||||||
it('should move node', () => {
|
it('should move node', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.last()
|
.last()
|
||||||
.then(($node) => {
|
.then(($node) => {
|
||||||
const { left, top } = $node.position();
|
const { left, top } = $node.position();
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
|
||||||
clickToFinish: true,
|
if (isCanvasV2()) {
|
||||||
});
|
cy.drag('.vue-flow__node', [300, 300], {
|
||||||
|
realMouse: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||||
|
clickToFinish: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.last()
|
.last()
|
||||||
|
@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should zoom in', () => {
|
describe('Canvas Zoom Functionality', () => {
|
||||||
WorkflowPage.getters.zoomInButton().should('be.visible').click();
|
const getContainer = () =>
|
||||||
WorkflowPage.getters
|
cy.ifCanvasVersion(
|
||||||
.nodeView()
|
() => WorkflowPage.getters.nodeView(),
|
||||||
.should(
|
() => WorkflowPage.getters.canvasViewport(),
|
||||||
'have.css',
|
|
||||||
'transform',
|
|
||||||
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
|
|
||||||
);
|
);
|
||||||
WorkflowPage.getters.zoomInButton().click();
|
const checkZoomLevel = (expectedFactor: number) => {
|
||||||
WorkflowPage.getters
|
return getContainer().should(($nodeView) => {
|
||||||
.nodeView()
|
const newTransform = $nodeView.css('transform');
|
||||||
.should(
|
const newScale = parseFloat(newTransform.split(',')[0].slice(7));
|
||||||
'have.css',
|
|
||||||
'transform',
|
|
||||||
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should zoom out', () => {
|
expect(newScale).to.be.closeTo(expectedFactor, 0.2);
|
||||||
WorkflowPage.getters.zoomOutButton().should('be.visible').click();
|
});
|
||||||
WorkflowPage.getters
|
};
|
||||||
.nodeView()
|
|
||||||
.should(
|
|
||||||
'have.css',
|
|
||||||
'transform',
|
|
||||||
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
|
|
||||||
);
|
|
||||||
WorkflowPage.getters.zoomOutButton().click();
|
|
||||||
WorkflowPage.getters
|
|
||||||
.nodeView()
|
|
||||||
.should(
|
|
||||||
'have.css',
|
|
||||||
'transform',
|
|
||||||
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should zoom using scroll or pinch gesture', () => {
|
const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
|
||||||
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
WorkflowPage.getters[`${action}Button`]().click();
|
||||||
WorkflowPage.getters
|
checkZoomLevel(expectedFactor);
|
||||||
.nodeView()
|
};
|
||||||
.should(
|
|
||||||
'have.css',
|
it('should zoom in', () => {
|
||||||
'transform',
|
WorkflowPage.getters.zoomInButton().should('be.visible');
|
||||||
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
|
getContainer().then(($nodeView) => {
|
||||||
|
const initialTransform = $nodeView.css('transform');
|
||||||
|
const initialScale =
|
||||||
|
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
|
||||||
|
|
||||||
|
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
|
||||||
|
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom out', () => {
|
||||||
|
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
|
||||||
|
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should zoom using scroll or pinch gesture', () => {
|
||||||
|
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
|
||||||
|
|
||||||
|
// V2 Canvas is using the same zoom factor for both pinch and scroll
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
|
||||||
|
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
|
||||||
);
|
);
|
||||||
|
|
||||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||||
// Zoom in 1x + Zoom out 1x should reset to default (=1)
|
checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
|
||||||
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
|
|
||||||
|
|
||||||
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
|
||||||
WorkflowPage.getters
|
|
||||||
.nodeView()
|
cy.ifCanvasVersion(
|
||||||
.should(
|
() => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
|
||||||
'have.css',
|
() => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
|
||||||
'transform',
|
|
||||||
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reset zoom', () => {
|
it('should reset zoom', () => {
|
||||||
// Reset zoom should not appear until zoom level changed
|
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
||||||
WorkflowPage.getters.resetZoomButton().should('not.exist');
|
WorkflowPage.getters.zoomInButton().click();
|
||||||
WorkflowPage.getters.zoomInButton().click();
|
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
|
||||||
WorkflowPage.getters.resetZoomButton().should('be.visible').click();
|
checkZoomLevel(DEFAULT_ZOOM_FACTOR);
|
||||||
WorkflowPage.getters
|
});
|
||||||
.nodeView()
|
|
||||||
.should(
|
|
||||||
'have.css',
|
|
||||||
'transform',
|
|
||||||
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should zoom to fit', () => {
|
it('should zoom to fit', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
// At this point last added node should be off-screen
|
// At this point last added node should be off-screen
|
||||||
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
|
WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
|
||||||
WorkflowPage.getters.zoomToFitButton().click();
|
WorkflowPage.getters.zoomToFitButton().click();
|
||||||
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
WorkflowPage.getters.canvasNodes().last().should('be.visible');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable node (context menu or shortcut)', () => {
|
it('should disable node (context menu or shortcut)', () => {
|
||||||
|
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
// FIXME: Canvas V2: Credentials should show issue on the first open
|
||||||
it('should remove unknown credentials on pasting workflow', () => {
|
it('should remove unknown credentials on pasting workflow', () => {
|
||||||
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
|
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
|
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
|
||||||
it('should render connections correctly if unkown nodes are present', () => {
|
it('should render connections correctly if unkown nodes are present', () => {
|
||||||
const unknownNodeName = 'Unknown node';
|
const unknownNodeName = 'Unknown node';
|
||||||
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');
|
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');
|
||||||
|
|
|
@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => {
|
||||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||||
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fill up form fields', () => {
|
it('should fill up form fields', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger');
|
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', {
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
isTrigger: true,
|
||||||
|
action: 'On new n8n Form event',
|
||||||
|
});
|
||||||
ndv.getters.parameterInput('formTitle').type('Test Form');
|
ndv.getters.parameterInput('formTitle').type('Test Form');
|
||||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||||
//fill up first field of type number
|
//fill up first field of type number
|
||||||
|
@ -96,6 +98,6 @@ describe('n8n Form Trigger', () => {
|
||||||
.type('Your test form was successfully submitted');
|
.type('Your test form was successfully submitted');
|
||||||
|
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||||
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
||||||
|
import { isCanvasV2 } from '../utils/workflowUtils';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPageClass();
|
const workflowPage = new WorkflowPageClass();
|
||||||
const executionsTab = new WorkflowExecutionsTab();
|
const executionsTab = new WorkflowExecutionsTab();
|
||||||
|
@ -117,15 +118,22 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Manual')
|
.canvasNodeByName('Manual')
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
.should('exist');
|
.should('exist');
|
||||||
workflowPage.getters
|
|
||||||
.canvasNodeByName('Wait')
|
if (isCanvasV2()) {
|
||||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
|
||||||
|
} else {
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
|
||||||
|
}
|
||||||
|
|
||||||
workflowPage.getters
|
workflowPage.getters
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
successToast().should('be.visible');
|
successToast().should('be.visible');
|
||||||
clearNotifications();
|
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
|
@ -206,6 +214,7 @@ describe('Execution', () => {
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
|
||||||
it('should test webhook workflow stop', () => {
|
it('should test webhook workflow stop', () => {
|
||||||
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
cy.createFixtureWorkflow('Webhook_wait_set.json');
|
||||||
|
|
||||||
|
@ -267,9 +276,17 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Webhook')
|
.canvasNodeByName('Webhook')
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
.should('exist');
|
.should('exist');
|
||||||
workflowPage.getters
|
|
||||||
.canvasNodeByName('Wait')
|
if (isCanvasV2()) {
|
||||||
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
|
||||||
|
} else {
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
|
||||||
|
}
|
||||||
|
|
||||||
workflowPage.getters
|
workflowPage.getters
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
@ -295,6 +312,7 @@ describe('Execution', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
|
||||||
describe('connections should be colored differently for pinned data', () => {
|
describe('connections should be colored differently for pinned data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.createFixtureWorkflow('Schedule_pinned.json');
|
cy.createFixtureWorkflow('Schedule_pinned.json');
|
||||||
|
|
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
86
cypress/e2e/2372-ado-prevent-clipping-params.cy.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { NDV, WorkflowPage } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => {
|
||||||
|
it('should show last parameters and open at scroll top of parameters', () => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||||
|
workflowPage.actions.openNode('Schedule Trigger');
|
||||||
|
|
||||||
|
ndv.getters.inlineExpressionEditorInput().should('be.visible');
|
||||||
|
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
workflowPage.actions.openNode('Edit Fields1');
|
||||||
|
|
||||||
|
// first parameter should be visible
|
||||||
|
ndv.getters.inputLabel().eq(0).should('include.text', 'Mode');
|
||||||
|
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||||
|
|
||||||
|
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||||
|
|
||||||
|
// last parameter in view should be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||||
|
|
||||||
|
// next parameter in view should not be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||||
|
|
||||||
|
ndv.actions.close();
|
||||||
|
workflowPage.actions.openNode('Schedule Trigger');
|
||||||
|
|
||||||
|
// first parameter (notice) should be visible
|
||||||
|
ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule ');
|
||||||
|
ndv.getters.inputLabel().eq(0).should('be.visible');
|
||||||
|
|
||||||
|
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
|
||||||
|
|
||||||
|
// last parameter in view should be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||||
|
|
||||||
|
// next parameter in view should not be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||||
|
|
||||||
|
ndv.actions.close();
|
||||||
|
workflowPage.actions.openNode('Slack');
|
||||||
|
|
||||||
|
// first field (credentials) should be visible
|
||||||
|
ndv.getters.nodeCredentialsLabel().should('be.visible');
|
||||||
|
|
||||||
|
// last parameter in view should be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||||
|
|
||||||
|
// next parameter in view should not be visible
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NODE-1272 ensure expressions scrolled to top, not middle', () => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
|
||||||
|
workflowPage.actions.openNode('With long expression');
|
||||||
|
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
|
||||||
|
// should be scrolled at top
|
||||||
|
ndv.getters
|
||||||
|
.inlineExpressionEditorInput()
|
||||||
|
.eq(0)
|
||||||
|
.find('.cm-line')
|
||||||
|
.eq(0)
|
||||||
|
.should('have.text', '1 visible!');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible');
|
||||||
|
ndv.getters
|
||||||
|
.inlineExpressionEditorInput()
|
||||||
|
.eq(0)
|
||||||
|
.find('.cm-line')
|
||||||
|
.eq(6)
|
||||||
|
.should('have.text', '7 not visible!');
|
||||||
|
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
|
@ -117,7 +117,8 @@ describe('Debug', () => {
|
||||||
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
|
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
|
||||||
|
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
workflowPage.getters.canvasNodes().first().dblclick();
|
||||||
ndv.getters.pinDataButton().click();
|
ndv.actions.unPinData();
|
||||||
|
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
|
||||||
|
|
|
@ -23,6 +23,7 @@ describe('Manual partial execution', () => {
|
||||||
canvas.actions.openNode('Webhook1');
|
canvas.actions.openNode('Webhook1');
|
||||||
|
|
||||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||||
|
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||||
ndv.getters.outputRunSelector().should('not.exist'); // single run
|
ndv.getters.outputRunSelector().should('not.exist'); // single run
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -133,9 +133,10 @@ describe('NDV', () => {
|
||||||
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
|
"An expression here won't work because it uses .item and n8n can't figure out the matching item.",
|
||||||
);
|
);
|
||||||
ndv.getters.nodeRunErrorIndicator().should('be.visible');
|
ndv.getters.nodeRunErrorIndicator().should('be.visible');
|
||||||
|
ndv.getters.nodeRunTooltipIndicator().should('be.visible');
|
||||||
// The error details should be hidden behind a tooltip
|
// The error details should be hidden behind a tooltip
|
||||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
|
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time');
|
||||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
|
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||||
|
@ -617,8 +618,10 @@ describe('NDV', () => {
|
||||||
// Should not show run info before execution
|
// Should not show run info before execution
|
||||||
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
|
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
|
||||||
ndv.getters.nodeRunErrorIndicator().should('not.exist');
|
ndv.getters.nodeRunErrorIndicator().should('not.exist');
|
||||||
|
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||||
|
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly show node execution indicator for multiple nodes', () => {
|
it('should properly show node execution indicator for multiple nodes', () => {
|
||||||
|
@ -630,6 +633,7 @@ describe('NDV', () => {
|
||||||
// Manual tigger node should show success indicator
|
// Manual tigger node should show success indicator
|
||||||
workflowPage.actions.openNode('When clicking ‘Test workflow’');
|
workflowPage.actions.openNode('When clicking ‘Test workflow’');
|
||||||
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
ndv.getters.nodeRunSuccessIndicator().should('exist');
|
||||||
|
ndv.getters.nodeRunTooltipIndicator().should('exist');
|
||||||
// Code node should show error
|
// Code node should show error
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
workflowPage.actions.openNode('Code');
|
workflowPage.actions.openNode('Code');
|
||||||
|
|
|
@ -162,21 +162,21 @@ return []
|
||||||
cy.get('#tab-code').should('have.class', 'is-active');
|
cy.get('#tab-code').should('have.class', 'is-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error based on status code', () => {
|
const handledCodes = [
|
||||||
const prompt = nanoid(20);
|
{ code: 400, message: 'Code generation failed due to an unknown reason' },
|
||||||
cy.get('#tab-ask-ai').click();
|
{ code: 413, message: 'Your workflow data is too large for AI to process' },
|
||||||
ndv.actions.executePrevious();
|
{ code: 429, message: "We've hit our rate limit with our AI partner" },
|
||||||
|
{ code: 500, message: 'Code generation failed due to an unknown reason' },
|
||||||
|
];
|
||||||
|
|
||||||
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
handledCodes.forEach(({ code, message }) => {
|
||||||
|
it(`should show error based on status code ${code}`, () => {
|
||||||
|
const prompt = nanoid(20);
|
||||||
|
cy.get('#tab-ask-ai').click();
|
||||||
|
ndv.actions.executePrevious();
|
||||||
|
|
||||||
const handledCodes = [
|
cy.getByTestId('ask-ai-prompt-input').type(prompt);
|
||||||
{ code: 400, message: 'Code generation failed due to an unknown reason' },
|
|
||||||
{ code: 413, message: 'Your workflow data is too large for AI to process' },
|
|
||||||
{ code: 429, message: "We've hit our rate limit with our AI partner" },
|
|
||||||
{ code: 500, message: 'Code generation failed due to an unknown reason' },
|
|
||||||
];
|
|
||||||
|
|
||||||
handledCodes.forEach(({ code, message }) => {
|
|
||||||
cy.intercept('POST', '/rest/ai/ask-ai', {
|
cy.intercept('POST', '/rest/ai/ask-ai', {
|
||||||
statusCode: code,
|
statusCode: code,
|
||||||
status: code,
|
status: code,
|
||||||
|
|
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
150
cypress/fixtures/Test-workflow-with-long-parameters.json
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee",
|
||||||
|
"name": "test",
|
||||||
|
"value": "test",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "beb8723f-6333-4186-ab88-41d4e2338866",
|
||||||
|
"name": "test",
|
||||||
|
"value": "test",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "85095836-4e94-442f-9270-e1a89008c129",
|
||||||
|
"name": "test",
|
||||||
|
"value": "test",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
|
||||||
|
"name": "=should be visible!",
|
||||||
|
"value": "=not visible",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff",
|
||||||
|
"name": "Edit Fields1",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
680,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"messageType": "block",
|
||||||
|
"blocksUi": "blocks",
|
||||||
|
"text": "=should be visible",
|
||||||
|
"otherOptions": {
|
||||||
|
"sendAsUser": "=not visible"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa",
|
||||||
|
"name": "Slack",
|
||||||
|
"type": "n8n-nodes-base.slack",
|
||||||
|
"typeVersion": 2.2,
|
||||||
|
"position": [
|
||||||
|
900,
|
||||||
|
460
|
||||||
|
],
|
||||||
|
"webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rule": {
|
||||||
|
"interval": [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"field": "=should be visible"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "=not visible"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4",
|
||||||
|
"name": "Schedule Trigger",
|
||||||
|
"type": "n8n-nodes-base.scheduleTrigger",
|
||||||
|
"typeVersion": 1.2,
|
||||||
|
"position": [
|
||||||
|
460,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "5dcaab37-1146-49c6-97a3-3b2f73483270",
|
||||||
|
"name": "object",
|
||||||
|
"value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"includeOtherFields": true,
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319",
|
||||||
|
"name": "With long expression",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.3,
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"Edit Fields1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Slack",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Slack": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "With long expression",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Schedule Trigger": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Edit Fields1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {}
|
||||||
|
}
|
|
@ -20,7 +20,8 @@ export class NDV extends BasePage {
|
||||||
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'),
|
||||||
outputDisplayMode: () =>
|
outputDisplayMode: () =>
|
||||||
this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
pinDataButton: () => this.getters.outputPanel().findChildByTestId('ndv-pin-data'),
|
||||||
|
unpinDataLink: () => this.getters.outputPanel().findChildByTestId('ndv-unpin-data'),
|
||||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
||||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||||
|
@ -63,6 +64,7 @@ export class NDV extends BasePage {
|
||||||
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
||||||
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
||||||
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
|
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
|
||||||
|
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||||
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
|
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
|
||||||
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
|
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
|
||||||
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
|
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
|
||||||
|
@ -130,8 +132,9 @@ export class NDV extends BasePage {
|
||||||
codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'),
|
codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'),
|
||||||
codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'),
|
codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'),
|
||||||
codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'),
|
codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'),
|
||||||
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'),
|
nodeRunTooltipIndicator: () => cy.getByTestId('node-run-info'),
|
||||||
nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'),
|
nodeRunSuccessIndicator: () => cy.getByTestId('node-run-status-success'),
|
||||||
|
nodeRunErrorIndicator: () => cy.getByTestId('node-run-status-danger'),
|
||||||
nodeRunErrorMessage: () => cy.getByTestId('node-error-message'),
|
nodeRunErrorMessage: () => cy.getByTestId('node-error-message'),
|
||||||
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
||||||
fixedCollectionParameter: (paramName: string) =>
|
fixedCollectionParameter: (paramName: string) =>
|
||||||
|
@ -146,6 +149,9 @@ export class NDV extends BasePage {
|
||||||
pinData: () => {
|
pinData: () => {
|
||||||
this.getters.pinDataButton().click({ force: true });
|
this.getters.pinDataButton().click({ force: true });
|
||||||
},
|
},
|
||||||
|
unPinData: () => {
|
||||||
|
this.getters.unpinDataLink().click({ force: true });
|
||||||
|
},
|
||||||
editPinnedData: () => {
|
editPinnedData: () => {
|
||||||
this.getters.editPinnedDataButton().click();
|
this.getters.editPinnedDataButton().click();
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { BasePage } from './base';
|
||||||
import { NodeCreator } from './features/node-creator';
|
import { NodeCreator } from './features/node-creator';
|
||||||
import { META_KEY } from '../constants';
|
import { META_KEY } from '../constants';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleSelect } from '../utils';
|
||||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||||
|
|
||||||
const nodeCreator = new NodeCreator();
|
const nodeCreator = new NodeCreator();
|
||||||
export class WorkflowPage extends BasePage {
|
export class WorkflowPage extends BasePage {
|
||||||
|
@ -27,7 +27,11 @@ export class WorkflowPage extends BasePage {
|
||||||
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
|
nodeCreatorSearchBar: () => cy.getByTestId('node-creator-search-bar'),
|
||||||
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
|
nodeCreatorPlusButton: () => cy.getByTestId('node-creator-plus-button'),
|
||||||
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
canvasPlusButton: () => cy.getByTestId('canvas-plus-button'),
|
||||||
canvasNodes: () => cy.getByTestId('canvas-node'),
|
canvasNodes: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('canvas-node'),
|
||||||
|
() => cy.getByTestId('canvas-node').not('[data-node-type="n8n-nodes-internal.addNodes"]'),
|
||||||
|
),
|
||||||
canvasNodeByName: (nodeName: string) =>
|
canvasNodeByName: (nodeName: string) =>
|
||||||
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
this.getters.canvasNodes().filter(`:contains(${nodeName})`),
|
||||||
nodeIssuesByName: (nodeName: string) =>
|
nodeIssuesByName: (nodeName: string) =>
|
||||||
|
@ -37,6 +41,17 @@ export class WorkflowPage extends BasePage {
|
||||||
.should('have.length.greaterThan', 0)
|
.should('have.length.greaterThan', 0)
|
||||||
.findChildByTestId('node-issues'),
|
.findChildByTestId('node-issues'),
|
||||||
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
|
||||||
|
if (isCanvasV2()) {
|
||||||
|
if (type === 'input') {
|
||||||
|
return `[data-test-id="canvas-node-input-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||||
|
}
|
||||||
|
if (type === 'output') {
|
||||||
|
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"]`;
|
||||||
|
}
|
||||||
|
if (type === 'plus') {
|
||||||
|
return `[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"][data-handle-index="${index}"] [data-test-id="canvas-handle-plus"] .clickable`;
|
||||||
|
}
|
||||||
|
}
|
||||||
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
|
||||||
},
|
},
|
||||||
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
|
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
|
||||||
|
@ -46,7 +61,15 @@ export class WorkflowPage extends BasePage {
|
||||||
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
|
return cy.get(this.getters.getEndpointSelector('output', nodeName, index));
|
||||||
},
|
},
|
||||||
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
|
canvasNodePlusEndpointByName: (nodeName: string, index = 0) => {
|
||||||
return cy.get(this.getters.getEndpointSelector('plus', nodeName, index));
|
return cy.ifCanvasVersion(
|
||||||
|
() => cy.get(this.getters.getEndpointSelector('plus', nodeName, index)),
|
||||||
|
() =>
|
||||||
|
cy
|
||||||
|
.get(
|
||||||
|
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"] .clickable`,
|
||||||
|
)
|
||||||
|
.eq(index),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
activatorSwitch: () => cy.getByTestId('workflow-activate-switch'),
|
||||||
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
workflowMenu: () => cy.getByTestId('workflow-menu'),
|
||||||
|
@ -56,13 +79,29 @@ export class WorkflowPage extends BasePage {
|
||||||
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
|
expressionModalInput: () => cy.getByTestId('expression-modal-input').find('[role=textbox]'),
|
||||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||||
|
|
||||||
nodeViewRoot: () => cy.getByTestId('node-view-root'),
|
nodeViewRoot: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('node-view-root'),
|
||||||
|
() => this.getters.nodeView(),
|
||||||
|
),
|
||||||
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
|
copyPasteInput: () => cy.getByTestId('hidden-copy-paste'),
|
||||||
nodeConnections: () => cy.get('.jtk-connector'),
|
nodeConnections: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.jtk-connector'),
|
||||||
|
() => cy.getByTestId('edge-label-wrapper'),
|
||||||
|
),
|
||||||
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
|
zoomToFitButton: () => cy.getByTestId('zoom-to-fit'),
|
||||||
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
|
nodeEndpoints: () => cy.get('.jtk-endpoint-connected'),
|
||||||
disabledNodes: () => cy.get('.node-box.disabled'),
|
disabledNodes: () =>
|
||||||
selectedNodes: () => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.get('.node-box.disabled'),
|
||||||
|
() => cy.get('[data-test-id="canvas-trigger-node"][class*="disabled"]'),
|
||||||
|
),
|
||||||
|
selectedNodes: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => this.getters.canvasNodes().filter('.jtk-drag-selected'),
|
||||||
|
() => this.getters.canvasNodes().parent().filter('.selected'),
|
||||||
|
),
|
||||||
// Workflow menu items
|
// Workflow menu items
|
||||||
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
|
workflowMenuItemDuplicate: () => cy.getByTestId('workflow-menu-item-duplicate'),
|
||||||
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
|
workflowMenuItemDownload: () => cy.getByTestId('workflow-menu-item-download'),
|
||||||
|
@ -92,8 +131,21 @@ export class WorkflowPage extends BasePage {
|
||||||
shareButton: () => cy.getByTestId('workflow-share-button'),
|
shareButton: () => cy.getByTestId('workflow-share-button'),
|
||||||
|
|
||||||
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
|
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
|
||||||
nodeViewBackground: () => cy.getByTestId('node-view-background'),
|
nodeViewBackground: () =>
|
||||||
nodeView: () => cy.getByTestId('node-view'),
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('node-view-background'),
|
||||||
|
() => cy.getByTestId('canvas'),
|
||||||
|
),
|
||||||
|
nodeView: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('node-view'),
|
||||||
|
() => cy.get('[data-test-id="canvas-wrapper"]'),
|
||||||
|
),
|
||||||
|
canvasViewport: () =>
|
||||||
|
cy.ifCanvasVersion(
|
||||||
|
() => cy.getByTestId('node-view'),
|
||||||
|
() => cy.get('.vue-flow__transformationpane.vue-flow__container'),
|
||||||
|
),
|
||||||
inlineExpressionEditorInput: () =>
|
inlineExpressionEditorInput: () =>
|
||||||
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
|
cy.getByTestId('inline-expression-editor-input').find('[role=textbox]'),
|
||||||
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
|
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
|
||||||
|
@ -115,12 +167,26 @@ export class WorkflowPage extends BasePage {
|
||||||
ndvParameters: () => cy.getByTestId('parameter-item'),
|
ndvParameters: () => cy.getByTestId('parameter-item'),
|
||||||
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
nodeCredentialsLabel: () => cy.getByTestId('credentials-label'),
|
||||||
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
getConnectionBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||||
cy.get(
|
cy.ifCanvasVersion(
|
||||||
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
() =>
|
||||||
|
cy.get(
|
||||||
|
`.jtk-connector[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
cy.get(
|
||||||
|
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
getConnectionActionsBetweenNodes: (sourceNodeName: string, targetNodeName: string) =>
|
||||||
cy.get(
|
cy.ifCanvasVersion(
|
||||||
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
() =>
|
||||||
|
cy.get(
|
||||||
|
`.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`,
|
||||||
|
),
|
||||||
|
() =>
|
||||||
|
cy.get(
|
||||||
|
`[data-test-id="edge-label-wrapper"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
addStickyButton: () => cy.getByTestId('add-sticky-button'),
|
addStickyButton: () => cy.getByTestId('add-sticky-button'),
|
||||||
stickies: () => cy.getByTestId('sticky'),
|
stickies: () => cy.getByTestId('sticky'),
|
||||||
|
@ -128,6 +194,18 @@ export class WorkflowPage extends BasePage {
|
||||||
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
workflowHistoryButton: () => cy.getByTestId('workflow-history-button'),
|
||||||
colors: () => cy.getByTestId('color'),
|
colors: () => cy.getByTestId('color'),
|
||||||
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
|
contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`),
|
||||||
|
getNodeLeftPosition: (element: JQuery<HTMLElement>) => {
|
||||||
|
if (isCanvasV2()) {
|
||||||
|
return parseFloat(element.parent().css('transform').split(',')[4]);
|
||||||
|
}
|
||||||
|
return parseFloat(element.css('left'));
|
||||||
|
},
|
||||||
|
getNodeTopPosition: (element: JQuery<HTMLElement>) => {
|
||||||
|
if (isCanvasV2()) {
|
||||||
|
return parseFloat(element.parent().css('transform').split(',')[5]);
|
||||||
|
}
|
||||||
|
return parseFloat(element.css('top'));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
@ -332,7 +410,7 @@ export class WorkflowPage extends BasePage {
|
||||||
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
|
pinchToZoom: (steps: number, mode: 'zoomIn' | 'zoomOut' = 'zoomIn') => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
|
// Pinch-to-zoom simulates a 'wheel' event with ctrlKey: true (same as zooming by scrolling)
|
||||||
this.getters.nodeViewBackground().trigger('wheel', {
|
this.getters.nodeView().trigger('wheel', {
|
||||||
force: true,
|
force: true,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
|
@ -391,9 +469,12 @@ export class WorkflowPage extends BasePage {
|
||||||
action?: string,
|
action?: string,
|
||||||
) => {
|
) => {
|
||||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||||
this.getters
|
const connectionsBetweenNodes = () =>
|
||||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||||
.find('.add')
|
cy.ifCanvasVersion(
|
||||||
|
() => connectionsBetweenNodes().find('.add'),
|
||||||
|
() => connectionsBetweenNodes().get('[data-test-id="add-connection-button"]'),
|
||||||
|
)
|
||||||
.first()
|
.first()
|
||||||
.click({ force: true });
|
.click({ force: true });
|
||||||
|
|
||||||
|
@ -401,9 +482,12 @@ export class WorkflowPage extends BasePage {
|
||||||
},
|
},
|
||||||
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
|
deleteNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string) => {
|
||||||
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
|
||||||
this.getters
|
const connectionsBetweenNodes = () =>
|
||||||
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
|
this.getters.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName);
|
||||||
.find('.delete')
|
cy.ifCanvasVersion(
|
||||||
|
() => connectionsBetweenNodes().find('.delete'),
|
||||||
|
() => connectionsBetweenNodes().get('[data-test-id="delete-connection-button"]'),
|
||||||
|
)
|
||||||
.first()
|
.first()
|
||||||
.click({ force: true });
|
.click({ force: true });
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
N8N_AUTH_COOKIE,
|
N8N_AUTH_COOKIE,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { WorkflowPage } from '../pages';
|
import { WorkflowPage } from '../pages';
|
||||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
import { getUniqueWorkflowName, isCanvasV2 } from '../utils/workflowUtils';
|
||||||
|
|
||||||
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
|
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
|
@ -26,6 +26,10 @@ Cypress.Commands.add('getByTestId', (selector, ...args) => {
|
||||||
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
return cy.get(`[data-test-id="${selector}"]`, ...args);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('ifCanvasVersion', (getterV1, getterV2) => {
|
||||||
|
return isCanvasV2() ? getterV2() : getterV1();
|
||||||
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
'createFixtureWorkflow',
|
'createFixtureWorkflow',
|
||||||
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {
|
(fixtureKey: string, workflowName = getUniqueWorkflowName()) => {
|
||||||
|
@ -70,6 +74,10 @@ Cypress.Commands.add('signin', ({ email, password }) => {
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
Cypress.env('currentUserId', response.body.data.id);
|
Cypress.env('currentUserId', response.body.data.id);
|
||||||
|
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.localStorage.setItem('NodeView.switcher.discovered', 'true'); // @TODO Remove this once the switcher is removed
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,11 @@ beforeEach(() => {
|
||||||
win.localStorage.setItem('N8N_THEME', 'light');
|
win.localStorage.setItem('N8N_THEME', 'light');
|
||||||
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
win.localStorage.setItem('N8N_AUTOCOMPLETE_ONBOARDED', 'true');
|
||||||
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
win.localStorage.setItem('N8N_MAPPING_ONBOARDED', 'true');
|
||||||
|
|
||||||
|
const nodeViewVersion = Cypress.env('NODE_VIEW_VERSION');
|
||||||
|
if (nodeViewVersion) {
|
||||||
|
win.localStorage.setItem('NodeView.version', nodeViewVersion);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.intercept('GET', '/rest/settings', (req) => {
|
cy.intercept('GET', '/rest/settings', (req) => {
|
||||||
|
|
|
@ -28,6 +28,7 @@ declare global {
|
||||||
selector: string,
|
selector: string,
|
||||||
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
|
...args: Array<Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined>
|
||||||
): Chainable<JQuery<HTMLElement>>;
|
): Chainable<JQuery<HTMLElement>>;
|
||||||
|
ifCanvasVersion<T1, T2>(getterV1: () => T1, getterV2: () => T2): T1 | T2;
|
||||||
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
|
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
|
||||||
/**
|
/**
|
||||||
* Creates a workflow from the given fixture and optionally renames it.
|
* Creates a workflow from the given fixture and optionally renames it.
|
||||||
|
|
|
@ -3,3 +3,7 @@ import { nanoid } from 'nanoid';
|
||||||
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
|
export function getUniqueWorkflowName(workflowNamePrefix?: string) {
|
||||||
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
|
return workflowNamePrefix ? `${workflowNamePrefix} ${nanoid(12)}` : nanoid(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCanvasV2() {
|
||||||
|
return Cypress.env('NODE_VIEW_VERSION') === 2;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-monorepo",
|
"name": "n8n-monorepo",
|
||||||
"version": "1.63.0",
|
"version": "1.64.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.15",
|
"node": ">=20.15",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/api-types",
|
"name": "@n8n/api-types",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -15,6 +15,12 @@ export default function () {
|
||||||
|
|
||||||
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
|
const res = http.post(`${apiBaseUrl}/webhook/binary-files-benchmark`, data);
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(
|
||||||
|
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
check(res, {
|
check(res, {
|
||||||
'is status 200': (r) => r.status === 200,
|
'is status 200': (r) => r.status === 200,
|
||||||
'has correct content type': (r) =>
|
'has correct content type': (r) =>
|
||||||
|
|
|
@ -6,6 +6,12 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
||||||
export default function () {
|
export default function () {
|
||||||
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
|
const res = http.post(`${apiBaseUrl}/webhook/benchmark-http-node`);
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(
|
||||||
|
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
check(res, {
|
check(res, {
|
||||||
'is status 200': (r) => r.status === 200,
|
'is status 200': (r) => r.status === 200,
|
||||||
'http requests were OK': (r) => {
|
'http requests were OK': (r) => {
|
||||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {});
|
const res = http.post(`${apiBaseUrl}/webhook/code-node-benchmark`, {});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(
|
||||||
|
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
check(res, {
|
check(res, {
|
||||||
'is status 200': (r) => r.status === 200,
|
'is status 200': (r) => r.status === 200,
|
||||||
'has items in response': (r) => {
|
'has items in response': (r) => {
|
||||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
|
const res = http.post(`${apiBaseUrl}/webhook/set-expressions-benchmark`, {});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(
|
||||||
|
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
check(res, {
|
check(res, {
|
||||||
'is status 200': (r) => r.status === 200,
|
'is status 200': (r) => r.status === 200,
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
"name": "SingleWebhook",
|
"name": "SingleWebhook",
|
||||||
"description": "A single webhook trigger that responds with a 200 status code",
|
"description": "A single webhook trigger that responds with a 200 status code",
|
||||||
"scenarioData": { "workflowFiles": ["single-webhook.json"] },
|
"scenarioData": { "workflowFiles": ["single-webhook.json"] },
|
||||||
"scriptPath": "single-webhook.script.ts"
|
"scriptPath": "single-webhook.script.js"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,13 @@ const apiBaseUrl = __ENV.API_BASE_URL;
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
const res = http.get(`${apiBaseUrl}/webhook/single-webhook`);
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(
|
||||||
|
`Invalid response. Received status ${res.status}. Body: ${JSON.stringify(res.body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
check(res, {
|
check(res, {
|
||||||
'is status 200': (r) => r.status === 200,
|
'is status 200': (r) => r.status === 200,
|
||||||
});
|
});
|
|
@ -176,7 +176,7 @@ services:
|
||||||
|
|
||||||
# Load balancer that acts as an entry point for n8n
|
# Load balancer that acts as an entry point for n8n
|
||||||
n8n:
|
n8n:
|
||||||
image: nginx:latest
|
image: nginx:1.27.2
|
||||||
ports:
|
ports:
|
||||||
- '5678:80'
|
- '5678:80'
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -3,6 +3,7 @@ events {}
|
||||||
http {
|
http {
|
||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
access_log off;
|
access_log off;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
|
||||||
upstream backend {
|
upstream backend {
|
||||||
server n8n_main1:5678;
|
server n8n_main1:5678;
|
||||||
|
|
|
@ -78,12 +78,6 @@ async function runBenchmarksOnVm(config, benchmarkEnv) {
|
||||||
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
||||||
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
||||||
|
|
||||||
// Benchmarking the VM
|
|
||||||
const vmBenchmarkScriptPath = path.join(scriptsDir, 'vm-benchmark.sh');
|
|
||||||
await sshClient.ssh(`chmod a+x ${vmBenchmarkScriptPath} && ${vmBenchmarkScriptPath}`, {
|
|
||||||
verbose: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Give some time for the VM to be ready
|
// Give some time for the VM to be ready
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Install fio
|
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install fio > /dev/null
|
|
||||||
|
|
||||||
# Run the disk benchmark
|
|
||||||
fio --name=rand_rw --ioengine=libaio --rw=randrw --rwmixread=70 --bs=4k --numjobs=4 --size=1G --runtime=30 --directory=/n8n --group_reporting
|
|
||||||
|
|
||||||
# Remove files
|
|
||||||
sudo rm /n8n/rand_rw.*
|
|
||||||
|
|
||||||
# Uninstall fio
|
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y remove fio > /dev/null
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { sleep } from 'zx';
|
||||||
|
|
||||||
import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
||||||
import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client';
|
import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client';
|
||||||
import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader';
|
import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader';
|
||||||
|
@ -47,6 +49,10 @@ export class ScenarioRunner {
|
||||||
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
||||||
await testDataImporter.importTestScenarioData(testData.workflows);
|
await testDataImporter.importTestScenarioData(testData.workflows);
|
||||||
|
|
||||||
|
// Wait for 1s before executing the scenario to ensure that the workflows are activated.
|
||||||
|
// In multi-main mode it can take some time before the workflow becomes active.
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
console.log('Executing scenario script');
|
console.log('Executing scenario script');
|
||||||
await this.k6Executor.executeTestScenario(scenario, {
|
await this.k6Executor.executeTestScenario(scenario, {
|
||||||
scenarioRunName,
|
scenarioRunName,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/chat",
|
"name": "@n8n/chat",
|
||||||
"version": "0.28.0",
|
"version": "0.29.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm run storybook",
|
"dev": "pnpm run storybook",
|
||||||
"build": "pnpm build:vite && pnpm build:bundle",
|
"build": "pnpm build:vite && pnpm build:bundle",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/config",
|
"name": "@n8n/config",
|
||||||
"version": "1.13.0",
|
"version": "1.14.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
15
packages/@n8n/config/src/configs/generic.config.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { Config, Env } from '../decorators';
|
||||||
|
|
||||||
|
@Config
|
||||||
|
export class GenericConfig {
|
||||||
|
/** Default timezone for the n8n instance. Can be overridden on a per-workflow basis. */
|
||||||
|
@Env('GENERIC_TIMEZONE')
|
||||||
|
timezone: string = 'America/New_York';
|
||||||
|
|
||||||
|
@Env('N8N_RELEASE_TYPE')
|
||||||
|
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev';
|
||||||
|
|
||||||
|
/** Grace period (in seconds) to wait for components to shut down before process exit. */
|
||||||
|
@Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT')
|
||||||
|
gracefulShutdownTimeout: number = 30;
|
||||||
|
}
|
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
16
packages/@n8n/config/src/configs/multi-main-setup.config.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Config, Env } from '../decorators';
|
||||||
|
|
||||||
|
@Config
|
||||||
|
export class MultiMainSetupConfig {
|
||||||
|
/** Whether to enable multi-main setup (if licensed) for scaling mode. */
|
||||||
|
@Env('N8N_MULTI_MAIN_SETUP_ENABLED')
|
||||||
|
enabled: boolean = false;
|
||||||
|
|
||||||
|
/** Time to live (in seconds) for leader key in multi-main setup. */
|
||||||
|
@Env('N8N_MULTI_MAIN_SETUP_KEY_TTL')
|
||||||
|
ttl: number = 10;
|
||||||
|
|
||||||
|
/** Interval (in seconds) for leader check in multi-main setup. */
|
||||||
|
@Env('N8N_MULTI_MAIN_SETUP_CHECK_INTERVAL')
|
||||||
|
interval: number = 3;
|
||||||
|
}
|
|
@ -82,10 +82,6 @@ class BullConfig {
|
||||||
@Nested
|
@Nested
|
||||||
redis: RedisConfig;
|
redis: RedisConfig;
|
||||||
|
|
||||||
/** How often (in seconds) to poll the Bull queue to identify executions finished during a Redis crash. `0` to disable. May increase Redis traffic significantly. */
|
|
||||||
@Env('QUEUE_RECOVERY_INTERVAL')
|
|
||||||
queueRecoveryInterval: number = 60; // watchdog interval
|
|
||||||
|
|
||||||
/** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */
|
/** @deprecated How long (in seconds) a worker must wait for active executions to finish before exiting. Use `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` instead */
|
||||||
@Env('QUEUE_WORKER_TIMEOUT')
|
@Env('QUEUE_WORKER_TIMEOUT')
|
||||||
gracefulShutdownTimeout: number = 30;
|
gracefulShutdownTimeout: number = 30;
|
||||||
|
|
|
@ -5,7 +5,9 @@ import { EndpointsConfig } from './configs/endpoints.config';
|
||||||
import { EventBusConfig } from './configs/event-bus.config';
|
import { EventBusConfig } from './configs/event-bus.config';
|
||||||
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
import { ExternalSecretsConfig } from './configs/external-secrets.config';
|
||||||
import { ExternalStorageConfig } from './configs/external-storage.config';
|
import { ExternalStorageConfig } from './configs/external-storage.config';
|
||||||
|
import { GenericConfig } from './configs/generic.config';
|
||||||
import { LoggingConfig } from './configs/logging.config';
|
import { LoggingConfig } from './configs/logging.config';
|
||||||
|
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
|
||||||
import { NodesConfig } from './configs/nodes.config';
|
import { NodesConfig } from './configs/nodes.config';
|
||||||
import { PublicApiConfig } from './configs/public-api.config';
|
import { PublicApiConfig } from './configs/public-api.config';
|
||||||
import { TaskRunnersConfig } from './configs/runners.config';
|
import { TaskRunnersConfig } from './configs/runners.config';
|
||||||
|
@ -93,4 +95,10 @@ export class GlobalConfig {
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
taskRunners: TaskRunnersConfig;
|
taskRunners: TaskRunnersConfig;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
multiMainSetup: MultiMainSetupConfig;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
generic: GenericConfig;
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,7 +211,6 @@ describe('GlobalConfig', () => {
|
||||||
clusterNodes: '',
|
clusterNodes: '',
|
||||||
tls: false,
|
tls: false,
|
||||||
},
|
},
|
||||||
queueRecoveryInterval: 60,
|
|
||||||
gracefulShutdownTimeout: 30,
|
gracefulShutdownTimeout: 30,
|
||||||
prefix: 'bull',
|
prefix: 'bull',
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -246,6 +245,16 @@ describe('GlobalConfig', () => {
|
||||||
},
|
},
|
||||||
scopes: [],
|
scopes: [],
|
||||||
},
|
},
|
||||||
|
multiMainSetup: {
|
||||||
|
enabled: false,
|
||||||
|
ttl: 10,
|
||||||
|
interval: 3,
|
||||||
|
},
|
||||||
|
generic: {
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
releaseChannel: 'dev',
|
||||||
|
gracefulShutdownTimeout: 30,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should use all default values when no env variables are defined', () => {
|
it('should use all default values when no env variables are defined', () => {
|
||||||
|
|
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
21
packages/@n8n/json-schema-to-zod/.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('@types/eslint').ESLint.ConfigData}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
extends: ['@n8n_io/eslint-config/node'],
|
||||||
|
|
||||||
|
...sharedOptions(__dirname),
|
||||||
|
|
||||||
|
ignorePatterns: ['jest.config.js'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
||||||
|
'@typescript-eslint/no-duplicate-imports': 'off',
|
||||||
|
'import/no-cycle': 'off',
|
||||||
|
'n8n-local-rules/no-plain-errors': 'off',
|
||||||
|
|
||||||
|
complexity: 'error',
|
||||||
|
},
|
||||||
|
};
|
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
4
packages/@n8n/json-schema-to-zod/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
coverage
|
||||||
|
test/output
|
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
3
packages/@n8n/json-schema-to-zod/.npmignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
src
|
||||||
|
tsconfig*
|
||||||
|
test
|
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
16
packages/@n8n/json-schema-to-zod/LICENSE
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2024, n8n
|
||||||
|
Copyright (c) 2021, Stefan Terdell
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
34
packages/@n8n/json-schema-to-zod/README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Json-Schema-to-Zod
|
||||||
|
|
||||||
|
A package to convert JSON schema (draft 4+) objects into Zod schemas in the form of Zod objects at runtime.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install @n8n/json-schema-to-zod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { jsonSchemaToZod } from "json-schema-to-zod";
|
||||||
|
|
||||||
|
const jsonSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
hello: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const zodSchema = jsonSchemaToZod(myObject);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overriding a parser
|
||||||
|
|
||||||
|
You can pass a function to the `overrideParser` option, which represents a function that receives the current schema node and the reference object, and should return a zod object when it wants to replace a default output. If the default output should be used for the node just return undefined.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
This is a fork of [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod).
|
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
5
packages/@n8n/json-schema-to-zod/jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
module.exports = {
|
||||||
|
...require('../../../jest.config'),
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/test/extend-expect.ts'],
|
||||||
|
};
|
69
packages/@n8n/json-schema-to-zod/package.json
Normal file
69
packages/@n8n/json-schema-to-zod/package.json
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "@n8n/json-schema-to-zod",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Converts JSON schema objects into Zod schemas",
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"exports": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"default": "./dist/esm/index.js"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"default": "./dist/cjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist .turbo",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dev": "tsc -w",
|
||||||
|
"format": "biome format --write src",
|
||||||
|
"format:check": "biome ci src",
|
||||||
|
"lint": "eslint . --quiet",
|
||||||
|
"lintfix": "eslint . --fix",
|
||||||
|
"build:types": "tsc -p tsconfig.types.json",
|
||||||
|
"build:cjs": "tsc -p tsconfig.cjs.json && node postcjs.js",
|
||||||
|
"build:esm": "tsc -p tsconfig.esm.json && node postesm.js",
|
||||||
|
"build": "rimraf ./dist && pnpm run build:types && pnpm run build:cjs && pnpm run build:esm",
|
||||||
|
"dry": "pnpm run build && pnpm pub --dry-run",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"zod",
|
||||||
|
"json",
|
||||||
|
"schema",
|
||||||
|
"converter",
|
||||||
|
"cli"
|
||||||
|
],
|
||||||
|
"author": "Stefan Terdell",
|
||||||
|
"contributors": [
|
||||||
|
"Chen (https://github.com/werifu)",
|
||||||
|
"Nuno Carduso (https://github.com/ncardoso-barracuda)",
|
||||||
|
"Lars Strojny (https://github.com/lstrojny)",
|
||||||
|
"Navtoj Chahal (https://github.com/navtoj)",
|
||||||
|
"Ben McCann (https://github.com/benmccann)",
|
||||||
|
"Dmitry Zakharov (https://github.com/DZakh)",
|
||||||
|
"Michel Turpin (https://github.com/grimly)",
|
||||||
|
"David Barratt (https://github.com/davidbarratt)",
|
||||||
|
"pevisscher (https://github.com/pevisscher)",
|
||||||
|
"Aidin Abedi (https://github.com/aidinabedi)",
|
||||||
|
"Brett Zamir (https://github.com/brettz9)",
|
||||||
|
"n8n (https://github.com/n8n-io)"
|
||||||
|
],
|
||||||
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/n8n-io/n8n"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"@types/node": "^20.9.0",
|
||||||
|
"zod": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
1
packages/@n8n/json-schema-to-zod/postcjs.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
require('fs').writeFileSync('./dist/cjs/package.json', '{"type":"commonjs"}', 'utf-8');
|
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
1
packages/@n8n/json-schema-to-zod/postesm.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
require('fs').writeFileSync('./dist/esm/package.json', '{"type":"module"}', 'utf-8');
|
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
2
packages/@n8n/json-schema-to-zod/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export type * from './types';
|
||||||
|
export { jsonSchemaToZod } from './json-schema-to-zod.js';
|
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/json-schema-to-zod.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parsers/parse-schema';
|
||||||
|
import type { JsonSchemaToZodOptions, JsonSchema } from './types';
|
||||||
|
|
||||||
|
export const jsonSchemaToZod = <T extends z.ZodTypeAny = z.ZodTypeAny>(
|
||||||
|
schema: JsonSchema,
|
||||||
|
options: JsonSchemaToZodOptions = {},
|
||||||
|
): T => {
|
||||||
|
return parseSchema(schema, {
|
||||||
|
path: [],
|
||||||
|
seen: new Map(),
|
||||||
|
...options,
|
||||||
|
}) as T;
|
||||||
|
};
|
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
46
packages/@n8n/json-schema-to-zod/src/parsers/parse-all-of.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||||
|
import { half } from '../utils/half';
|
||||||
|
|
||||||
|
const originalIndex = Symbol('Original index');
|
||||||
|
|
||||||
|
const ensureOriginalIndex = (arr: JsonSchema[]) => {
|
||||||
|
const newArr = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const item = arr[i];
|
||||||
|
if (typeof item === 'boolean') {
|
||||||
|
newArr.push(item ? { [originalIndex]: i } : { [originalIndex]: i, not: {} });
|
||||||
|
} else if (originalIndex in item) {
|
||||||
|
return arr;
|
||||||
|
} else {
|
||||||
|
newArr.push({ ...item, [originalIndex]: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseAllOf(
|
||||||
|
jsonSchema: JsonSchemaObject & { allOf: JsonSchema[] },
|
||||||
|
refs: Refs,
|
||||||
|
): z.ZodTypeAny {
|
||||||
|
if (jsonSchema.allOf.length === 0) {
|
||||||
|
return z.never();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonSchema.allOf.length === 1) {
|
||||||
|
const item = jsonSchema.allOf[0];
|
||||||
|
|
||||||
|
return parseSchema(item, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'allOf', (item as never)[originalIndex]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [left, right] = half(ensureOriginalIndex(jsonSchema.allOf));
|
||||||
|
|
||||||
|
return z.intersection(parseAllOf({ allOf: left }, refs), parseAllOf({ allOf: right }, refs));
|
||||||
|
}
|
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
19
packages/@n8n/json-schema-to-zod/src/parsers/parse-any-of.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||||
|
|
||||||
|
export const parseAnyOf = (jsonSchema: JsonSchemaObject & { anyOf: JsonSchema[] }, refs: Refs) => {
|
||||||
|
return jsonSchema.anyOf.length
|
||||||
|
? jsonSchema.anyOf.length === 1
|
||||||
|
? parseSchema(jsonSchema.anyOf[0], {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'anyOf', 0],
|
||||||
|
})
|
||||||
|
: z.union(
|
||||||
|
jsonSchema.anyOf.map((schema, i) =>
|
||||||
|
parseSchema(schema, { ...refs, path: [...refs.path, 'anyOf', i] }),
|
||||||
|
) as [z.ZodTypeAny, z.ZodTypeAny],
|
||||||
|
)
|
||||||
|
: z.any();
|
||||||
|
};
|
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
34
packages/@n8n/json-schema-to-zod/src/parsers/parse-array.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, Refs } from '../types';
|
||||||
|
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||||
|
|
||||||
|
export const parseArray = (jsonSchema: JsonSchemaObject & { type: 'array' }, refs: Refs) => {
|
||||||
|
if (Array.isArray(jsonSchema.items)) {
|
||||||
|
return z.tuple(
|
||||||
|
jsonSchema.items.map((v, i) =>
|
||||||
|
parseSchema(v, { ...refs, path: [...refs.path, 'items', i] }),
|
||||||
|
) as [z.ZodTypeAny],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let zodSchema = !jsonSchema.items
|
||||||
|
? z.array(z.any())
|
||||||
|
: z.array(parseSchema(jsonSchema.items, { ...refs, path: [...refs.path, 'items'] }));
|
||||||
|
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'minItems',
|
||||||
|
(zs, minItems, errorMessage) => zs.min(minItems, errorMessage),
|
||||||
|
);
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'maxItems',
|
||||||
|
(zs, maxItems, errorMessage) => zs.max(maxItems, errorMessage),
|
||||||
|
);
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
|
||||||
|
export const parseBoolean = (_jsonSchema: JsonSchemaObject & { type: 'boolean' }) => {
|
||||||
|
return z.boolean();
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject, Serializable } from '../types';
|
||||||
|
|
||||||
|
export const parseConst = (jsonSchema: JsonSchemaObject & { const: Serializable }) => {
|
||||||
|
return z.literal(jsonSchema.const as z.Primitive);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
|
||||||
|
export const parseDefault = (_jsonSchema: JsonSchemaObject) => {
|
||||||
|
return z.any();
|
||||||
|
};
|
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
25
packages/@n8n/json-schema-to-zod/src/parsers/parse-enum.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject, Serializable } from '../types';
|
||||||
|
|
||||||
|
export const parseEnum = (jsonSchema: JsonSchemaObject & { enum: Serializable[] }) => {
|
||||||
|
if (jsonSchema.enum.length === 0) {
|
||||||
|
return z.never();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonSchema.enum.length === 1) {
|
||||||
|
// union does not work when there is only one element
|
||||||
|
return z.literal(jsonSchema.enum[0] as z.Primitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonSchema.enum.every((x) => typeof x === 'string')) {
|
||||||
|
return z.enum(jsonSchema.enum as [string]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.union(
|
||||||
|
jsonSchema.enum.map((x) => z.literal(x as z.Primitive)) as unknown as [
|
||||||
|
z.ZodTypeAny,
|
||||||
|
z.ZodTypeAny,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||||
|
|
||||||
|
export const parseIfThenElse = (
|
||||||
|
jsonSchema: JsonSchemaObject & {
|
||||||
|
if: JsonSchema;
|
||||||
|
then: JsonSchema;
|
||||||
|
else: JsonSchema;
|
||||||
|
},
|
||||||
|
refs: Refs,
|
||||||
|
) => {
|
||||||
|
const $if = parseSchema(jsonSchema.if, { ...refs, path: [...refs.path, 'if'] });
|
||||||
|
const $then = parseSchema(jsonSchema.then, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'then'],
|
||||||
|
});
|
||||||
|
const $else = parseSchema(jsonSchema.else, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'else'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return z.union([$then, $else]).superRefine((value, ctx) => {
|
||||||
|
const result = $if.safeParse(value).success ? $then.safeParse(value) : $else.safeParse(value);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchema, JsonSchemaObject, Refs } from '../types';
|
||||||
|
|
||||||
|
export const parseMultipleType = (
|
||||||
|
jsonSchema: JsonSchemaObject & { type: string[] },
|
||||||
|
refs: Refs,
|
||||||
|
) => {
|
||||||
|
return z.union(
|
||||||
|
jsonSchema.type.map((type) => parseSchema({ ...jsonSchema, type } as JsonSchema, refs)) as [
|
||||||
|
z.ZodTypeAny,
|
||||||
|
z.ZodTypeAny,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
15
packages/@n8n/json-schema-to-zod/src/parsers/parse-not.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||||
|
|
||||||
|
export const parseNot = (jsonSchema: JsonSchemaObject & { not: JsonSchema }, refs: Refs) => {
|
||||||
|
return z.any().refine(
|
||||||
|
(value) =>
|
||||||
|
!parseSchema(jsonSchema.not, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'not'],
|
||||||
|
}).safeParse(value).success,
|
||||||
|
'Invalid input: Should NOT be valid against schema',
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
|
||||||
|
export const parseNull = (_jsonSchema: JsonSchemaObject & { type: 'null' }) => {
|
||||||
|
return z.null();
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, Refs } from '../types';
|
||||||
|
import { omit } from '../utils/omit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For compatibility with open api 3.0 nullable
|
||||||
|
*/
|
||||||
|
export const parseNullable = (jsonSchema: JsonSchemaObject & { nullable: true }, refs: Refs) => {
|
||||||
|
return parseSchema(omit(jsonSchema, 'nullable'), refs, true).nullable();
|
||||||
|
};
|
88
packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts
Normal file
88
packages/@n8n/json-schema-to-zod/src/parsers/parse-number.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||||
|
|
||||||
|
export const parseNumber = (jsonSchema: JsonSchemaObject & { type: 'number' | 'integer' }) => {
|
||||||
|
let zodSchema = z.number();
|
||||||
|
|
||||||
|
let isInteger = false;
|
||||||
|
if (jsonSchema.type === 'integer') {
|
||||||
|
isInteger = true;
|
||||||
|
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'type', (zs, _, errorMsg) =>
|
||||||
|
zs.int(errorMsg),
|
||||||
|
);
|
||||||
|
} else if (jsonSchema.format === 'int64') {
|
||||||
|
isInteger = true;
|
||||||
|
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, _, errorMsg) =>
|
||||||
|
zs.int(errorMsg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'multipleOf',
|
||||||
|
(zs, multipleOf, errorMsg) => {
|
||||||
|
if (multipleOf === 1) {
|
||||||
|
if (isInteger) return zs;
|
||||||
|
|
||||||
|
return zs.int(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zs.multipleOf(multipleOf, errorMsg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof jsonSchema.minimum === 'number') {
|
||||||
|
if (jsonSchema.exclusiveMinimum === true) {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'minimum',
|
||||||
|
(zs, minimum, errorMsg) => zs.gt(minimum, errorMsg),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'minimum',
|
||||||
|
(zs, minimum, errorMsg) => zs.gte(minimum, errorMsg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (typeof jsonSchema.exclusiveMinimum === 'number') {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'exclusiveMinimum',
|
||||||
|
(zs, exclusiveMinimum, errorMsg) => zs.gt(exclusiveMinimum as number, errorMsg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof jsonSchema.maximum === 'number') {
|
||||||
|
if (jsonSchema.exclusiveMaximum === true) {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'maximum',
|
||||||
|
(zs, maximum, errorMsg) => zs.lt(maximum, errorMsg),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'maximum',
|
||||||
|
(zs, maximum, errorMsg) => zs.lte(maximum, errorMsg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (typeof jsonSchema.exclusiveMaximum === 'number') {
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'exclusiveMaximum',
|
||||||
|
(zs, exclusiveMaximum, errorMsg) => zs.lt(exclusiveMaximum as number, errorMsg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
219
packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts
Normal file
219
packages/@n8n/json-schema-to-zod/src/parsers/parse-object.ts
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { parseAllOf } from './parse-all-of';
|
||||||
|
import { parseAnyOf } from './parse-any-of';
|
||||||
|
import { parseOneOf } from './parse-one-of';
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, Refs } from '../types';
|
||||||
|
import { its } from '../utils/its';
|
||||||
|
|
||||||
|
function parseObjectProperties(objectSchema: JsonSchemaObject & { type: 'object' }, refs: Refs) {
|
||||||
|
if (!objectSchema.properties) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyKeys = Object.keys(objectSchema.properties);
|
||||||
|
if (propertyKeys.length === 0) {
|
||||||
|
return z.object({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const properties: Record<string, z.ZodTypeAny> = {};
|
||||||
|
|
||||||
|
for (const key of propertyKeys) {
|
||||||
|
const propJsonSchema = objectSchema.properties[key];
|
||||||
|
|
||||||
|
const propZodSchema = parseSchema(propJsonSchema, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'properties', key],
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDefault = typeof propJsonSchema === 'object' && propJsonSchema.default !== undefined;
|
||||||
|
|
||||||
|
const required = Array.isArray(objectSchema.required)
|
||||||
|
? objectSchema.required.includes(key)
|
||||||
|
: typeof propJsonSchema === 'object' && propJsonSchema.required === true;
|
||||||
|
|
||||||
|
const isOptional = !hasDefault && !required;
|
||||||
|
|
||||||
|
properties[key] = isOptional ? propZodSchema.optional() : propZodSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.object(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseObject(
|
||||||
|
objectSchema: JsonSchemaObject & { type: 'object' },
|
||||||
|
refs: Refs,
|
||||||
|
): z.ZodTypeAny {
|
||||||
|
const hasPatternProperties = Object.keys(objectSchema.patternProperties ?? {}).length > 0;
|
||||||
|
|
||||||
|
const propertiesSchema:
|
||||||
|
| z.ZodObject<Record<string, z.ZodTypeAny>, 'strip', z.ZodTypeAny>
|
||||||
|
| undefined = parseObjectProperties(objectSchema, refs);
|
||||||
|
let zodSchema: z.ZodTypeAny | undefined = propertiesSchema;
|
||||||
|
|
||||||
|
const additionalProperties =
|
||||||
|
objectSchema.additionalProperties !== undefined
|
||||||
|
? parseSchema(objectSchema.additionalProperties, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'additionalProperties'],
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (objectSchema.patternProperties) {
|
||||||
|
const parsedPatternProperties = Object.fromEntries(
|
||||||
|
Object.entries(objectSchema.patternProperties).map(([key, value]) => {
|
||||||
|
return [
|
||||||
|
key,
|
||||||
|
parseSchema(value, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'patternProperties', key],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const patternPropertyValues = Object.values(parsedPatternProperties);
|
||||||
|
|
||||||
|
if (propertiesSchema) {
|
||||||
|
if (additionalProperties) {
|
||||||
|
zodSchema = propertiesSchema.catchall(
|
||||||
|
z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||||
|
);
|
||||||
|
} else if (Object.keys(parsedPatternProperties).length > 1) {
|
||||||
|
zodSchema = propertiesSchema.catchall(
|
||||||
|
z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
zodSchema = propertiesSchema.catchall(patternPropertyValues[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (additionalProperties) {
|
||||||
|
zodSchema = z.record(
|
||||||
|
z.union([...patternPropertyValues, additionalProperties] as [z.ZodTypeAny, z.ZodTypeAny]),
|
||||||
|
);
|
||||||
|
} else if (patternPropertyValues.length > 1) {
|
||||||
|
zodSchema = z.record(z.union(patternPropertyValues as [z.ZodTypeAny, z.ZodTypeAny]));
|
||||||
|
} else {
|
||||||
|
zodSchema = z.record(patternPropertyValues[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectPropertyKeys = new Set(Object.keys(objectSchema.properties ?? {}));
|
||||||
|
zodSchema = zodSchema.superRefine((value: Record<string, unknown>, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
let wasMatched = objectPropertyKeys.has(key);
|
||||||
|
|
||||||
|
for (const patternPropertyKey in objectSchema.patternProperties) {
|
||||||
|
const regex = new RegExp(patternPropertyKey);
|
||||||
|
if (key.match(regex)) {
|
||||||
|
wasMatched = true;
|
||||||
|
const result = parsedPatternProperties[patternPropertyKey].safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wasMatched && additionalProperties) {
|
||||||
|
const result = additionalProperties.safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Invalid input: must match catchall schema',
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let output: z.ZodTypeAny;
|
||||||
|
if (propertiesSchema) {
|
||||||
|
if (hasPatternProperties) {
|
||||||
|
output = zodSchema!;
|
||||||
|
} else if (additionalProperties) {
|
||||||
|
if (additionalProperties instanceof z.ZodNever) {
|
||||||
|
output = propertiesSchema.strict();
|
||||||
|
} else {
|
||||||
|
output = propertiesSchema.catchall(additionalProperties);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output = zodSchema!;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasPatternProperties) {
|
||||||
|
output = zodSchema!;
|
||||||
|
} else if (additionalProperties) {
|
||||||
|
output = z.record(additionalProperties);
|
||||||
|
} else {
|
||||||
|
output = z.record(z.any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (its.an.anyOf(objectSchema)) {
|
||||||
|
output = output.and(
|
||||||
|
parseAnyOf(
|
||||||
|
{
|
||||||
|
...objectSchema,
|
||||||
|
anyOf: objectSchema.anyOf.map((x) =>
|
||||||
|
typeof x === 'object' &&
|
||||||
|
!x.type &&
|
||||||
|
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||||
|
? { ...x, type: 'object' }
|
||||||
|
: x,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (its.a.oneOf(objectSchema)) {
|
||||||
|
output = output.and(
|
||||||
|
parseOneOf(
|
||||||
|
{
|
||||||
|
...objectSchema,
|
||||||
|
oneOf: objectSchema.oneOf.map((x) =>
|
||||||
|
typeof x === 'object' &&
|
||||||
|
!x.type &&
|
||||||
|
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||||
|
? { ...x, type: 'object' }
|
||||||
|
: x,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (its.an.allOf(objectSchema)) {
|
||||||
|
output = output.and(
|
||||||
|
parseAllOf(
|
||||||
|
{
|
||||||
|
...objectSchema,
|
||||||
|
allOf: objectSchema.allOf.map((x) =>
|
||||||
|
typeof x === 'object' &&
|
||||||
|
!x.type &&
|
||||||
|
(x.properties ?? x.additionalProperties ?? x.patternProperties)
|
||||||
|
? { ...x, type: 'object' }
|
||||||
|
: x,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
refs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
41
packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts
Normal file
41
packages/@n8n/json-schema-to-zod/src/parsers/parse-one-of.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from './parse-schema';
|
||||||
|
import type { JsonSchemaObject, JsonSchema, Refs } from '../types';
|
||||||
|
|
||||||
|
export const parseOneOf = (jsonSchema: JsonSchemaObject & { oneOf: JsonSchema[] }, refs: Refs) => {
|
||||||
|
if (!jsonSchema.oneOf.length) {
|
||||||
|
return z.any();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonSchema.oneOf.length === 1) {
|
||||||
|
return parseSchema(jsonSchema.oneOf[0], {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'oneOf', 0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return z.any().superRefine((x, ctx) => {
|
||||||
|
const schemas = jsonSchema.oneOf.map((schema, i) =>
|
||||||
|
parseSchema(schema, {
|
||||||
|
...refs,
|
||||||
|
path: [...refs.path, 'oneOf', i],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const unionErrors = schemas.reduce<z.ZodError[]>(
|
||||||
|
(errors, schema) =>
|
||||||
|
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (schemas.length - unionErrors.length !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ctx.path,
|
||||||
|
code: 'invalid_union',
|
||||||
|
unionErrors,
|
||||||
|
message: 'Invalid input: Should pass single schema',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
130
packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts
Normal file
130
packages/@n8n/json-schema-to-zod/src/parsers/parse-schema.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { parseAllOf } from './parse-all-of';
|
||||||
|
import { parseAnyOf } from './parse-any-of';
|
||||||
|
import { parseArray } from './parse-array';
|
||||||
|
import { parseBoolean } from './parse-boolean';
|
||||||
|
import { parseConst } from './parse-const';
|
||||||
|
import { parseDefault } from './parse-default';
|
||||||
|
import { parseEnum } from './parse-enum';
|
||||||
|
import { parseIfThenElse } from './parse-if-then-else';
|
||||||
|
import { parseMultipleType } from './parse-multiple-type';
|
||||||
|
import { parseNot } from './parse-not';
|
||||||
|
import { parseNull } from './parse-null';
|
||||||
|
import { parseNullable } from './parse-nullable';
|
||||||
|
import { parseNumber } from './parse-number';
|
||||||
|
import { parseObject } from './parse-object';
|
||||||
|
import { parseOneOf } from './parse-one-of';
|
||||||
|
import { parseString } from './parse-string';
|
||||||
|
import type { ParserSelector, Refs, JsonSchemaObject, JsonSchema } from '../types';
|
||||||
|
import { its } from '../utils/its';
|
||||||
|
|
||||||
|
const addDescribes = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||||
|
if (jsonSchema.description) {
|
||||||
|
zodSchema = zodSchema.describe(jsonSchema.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDefaults = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||||
|
if (jsonSchema.default !== undefined) {
|
||||||
|
zodSchema = zodSchema.default(jsonSchema.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAnnotations = (jsonSchema: JsonSchemaObject, zodSchema: z.ZodTypeAny): z.ZodTypeAny => {
|
||||||
|
if (jsonSchema.readOnly) {
|
||||||
|
zodSchema = zodSchema.readonly();
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectParser: ParserSelector = (schema, refs) => {
|
||||||
|
if (its.a.nullable(schema)) {
|
||||||
|
return parseNullable(schema, refs);
|
||||||
|
} else if (its.an.object(schema)) {
|
||||||
|
return parseObject(schema, refs);
|
||||||
|
} else if (its.an.array(schema)) {
|
||||||
|
return parseArray(schema, refs);
|
||||||
|
} else if (its.an.anyOf(schema)) {
|
||||||
|
return parseAnyOf(schema, refs);
|
||||||
|
} else if (its.an.allOf(schema)) {
|
||||||
|
return parseAllOf(schema, refs);
|
||||||
|
} else if (its.a.oneOf(schema)) {
|
||||||
|
return parseOneOf(schema, refs);
|
||||||
|
} else if (its.a.not(schema)) {
|
||||||
|
return parseNot(schema, refs);
|
||||||
|
} else if (its.an.enum(schema)) {
|
||||||
|
return parseEnum(schema); //<-- needs to come before primitives
|
||||||
|
} else if (its.a.const(schema)) {
|
||||||
|
return parseConst(schema);
|
||||||
|
} else if (its.a.multipleType(schema)) {
|
||||||
|
return parseMultipleType(schema, refs);
|
||||||
|
} else if (its.a.primitive(schema, 'string')) {
|
||||||
|
return parseString(schema);
|
||||||
|
} else if (its.a.primitive(schema, 'number') || its.a.primitive(schema, 'integer')) {
|
||||||
|
return parseNumber(schema);
|
||||||
|
} else if (its.a.primitive(schema, 'boolean')) {
|
||||||
|
return parseBoolean(schema);
|
||||||
|
} else if (its.a.primitive(schema, 'null')) {
|
||||||
|
return parseNull(schema);
|
||||||
|
} else if (its.a.conditional(schema)) {
|
||||||
|
return parseIfThenElse(schema, refs);
|
||||||
|
} else {
|
||||||
|
return parseDefault(schema);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseSchema = (
|
||||||
|
jsonSchema: JsonSchema,
|
||||||
|
refs: Refs = { seen: new Map(), path: [] },
|
||||||
|
blockMeta?: boolean,
|
||||||
|
): z.ZodTypeAny => {
|
||||||
|
if (typeof jsonSchema !== 'object') return jsonSchema ? z.any() : z.never();
|
||||||
|
|
||||||
|
if (refs.parserOverride) {
|
||||||
|
const custom = refs.parserOverride(jsonSchema, refs);
|
||||||
|
|
||||||
|
if (custom instanceof z.ZodType) {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let seen = refs.seen.get(jsonSchema);
|
||||||
|
|
||||||
|
if (seen) {
|
||||||
|
if (seen.r !== undefined) {
|
||||||
|
return seen.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refs.depth === undefined || seen.n >= refs.depth) {
|
||||||
|
return z.any();
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.n += 1;
|
||||||
|
} else {
|
||||||
|
seen = { r: undefined, n: 0 };
|
||||||
|
refs.seen.set(jsonSchema, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedZodSchema = selectParser(jsonSchema, refs);
|
||||||
|
if (!blockMeta) {
|
||||||
|
if (!refs.withoutDescribes) {
|
||||||
|
parsedZodSchema = addDescribes(jsonSchema, parsedZodSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refs.withoutDefaults) {
|
||||||
|
parsedZodSchema = addDefaults(jsonSchema, parsedZodSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedZodSchema = addAnnotations(jsonSchema, parsedZodSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.r = parsedZodSchema;
|
||||||
|
|
||||||
|
return parsedZodSchema;
|
||||||
|
};
|
58
packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts
Normal file
58
packages/@n8n/json-schema-to-zod/src/parsers/parse-string.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
import { extendSchemaWithMessage } from '../utils/extend-schema';
|
||||||
|
|
||||||
|
export const parseString = (jsonSchema: JsonSchemaObject & { type: 'string' }) => {
|
||||||
|
let zodSchema = z.string();
|
||||||
|
|
||||||
|
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'format', (zs, format, errorMsg) => {
|
||||||
|
switch (format) {
|
||||||
|
case 'email':
|
||||||
|
return zs.email(errorMsg);
|
||||||
|
case 'ip':
|
||||||
|
return zs.ip(errorMsg);
|
||||||
|
case 'ipv4':
|
||||||
|
return zs.ip({ version: 'v4', message: errorMsg });
|
||||||
|
case 'ipv6':
|
||||||
|
return zs.ip({ version: 'v6', message: errorMsg });
|
||||||
|
case 'uri':
|
||||||
|
return zs.url(errorMsg);
|
||||||
|
case 'uuid':
|
||||||
|
return zs.uuid(errorMsg);
|
||||||
|
case 'date-time':
|
||||||
|
return zs.datetime({ offset: true, message: errorMsg });
|
||||||
|
case 'time':
|
||||||
|
return zs.time(errorMsg);
|
||||||
|
case 'date':
|
||||||
|
return zs.date(errorMsg);
|
||||||
|
case 'binary':
|
||||||
|
return zs.base64(errorMsg);
|
||||||
|
case 'duration':
|
||||||
|
return zs.duration(errorMsg);
|
||||||
|
default:
|
||||||
|
return zs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'contentEncoding', (zs, _, errorMsg) =>
|
||||||
|
zs.base64(errorMsg),
|
||||||
|
);
|
||||||
|
zodSchema = extendSchemaWithMessage(zodSchema, jsonSchema, 'pattern', (zs, pattern, errorMsg) =>
|
||||||
|
zs.regex(new RegExp(pattern), errorMsg),
|
||||||
|
);
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'minLength',
|
||||||
|
(zs, minLength, errorMsg) => zs.min(minLength, errorMsg),
|
||||||
|
);
|
||||||
|
zodSchema = extendSchemaWithMessage(
|
||||||
|
zodSchema,
|
||||||
|
jsonSchema,
|
||||||
|
'maxLength',
|
||||||
|
(zs, maxLength, errorMsg) => zs.max(maxLength, errorMsg),
|
||||||
|
);
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
};
|
82
packages/@n8n/json-schema-to-zod/src/types.ts
Normal file
82
packages/@n8n/json-schema-to-zod/src/types.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import type { ZodTypeAny } from 'zod';
|
||||||
|
|
||||||
|
export type Serializable =
|
||||||
|
| { [key: string]: Serializable }
|
||||||
|
| Serializable[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export type JsonSchema = JsonSchemaObject | boolean;
|
||||||
|
export type JsonSchemaObject = {
|
||||||
|
// left permissive by design
|
||||||
|
type?: string | string[];
|
||||||
|
|
||||||
|
// object
|
||||||
|
properties?: { [key: string]: JsonSchema };
|
||||||
|
additionalProperties?: JsonSchema;
|
||||||
|
unevaluatedProperties?: JsonSchema;
|
||||||
|
patternProperties?: { [key: string]: JsonSchema };
|
||||||
|
minProperties?: number;
|
||||||
|
maxProperties?: number;
|
||||||
|
required?: string[] | boolean;
|
||||||
|
propertyNames?: JsonSchema;
|
||||||
|
|
||||||
|
// array
|
||||||
|
items?: JsonSchema | JsonSchema[];
|
||||||
|
additionalItems?: JsonSchema;
|
||||||
|
minItems?: number;
|
||||||
|
maxItems?: number;
|
||||||
|
uniqueItems?: boolean;
|
||||||
|
|
||||||
|
// string
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
pattern?: string;
|
||||||
|
format?: string;
|
||||||
|
|
||||||
|
// number
|
||||||
|
minimum?: number;
|
||||||
|
maximum?: number;
|
||||||
|
exclusiveMinimum?: number | boolean;
|
||||||
|
exclusiveMaximum?: number | boolean;
|
||||||
|
multipleOf?: number;
|
||||||
|
|
||||||
|
// unions
|
||||||
|
anyOf?: JsonSchema[];
|
||||||
|
allOf?: JsonSchema[];
|
||||||
|
oneOf?: JsonSchema[];
|
||||||
|
|
||||||
|
if?: JsonSchema;
|
||||||
|
then?: JsonSchema;
|
||||||
|
else?: JsonSchema;
|
||||||
|
|
||||||
|
// shared
|
||||||
|
const?: Serializable;
|
||||||
|
enum?: Serializable[];
|
||||||
|
|
||||||
|
errorMessage?: { [key: string]: string | undefined };
|
||||||
|
|
||||||
|
description?: string;
|
||||||
|
default?: Serializable;
|
||||||
|
readOnly?: boolean;
|
||||||
|
not?: JsonSchema;
|
||||||
|
contentEncoding?: string;
|
||||||
|
nullable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParserSelector = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny;
|
||||||
|
export type ParserOverride = (schema: JsonSchemaObject, refs: Refs) => ZodTypeAny | undefined;
|
||||||
|
|
||||||
|
export type JsonSchemaToZodOptions = {
|
||||||
|
withoutDefaults?: boolean;
|
||||||
|
withoutDescribes?: boolean;
|
||||||
|
parserOverride?: ParserOverride;
|
||||||
|
depth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Refs = JsonSchemaToZodOptions & {
|
||||||
|
path: Array<string | number>;
|
||||||
|
seen: Map<object | boolean, { n: number; r: ZodTypeAny | undefined }>;
|
||||||
|
};
|
23
packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts
Normal file
23
packages/@n8n/json-schema-to-zod/src/utils/extend-schema.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JsonSchemaObject } from '../types';
|
||||||
|
|
||||||
|
export function extendSchemaWithMessage<
|
||||||
|
TZod extends z.ZodTypeAny,
|
||||||
|
TJson extends JsonSchemaObject,
|
||||||
|
TKey extends keyof TJson,
|
||||||
|
>(
|
||||||
|
zodSchema: TZod,
|
||||||
|
jsonSchema: TJson,
|
||||||
|
key: TKey,
|
||||||
|
extend: (zodSchema: TZod, value: NonNullable<TJson[TKey]>, errorMessage?: string) => TZod,
|
||||||
|
) {
|
||||||
|
const value = jsonSchema[key];
|
||||||
|
|
||||||
|
if (value !== undefined) {
|
||||||
|
const errorMessage = jsonSchema.errorMessage?.[key as string];
|
||||||
|
return extend(zodSchema, value as NonNullable<TJson[TKey]>, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return zodSchema;
|
||||||
|
}
|
3
packages/@n8n/json-schema-to-zod/src/utils/half.ts
Normal file
3
packages/@n8n/json-schema-to-zod/src/utils/half.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const half = <T>(arr: T[]): [T[], T[]] => {
|
||||||
|
return [arr.slice(0, arr.length / 2), arr.slice(arr.length / 2)];
|
||||||
|
};
|
57
packages/@n8n/json-schema-to-zod/src/utils/its.ts
Normal file
57
packages/@n8n/json-schema-to-zod/src/utils/its.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import type { JsonSchema, JsonSchemaObject, Serializable } from '../types';
|
||||||
|
|
||||||
|
export const its = {
|
||||||
|
an: {
|
||||||
|
object: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'object' } =>
|
||||||
|
x.type === 'object',
|
||||||
|
array: (x: JsonSchemaObject): x is JsonSchemaObject & { type: 'array' } => x.type === 'array',
|
||||||
|
anyOf: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
anyOf: JsonSchema[];
|
||||||
|
} => x.anyOf !== undefined,
|
||||||
|
allOf: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
allOf: JsonSchema[];
|
||||||
|
} => x.allOf !== undefined,
|
||||||
|
enum: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
enum: Serializable | Serializable[];
|
||||||
|
} => x.enum !== undefined,
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
nullable: (x: JsonSchemaObject): x is JsonSchemaObject & { nullable: true } =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||||
|
(x as any).nullable === true,
|
||||||
|
multipleType: (x: JsonSchemaObject): x is JsonSchemaObject & { type: string[] } =>
|
||||||
|
Array.isArray(x.type),
|
||||||
|
not: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
not: JsonSchema;
|
||||||
|
} => x.not !== undefined,
|
||||||
|
const: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
const: Serializable;
|
||||||
|
} => x.const !== undefined,
|
||||||
|
primitive: <T extends 'string' | 'number' | 'integer' | 'boolean' | 'null'>(
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
p: T,
|
||||||
|
): x is JsonSchemaObject & { type: T } => x.type === p,
|
||||||
|
conditional: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
if: JsonSchema;
|
||||||
|
then: JsonSchema;
|
||||||
|
else: JsonSchema;
|
||||||
|
} => Boolean('if' in x && x.if && 'then' in x && 'else' in x && x.then && x.else),
|
||||||
|
oneOf: (
|
||||||
|
x: JsonSchemaObject,
|
||||||
|
): x is JsonSchemaObject & {
|
||||||
|
oneOf: JsonSchema[];
|
||||||
|
} => x.oneOf !== undefined,
|
||||||
|
},
|
||||||
|
};
|
8
packages/@n8n/json-schema-to-zod/src/utils/omit.ts
Normal file
8
packages/@n8n/json-schema-to-zod/src/utils/omit.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> =>
|
||||||
|
Object.keys(obj).reduce((acc: Record<string, unknown>, key) => {
|
||||||
|
if (!keys.includes(key as K)) {
|
||||||
|
acc[key] = obj[key as K];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}) as Omit<T, K>;
|
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
143
packages/@n8n/json-schema-to-zod/test/all.json
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
|
"properties": {
|
||||||
|
"allOf": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"anyOf": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"oneOf": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"array": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 3
|
||||||
|
},
|
||||||
|
"tuple": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 3
|
||||||
|
},
|
||||||
|
"const": {
|
||||||
|
"const": "xbox"
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"enum": ["ps4", "ps5"]
|
||||||
|
},
|
||||||
|
"ifThenElse": {
|
||||||
|
"if": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"const": "x"
|
||||||
|
},
|
||||||
|
"else": {
|
||||||
|
"enum": [1, 2, 3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"null": {
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
"multiple": {
|
||||||
|
"type": ["array", "boolean"]
|
||||||
|
},
|
||||||
|
"objAdditionalTrue": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"objAdditionalFalse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"objAdditionalNumber": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objAdditionalOnly": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patternProps": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^x": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"^y": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"z": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
16
packages/@n8n/json-schema-to-zod/test/extend-expect.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toMatchZod(this: jest.MatcherContext, actual: z.ZodTypeAny, expected: z.ZodTypeAny) {
|
||||||
|
const actualSerialized = JSON.stringify(actual._def, null, 2);
|
||||||
|
const expectedSerialized = JSON.stringify(expected._def, null, 2);
|
||||||
|
const pass = this.equals(actualSerialized, expectedSerialized);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: pass
|
||||||
|
? () => `Expected ${actualSerialized} not to match ${expectedSerialized}`
|
||||||
|
: () => `Expected ${actualSerialized} to match ${expectedSerialized}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
5
packages/@n8n/json-schema-to-zod/test/jest.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace jest {
|
||||||
|
interface Matchers<R, T> {
|
||||||
|
toMatchZod(expected: unknown): T;
|
||||||
|
}
|
||||||
|
}
|
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
106
packages/@n8n/json-schema-to-zod/test/json-schema-to-zod.test.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import type { JSONSchema4, JSONSchema6Definition, JSONSchema7Definition } from 'json-schema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { jsonSchemaToZod } from '../src';
|
||||||
|
|
||||||
|
describe('jsonSchemaToZod', () => {
|
||||||
|
test('should accept json schema 7 and 4', () => {
|
||||||
|
const schema = { type: 'string' } as unknown;
|
||||||
|
|
||||||
|
expect(jsonSchemaToZod(schema as JSONSchema4));
|
||||||
|
expect(jsonSchemaToZod(schema as JSONSchema6Definition));
|
||||||
|
expect(jsonSchemaToZod(schema as JSONSchema7Definition));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can exclude defaults', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod(
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
default: 'foo',
|
||||||
|
},
|
||||||
|
{ withoutDefaults: true },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.string());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should include describes', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod({
|
||||||
|
type: 'string',
|
||||||
|
description: 'foo',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().describe('foo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can exclude describes', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod(
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
description: 'foo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withoutDescribes: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchZod(z.string());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('will remove optionality if default is present', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prop: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'def',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.object({ prop: z.string().default('def') }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('will handle falsy defaults', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod({
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.boolean().default(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('will ignore undefined as default', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod({
|
||||||
|
type: 'null',
|
||||||
|
default: undefined,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.null());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be possible to define a custom parser', () => {
|
||||||
|
expect(
|
||||||
|
jsonSchemaToZod(
|
||||||
|
{
|
||||||
|
allOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean', description: 'foo' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parserOverride: (schema, refs) => {
|
||||||
|
if (
|
||||||
|
refs.path.length === 2 &&
|
||||||
|
refs.path[0] === 'allOf' &&
|
||||||
|
refs.path[1] === 2 &&
|
||||||
|
schema.type === 'boolean' &&
|
||||||
|
schema.description === 'foo'
|
||||||
|
) {
|
||||||
|
return z.null();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toMatchZod(z.intersection(z.string(), z.intersection(z.number(), z.null())));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseAllOf } from '../../src/parsers/parse-all-of';
|
||||||
|
|
||||||
|
describe('parseAllOf', () => {
|
||||||
|
test('should create never if empty', () => {
|
||||||
|
expect(
|
||||||
|
parseAllOf(
|
||||||
|
{
|
||||||
|
allOf: [],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.never());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle true values', () => {
|
||||||
|
expect(
|
||||||
|
parseAllOf(
|
||||||
|
{
|
||||||
|
allOf: [{ type: 'string' }, true],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.intersection(z.string(), z.any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle false values', () => {
|
||||||
|
expect(
|
||||||
|
parseAllOf(
|
||||||
|
{
|
||||||
|
allOf: [{ type: 'string' }, false],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.intersection(
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.any()
|
||||||
|
.refine(
|
||||||
|
(value) => !z.any().safeParse(value).success,
|
||||||
|
'Invalid input: Should NOT be valid against schema',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseAnyOf } from '../../src/parsers/parse-any-of';
|
||||||
|
|
||||||
|
describe('parseAnyOf', () => {
|
||||||
|
test('should create a union from two or more schemas', () => {
|
||||||
|
expect(
|
||||||
|
parseAnyOf(
|
||||||
|
{
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{ type: 'number' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.union([z.string(), z.number()]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract a single schema', () => {
|
||||||
|
expect(parseAnyOf({ anyOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||||
|
z.string(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return z.any() if array is empty', () => {
|
||||||
|
expect(parseAnyOf({ anyOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseArray } from '../../src/parsers/parse-array';
|
||||||
|
|
||||||
|
describe('parseArray', () => {
|
||||||
|
test('should create tuple with items array', () => {
|
||||||
|
expect(
|
||||||
|
parseArray(
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.tuple([z.string(), z.number()]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create array with items object', () => {
|
||||||
|
expect(
|
||||||
|
parseArray(
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.array(z.string()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create min for minItems', () => {
|
||||||
|
expect(
|
||||||
|
parseArray(
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
minItems: 2,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.array(z.string()).min(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create max for maxItems', () => {
|
||||||
|
expect(
|
||||||
|
parseArray(
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
maxItems: 2,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.array(z.string()).max(2));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseConst } from '../../src/parsers/parse-const';
|
||||||
|
|
||||||
|
describe('parseConst', () => {
|
||||||
|
test('should handle falsy constants', () => {
|
||||||
|
expect(
|
||||||
|
parseConst({
|
||||||
|
const: false,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.literal(false));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseEnum } from '../../src/parsers/parse-enum';
|
||||||
|
|
||||||
|
describe('parseEnum', () => {
|
||||||
|
test('should create never with empty enum', () => {
|
||||||
|
expect(
|
||||||
|
parseEnum({
|
||||||
|
enum: [],
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.never());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create literal with single item enum', () => {
|
||||||
|
expect(
|
||||||
|
parseEnum({
|
||||||
|
enum: ['someValue'],
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.literal('someValue'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create enum array with string enums', () => {
|
||||||
|
expect(
|
||||||
|
parseEnum({
|
||||||
|
enum: ['someValue', 'anotherValue'],
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.enum(['someValue', 'anotherValue']));
|
||||||
|
});
|
||||||
|
test('should create union with mixed enums', () => {
|
||||||
|
expect(
|
||||||
|
parseEnum({
|
||||||
|
enum: ['someValue', 57],
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.union([z.literal('someValue'), z.literal(57)]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseNot } from '../../src/parsers/parse-not';
|
||||||
|
|
||||||
|
describe('parseNot', () => {
|
||||||
|
test('parseNot', () => {
|
||||||
|
expect(
|
||||||
|
parseNot(
|
||||||
|
{
|
||||||
|
not: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z
|
||||||
|
.any()
|
||||||
|
.refine(
|
||||||
|
(value) => !z.string().safeParse(value).success,
|
||||||
|
'Invalid input: Should NOT be valid against schema',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||||
|
|
||||||
|
describe('parseNullable', () => {
|
||||||
|
test('parseSchema should not add default twice', () => {
|
||||||
|
expect(
|
||||||
|
parseSchema(
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.string().nullable().default(null));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseNumber } from '../../src/parsers/parse-number';
|
||||||
|
|
||||||
|
describe('parseNumber', () => {
|
||||||
|
test('should handle integer', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'integer',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().int());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'integer',
|
||||||
|
multipleOf: 1,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().int());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
multipleOf: 1,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().int());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle maximum with exclusiveMinimum', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
exclusiveMinimum: true,
|
||||||
|
minimum: 2,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().gt(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle maximum with exclusiveMinimum', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
minimum: 2,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().gte(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle maximum with exclusiveMaximum', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
exclusiveMaximum: true,
|
||||||
|
maximum: 2,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().lt(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle numeric exclusiveMaximum', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
exclusiveMaximum: 2,
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().lt(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept errorMessage', () => {
|
||||||
|
expect(
|
||||||
|
parseNumber({
|
||||||
|
type: 'number',
|
||||||
|
format: 'int64',
|
||||||
|
exclusiveMinimum: 0,
|
||||||
|
maximum: 2,
|
||||||
|
multipleOf: 2,
|
||||||
|
errorMessage: {
|
||||||
|
format: 'ayy',
|
||||||
|
multipleOf: 'lmao',
|
||||||
|
exclusiveMinimum: 'deez',
|
||||||
|
maximum: 'nuts',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.number().int('ayy').multipleOf(2, 'lmao').gt(0, 'deez').lte(2, 'nuts'));
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,904 @@
|
||||||
|
/* eslint-disable n8n-local-rules/no-skipped-tests */
|
||||||
|
import type { JSONSchema7 } from 'json-schema';
|
||||||
|
import { z, ZodError } from 'zod';
|
||||||
|
|
||||||
|
import { parseObject } from '../../src/parsers/parse-object';
|
||||||
|
|
||||||
|
describe('parseObject', () => {
|
||||||
|
test('should handle with missing properties', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.record(z.any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle with empty properties', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With properties - should handle optional and required properties', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['myRequiredString'],
|
||||||
|
properties: {
|
||||||
|
myOptionalString: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
myRequiredString: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.object({ myOptionalString: z.string().optional(), myRequiredString: z.string() }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With properties - should handle additionalProperties when set to false', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['myString'],
|
||||||
|
properties: {
|
||||||
|
myString: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({ myString: z.string() }).strict());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With properties - should handle additionalProperties when set to true', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['myString'],
|
||||||
|
properties: {
|
||||||
|
myString: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({ myString: z.string() }).catchall(z.any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('With properties - should handle additionalProperties when provided a schema', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['myString'],
|
||||||
|
properties: {
|
||||||
|
myString: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: { type: 'number' },
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({ myString: z.string() }).catchall(z.number()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Without properties - should handle additionalProperties when set to false', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.record(z.never()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Without properties - should handle additionalProperties when set to true', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.record(z.any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Without properties - should handle additionalProperties when provided a schema', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { type: 'number' },
|
||||||
|
},
|
||||||
|
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.record(z.number()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Without properties - should include falsy defaults', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
s: {
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({ s: z.string().default('') }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eh', () => {
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: ['c'],
|
||||||
|
properties: {
|
||||||
|
c: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z
|
||||||
|
.object({ a: z.string() })
|
||||||
|
.and(z.union([z.object({ b: z.string() }), z.object({ c: z.string() })])),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.object({ a: z.string() }).and(z.union([z.object({ b: z.string() }), z.any()])));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: ['c'],
|
||||||
|
properties: {
|
||||||
|
c: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.object({ a: z.string() }).and(
|
||||||
|
z.any().superRefine((x, ctx) => {
|
||||||
|
const schemas = [z.object({ b: z.string() }), z.object({ c: z.string() })];
|
||||||
|
const errors = schemas.reduce<z.ZodError[]>(
|
||||||
|
(errors, schema) =>
|
||||||
|
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||||
|
schema.safeParse(x),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
if (schemas.length - errors.length !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ctx.path,
|
||||||
|
code: 'invalid_union',
|
||||||
|
unionErrors: errors,
|
||||||
|
message: 'Invalid input: Should pass single schema',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.object({ a: z.string() }).and(
|
||||||
|
z.any().superRefine((x, ctx) => {
|
||||||
|
const schemas = [z.object({ b: z.string() }), z.any()];
|
||||||
|
const errors = schemas.reduce<z.ZodError[]>(
|
||||||
|
(errors, schema) =>
|
||||||
|
((result) => (result.error ? [...errors, result.error] : errors))(
|
||||||
|
schema.safeParse(x),
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
if (schemas.length - errors.length !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ctx.path,
|
||||||
|
code: 'invalid_union',
|
||||||
|
unionErrors: errors,
|
||||||
|
message: 'Invalid input: Should pass single schema',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
required: ['c'],
|
||||||
|
properties: {
|
||||||
|
c: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z
|
||||||
|
.object({ a: z.string() })
|
||||||
|
.and(z.intersection(z.object({ b: z.string() }), z.object({ c: z.string() }))),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseObject(
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allOf: [
|
||||||
|
{
|
||||||
|
required: ['b'],
|
||||||
|
properties: {
|
||||||
|
b: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.object({ a: z.string() }).and(z.intersection(z.object({ b: z.string() }), z.any())),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = (zodSchema: z.ZodTypeAny, data: unknown) => zodSchema.safeParse(data);
|
||||||
|
|
||||||
|
test('Functional tests - run', () => {
|
||||||
|
expect(run(z.string(), 'hello')).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: 'hello',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - properties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z.object({ a: z.string(), b: z.number().optional() });
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
|
||||||
|
expect(run(result, { a: 'hello' })).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
a: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(run(result, { a: 'hello', b: 123 })).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
a: 'hello',
|
||||||
|
b: 123,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(run(result, { b: 'hello', x: true })).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'string',
|
||||||
|
received: 'undefined',
|
||||||
|
path: ['a'],
|
||||||
|
message: 'Required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'number',
|
||||||
|
received: 'string',
|
||||||
|
path: ['b'],
|
||||||
|
message: 'Expected number, received string',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - properties and additionalProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: { type: 'boolean' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z.object({ a: z.string(), b: z.number().optional() }).catchall(z.boolean());
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
|
||||||
|
expect(run(result, { b: 'hello', x: 'true' })).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'string',
|
||||||
|
received: 'undefined',
|
||||||
|
path: ['a'],
|
||||||
|
message: 'Required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'number',
|
||||||
|
received: 'string',
|
||||||
|
path: ['b'],
|
||||||
|
message: 'Expected number, received string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'boolean',
|
||||||
|
received: 'string',
|
||||||
|
path: ['x'],
|
||||||
|
message: 'Expected boolean, received string',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - properties and single-item patternProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z
|
||||||
|
.object({ a: z.string(), b: z.number().optional() })
|
||||||
|
.catchall(z.array(z.any()))
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
if (key.match(new RegExp('\\\\.'))) {
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
|
||||||
|
expect(run(result, { a: 'a', b: 2, '.': [] })).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { a: 'a', b: 2, '.': [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(run(result, { a: 'a', b: 2, '.': '[]' })).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([
|
||||||
|
{
|
||||||
|
code: 'invalid_type',
|
||||||
|
expected: 'array',
|
||||||
|
received: 'string',
|
||||||
|
path: ['.'],
|
||||||
|
message: 'Expected array, received string',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - properties, additionalProperties and patternProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: { type: 'boolean' },
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
'\\,': { type: 'array', minItems: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z
|
||||||
|
.object({ a: z.string(), b: z.number().optional() })
|
||||||
|
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
let evaluated = ['a', 'b'].includes(key);
|
||||||
|
if (key.match(new RegExp('\\\\.'))) {
|
||||||
|
evaluated = true;
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.match(new RegExp('\\\\,'))) {
|
||||||
|
evaluated = true;
|
||||||
|
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!evaluated) {
|
||||||
|
const result = z.boolean().safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Invalid input: must match catchall schema',
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - additionalProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { type: 'boolean' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z.record(z.boolean());
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - additionalProperties and patternProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { type: 'boolean' },
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
'\\,': { type: 'array', minItems: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z
|
||||||
|
.record(z.union([z.array(z.any()), z.array(z.any()).min(1), z.boolean()]))
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
let evaluated = false;
|
||||||
|
if (key.match(new RegExp('\\\\.'))) {
|
||||||
|
evaluated = true;
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.match(new RegExp('\\\\,'))) {
|
||||||
|
evaluated = true;
|
||||||
|
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!evaluated) {
|
||||||
|
const result = z.boolean().safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Invalid input: must match catchall schema',
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
|
||||||
|
expect(run(result, { x: true, '.': [], ',': [] })).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([
|
||||||
|
{
|
||||||
|
path: [','],
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||||
|
params: {
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
code: 'too_small',
|
||||||
|
minimum: 1,
|
||||||
|
type: 'array',
|
||||||
|
inclusive: true,
|
||||||
|
exact: false,
|
||||||
|
message: 'Array must contain at least 1 element(s)',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - single-item patternProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z.record(z.array(z.any())).superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
if (key.match(new RegExp('\\\\.'))) {
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - patternProperties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
'\\,': { type: 'array', minItems: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z
|
||||||
|
.record(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
if (key.match(new RegExp('\\.'))) {
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.match(new RegExp('\\,'))) {
|
||||||
|
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(run(result, { '.': [] })).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { '.': [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(run(result, { ',': [] })).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: new ZodError([
|
||||||
|
{
|
||||||
|
path: [','],
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Invalid input: Key matching regex /,/ must match schema',
|
||||||
|
params: {
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
code: 'too_small',
|
||||||
|
minimum: 1,
|
||||||
|
type: 'array',
|
||||||
|
inclusive: true,
|
||||||
|
exact: false,
|
||||||
|
message: 'Array must contain at least 1 element(s)',
|
||||||
|
path: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Functional tests - patternProperties and properties', () => {
|
||||||
|
const schema: JSONSchema7 & { type: 'object' } = {
|
||||||
|
type: 'object',
|
||||||
|
required: ['a'],
|
||||||
|
properties: {
|
||||||
|
a: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
patternProperties: {
|
||||||
|
'\\.': { type: 'array' },
|
||||||
|
'\\,': { type: 'array', minItems: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = z
|
||||||
|
.object({ a: z.string(), b: z.number().optional() })
|
||||||
|
.catchall(z.union([z.array(z.any()), z.array(z.any()).min(1)]))
|
||||||
|
.superRefine((value, ctx) => {
|
||||||
|
for (const key in value) {
|
||||||
|
if (key.match(new RegExp('\\.'))) {
|
||||||
|
const result = z.array(z.any()).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key.match(new RegExp('\\,'))) {
|
||||||
|
const result = z.array(z.any()).min(1).safeParse(value[key]);
|
||||||
|
if (!result.success) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: [...ctx.path, key],
|
||||||
|
code: 'custom',
|
||||||
|
message: `Invalid input: Key matching regex /${key}/ must match schema`,
|
||||||
|
params: {
|
||||||
|
issues: result.error.issues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseObject(schema, { path: [], seen: new Map() });
|
||||||
|
|
||||||
|
expect(result).toMatchZod(expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseOneOf } from '../../src/parsers/parse-one-of';
|
||||||
|
|
||||||
|
describe('parseOneOf', () => {
|
||||||
|
test('should create a union from two or more schemas', () => {
|
||||||
|
expect(
|
||||||
|
parseOneOf(
|
||||||
|
{
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{ type: 'number' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(
|
||||||
|
z.any().superRefine((x, ctx) => {
|
||||||
|
const schemas = [z.string(), z.number()];
|
||||||
|
const errors = schemas.reduce<z.ZodError[]>(
|
||||||
|
(errors, schema) =>
|
||||||
|
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
if (schemas.length - errors.length !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ctx.path,
|
||||||
|
code: 'invalid_union',
|
||||||
|
unionErrors: errors,
|
||||||
|
message: 'Invalid input: Should pass single schema',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should extract a single schema', () => {
|
||||||
|
expect(parseOneOf({ oneOf: [{ type: 'string' }] }, { path: [], seen: new Map() })).toMatchZod(
|
||||||
|
z.string(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return z.any() if array is empty', () => {
|
||||||
|
expect(parseOneOf({ oneOf: [] }, { path: [], seen: new Map() })).toMatchZod(z.any());
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseSchema } from '../../src/parsers/parse-schema';
|
||||||
|
|
||||||
|
describe('parseSchema', () => {
|
||||||
|
test('should be usable without providing refs', () => {
|
||||||
|
expect(parseSchema({ type: 'string' })).toMatchZod(z.string());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return a seen and processed ref', () => {
|
||||||
|
const seen = new Map();
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
prop: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(parseSchema(schema, { seen, path: [] }));
|
||||||
|
expect(parseSchema(schema, { seen, path: [] }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be possible to describe a readonly schema', () => {
|
||||||
|
expect(parseSchema({ type: 'string', readOnly: true })).toMatchZod(z.string().readonly());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle nullable', () => {
|
||||||
|
expect(
|
||||||
|
parseSchema(
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
{ path: [], seen: new Map() },
|
||||||
|
),
|
||||||
|
).toMatchZod(z.string().nullable());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle enum', () => {
|
||||||
|
expect(parseSchema({ enum: ['someValue', 57] })).toMatchZod(
|
||||||
|
z.union([z.literal('someValue'), z.literal(57)]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple type', () => {
|
||||||
|
expect(parseSchema({ type: ['string', 'number'] })).toMatchZod(
|
||||||
|
z.union([z.string(), z.number()]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle if-then-else type', () => {
|
||||||
|
expect(
|
||||||
|
parseSchema({
|
||||||
|
if: { type: 'string' },
|
||||||
|
then: { type: 'number' },
|
||||||
|
else: { type: 'boolean' },
|
||||||
|
}),
|
||||||
|
).toMatchZod(
|
||||||
|
z.union([z.number(), z.boolean()]).superRefine((value, ctx) => {
|
||||||
|
const result = z.string().safeParse(value).success
|
||||||
|
? z.number().safeParse(value)
|
||||||
|
: z.boolean().safeParse(value);
|
||||||
|
if (!result.success) {
|
||||||
|
result.error.errors.forEach((error) => ctx.addIssue(error));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle anyOf', () => {
|
||||||
|
expect(
|
||||||
|
parseSchema({
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{ type: 'number' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.union([z.string(), z.number()]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle oneOf', () => {
|
||||||
|
expect(
|
||||||
|
parseSchema({
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{ type: 'number' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toMatchZod(
|
||||||
|
z.any().superRefine((x, ctx) => {
|
||||||
|
const schemas = [z.string(), z.number()];
|
||||||
|
const errors = schemas.reduce<z.ZodError[]>(
|
||||||
|
(errors, schema) =>
|
||||||
|
((result) => (result.error ? [...errors, result.error] : errors))(schema.safeParse(x)),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
if (schemas.length - errors.length !== 1) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ctx.path,
|
||||||
|
code: 'invalid_union',
|
||||||
|
unionErrors: errors,
|
||||||
|
message: 'Invalid input: Should pass single schema',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseString } from '../../src/parsers/parse-string';
|
||||||
|
|
||||||
|
describe('parseString', () => {
|
||||||
|
const run = (schema: z.ZodString, data: unknown) => schema.safeParse(data);
|
||||||
|
|
||||||
|
test('DateTime format', () => {
|
||||||
|
const datetime = '2018-11-13T20:20:39Z';
|
||||||
|
|
||||||
|
const code = parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
errorMessage: { format: 'hello' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(code).toMatchZod(z.string().datetime({ offset: true, message: 'hello' }));
|
||||||
|
|
||||||
|
expect(run(code, datetime)).toEqual({ success: true, data: datetime });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('email', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'email',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().email());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ip', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'ip',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().ip());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'ipv6',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().ip({ version: 'v6' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uri', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().url());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uuid', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().uuid());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('time', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'time',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().time());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('date', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'date',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().date());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('duration', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'duration',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().duration());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('base64', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
contentEncoding: 'base64',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().base64());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
contentEncoding: 'base64',
|
||||||
|
errorMessage: {
|
||||||
|
contentEncoding: 'x',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().base64('x'));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary',
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().base64());
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary',
|
||||||
|
errorMessage: {
|
||||||
|
format: 'x',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchZod(z.string().base64('x'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should accept errorMessage', () => {
|
||||||
|
expect(
|
||||||
|
parseString({
|
||||||
|
type: 'string',
|
||||||
|
format: 'ipv4',
|
||||||
|
pattern: 'x',
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 2,
|
||||||
|
errorMessage: {
|
||||||
|
format: 'ayy',
|
||||||
|
pattern: 'lmao',
|
||||||
|
minLength: 'deez',
|
||||||
|
maxLength: 'nuts',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toMatchZod(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.ip({ version: 'v4', message: 'ayy' })
|
||||||
|
.regex(new RegExp('x'), 'lmao')
|
||||||
|
.min(1, 'deez')
|
||||||
|
.max(2, 'nuts'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
15
packages/@n8n/json-schema-to-zod/test/utils/half.test.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { half } from '../../src/utils/half';
|
||||||
|
|
||||||
|
describe('half', () => {
|
||||||
|
test('half', () => {
|
||||||
|
const [a, b] = half(['A', 'B', 'C', 'D', 'E']);
|
||||||
|
|
||||||
|
if (1 < 0) {
|
||||||
|
// type should be string
|
||||||
|
a[0].endsWith('');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(a).toEqual(['A', 'B']);
|
||||||
|
expect(b).toEqual(['C', 'D', 'E']);
|
||||||
|
});
|
||||||
|
});
|
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
27
packages/@n8n/json-schema-to-zod/test/utils/omit.test.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { omit } from '../../src/utils/omit';
|
||||||
|
|
||||||
|
describe('omit', () => {
|
||||||
|
test('omit', () => {
|
||||||
|
const input = {
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
omit(
|
||||||
|
input,
|
||||||
|
'b',
|
||||||
|
// @ts-expect-error
|
||||||
|
'c',
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = omit(input, 'b');
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
output.b;
|
||||||
|
|
||||||
|
expect(output.a).toBe(true);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
expect(output.b).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
11
packages/@n8n/json-schema-to-zod/tsconfig.cjs.json
Normal file
11
packages/@n8n/json-schema-to-zod/tsconfig.cjs.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "dist/cjs",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
12
packages/@n8n/json-schema-to-zod/tsconfig.esm.json
Normal file
12
packages/@n8n/json-schema-to-zod/tsconfig.esm.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"module": "es2020",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "dist/esm",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
12
packages/@n8n/json-schema-to-zod/tsconfig.json
Normal file
12
packages/@n8n/json-schema-to-zod/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": ["../../../tsconfig.json"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"baseUrl": "src",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||||
|
}
|
11
packages/@n8n/json-schema-to-zod/tsconfig.types.json
Normal file
11
packages/@n8n/json-schema-to-zod/tsconfig.types.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "dist/types",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { promptTypeOptions, textInput } from '../../../utils/descriptions';
|
|
||||||
import { conversationalAgentProperties } from './agents/ConversationalAgent/description';
|
import { conversationalAgentProperties } from './agents/ConversationalAgent/description';
|
||||||
import { conversationalAgentExecute } from './agents/ConversationalAgent/execute';
|
import { conversationalAgentExecute } from './agents/ConversationalAgent/execute';
|
||||||
import { openAiFunctionsAgentProperties } from './agents/OpenAiFunctionsAgent/description';
|
import { openAiFunctionsAgentProperties } from './agents/OpenAiFunctionsAgent/description';
|
||||||
|
@ -21,6 +21,7 @@ import { sqlAgentAgentProperties } from './agents/SqlAgent/description';
|
||||||
import { sqlAgentAgentExecute } from './agents/SqlAgent/execute';
|
import { sqlAgentAgentExecute } from './agents/SqlAgent/execute';
|
||||||
import { toolsAgentProperties } from './agents/ToolsAgent/description';
|
import { toolsAgentProperties } from './agents/ToolsAgent/description';
|
||||||
import { toolsAgentExecute } from './agents/ToolsAgent/execute';
|
import { toolsAgentExecute } from './agents/ToolsAgent/execute';
|
||||||
|
import { promptTypeOptions, textInput } from '../../../utils/descriptions';
|
||||||
|
|
||||||
// Function used in the inputs expression to figure out which inputs to
|
// Function used in the inputs expression to figure out which inputs to
|
||||||
// display based on the agent type
|
// display based on the agent type
|
||||||
|
@ -351,6 +352,23 @@ export class Agent implements INodeType {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'For more reliable structured output parsing, consider using the Tools agent',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
hasOutputParser: [true],
|
||||||
|
agent: [
|
||||||
|
'conversationalAgent',
|
||||||
|
'reActAgent',
|
||||||
|
'planAndExecuteAgent',
|
||||||
|
'openAiFunctionsAgent',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Require Specific Output Format',
|
displayName: 'Require Specific Output Format',
|
||||||
name: 'hasOutputParser',
|
name: 'hasOutputParser',
|
||||||
|
@ -372,6 +390,7 @@ export class Agent implements INodeType {
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
hasOutputParser: [true],
|
hasOutputParser: [true],
|
||||||
|
agent: ['toolsAgent'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
|
||||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { initializeAgentExecutorWithOptions } from 'langchain/agents';
|
|
||||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||||
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
|
import { initializeAgentExecutorWithOptions } from 'langchain/agents';
|
||||||
import { CombiningOutputParser } from 'langchain/output_parsers';
|
import { CombiningOutputParser } from 'langchain/output_parsers';
|
||||||
|
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||||
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isChatInstance,
|
isChatInstance,
|
||||||
getPromptInputByType,
|
getPromptInputByType,
|
||||||
getOptionalOutputParsers,
|
|
||||||
getConnectedTools,
|
getConnectedTools,
|
||||||
} from '../../../../../utils/helpers';
|
} from '../../../../../utils/helpers';
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
|
||||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
|
||||||
export async function conversationalAgentExecute(
|
export async function conversationalAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
||||||
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
|
import type { AgentExecutorInput } from 'langchain/agents';
|
||||||
|
import { AgentExecutor, OpenAIAgent } from 'langchain/agents';
|
||||||
|
import { BufferMemory, type BaseChatMemory } from 'langchain/memory';
|
||||||
|
import { CombiningOutputParser } from 'langchain/output_parsers';
|
||||||
import {
|
import {
|
||||||
type IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
|
@ -5,18 +12,8 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { AgentExecutorInput } from 'langchain/agents';
|
import { getConnectedTools, getPromptInputByType } from '../../../../../utils/helpers';
|
||||||
import { AgentExecutor, OpenAIAgent } from 'langchain/agents';
|
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
|
||||||
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
|
||||||
import { CombiningOutputParser } from 'langchain/output_parsers';
|
|
||||||
import { BufferMemory, type BaseChatMemory } from 'langchain/memory';
|
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
|
||||||
import {
|
|
||||||
getConnectedTools,
|
|
||||||
getOptionalOutputParsers,
|
|
||||||
getPromptInputByType,
|
|
||||||
} from '../../../../../utils/helpers';
|
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
|
||||||
export async function openAiFunctionsAgentExecute(
|
export async function openAiFunctionsAgentExecute(
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
||||||
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
|
import { PlanAndExecuteAgentExecutor } from 'langchain/experimental/plan_and_execute';
|
||||||
|
import { CombiningOutputParser } from 'langchain/output_parsers';
|
||||||
import {
|
import {
|
||||||
type IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
|
@ -5,18 +10,10 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
import { getConnectedTools, getPromptInputByType } from '../../../../../utils/helpers';
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
|
||||||
import { CombiningOutputParser } from 'langchain/output_parsers';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import { PlanAndExecuteAgentExecutor } from 'langchain/experimental/plan_and_execute';
|
|
||||||
import {
|
|
||||||
getConnectedTools,
|
|
||||||
getOptionalOutputParsers,
|
|
||||||
getPromptInputByType,
|
|
||||||
} from '../../../../../utils/helpers';
|
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
|
||||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
|
||||||
export async function planAndExecuteAgentExecute(
|
export async function planAndExecuteAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
|
||||||
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
|
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
||||||
|
import { PromptTemplate } from '@langchain/core/prompts';
|
||||||
|
import { AgentExecutor, ChatAgent, ZeroShotAgent } from 'langchain/agents';
|
||||||
|
import { CombiningOutputParser } from 'langchain/output_parsers';
|
||||||
import {
|
import {
|
||||||
type IExecuteFunctions,
|
type IExecuteFunctions,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
|
@ -5,20 +11,14 @@ import {
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { AgentExecutor, ChatAgent, ZeroShotAgent } from 'langchain/agents';
|
|
||||||
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
|
|
||||||
import type { BaseOutputParser } from '@langchain/core/output_parsers';
|
|
||||||
import { PromptTemplate } from '@langchain/core/prompts';
|
|
||||||
import { CombiningOutputParser } from 'langchain/output_parsers';
|
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
||||||
import {
|
import {
|
||||||
getConnectedTools,
|
getConnectedTools,
|
||||||
getOptionalOutputParsers,
|
|
||||||
getPromptInputByType,
|
getPromptInputByType,
|
||||||
isChatInstance,
|
isChatInstance,
|
||||||
} from '../../../../../utils/helpers';
|
} from '../../../../../utils/helpers';
|
||||||
import { getTracingConfig } from '../../../../../utils/tracing';
|
import { getOptionalOutputParsers } from '../../../../../utils/output_parsers/N8nOutputParser';
|
||||||
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
import { throwIfToolSchema } from '../../../../../utils/schemaParsing';
|
||||||
|
import { getTracingConfig } from '../../../../../utils/tracing';
|
||||||
|
|
||||||
export async function reActAgentAgentExecute(
|
export async function reActAgentAgentExecute(
|
||||||
this: IExecuteFunctions,
|
this: IExecuteFunctions,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue