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 pay-36-store-execution-metadata
# Conflicts: # packages/cli/src/databases/migrations/mysqldb/index.ts # packages/cli/src/databases/migrations/postgresdb/index.ts # packages/cli/src/databases/migrations/sqlite/index.ts
This commit is contained in:
commit
a1be6795a1
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -1,3 +1,87 @@
|
||||||
|
## [0.217.1](https://github.com/n8n-io/n8n/compare/n8n@0.217.0...n8n@0.217.1) (2023-02-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Prevent executions from displaying as running forever ([#5563](https://github.com/n8n-io/n8n/issues/5563)) ([b30db10](https://github.com/n8n-io/n8n/commit/b30db10d898fa791d99d13192ef411cace4f7c05))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.217.0](https://github.com/n8n-io/n8n/compare/n8n@0.216.1...n8n@0.217.0) (2023-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Baserow Node:** Fix issue with get all not correctly using filters ([#5519](https://github.com/n8n-io/n8n/issues/5519)) ([ee21b7a](https://github.com/n8n-io/n8n/commit/ee21b7a1cfed17936eb6bf50221b7f9983dd38e6))
|
||||||
|
* **Compare Datasets Node:** UI tweaks and fixes ([7ecd5e5](https://github.com/n8n-io/n8n/commit/7ecd5e59eca01ca2b1a01e0a3e3871bd5d322eea))
|
||||||
|
* **core:** Do not allow arbitrary path traversal in BinaryDataManager ([#5523](https://github.com/n8n-io/n8n/issues/5523)) ([eef2574](https://github.com/n8n-io/n8n/commit/eef257406730a989ec8e7a056c3d4234300fdb25))
|
||||||
|
* **core:** Do not allow arbitrary path traversal in the credential-translation endpoint ([#5522](https://github.com/n8n-io/n8n/issues/5522)) ([f0f8d59](https://github.com/n8n-io/n8n/commit/f0f8d59fee223c6bc9f8459890ed4a31ef8cb0af))
|
||||||
|
* **core:** Do not explicitly bypass auth on urls containing `.svg` ([#5525](https://github.com/n8n-io/n8n/issues/5525)) ([f58573d](https://github.com/n8n-io/n8n/commit/f58573dba30eba8fe3d844d1b7b2dbbb8d51b8b5))
|
||||||
|
* **core:** Do not remove empty output connections arrays in PurgeInvalidWorkflowConnections migration ([#5546](https://github.com/n8n-io/n8n/issues/5546)) ([0fbb3f0](https://github.com/n8n-io/n8n/commit/0fbb3f0f026432f1aea87b106a0c1f732f93c792))
|
||||||
|
* **core:** Fix execution status filters ([#5533](https://github.com/n8n-io/n8n/issues/5533)) ([17eff4d](https://github.com/n8n-io/n8n/commit/17eff4d7d6692bfdc251bfa16ce7334858642ce5))
|
||||||
|
* **core:** User update endpoint should only allow updating email, firstName, and lastName ([#5526](https://github.com/n8n-io/n8n/issues/5526)) ([510855d](https://github.com/n8n-io/n8n/commit/510855d9581f07e5081a7bc11377cd6216ba7edf))
|
||||||
|
* **Discord Node:** Fix wrong error message being displayed ([#5547](https://github.com/n8n-io/n8n/issues/5547)) ([e251439](https://github.com/n8n-io/n8n/commit/e2514393335e555af47c9aca4f81b31608df9cb5))
|
||||||
|
* **Discourse Node:** Fix issue with credential test not working ([#5520](https://github.com/n8n-io/n8n/issues/5520)) ([b3e1793](https://github.com/n8n-io/n8n/commit/b3e1793ac0f304ea72d565097b6766bc278e1238))
|
||||||
|
* **editor:** Apply correct IRunExecutionData to finished workflow ([#5552](https://github.com/n8n-io/n8n/issues/5552)) ([e2d7c18](https://github.com/n8n-io/n8n/commit/e2d7c1804f2d5da15d96edeefd50c5b8e2753fd1))
|
||||||
|
* **editor:** Fix an issue with zoom and canvas nodes connections ([#5548](https://github.com/n8n-io/n8n/issues/5548)) ([4998ab2](https://github.com/n8n-io/n8n/commit/4998ab23508adf9a244885509b2d5c7c9c9c48f0))
|
||||||
|
* **editor:** Fix unexpected date rendering on front-end ([#5528](https://github.com/n8n-io/n8n/issues/5528)) ([684d717](https://github.com/n8n-io/n8n/commit/684d71752064e25143e09666e539b91b3dcd5f71))
|
||||||
|
* **editor:** Remove 'crashed' status from filter ([#5524](https://github.com/n8n-io/n8n/issues/5524)) ([7c517cb](https://github.com/n8n-io/n8n/commit/7c517cb5300481908dd653426089a6a9291e79ca))
|
||||||
|
* fix typo in error messages when a property does not exist ([#4310](https://github.com/n8n-io/n8n/issues/4310)) ([3af3db1](https://github.com/n8n-io/n8n/commit/3af3db160b5798fe948159b6f3dd48ec743512e7))
|
||||||
|
* Fixes an issue when saving an active workflow without triggers would cause n8n to be stuck ([#5513](https://github.com/n8n-io/n8n/issues/5513)) ([75a094a](https://github.com/n8n-io/n8n/commit/75a094a8c03afc40b7872cd2115d82e69455286e))
|
||||||
|
* **Google Calendar Node:** Fix incorrect labels for start and end times when getting all events ([#5529](https://github.com/n8n-io/n8n/issues/5529)) ([f965469](https://github.com/n8n-io/n8n/commit/f965469e13a45d3a7b796dfd6be44573bf8b13d0))
|
||||||
|
* **Postgres Node:** Fix for tables containing field named json ([5d74a2f](https://github.com/n8n-io/n8n/commit/5d74a2f89a31ee1a386a52d0d71858f73d734e31))
|
||||||
|
* **S3 Node:** Fix issue with get many buckets not outputting data ([#5514](https://github.com/n8n-io/n8n/issues/5514)) ([1c47677](https://github.com/n8n-io/n8n/commit/1c476770a778b7d034924db847a8757c383bd281))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add new event hooks ([#5530](https://github.com/n8n-io/n8n/issues/5530)) ([d47d008](https://github.com/n8n-io/n8n/commit/d47d0086cc2b0af5338598de1fc496b9d825f9a4))
|
||||||
|
* Add Required path name mapping to multiple nodes ([#5369](https://github.com/n8n-io/n8n/issues/5369)) ([f1589d4](https://github.com/n8n-io/n8n/commit/f1589d4f0f9f7cc7beec12d9f6598f8286484989))
|
||||||
|
* **core:** Add configurable execution history limit ([#5505](https://github.com/n8n-io/n8n/issues/5505)) ([db70293](https://github.com/n8n-io/n8n/commit/db702932f3f2b14a097e7f4364c06cbb4f001ebc))
|
||||||
|
* **core:** Add execution runData recovery and status field ([#5112](https://github.com/n8n-io/n8n/issues/5112)) ([d143f3f](https://github.com/n8n-io/n8n/commit/d143f3f2ec9ce42cfa4db2b41dab019b7b42f379))
|
||||||
|
* **core:** Add saml feature flag ([#5494](https://github.com/n8n-io/n8n/issues/5494)) ([3a9c257](https://github.com/n8n-io/n8n/commit/3a9c257f55a87890c7456601de13f182cec89fde))
|
||||||
|
* Deprecate Read Binary File node ([#5490](https://github.com/n8n-io/n8n/issues/5490)) ([11b4671](https://github.com/n8n-io/n8n/commit/11b467137e7652c03c0578654b19dbc157b23220))
|
||||||
|
* **editor:** Unify regular and trigger node creator panels ([#5315](https://github.com/n8n-io/n8n/issues/5315)) ([9a1e7b5](https://github.com/n8n-io/n8n/commit/9a1e7b52f7ce698f1492af15d0139fb015ba5d30))
|
||||||
|
* Hide sensitive value in Auth Header Credentials and Auth Query Credentials ([#5534](https://github.com/n8n-io/n8n/issues/5534)) ([4a209e1](https://github.com/n8n-io/n8n/commit/4a209e1dd98ea4b43d0a4d9cd688615cd6d4d5dd))
|
||||||
|
* Support feature flag evaluation server side ([#5511](https://github.com/n8n-io/n8n/issues/5511)) ([26a20ed](https://github.com/n8n-io/n8n/commit/26a20ed47e8f580504b80150d7550ecb9a49be0d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.216.2](https://github.com/n8n-io/n8n/compare/n8n@0.216.1...n8n@0.216.2) (2023-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Do not remove empty output connections arrays in PurgeInvalidWorkflowConnections migration ([#5546](https://github.com/n8n-io/n8n/issues/5546)) ([ac86abe](https://github.com/n8n-io/n8n/commit/ac86abe2457d9f54fcd23ac0c8d5f39d565bdcdf))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.215.3](https://github.com/n8n-io/n8n/compare/n8n@0.215.2...n8n@0.215.3) (2023-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Do not allow arbitrary path traversal in BinaryDataManager ([#5523](https://github.com/n8n-io/n8n/issues/5523)) ([f7079da](https://github.com/n8n-io/n8n/commit/f7079daecd210a3a2a94e07c4782d15ee2a995e0))
|
||||||
|
* **core:** Do not allow arbitrary path traversal in the credential-translation endpoint ([#5522](https://github.com/n8n-io/n8n/issues/5522)) ([14d2a88](https://github.com/n8n-io/n8n/commit/14d2a88120c966a6493c3a64a7a2925af0731b8f))
|
||||||
|
* **core:** Do not explicitly bypass auth on urls containing `.svg` ([#5525](https://github.com/n8n-io/n8n/issues/5525)) ([0b568ee](https://github.com/n8n-io/n8n/commit/0b568ee5c3d3259aaa43f757ded5583bf9d1e221))
|
||||||
|
* **core:** Do not remove empty output connections arrays in PurgeInvalidWorkflowConnections migration ([#5546](https://github.com/n8n-io/n8n/issues/5546)) ([a31cb05](https://github.com/n8n-io/n8n/commit/a31cb05fecb3c7fcb8f3def33206bb7676358561))
|
||||||
|
* **core:** User update endpoint should only allow updating email, firstName, and lastName ([#5526](https://github.com/n8n-io/n8n/issues/5526)) ([d530e20](https://github.com/n8n-io/n8n/commit/d530e20669e90e12a2d2895ae31d0018a53b817a))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.214.4](https://github.com/n8n-io/n8n/compare/n8n@0.214.3...n8n@0.214.4) (2023-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** Do not allow arbitrary path traversal in BinaryDataManager ([#5523](https://github.com/n8n-io/n8n/issues/5523)) ([df3f23e](https://github.com/n8n-io/n8n/commit/df3f23e2b8103a15632521e4ba6cf332693acf81))
|
||||||
|
* **core:** Do not allow arbitrary path traversal in the credential-translation endpoint ([#5522](https://github.com/n8n-io/n8n/issues/5522)) ([397e42d](https://github.com/n8n-io/n8n/commit/397e42d63e80577a0b897873a1d2f19533e27da7))
|
||||||
|
* **core:** Do not explicitly bypass auth on urls containing `.svg` ([#5525](https://github.com/n8n-io/n8n/issues/5525)) ([a8ca2b1](https://github.com/n8n-io/n8n/commit/a8ca2b1aea687256c7d7d3525a2c50659935d7b8))
|
||||||
|
* **core:** Do not remove empty output connections arrays in PurgeInvalidWorkflowConnections migration ([#5546](https://github.com/n8n-io/n8n/issues/5546)) ([e6a554f](https://github.com/n8n-io/n8n/commit/e6a554f884d0d8d1e5c3890745986ecc179846d5))
|
||||||
|
* **core:** User update endpoint should only allow updating email, firstName, and lastName ([#5526](https://github.com/n8n-io/n8n/issues/5526)) ([d622827](https://github.com/n8n-io/n8n/commit/d6228276a26d9dc1bf2b2c5452bc0644b6df0c63))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.216.1](https://github.com/n8n-io/n8n/compare/n8n@0.216.0...n8n@0.216.1) (2023-02-21)
|
## [0.216.1](https://github.com/n8n-io/n8n/compare/n8n@0.216.0...n8n@0.216.1) (2023-02-21)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,12 @@ const WorkflowPage = new WorkflowPageClass();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Undo/Redo', () => {
|
describe('Undo/Redo', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
});
|
});
|
||||||
|
@ -38,7 +41,11 @@ describe('Undo/Redo', () => {
|
||||||
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);
|
||||||
WorkflowPage.actions.addNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME, SET_NODE_NAME)
|
WorkflowPage.actions.addNodeBetweenNodes(
|
||||||
|
SCHEDULE_TRIGGER_NODE_NAME,
|
||||||
|
CODE_NODE_NAME,
|
||||||
|
SET_NODE_NAME,
|
||||||
|
);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||||
|
|
|
@ -21,9 +21,12 @@ const ZOOM_OUT_X2_FACTOR = 0.64;
|
||||||
const RENAME_NODE_NAME = 'Something else';
|
const RENAME_NODE_NAME = 'Something else';
|
||||||
|
|
||||||
describe('Canvas Actions', () => {
|
describe('Canvas Actions', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
});
|
});
|
||||||
|
@ -46,14 +49,16 @@ describe('Canvas Actions', () => {
|
||||||
// Change connection from Set to Set1
|
// Change connection from Set to Set1
|
||||||
cy.draganddrop(
|
cy.draganddrop(
|
||||||
WorkflowPage.getters.getEndpointSelector('input', SET_NODE_NAME),
|
WorkflowPage.getters.getEndpointSelector('input', SET_NODE_NAME),
|
||||||
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`)
|
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`),
|
||||||
)
|
);
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodeInputEndpointByName(`${SET_NODE_NAME}1`).should('have.class', 'jtk-endpoint-connected');
|
WorkflowPage.getters
|
||||||
|
.canvasNodeInputEndpointByName(`${SET_NODE_NAME}1`)
|
||||||
|
.should('have.class', 'jtk-endpoint-connected');
|
||||||
|
|
||||||
cy.get('.jtk-connector').should('have.length', 1);
|
cy.get('.jtk-connector').should('have.length', 1);
|
||||||
// Disconnect Set1
|
// Disconnect Set1
|
||||||
cy.drag(WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`), [-200, 100])
|
cy.drag(WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`), [-200, 100]);
|
||||||
cy.get('.jtk-connector').should('have.length', 0);
|
cy.get('.jtk-connector').should('have.length', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -67,7 +72,10 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, true);
|
||||||
|
|
||||||
cy.drag(WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME), [100, 100])
|
cy.drag(
|
||||||
|
WorkflowPage.getters.getEndpointSelector('plus', SCHEDULE_TRIGGER_NODE_NAME),
|
||||||
|
[100, 100],
|
||||||
|
);
|
||||||
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
WorkflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
||||||
|
@ -79,18 +87,20 @@ describe('Canvas Actions', () => {
|
||||||
|
|
||||||
// Switch has 4 output endpoints
|
// Switch has 4 output endpoints
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true })
|
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true });
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, false);
|
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME, false);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
}
|
}
|
||||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
cy.reload()
|
cy.reload();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
// Make sure all connections are there after reload
|
// Make sure all connections are there after reload
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
const setName = `${SET_NODE_NAME}${i > 0 ? i : ''}`;
|
const setName = `${SET_NODE_NAME}${i > 0 ? i : ''}`;
|
||||||
WorkflowPage.getters.canvasNodeInputEndpointByName(setName).should('have.class', 'jtk-endpoint-connected');
|
WorkflowPage.getters
|
||||||
|
.canvasNodeInputEndpointByName(setName)
|
||||||
|
.should('have.class', 'jtk-endpoint-connected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -109,26 +119,26 @@ describe('Canvas Actions', () => {
|
||||||
// Connect manual to Set1
|
// Connect manual to Set1
|
||||||
cy.draganddrop(
|
cy.draganddrop(
|
||||||
WorkflowPage.getters.getEndpointSelector('output', MANUAL_TRIGGER_NODE_DISPLAY_NAME),
|
WorkflowPage.getters.getEndpointSelector('output', MANUAL_TRIGGER_NODE_DISPLAY_NAME),
|
||||||
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`)
|
WorkflowPage.getters.getEndpointSelector('input', `${SET_NODE_NAME}1`),
|
||||||
)
|
);
|
||||||
|
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
|
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', SET_NODE_NAME),
|
WorkflowPage.getters.getEndpointSelector('plus', SET_NODE_NAME),
|
||||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0)
|
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 0),
|
||||||
)
|
);
|
||||||
cy.draganddrop(
|
cy.draganddrop(
|
||||||
WorkflowPage.getters.getEndpointSelector('plus', `${SET_NODE_NAME}1`),
|
WorkflowPage.getters.getEndpointSelector('plus', `${SET_NODE_NAME}1`),
|
||||||
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1)
|
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
|
||||||
)
|
);
|
||||||
|
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
|
||||||
|
@ -156,7 +166,7 @@ describe('Canvas Actions', () => {
|
||||||
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
|
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success');
|
||||||
cy.get('.jtk-connector.success').should('have.length', 3);
|
cy.get('.jtk-connector.success').should('have.length', 3);
|
||||||
cy.get('.jtk-connector').should('have.length', 4);
|
cy.get('.jtk-connector').should('have.length', 4);
|
||||||
})
|
});
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -4,15 +4,18 @@ const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Data pinning', () => {
|
describe('Data pinning', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should be able to pin node output', () => {
|
it('Should be able to pin node output', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true});
|
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true });
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
ndv.getters.pinDataButton().should('not.exist');
|
ndv.getters.pinDataButton().should('not.exist');
|
||||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||||
|
@ -43,7 +46,7 @@ describe('Data pinning', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should be be able to set pinned data', () => {
|
it('Should be be able to set pinned data', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true});
|
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger', { keepNdvOpen: true });
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
ndv.getters.pinDataButton().should('not.exist');
|
ndv.getters.pinDataButton().should('not.exist');
|
||||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||||
|
|
|
@ -4,15 +4,20 @@ const wf = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Data transformation expressions', () => {
|
describe('Data transformation expressions', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
wf.actions.visit();
|
wf.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
|
|
||||||
cy.window()
|
cy.window()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.then(win => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload));
|
.then(
|
||||||
|
(win) => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('$json + native string methods', () => {
|
it('$json + native string methods', () => {
|
||||||
|
@ -26,7 +31,7 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible')
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().contains(output);
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,7 +46,7 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible')
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().contains(output);
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible')
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().contains(output);
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,7 +76,7 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible')
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().contains(output);
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ describe('Data transformation expressions', () => {
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
ndv.getters.outputDataContainer().should('be.visible')
|
ndv.getters.outputDataContainer().should('be.visible');
|
||||||
ndv.getters.outputDataContainer().contains(output);
|
ndv.getters.outputDataContainer().contains(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,20 @@ const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Data mapping', () => {
|
describe('Data mapping', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
|
|
||||||
cy.window()
|
cy.window()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.then(win => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload))
|
.then(
|
||||||
|
(win) => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps expressions from table header', () => {
|
it('maps expressions from table header', () => {
|
||||||
|
@ -30,15 +35,29 @@ describe('Data mapping', () => {
|
||||||
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
||||||
|
|
||||||
ndv.getters.nodeParameters().find('input[placeholder*="Add Value"]').click();
|
ndv.getters.nodeParameters().find('input[placeholder*="Add Value"]').click();
|
||||||
ndv.getters.nodeParameters().find('.el-select-dropdown__list li:nth-child(3)').should('have.text', 'String').click();
|
ndv.getters
|
||||||
ndv.getters.parameterInput('name').should('have.length', 1).find('input').should('have.value', 'propertyName');
|
.nodeParameters()
|
||||||
ndv.getters.parameterInput('value').should('have.length', 1).find('input').should('have.value', '');
|
.find('.el-select-dropdown__list li:nth-child(3)')
|
||||||
|
.should('have.text', 'String')
|
||||||
|
.click();
|
||||||
|
ndv.getters
|
||||||
|
.parameterInput('name')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'propertyName');
|
||||||
|
ndv.getters
|
||||||
|
.parameterInput('value')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', '');
|
||||||
|
|
||||||
ndv.actions.mapDataFromHeader(1, 'value');
|
ndv.actions.mapDataFromHeader(1, 'value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
|
||||||
|
|
||||||
ndv.actions.mapDataFromHeader(2, 'value');
|
ndv.actions.mapDataFromHeader(2, 'value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }} {{ $json["Readable date"] }}');
|
ndv.getters
|
||||||
|
.inlineExpressionEditorInput()
|
||||||
|
.should('have.text', '{{ $json.timestamp }} {{ $json["Readable date"] }}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps expressions from table json, and resolves value based on hover', () => {
|
it('maps expressions from table json, and resolves value based on hover', () => {
|
||||||
|
@ -50,40 +69,56 @@ describe('Data mapping', () => {
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
||||||
|
|
||||||
ndv.getters.parameterInput('name').should('have.length', 1).find('input').should('have.value', 'other');
|
ndv.getters
|
||||||
ndv.getters.parameterInput('value').should('have.length', 1).find('input').should('have.value', '');
|
.parameterInput('name')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'other');
|
||||||
|
ndv.getters
|
||||||
|
.parameterInput('value')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', '');
|
||||||
|
|
||||||
ndv.getters.inputTbodyCell(1, 0).find('span').contains('count').trigger('mousedown', {force: true});
|
ndv.getters
|
||||||
|
.inputTbodyCell(1, 0)
|
||||||
|
.find('span')
|
||||||
|
.contains('count')
|
||||||
|
.trigger('mousedown', { force: true });
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '0')
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||||
|
|
||||||
ndv.getters.inputTbodyCell(1, 0).realHover();
|
ndv.getters.inputTbodyCell(1, 0).realHover();
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
.should('include.text', '0')
|
.should('include.text', '0')
|
||||||
.invoke('css', 'color')
|
.invoke('css', 'color')
|
||||||
.should('equal', 'rgb(125, 125, 135)');
|
.should('equal', 'rgb(125, 125, 135)');
|
||||||
|
|
||||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters
|
||||||
.should('include.text', '1')
|
.parameterExpressionPreview('value')
|
||||||
.invoke('css', 'color')
|
.should('include.text', '1')
|
||||||
.should('equal', 'rgb(125, 125, 135)');
|
.invoke('css', 'color')
|
||||||
|
.should('equal', 'rgb(125, 125, 135)');
|
||||||
|
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
|
|
||||||
ndv.getters.outputTbodyCell(1, 0).realHover();
|
ndv.getters.outputTbodyCell(1, 0).realHover();
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
.should('include.text', '0')
|
.should('include.text', '0')
|
||||||
.invoke('css', 'color')
|
.invoke('css', 'color')
|
||||||
.should('equal', 'rgb(125, 125, 135)'); // todo update color
|
.should('equal', 'rgb(125, 125, 135)'); // todo update color
|
||||||
|
|
||||||
ndv.getters.outputTbodyCell(2, 0).realHover();
|
ndv.getters.outputTbodyCell(2, 0).realHover();
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters
|
||||||
.should('include.text', '1')
|
.parameterExpressionPreview('value')
|
||||||
.invoke('css', 'color')
|
.should('include.text', '1')
|
||||||
.should('equal', 'rgb(125, 125, 135)');
|
.invoke('css', 'color')
|
||||||
|
.should('equal', 'rgb(125, 125, 135)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps expressions from json view', () => {
|
it('maps expressions from json view', () => {
|
||||||
|
@ -94,24 +129,34 @@ describe('Data mapping', () => {
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.switchInputMode('JSON');
|
ndv.actions.switchInputMode('JSON');
|
||||||
|
|
||||||
ndv.getters.inputDataContainer().should('exist').find('.json-data')
|
ndv.getters
|
||||||
.should('have.text', '[{"input":[{"count":0,"with space":"!!","with.dot":"!!","with"quotes":"!!"}]},{"input":[{"count":1}]}]')
|
.inputDataContainer()
|
||||||
.find('span').contains('"count"')
|
.should('exist')
|
||||||
|
.find('.json-data')
|
||||||
|
.should(
|
||||||
|
'have.text',
|
||||||
|
'[{"input":[{"count":0,"with space":"!!","with.dot":"!!","with"quotes":"!!"}]},{"input":[{"count":1}]}]',
|
||||||
|
)
|
||||||
|
.find('span')
|
||||||
|
.contains('"count"')
|
||||||
.realMouseDown();
|
.realMouseDown();
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||||
.should('include.text', '0');
|
|
||||||
|
|
||||||
ndv.getters.inputDataContainer().find('.json-data')
|
ndv.getters
|
||||||
.find('span').contains('"input"')
|
.inputDataContainer()
|
||||||
|
.find('.json-data')
|
||||||
|
.find('span')
|
||||||
|
.contains('"input"')
|
||||||
.realMouseDown();
|
.realMouseDown();
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
ndv.getters
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
.inlineExpressionEditorInput()
|
||||||
.should('include.text', '0 [object Object]');
|
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||||
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0 [object Object]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps expressions from schema view', () => {
|
it('maps expressions from schema view', () => {
|
||||||
|
@ -123,25 +168,19 @@ describe('Data mapping', () => {
|
||||||
ndv.actions.clearParameterInput('value');
|
ndv.actions.clearParameterInput('value');
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
ndv.getters.inputDataContainer()
|
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||||
.should('exist')
|
|
||||||
.find('span').contains('count')
|
|
||||||
.realMouseDown();
|
|
||||||
|
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
|
||||||
.should('include.text', '0');
|
|
||||||
|
|
||||||
ndv.getters.inputDataContainer()
|
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||||
.find('span').contains('input')
|
|
||||||
.realMouseDown();
|
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
ndv.getters
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
.inlineExpressionEditorInput()
|
||||||
.should('include.text', '0 [object Object]');
|
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||||
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0 [object Object]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps expressions from previous nodes', () => {
|
it('maps expressions from previous nodes', () => {
|
||||||
|
@ -150,32 +189,33 @@ describe('Data mapping', () => {
|
||||||
|
|
||||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
|
|
||||||
ndv.getters.inputDataContainer()
|
ndv.getters.inputDataContainer().find('span').contains('count').realMouseDown();
|
||||||
.find('span').contains('count')
|
|
||||||
.realMouseDown();
|
|
||||||
|
|
||||||
ndv.actions.mapToParameter('value');
|
ndv.actions.mapToParameter('value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }}`);
|
ndv.getters
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
.inlineExpressionEditorInput()
|
||||||
.should('not.exist');
|
.should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }}`);
|
||||||
|
ndv.getters.parameterExpressionPreview('value').should('not.exist');
|
||||||
|
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
ndv.actions.mapDataFromHeader(1, 'value');
|
ndv.actions.mapDataFromHeader(1, 'value');
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }} {{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input }}`);
|
ndv.getters
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
.inlineExpressionEditorInput()
|
||||||
.should('not.exist');
|
.should(
|
||||||
|
'have.text',
|
||||||
|
`{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }} {{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input }}`,
|
||||||
|
);
|
||||||
|
ndv.getters.parameterExpressionPreview('value').should('not.exist');
|
||||||
|
|
||||||
ndv.actions.selectInputNode('Set');
|
ndv.actions.selectInputNode('Set');
|
||||||
|
|
||||||
ndv.actions.executePrevious();
|
ndv.actions.executePrevious();
|
||||||
ndv.getters.executingLoader().should('not.exist');
|
ndv.getters.executingLoader().should('not.exist');
|
||||||
ndv.getters.inputDataContainer().should('exist');
|
ndv.getters.inputDataContainer().should('exist');
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '0 [object Object]');
|
||||||
.should('include.text', '0 [object Object]');
|
|
||||||
|
|
||||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||||
ndv.getters.parameterExpressionPreview('value')
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '1 [object Object]');
|
||||||
.should('include.text', '1 [object Object]');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps keys to path', () => {
|
it('maps keys to path', () => {
|
||||||
|
@ -186,20 +226,20 @@ describe('Data mapping', () => {
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
"hello.world": {
|
'hello.world': {
|
||||||
"my count": 0,
|
'my count': 0,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: [
|
input: [
|
||||||
{
|
{
|
||||||
"hello.world": {
|
'hello.world': {
|
||||||
"my count": 1,
|
'my count': 1,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -208,21 +248,18 @@ describe('Data mapping', () => {
|
||||||
workflowPage.actions.addNodeToCanvas('Item Lists');
|
workflowPage.actions.addNodeToCanvas('Item Lists');
|
||||||
workflowPage.actions.openNode('Item Lists');
|
workflowPage.actions.openNode('Item Lists');
|
||||||
|
|
||||||
ndv.getters.parameterInput('operation')
|
ndv.getters.parameterInput('operation').click().find('li').contains('Sort').click();
|
||||||
.click()
|
|
||||||
.find('li').contains('Sort')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click();
|
ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click();
|
||||||
|
|
||||||
ndv.getters.inputDataContainer()
|
ndv.getters.inputDataContainer().find('span').contains('my count').realMouseDown();
|
||||||
.find('span').contains('my count')
|
|
||||||
.realMouseDown();
|
|
||||||
|
|
||||||
ndv.actions.mapToParameter('fieldName');
|
ndv.actions.mapToParameter('fieldName');
|
||||||
|
|
||||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 0);
|
ndv.getters.inlineExpressionEditorInput().should('have.length', 0);
|
||||||
ndv.getters.parameterInput('fieldName')
|
ndv.getters
|
||||||
.find('input').should('have.value', 'input[0]["hello.world"]["my count"]');
|
.parameterInput('fieldName')
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'input[0]["hello.world"]["my count"]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Schedule Trigger node', async () => {
|
describe('Schedule Trigger node', async () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
});
|
});
|
||||||
|
@ -42,34 +42,44 @@ describe('Schedule Trigger node', async () => {
|
||||||
workflowPage.actions.activateWorkflow();
|
workflowPage.actions.activateWorkflow();
|
||||||
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
|
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
|
||||||
|
|
||||||
cy.request("GET", '/rest/workflows').then((response) => {
|
cy.request('GET', '/rest/workflows')
|
||||||
expect(response.status).to.eq(200);
|
.then((response) => {
|
||||||
expect(response.body.data).to.have.length(1);
|
|
||||||
const workflowId = response.body.data[0].id.toString();
|
|
||||||
expect(workflowId).to.not.be.empty;
|
|
||||||
return workflowId;
|
|
||||||
}).then((workflowId) => {
|
|
||||||
cy.wait(1200);
|
|
||||||
cy.request("GET", '/rest/executions').then((response) => {
|
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
expect(response.body.data.results.length).to.be.greaterThan(0);
|
expect(response.body.data).to.have.length(1);
|
||||||
const matchingExecutions = response.body.data.results.filter((execution: any) => execution.workflowId === workflowId);
|
const workflowId = response.body.data[0].id.toString();
|
||||||
expect(matchingExecutions).to.have.length(1);
|
expect(workflowId).to.not.be.empty;
|
||||||
return workflowId;
|
return workflowId;
|
||||||
}).then((workflowId) => {
|
})
|
||||||
|
.then((workflowId) => {
|
||||||
cy.wait(1200);
|
cy.wait(1200);
|
||||||
cy.request("GET", '/rest/executions').then((response) => {
|
cy.request('GET', '/rest/executions')
|
||||||
expect(response.status).to.eq(200);
|
.then((response) => {
|
||||||
expect(response.body.data.results.length).to.be.greaterThan(0);
|
expect(response.status).to.eq(200);
|
||||||
const matchingExecutions = response.body.data.results.filter((execution: any) => execution.workflowId === workflowId);
|
expect(response.body.data.results.length).to.be.greaterThan(0);
|
||||||
expect(matchingExecutions).to.have.length(2);
|
const matchingExecutions = response.body.data.results.filter(
|
||||||
}).then(()=>{
|
(execution: any) => execution.workflowId === workflowId,
|
||||||
workflowPage.actions.activateWorkflow();
|
);
|
||||||
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
|
expect(matchingExecutions).to.have.length(1);
|
||||||
cy.visit(workflowsPage.url);
|
return workflowId;
|
||||||
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
|
})
|
||||||
});
|
.then((workflowId) => {
|
||||||
|
cy.wait(1200);
|
||||||
|
cy.request('GET', '/rest/executions')
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
expect(response.body.data.results.length).to.be.greaterThan(0);
|
||||||
|
const matchingExecutions = response.body.data.results.filter(
|
||||||
|
(execution: any) => execution.workflowId === workflowId,
|
||||||
|
);
|
||||||
|
expect(matchingExecutions).to.have.length(2);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
workflowPage.actions.activateWorkflow();
|
||||||
|
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
|
||||||
|
cy.visit(workflowsPage.url);
|
||||||
|
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||||
respondWith,
|
respondWith,
|
||||||
responseData,
|
responseData,
|
||||||
executeNow = true,
|
executeNow = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||||
workflowPage.actions.openNode('Webhook');
|
workflowPage.actions.openNode('Webhook');
|
||||||
|
@ -47,43 +47,43 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||||
if (authentication) {
|
if (authentication) {
|
||||||
cy.getByTestId('parameter-input-authentication').click();
|
cy.getByTestId('parameter-input-authentication').click();
|
||||||
cy.getByTestId('parameter-input-authentication')
|
cy.getByTestId('parameter-input-authentication')
|
||||||
.find('.el-select-dropdown')
|
.find('.el-select-dropdown')
|
||||||
.find('.option-headline')
|
.find('.option-headline')
|
||||||
.contains(authentication)
|
.contains(authentication)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseCode) {
|
if (responseCode) {
|
||||||
cy.getByTestId('parameter-input-responseCode')
|
cy.getByTestId('parameter-input-responseCode')
|
||||||
.find('.parameter-input')
|
.find('.parameter-input')
|
||||||
.find('input')
|
.find('input')
|
||||||
.clear()
|
.clear()
|
||||||
.type(responseCode.toString());
|
.type(responseCode.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (respondWith) {
|
if (respondWith) {
|
||||||
cy.getByTestId('parameter-input-responseMode').click();
|
cy.getByTestId('parameter-input-responseMode').click();
|
||||||
cy.getByTestId('parameter-input-responseMode')
|
cy.getByTestId('parameter-input-responseMode')
|
||||||
.find('.el-select-dropdown')
|
.find('.el-select-dropdown')
|
||||||
.find('.option-headline')
|
.find('.option-headline')
|
||||||
.contains(respondWith)
|
.contains(respondWith)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseData) {
|
if (responseData) {
|
||||||
cy.getByTestId('parameter-input-responseData').click();
|
cy.getByTestId('parameter-input-responseData').click();
|
||||||
cy.getByTestId('parameter-input-responseData')
|
cy.getByTestId('parameter-input-responseData')
|
||||||
.find('.el-select-dropdown')
|
.find('.el-select-dropdown')
|
||||||
.find('.option-headline')
|
.find('.option-headline')
|
||||||
.contains(responseData)
|
.contains(responseData)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (executeNow) {
|
if (executeNow) {
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
cy.request(method, '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request(method, '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
ndv.getters.outputPanel().contains('headers');
|
ndv.getters.outputPanel().contains('headers');
|
||||||
});
|
});
|
||||||
|
@ -91,36 +91,41 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Webhook Trigger node', async () => {
|
describe('Webhook Trigger node', async () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
|
|
||||||
cy.window()
|
cy.window()
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.then(win => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload));
|
.then(
|
||||||
|
(win) => win.onBeforeUnload && win.removeEventListener('beforeunload', win.onBeforeUnload),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen for a GET request', () => {
|
it('should listen for a GET request', () => {
|
||||||
simpleWebhookCall({method: 'GET', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'GET', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen for a POST request', () => {
|
it('should listen for a POST request', () => {
|
||||||
simpleWebhookCall({method: 'POST', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'POST', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen for a DELETE request', () => {
|
it('should listen for a DELETE request', () => {
|
||||||
simpleWebhookCall({method: 'DELETE', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'DELETE', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
it('should listen for a HEAD request', () => {
|
it('should listen for a HEAD request', () => {
|
||||||
simpleWebhookCall({method: 'HEAD', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'HEAD', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
it('should listen for a PATCH request', () => {
|
it('should listen for a PATCH request', () => {
|
||||||
simpleWebhookCall({method: 'PATCH', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'PATCH', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
it('should listen for a PUT request', () => {
|
it('should listen for a PUT request', () => {
|
||||||
simpleWebhookCall({method: 'PUT', webhookPath: uuid(), executeNow: true});
|
simpleWebhookCall({ method: 'PUT', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen for a GET request and respond with Respond to Webhook node', () => {
|
it('should listen for a GET request and respond with Respond to Webhook node', () => {
|
||||||
|
@ -138,7 +143,10 @@ describe('Webhook Trigger node', async () => {
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
cy.get('.add-option').click();
|
cy.get('.add-option').click();
|
||||||
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click();
|
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click();
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('MyValue');
|
cy.get('.fixed-collection-parameter')
|
||||||
|
.getByTestId('parameter-input-name')
|
||||||
|
.clear()
|
||||||
|
.type('MyValue');
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
|
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
|
|
||||||
|
@ -147,7 +155,7 @@ describe('Webhook Trigger node', async () => {
|
||||||
workflowPage.actions.executeWorkflow();
|
workflowPage.actions.executeWorkflow();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request('GET', '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
expect(response.body.MyValue).to.eq(1234);
|
expect(response.body.MyValue).to.eq(1234);
|
||||||
});
|
});
|
||||||
|
@ -165,7 +173,7 @@ describe('Webhook Trigger node', async () => {
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request('GET', '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(201);
|
expect(response.status).to.eq(201);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -184,14 +192,17 @@ describe('Webhook Trigger node', async () => {
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
cy.get('.add-option').click();
|
cy.get('.add-option').click();
|
||||||
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click();
|
cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click();
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('MyValue');
|
cy.get('.fixed-collection-parameter')
|
||||||
|
.getByTestId('parameter-input-name')
|
||||||
|
.clear()
|
||||||
|
.type('MyValue');
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
|
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
|
|
||||||
workflowPage.actions.executeWorkflow();
|
workflowPage.actions.executeWorkflow();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request('GET', '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
expect(response.body.MyValue).to.eq(1234);
|
expect(response.body.MyValue).to.eq(1234);
|
||||||
});
|
});
|
||||||
|
@ -213,10 +224,14 @@ describe('Webhook Trigger node', async () => {
|
||||||
cy.get('.add-option').click();
|
cy.get('.add-option').click();
|
||||||
cy.get('.add-option').find('.el-select-dropdown__item').contains('String').click();
|
cy.get('.add-option').find('.el-select-dropdown__item').contains('String').click();
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data');
|
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data');
|
||||||
cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().find('input').invoke('val', cowBase64).trigger('blur');
|
cy.get('.fixed-collection-parameter')
|
||||||
|
.getByTestId('parameter-input-value')
|
||||||
|
.clear()
|
||||||
|
.find('input')
|
||||||
|
.invoke('val', cowBase64)
|
||||||
|
.trigger('blur');
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
|
|
||||||
|
|
||||||
workflowPage.actions.addNodeToCanvas('Move Binary Data');
|
workflowPage.actions.addNodeToCanvas('Move Binary Data');
|
||||||
workflowPage.actions.zoomToFit();
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
|
@ -232,7 +247,7 @@ describe('Webhook Trigger node', async () => {
|
||||||
workflowPage.actions.executeWorkflow();
|
workflowPage.actions.executeWorkflow();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request('GET', '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
expect(Object.keys(response.body).includes('data')).to.be.true;
|
expect(Object.keys(response.body).includes('data')).to.be.true;
|
||||||
});
|
});
|
||||||
|
@ -249,7 +264,7 @@ describe('Webhook Trigger node', async () => {
|
||||||
});
|
});
|
||||||
ndv.actions.execute();
|
ndv.actions.execute();
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => {
|
cy.request('GET', '/webhook-test/' + webhookPath).then((response) => {
|
||||||
expect(response.status).to.eq(200);
|
expect(response.status).to.eq(200);
|
||||||
expect(response.body.MyValue).to.be.undefined;
|
expect(response.body.MyValue).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
@ -273,28 +288,29 @@ describe('Webhook Trigger node', async () => {
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/webhook-test/'+ webhookPath,
|
url: '/webhook-test/' + webhookPath,
|
||||||
auth: {
|
auth: {
|
||||||
'user': 'username',
|
user: 'username',
|
||||||
'pass': 'password',
|
pass: 'password',
|
||||||
},
|
},
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.status).to.eq(403);
|
expect(response.status).to.eq(403);
|
||||||
}).then(() => {
|
})
|
||||||
cy.request({
|
.then(() => {
|
||||||
method: 'GET',
|
cy.request({
|
||||||
url: '/webhook-test/'+ webhookPath,
|
method: 'GET',
|
||||||
auth: {
|
url: '/webhook-test/' + webhookPath,
|
||||||
'user': 'test',
|
auth: {
|
||||||
'pass': 'test',
|
user: 'test',
|
||||||
},
|
pass: 'test',
|
||||||
failOnStatusCode: true,
|
},
|
||||||
}).then((response) => {
|
failOnStatusCode: true,
|
||||||
expect(response.status).to.eq(200);
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should listen for a GET request with Header Authentication', () => {
|
it('should listen for a GET request with Header Authentication', () => {
|
||||||
|
@ -315,25 +331,26 @@ describe('Webhook Trigger node', async () => {
|
||||||
cy.wait(waitForWebhook);
|
cy.wait(waitForWebhook);
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/webhook-test/'+ webhookPath,
|
url: '/webhook-test/' + webhookPath,
|
||||||
headers: {
|
headers: {
|
||||||
test: 'wrong',
|
test: 'wrong',
|
||||||
},
|
},
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
expect(response.status).to.eq(403);
|
expect(response.status).to.eq(403);
|
||||||
}).then(() => {
|
})
|
||||||
cy.request({
|
.then(() => {
|
||||||
method: 'GET',
|
cy.request({
|
||||||
url: '/webhook-test/'+ webhookPath,
|
method: 'GET',
|
||||||
headers: {
|
url: '/webhook-test/' + webhookPath,
|
||||||
test: 'test',
|
headers: {
|
||||||
},
|
test: 'test',
|
||||||
failOnStatusCode: true,
|
},
|
||||||
}).then((response) => {
|
failOnStatusCode: true,
|
||||||
expect(response.status).to.eq(200);
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,9 +5,12 @@ const wf = new WorkflowPage();
|
||||||
const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3'];
|
const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3'];
|
||||||
|
|
||||||
describe('Workflow tags', () => {
|
describe('Workflow tags', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
wf.actions.visit();
|
wf.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,8 +42,8 @@ const updatedPersonalData = {
|
||||||
newLastName: 'Else',
|
newLastName: 'Else',
|
||||||
newEmail: 'something_else@acme.corp',
|
newEmail: 'something_else@acme.corp',
|
||||||
newPassword: 'Keybo4rd',
|
newPassword: 'Keybo4rd',
|
||||||
invalidPasswords: ['abc', 'longEnough', 'longenough123']
|
invalidPasswords: ['abc', 'longEnough', 'longenough123'],
|
||||||
}
|
};
|
||||||
|
|
||||||
const usersSettingsPage = new SettingsUsersPage();
|
const usersSettingsPage = new SettingsUsersPage();
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
@ -67,7 +67,7 @@ describe('User Management', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should prevent non-owners to access UM settings', () => {
|
it('should prevent non-owners to access UM settings', () => {
|
||||||
usersSettingsPage.actions.loginAndVisit(users[0].email, users[0].password, false)
|
usersSettingsPage.actions.loginAndVisit(users[0].email, users[0].password, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow instance owner to access UM settings', () => {
|
it('should allow instance owner to access UM settings', () => {
|
||||||
|
@ -79,7 +79,10 @@ describe('User Management', () => {
|
||||||
// All items in user list should be there
|
// All items in user list should be there
|
||||||
usersSettingsPage.getters.userListItems().should('have.length', 3);
|
usersSettingsPage.getters.userListItems().should('have.length', 3);
|
||||||
// List item for current user should have the `Owner` badge
|
// List item for current user should have the `Owner` badge
|
||||||
usersSettingsPage.getters.userItem(instanceOwner.email).find('.n8n-badge:contains("Owner")').should('exist');
|
usersSettingsPage.getters
|
||||||
|
.userItem(instanceOwner.email)
|
||||||
|
.find('.n8n-badge:contains("Owner")')
|
||||||
|
.should('exist');
|
||||||
// Other users list items should contain action pop-up list
|
// Other users list items should contain action pop-up list
|
||||||
usersSettingsPage.getters.userActionsToggle(users[0].email).should('exist');
|
usersSettingsPage.getters.userActionsToggle(users[0].email).should('exist');
|
||||||
usersSettingsPage.getters.userActionsToggle(users[1].email).should('exist');
|
usersSettingsPage.getters.userActionsToggle(users[1].email).should('exist');
|
||||||
|
@ -106,8 +109,13 @@ describe('User Management', () => {
|
||||||
|
|
||||||
it(`should allow user to change their personal data`, () => {
|
it(`should allow user to change their personal data`, () => {
|
||||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
||||||
personalSettingsPage.actions.updateFirstAndLastName(updatedPersonalData.newFirstName, updatedPersonalData.newLastName);
|
personalSettingsPage.actions.updateFirstAndLastName(
|
||||||
personalSettingsPage.getters.currentUserName().should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
updatedPersonalData.newFirstName,
|
||||||
|
updatedPersonalData.newLastName,
|
||||||
|
);
|
||||||
|
personalSettingsPage.getters
|
||||||
|
.currentUserName()
|
||||||
|
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
||||||
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,18 +129,30 @@ describe('User Management', () => {
|
||||||
it(`shouldn't allow user to change password if old password is wrong`, () => {
|
it(`shouldn't allow user to change password if old password is wrong`, () => {
|
||||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
||||||
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
|
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
|
||||||
workflowPage.getters.errorToast().closest('div').should('contain', 'Provided current password is incorrect.');
|
workflowPage.getters
|
||||||
|
.errorToast()
|
||||||
|
.closest('div')
|
||||||
|
.should('contain', 'Provided current password is incorrect.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should change current user password`, () => {
|
it(`should change current user password`, () => {
|
||||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
||||||
personalSettingsPage.actions.updatePassword(instanceOwner.password, updatedPersonalData.newPassword);
|
personalSettingsPage.actions.updatePassword(
|
||||||
|
instanceOwner.password,
|
||||||
|
updatedPersonalData.newPassword,
|
||||||
|
);
|
||||||
workflowPage.getters.successToast().should('contain', 'Password updated');
|
workflowPage.getters.successToast().should('contain', 'Password updated');
|
||||||
personalSettingsPage.actions.loginWithNewData(instanceOwner.email, updatedPersonalData.newPassword);
|
personalSettingsPage.actions.loginWithNewData(
|
||||||
|
instanceOwner.email,
|
||||||
|
updatedPersonalData.newPassword,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`shouldn't allow users to set invalid email`, () => {
|
it(`shouldn't allow users to set invalid email`, () => {
|
||||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, updatedPersonalData.newPassword);
|
personalSettingsPage.actions.loginAndVisit(
|
||||||
|
instanceOwner.email,
|
||||||
|
updatedPersonalData.newPassword,
|
||||||
|
);
|
||||||
// try without @ part
|
// try without @ part
|
||||||
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('@')[0]);
|
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('@')[0]);
|
||||||
// try without domain
|
// try without domain
|
||||||
|
@ -140,9 +160,15 @@ describe('User Management', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should change user email`, () => {
|
it(`should change user email`, () => {
|
||||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, updatedPersonalData.newPassword);
|
personalSettingsPage.actions.loginAndVisit(
|
||||||
|
instanceOwner.email,
|
||||||
|
updatedPersonalData.newPassword,
|
||||||
|
);
|
||||||
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
|
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
|
||||||
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
workflowPage.getters.successToast().should('contain', 'Personal details updated');
|
||||||
personalSettingsPage.actions.loginWithNewData(updatedPersonalData.newEmail, updatedPersonalData.newPassword);
|
personalSettingsPage.actions.loginWithNewData(
|
||||||
|
updatedPersonalData.newEmail,
|
||||||
|
updatedPersonalData.newPassword,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,10 +5,13 @@ const workflowsPage = new WorkflowsPage();
|
||||||
const workflowPage = new WorkflowPageClass();
|
const workflowPage = new WorkflowPageClass();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Execution',() => {
|
describe('Execution', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,17 +37,36 @@ describe('Execution',() => {
|
||||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
||||||
|
|
||||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||||
workflowPage.getters.canvasNodeByName('Manual').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check').should('not.exist'));
|
.canvasNodeByName('Manual')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
|
|
||||||
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
||||||
workflowPage.getters.canvasNodeByName('Manual').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
.canvasNodeByName('Manual')
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
|
@ -77,20 +99,39 @@ describe('Execution',() => {
|
||||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
||||||
|
|
||||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||||
workflowPage.getters.canvasNodeByName('Manual').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check').should('not.exist'));
|
.canvasNodeByName('Manual')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
workflowPage.getters.stopExecutionButton().click();
|
workflowPage.getters.stopExecutionButton().click();
|
||||||
|
|
||||||
// Check canvas nodes after workflow stopped
|
// Check canvas nodes after workflow stopped
|
||||||
workflowPage.getters.canvasNodeByName('Manual').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
.canvasNodeByName('Manual')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
|
@ -140,17 +181,36 @@ describe('Execution',() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||||
workflowPage.getters.canvasNodeByName('Webhook').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check').should('not.exist'));
|
.canvasNodeByName('Webhook')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
cy.wait(2000);
|
cy.wait(2000);
|
||||||
|
|
||||||
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
// Check canvas nodes after 2nd step (waiting node finished its execution and the http request node is about to start)
|
||||||
workflowPage.getters.canvasNodeByName('Webhook').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
.canvasNodeByName('Webhook')
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
|
@ -200,19 +260,39 @@ describe('Execution',() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||||
workflowPage.getters.canvasNodeByName('Webhook').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check').should('not.exist'));
|
.canvasNodeByName('Webhook')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt')).should('be.visible');
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().click();
|
workflowPage.getters.stopExecutionWaitingForWebhookButton().click();
|
||||||
|
|
||||||
// Check canvas nodes after workflow stopped
|
// Check canvas nodes after workflow stopped
|
||||||
workflowPage.getters.canvasNodeByName('Webhook').within(() => cy.get('.fa-check')).should('be.visible');
|
workflowPage.getters
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
.canvasNodeByName('Webhook')
|
||||||
workflowPage.getters.canvasNodeByName('Wait').within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
.within(() => cy.get('.fa-check'))
|
||||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check').should('not.exist'));
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-check'))
|
||||||
|
.should('be.visible');
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Wait')
|
||||||
|
.within(() => cy.get('.fa-sync-alt').should('not.visible'));
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('Set')
|
||||||
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
|
|
|
@ -259,7 +259,7 @@ describe('Credentials', () => {
|
||||||
cy.contains('Create New Credential').click();
|
cy.contains('Create New Credential').click();
|
||||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should render custom node with custom credential', () => {
|
it('should render custom node with custom credential', () => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
|
@ -269,5 +269,5 @@ describe('Credentials', () => {
|
||||||
cy.contains('Create New Credential').click();
|
cy.contains('Create New Credential').click();
|
||||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { WorkflowPage } from "../pages";
|
import { WorkflowPage } from '../pages';
|
||||||
import { WorkflowExecutionsTab } from "../pages/workflow-executions-tab";
|
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const executionsTab = new WorkflowExecutionsTab();
|
const executionsTab = new WorkflowExecutionsTab();
|
||||||
|
@ -9,6 +9,9 @@ describe('Current Workflow Executions', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
|
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
|
||||||
|
@ -20,12 +23,14 @@ describe('Current Workflow Executions', () => {
|
||||||
executionsTab.getters.executionListItems().should('have.length', 11);
|
executionsTab.getters.executionListItems().should('have.length', 11);
|
||||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||||
executionsTab.getters.executionListItems().first().invoke('attr','class').should('match', /_active_/);
|
executionsTab.getters
|
||||||
|
.executionListItems()
|
||||||
|
.first()
|
||||||
|
.invoke('attr', 'class')
|
||||||
|
.should('match', /_active_/);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const createMockExecutions = () => {
|
const createMockExecutions = () => {
|
||||||
workflowPage.actions.turnOnManualExecutionSaving();
|
workflowPage.actions.turnOnManualExecutionSaving();
|
||||||
executionsTab.actions.createManualExecutions(5);
|
executionsTab.actions.createManualExecutions(5);
|
||||||
|
@ -37,4 +42,4 @@ const createMockExecutions = () => {
|
||||||
executionsTab.actions.createManualExecutions(4);
|
executionsTab.actions.createManualExecutions(4);
|
||||||
executionsTab.actions.switchToExecutionsTab();
|
executionsTab.actions.switchToExecutionsTab();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
}
|
};
|
||||||
|
|
|
@ -28,11 +28,7 @@ describe('Node Creator', () => {
|
||||||
it('should open node creator on trigger tab if no trigger is on canvas', () => {
|
it('should open node creator on trigger tab if no trigger is on canvas', () => {
|
||||||
nodeCreatorFeature.getters.canvasAddButton().click();
|
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||||
|
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
||||||
.nodeCreator()
|
|
||||||
.contains('Select a trigger')
|
|
||||||
.should('be.visible');
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate subcategory', () => {
|
it('should navigate subcategory', () => {
|
||||||
|
@ -86,20 +82,14 @@ describe('Node Creator', () => {
|
||||||
|
|
||||||
// TODO: Replace once we have canvas feature utils
|
// TODO: Replace once we have canvas feature utils
|
||||||
cy.get('div').contains('Add first step').should('be.hidden');
|
cy.get('div').contains('Add first step').should('be.hidden');
|
||||||
nodeCreatorFeature.actions.openNodeCreator()
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
|
||||||
.nodeCreator()
|
|
||||||
.contains('What happens next?')
|
|
||||||
.should('be.visible');
|
|
||||||
|
|
||||||
nodeCreatorFeature.getters.getCreatorItem('Add another trigger').click();
|
nodeCreatorFeature.getters.getCreatorItem('Add another trigger').click();
|
||||||
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
||||||
nodeCreatorFeature.getters.activeSubcategory().find('button').should('exist');
|
nodeCreatorFeature.getters.activeSubcategory().find('button').should('exist');
|
||||||
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
||||||
nodeCreatorFeature.getters
|
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
|
||||||
.nodeCreator()
|
|
||||||
.contains('What happens next?')
|
|
||||||
.should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add node to canvas from actions panel', () => {
|
it('should add node to canvas from actions panel', () => {
|
||||||
|
@ -110,7 +100,7 @@ describe('Node Creator', () => {
|
||||||
nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode);
|
nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode);
|
||||||
nodeCreatorFeature.getters.getCreatorItem('Crop Image').click();
|
nodeCreatorFeature.getters.getCreatorItem('Crop Image').click();
|
||||||
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop');
|
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop');
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should search through actions and confirm added action', () => {
|
it('should search through actions and confirm added action', () => {
|
||||||
nodeCreatorFeature.actions.openNodeCreator();
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
|
|
|
@ -6,16 +6,17 @@ const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('NDV', () => {
|
describe('NDV', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
workflowsPage.actions.createWorkflowFromCard();
|
workflowsPage.actions.createWorkflowFromCard();
|
||||||
workflowPage.actions.renameWorkflow(uuid());
|
workflowPage.actions.renameWorkflow(uuid());
|
||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
|
it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
workflowPage.getters.canvasNodes().first().dblclick();
|
||||||
|
|
|
@ -5,7 +5,7 @@ const WorkflowPage = new WorkflowPageClass();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('Code node', () => {
|
describe('Code node', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,9 +14,12 @@ const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
|
||||||
describe('Workflow Actions', () => {
|
describe('Workflow Actions', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
|
||||||
describe('HTTP Request node', () => {
|
describe('HTTP Request node', () => {
|
||||||
beforeEach(() => {
|
before(() => {
|
||||||
cy.resetAll();
|
cy.resetAll();
|
||||||
cy.skipSetup();
|
cy.skipSetup();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.216.1",
|
"version": "0.217.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.216.1",
|
"version": "0.217.1",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -76,8 +76,8 @@
|
||||||
"@types/json-diff": "^0.5.1",
|
"@types/json-diff": "^0.5.1",
|
||||||
"@types/jsonwebtoken": "^9.0.1",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/localtunnel": "^1.9.0",
|
"@types/localtunnel": "^1.9.0",
|
||||||
"@types/lodash.get": "^4.4.6",
|
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
|
"@types/lodash.get": "^4.4.6",
|
||||||
"@types/lodash.intersection": "^4.4.7",
|
"@types/lodash.intersection": "^4.4.7",
|
||||||
"@types/lodash.iteratee": "^4.7.7",
|
"@types/lodash.iteratee": "^4.7.7",
|
||||||
"@types/lodash.merge": "^4.6.6",
|
"@types/lodash.merge": "^4.6.6",
|
||||||
|
@ -191,6 +191,7 @@
|
||||||
"psl": "^1.8.0",
|
"psl": "^1.8.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"replacestream": "^4.0.3",
|
"replacestream": "^4.0.3",
|
||||||
|
"samlify": "^2.8.9",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"shelljs": "^0.8.5",
|
"shelljs": "^0.8.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
|
|
|
@ -190,7 +190,7 @@ export function send<T, R extends Request, S extends Response>(
|
||||||
try {
|
try {
|
||||||
const data = await processFunction(req, res);
|
const data = await processFunction(req, res);
|
||||||
|
|
||||||
sendSuccessResponse(res, data, raw);
|
if (!res.headersSent) sendSuccessResponse(res, data, raw);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
if (!(error instanceof ResponseError) || error.httpStatusCode > 404) {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { getLicense } from '../License';
|
|
||||||
import { isUserManagementEnabled } from '../UserManagement/UserManagementHelper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the SAML feature is licensed and enabled in the instance
|
|
||||||
*/
|
|
||||||
export function isSamlEnabled(): boolean {
|
|
||||||
const license = getLicense();
|
|
||||||
return isUserManagementEnabled() && license.isSamlEnabled();
|
|
||||||
}
|
|
|
@ -142,10 +142,13 @@ import { setupBasicAuth } from './middlewares/basicAuth';
|
||||||
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
|
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
|
||||||
import { PostHogClient } from './posthog';
|
import { PostHogClient } from './posthog';
|
||||||
import { eventBus } from './eventbus';
|
import { eventBus } from './eventbus';
|
||||||
import { isSamlEnabled } from './Saml/helpers';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { InternalHooks } from './InternalHooks';
|
import { InternalHooks } from './InternalHooks';
|
||||||
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
|
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
|
||||||
|
import { isSamlLicensed } from './sso/saml/samlHelpers';
|
||||||
|
import { samlControllerPublic } from './sso/saml/routes/saml.controller.public.ee';
|
||||||
|
import { SamlService } from './sso/saml/saml.service.ee';
|
||||||
|
import { samlControllerProtected } from './sso/saml/routes/saml.controller.protected.ee';
|
||||||
|
|
||||||
const exec = promisify(callbackExec);
|
const exec = promisify(callbackExec);
|
||||||
|
|
||||||
|
@ -318,7 +321,7 @@ class Server extends AbstractServer {
|
||||||
sharing: isSharingEnabled(),
|
sharing: isSharingEnabled(),
|
||||||
logStreaming: isLogStreamingEnabled(),
|
logStreaming: isLogStreamingEnabled(),
|
||||||
ldap: isLdapEnabled(),
|
ldap: isLdapEnabled(),
|
||||||
saml: isSamlEnabled(),
|
saml: isSamlLicensed(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLdapEnabled()) {
|
if (isLdapEnabled()) {
|
||||||
|
@ -495,6 +498,19 @@ class Server extends AbstractServer {
|
||||||
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
this.app.use(`/${this.restEndpoint}/ldap`, ldapController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// SAML
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// initialize SamlService
|
||||||
|
await SamlService.getInstance().init();
|
||||||
|
|
||||||
|
// public SAML endpoints
|
||||||
|
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerPublic);
|
||||||
|
this.app.use(`/${this.restEndpoint}/sso/saml`, samlControllerProtected);
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
// Returns parameter values which normally get loaded from an external API or
|
// Returns parameter values which normally get loaded from an external API or
|
||||||
// get generated dynamically
|
// get generated dynamically
|
||||||
this.app.get(
|
this.app.get(
|
||||||
|
|
|
@ -44,7 +44,7 @@ import {
|
||||||
|
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
import { LessThanOrEqual } from 'typeorm';
|
import { LessThanOrEqual, In } from 'typeorm';
|
||||||
import { DateUtils } from 'typeorm/util/DateUtils';
|
import { DateUtils } from 'typeorm/util/DateUtils';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
|
@ -212,7 +212,9 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
|
const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
|
||||||
|
|
||||||
const toPrune: FindOptionsWhere<IExecutionFlattedDb> = { stoppedAt: LessThanOrEqual(utcDate) };
|
const toPrune: Array<FindOptionsWhere<IExecutionFlattedDb>> = [
|
||||||
|
{ stoppedAt: LessThanOrEqual(utcDate) },
|
||||||
|
];
|
||||||
|
|
||||||
if (maxCount > 0) {
|
if (maxCount > 0) {
|
||||||
const executions = await Db.collections.Execution.find({
|
const executions = await Db.collections.Execution.find({
|
||||||
|
@ -223,27 +225,29 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (executions[0]) {
|
if (executions[0]) {
|
||||||
toPrune.id = LessThanOrEqual(executions[0].id);
|
toPrune.push({ id: LessThanOrEqual(executions[0].id) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBinaryModeDefaultMode = config.getEnv('binaryDataManager.mode') === 'default';
|
const isBinaryModeDefaultMode = config.getEnv('binaryDataManager.mode') === 'default';
|
||||||
try {
|
try {
|
||||||
const executions = isBinaryModeDefaultMode
|
|
||||||
? []
|
|
||||||
: await Db.collections.Execution.find({
|
|
||||||
select: ['id'],
|
|
||||||
where: toPrune,
|
|
||||||
});
|
|
||||||
await Db.collections.Execution.delete(toPrune);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
throttling = false;
|
throttling = false;
|
||||||
}, timeout * 1000);
|
}, timeout * 1000);
|
||||||
// Mark binary data for deletion for all executions
|
let executionIds: Array<IExecutionFlattedDb['id']>;
|
||||||
if (!isBinaryModeDefaultMode)
|
do {
|
||||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds(
|
executionIds = (
|
||||||
executions.map(({ id }) => id),
|
await Db.collections.Execution.find({
|
||||||
);
|
select: ['id'],
|
||||||
|
where: toPrune,
|
||||||
|
take: 100,
|
||||||
|
})
|
||||||
|
).map(({ id }) => id);
|
||||||
|
await Db.collections.Execution.delete({ id: In(executionIds) });
|
||||||
|
// Mark binary data for deletion for all executions
|
||||||
|
if (!isBinaryModeDefaultMode)
|
||||||
|
await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds(executionIds);
|
||||||
|
} while (executionIds.length > 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorReporter.error(error);
|
ErrorReporter.error(error);
|
||||||
throttling = false;
|
throttling = false;
|
||||||
|
@ -472,7 +476,6 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
||||||
fullExecutionData.status = 'running';
|
fullExecutionData.status = 'running';
|
||||||
|
|
||||||
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||||
|
|
||||||
await Db.collections.Execution.update(
|
await Db.collections.Execution.update(
|
||||||
this.executionId,
|
this.executionId,
|
||||||
flattenedExecutionData as IExecutionFlattedDb,
|
flattenedExecutionData as IExecutionFlattedDb,
|
||||||
|
@ -578,7 +581,11 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
saveDataSuccessExecution;
|
saveDataSuccessExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowDidSucceed = !fullRunData.data.resultData.error;
|
const workflowHasCrashed = fullRunData.status === 'crashed';
|
||||||
|
const workflowDidSucceed = !fullRunData.data.resultData.error && !workflowHasCrashed;
|
||||||
|
let workflowStatusFinal: ExecutionStatus = workflowDidSucceed ? 'success' : 'failed';
|
||||||
|
if (workflowHasCrashed) workflowStatusFinal = 'crashed';
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
||||||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
||||||
|
@ -626,7 +633,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
stoppedAt: fullRunData.stoppedAt,
|
stoppedAt: fullRunData.stoppedAt,
|
||||||
workflowData: pristineWorkflowData,
|
workflowData: pristineWorkflowData,
|
||||||
waitTill: fullRunData.waitTill,
|
waitTill: fullRunData.waitTill,
|
||||||
status: fullRunData.status,
|
status: workflowStatusFinal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.retryOf !== undefined) {
|
if (this.retryOf !== undefined) {
|
||||||
|
|
|
@ -237,7 +237,7 @@ export class Start extends BaseCommand {
|
||||||
// Load settings from database and set them to config.
|
// Load settings from database and set them to config.
|
||||||
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
||||||
databaseSettings.forEach((setting) => {
|
databaseSettings.forEach((setting) => {
|
||||||
config.set(setting.key, jsonParse(setting.value));
|
config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value }));
|
||||||
});
|
});
|
||||||
|
|
||||||
config.set('nodes.packagesMissing', '');
|
config.set('nodes.packagesMissing', '');
|
||||||
|
|
|
@ -813,6 +813,11 @@ export const schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
authenticationMethod: {
|
||||||
|
doc: 'How to authenticate users (e.g. "email", "ldap", "saml")',
|
||||||
|
format: ['email', 'ldap', 'saml'] as const,
|
||||||
|
default: 'email',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
externalFrontendHooksUrls: {
|
externalFrontendHooksUrls: {
|
||||||
|
@ -1006,6 +1011,27 @@ export const schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sso: {
|
||||||
|
justInTimeProvisioning: {
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
doc: 'Whether to automatically create users when they login via SSO.',
|
||||||
|
},
|
||||||
|
redirectLoginToSso: {
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
doc: 'Whether to automatically redirect users from login dialog to initialize SSO flow.',
|
||||||
|
},
|
||||||
|
saml: {
|
||||||
|
enabled: {
|
||||||
|
format: Boolean,
|
||||||
|
default: false,
|
||||||
|
doc: 'Whether to enable SAML SSO.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: move into sso settings
|
||||||
ldap: {
|
ldap: {
|
||||||
loginEnabled: {
|
loginEnabled: {
|
||||||
format: Boolean,
|
format: Boolean,
|
||||||
|
|
|
@ -19,6 +19,8 @@ import type {
|
||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||||
import type { PostHogClient } from '@/posthog';
|
import type { PostHogClient } from '@/posthog';
|
||||||
|
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
||||||
|
import { SamlUrls } from '../sso/saml/constants';
|
||||||
|
|
||||||
@RestController()
|
@RestController()
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
|
@ -57,14 +59,34 @@ export class AuthController {
|
||||||
* Authless endpoint.
|
* Authless endpoint.
|
||||||
*/
|
*/
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(req: LoginRequest, res: Response): Promise<PublicUser> {
|
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
if (!email) throw new Error('Email is required to log in');
|
if (!email) throw new Error('Email is required to log in');
|
||||||
if (!password) throw new Error('Password is required to log in');
|
if (!password) throw new Error('Password is required to log in');
|
||||||
|
|
||||||
const user =
|
let user: User | undefined;
|
||||||
(await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
|
||||||
|
|
||||||
|
if (isSamlCurrentAuthenticationMethod()) {
|
||||||
|
// attempt to fetch user data with the credentials, but don't log in yet
|
||||||
|
const preliminaryUser = await handleEmailLogin(email, password);
|
||||||
|
// if the user is an owner, continue with the login
|
||||||
|
if (preliminaryUser?.globalRole?.name === 'owner') {
|
||||||
|
user = preliminaryUser;
|
||||||
|
} else {
|
||||||
|
// TODO:SAML - uncomment this block when we have a way to redirect users to the SSO flow
|
||||||
|
// if (doRedirectUsersFromLoginToSsoFlow()) {
|
||||||
|
res.redirect(SamlUrls.restInitSSO);
|
||||||
|
return;
|
||||||
|
// return withFeatureFlags(this.postHog, sanitizeUser(preliminaryUser));
|
||||||
|
// } else {
|
||||||
|
// throw new AuthError(
|
||||||
|
// 'Login with username and password is disabled due to SAML being the default authentication method. Please use SAML to log in.',
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user = (await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
||||||
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
await issueCookie(res, user);
|
await issueCookie(res, user);
|
||||||
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryColumn, Unique } from 'typeorm';
|
||||||
import { AbstractEntity } from './AbstractEntity';
|
import { AbstractEntity } from './AbstractEntity';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
export type AuthProviderType = 'ldap' | 'email'; //| 'saml' | 'google';
|
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Unique(['providerId', 'providerType'])
|
@Unique(['providerId', 'providerType'])
|
||||||
|
|
|
@ -111,9 +111,6 @@ export class User extends AbstractEntity implements IUser {
|
||||||
@AfterLoad()
|
@AfterLoad()
|
||||||
@AfterUpdate()
|
@AfterUpdate()
|
||||||
computeIsPending(): void {
|
computeIsPending(): void {
|
||||||
this.isPending =
|
this.isPending = this.password === null;
|
||||||
this.globalRole?.name === 'owner' && this.globalRole.scope === 'global'
|
|
||||||
? false
|
|
||||||
: this.password === null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export class UpdateRunningExecutionStatus1677236788851 implements MigrationInterface {
|
||||||
|
name = 'UpdateRunningExecutionStatus1677236788851';
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE \`${tablePrefix}execution_entity\` SET status='failed' WHERE status = 'running' AND finished=0 AND \`stoppedAt\` IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE \`${tablePrefix}execution_entity\` SET status='success' WHERE status = 'running' AND finished=1 AND \`stoppedAt\` IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntit
|
||||||
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
|
import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus';
|
||||||
import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable';
|
import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
export const mysqlMigrations = [
|
export const mysqlMigrations = [
|
||||||
|
@ -71,6 +72,7 @@ export const mysqlMigrations = [
|
||||||
PurgeInvalidWorkflowConnections1675940580449,
|
PurgeInvalidWorkflowConnections1675940580449,
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
|
UpdateRunningExecutionStatus1677236788851,
|
||||||
PurgeInvalidWorkflowConnections1675940580449,
|
PurgeInvalidWorkflowConnections1675940580449,
|
||||||
CreateExecutionMetadataTable1674133106779,
|
CreateExecutionMetadataTable1674133106779,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export class UpdateRunningExecutionStatus1677236854063 implements MigrationInterface {
|
||||||
|
name = 'UpdateRunningExecutionStatus1677236854063';
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'failed' WHERE "status" = 'running' AND "finished"=false AND "stoppedAt" IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'success' WHERE "status" = 'running' AND "finished"=true AND "stoppedAt" IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntit
|
||||||
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
|
import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus';
|
||||||
import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable';
|
import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
export const postgresMigrations = [
|
export const postgresMigrations = [
|
||||||
|
@ -67,6 +68,7 @@ export const postgresMigrations = [
|
||||||
PurgeInvalidWorkflowConnections1675940580449,
|
PurgeInvalidWorkflowConnections1675940580449,
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
|
UpdateRunningExecutionStatus1677236854063,
|
||||||
PurgeInvalidWorkflowConnections1675940580449,
|
PurgeInvalidWorkflowConnections1675940580449,
|
||||||
CreateExecutionMetadataTable1674133106778,
|
CreateExecutionMetadataTable1674133106778,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export class UpdateRunningExecutionStatus1677237073720 implements MigrationInterface {
|
||||||
|
name = 'UpdateRunningExecutionStatus1677237073720';
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
logMigrationStart(this.name);
|
||||||
|
const tablePrefix = config.getEnv('database.tablePrefix');
|
||||||
|
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'failed' WHERE "status" = 'running' AND "finished"=0 AND "stoppedAt" IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'success' WHERE "status" = 'running' AND "finished"=1 AND "stoppedAt" IS NOT NULL;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
logMigrationEnd(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntit
|
||||||
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
|
||||||
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||||
|
import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus';
|
||||||
import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable';
|
import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable';
|
||||||
|
|
||||||
const sqliteMigrations = [
|
const sqliteMigrations = [
|
||||||
|
@ -65,6 +66,7 @@ const sqliteMigrations = [
|
||||||
PurgeInvalidWorkflowConnections1675940580449,
|
PurgeInvalidWorkflowConnections1675940580449,
|
||||||
AddStatusToExecutions1674138566000,
|
AddStatusToExecutions1674138566000,
|
||||||
MigrateExecutionStatus1676996103000,
|
MigrateExecutionStatus1676996103000,
|
||||||
|
UpdateRunningExecutionStatus1677237073720,
|
||||||
CreateExecutionMetadataTable1674133106777,
|
CreateExecutionMetadataTable1674133106777,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from '@/UserManagement/UserManagementHelper';
|
} from '@/UserManagement/UserManagementHelper';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
import type { User } from '@db/entities/User';
|
import type { User } from '@db/entities/User';
|
||||||
|
import { SamlUrls } from '../sso/saml/constants';
|
||||||
|
|
||||||
const jwtFromRequest = (req: Request) => {
|
const jwtFromRequest = (req: Request) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
@ -95,6 +96,9 @@ export const setupAuthMiddlewares = (
|
||||||
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
req.url.startsWith(`/${restEndpoint}/change-password`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
|
||||||
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) ||
|
||||||
|
req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) ||
|
||||||
isAuthExcluded(req.url, ignoredEndpoints)
|
isAuthExcluded(req.url, ignoredEndpoints)
|
||||||
) {
|
) {
|
||||||
return next();
|
return next();
|
||||||
|
|
25
packages/cli/src/sso/saml/constants.ts
Normal file
25
packages/cli/src/sso/saml/constants.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export class SamlUrls {
|
||||||
|
static readonly samlRESTRoot = '/rest/sso/saml';
|
||||||
|
|
||||||
|
static readonly initSSO = '/initsso';
|
||||||
|
|
||||||
|
static readonly restInitSSO = this.samlRESTRoot + this.initSSO;
|
||||||
|
|
||||||
|
static readonly acs = '/acs';
|
||||||
|
|
||||||
|
static readonly restAcs = this.samlRESTRoot + this.acs;
|
||||||
|
|
||||||
|
static readonly metadata = '/metadata';
|
||||||
|
|
||||||
|
static readonly restMetadata = this.samlRESTRoot + this.metadata;
|
||||||
|
|
||||||
|
static readonly config = '/config';
|
||||||
|
|
||||||
|
static readonly restConfig = this.samlRESTRoot + this.config;
|
||||||
|
|
||||||
|
static readonly defaultRedirect = '/';
|
||||||
|
|
||||||
|
static readonly samlOnboarding = '/settings/personal'; // TODO:SAML: implement signup page
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAML_PREFERENCES_DB_KEY = 'features.saml';
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { RequestHandler } from 'express';
|
||||||
|
import type { AuthenticatedRequest } from '../../../requests';
|
||||||
|
import { isSamlCurrentAuthenticationMethod } from '../../ssoHelpers';
|
||||||
|
import { isSamlEnabled, isSamlLicensed } from '../samlHelpers';
|
||||||
|
|
||||||
|
export const samlLicensedOwnerMiddleware: RequestHandler = (
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res,
|
||||||
|
next,
|
||||||
|
) => {
|
||||||
|
if (isSamlLicensed() && req.user?.globalRole.name === 'owner') {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const samlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
|
||||||
|
if (isSamlEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
};
|
105
packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts
Normal file
105
packages/cli/src/sso/saml/routes/saml.controller.protected.ee.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import express from 'express';
|
||||||
|
import {
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
} from '../middleware/samlEnabledMiddleware';
|
||||||
|
import { SamlService } from '../saml.service.ee';
|
||||||
|
import { SamlUrls } from '../constants';
|
||||||
|
import type { SamlConfiguration } from '../types/requests';
|
||||||
|
import { AuthError } from '../../../ResponseHelper';
|
||||||
|
import { issueCookie } from '../../../auth/jwt';
|
||||||
|
|
||||||
|
export const samlControllerProtected = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/config
|
||||||
|
* Return SAML config
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.config,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
async (req: SamlConfiguration.Read, res: express.Response) => {
|
||||||
|
const prefs = await SamlService.getInstance().getSamlPreferences();
|
||||||
|
return res.send(prefs);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sso/saml/config
|
||||||
|
* Return SAML config
|
||||||
|
*/
|
||||||
|
samlControllerProtected.post(
|
||||||
|
SamlUrls.config,
|
||||||
|
samlLicensedOwnerMiddleware,
|
||||||
|
async (req: SamlConfiguration.Update, res: express.Response) => {
|
||||||
|
const result = await SamlService.getInstance().setSamlPreferences({
|
||||||
|
metadata: req.body.metadata,
|
||||||
|
mapping: req.body.mapping,
|
||||||
|
});
|
||||||
|
return res.send(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/acs
|
||||||
|
* Assertion Consumer Service endpoint
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.acs,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const loginResult = await SamlService.getInstance().handleSamlLogin(req, 'redirect');
|
||||||
|
if (loginResult) {
|
||||||
|
if (loginResult.authenticatedUser) {
|
||||||
|
await issueCookie(res, loginResult.authenticatedUser);
|
||||||
|
if (loginResult.onboardingRequired) {
|
||||||
|
return res.redirect(SamlUrls.samlOnboarding);
|
||||||
|
} else {
|
||||||
|
return res.redirect(SamlUrls.defaultRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AuthError('SAML Authentication failed');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /sso/saml/acs
|
||||||
|
* Assertion Consumer Service endpoint
|
||||||
|
*/
|
||||||
|
samlControllerProtected.post(
|
||||||
|
SamlUrls.acs,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const loginResult = await SamlService.getInstance().handleSamlLogin(req, 'post');
|
||||||
|
if (loginResult) {
|
||||||
|
if (loginResult.authenticatedUser) {
|
||||||
|
await issueCookie(res, loginResult.authenticatedUser);
|
||||||
|
if (loginResult.onboardingRequired) {
|
||||||
|
return res.redirect(SamlUrls.samlOnboarding);
|
||||||
|
} else {
|
||||||
|
return res.redirect(SamlUrls.defaultRedirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AuthError('SAML Authentication failed');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/initsso
|
||||||
|
* Access URL for implementing SP-init SSO
|
||||||
|
*/
|
||||||
|
samlControllerProtected.get(
|
||||||
|
SamlUrls.initSSO,
|
||||||
|
samlLicensedAndEnabledMiddleware,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
const url = SamlService.getInstance().getRedirectLoginRequestUrl();
|
||||||
|
if (url) {
|
||||||
|
// TODO:SAML: redirect to the URL on the client side
|
||||||
|
return res.status(301).send(url);
|
||||||
|
} else {
|
||||||
|
throw new AuthError('SAML redirect failed, please check your SAML configuration.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { SamlUrls } from '../constants';
|
||||||
|
import { getServiceProviderInstance } from '../serviceProvider.ee';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSO Endpoints that are public
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const samlControllerPublic = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /sso/saml/metadata
|
||||||
|
* Return Service Provider metadata
|
||||||
|
*/
|
||||||
|
samlControllerPublic.get(SamlUrls.metadata, async (req: express.Request, res: express.Response) => {
|
||||||
|
return res.header('Content-Type', 'text/xml').send(getServiceProviderInstance().getMetadata());
|
||||||
|
});
|
228
packages/cli/src/sso/saml/saml.service.ee.ts
Normal file
228
packages/cli/src/sso/saml/saml.service.ee.ts
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import type express from 'express';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import type { User } from '@/databases/entities/User';
|
||||||
|
import { jsonParse, LoggerProxy } from 'n8n-workflow';
|
||||||
|
import { AuthError } from '@/ResponseHelper';
|
||||||
|
import { getServiceProviderInstance } from './serviceProvider.ee';
|
||||||
|
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||||
|
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||||
|
import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers';
|
||||||
|
import type { SamlPreferences } from './types/samlPreferences';
|
||||||
|
import { SAML_PREFERENCES_DB_KEY } from './constants';
|
||||||
|
import type { IdentityProviderInstance } from 'samlify';
|
||||||
|
import { IdentityProvider } from 'samlify';
|
||||||
|
import {
|
||||||
|
createUserFromSamlAttributes,
|
||||||
|
getMappedSamlAttributesFromFlowResult,
|
||||||
|
updateUserFromSamlAttributes,
|
||||||
|
} from './samlHelpers';
|
||||||
|
|
||||||
|
export class SamlService {
|
||||||
|
private static instance: SamlService;
|
||||||
|
|
||||||
|
private identityProviderInstance: IdentityProviderInstance | undefined;
|
||||||
|
|
||||||
|
private _attributeMapping: SamlAttributeMapping = {
|
||||||
|
email: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
||||||
|
firstName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstname',
|
||||||
|
lastName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastname',
|
||||||
|
userPrincipalName: 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn',
|
||||||
|
};
|
||||||
|
|
||||||
|
public get attributeMapping(): SamlAttributeMapping {
|
||||||
|
return this._attributeMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set attributeMapping(mapping: SamlAttributeMapping) {
|
||||||
|
// TODO:SAML: add validation
|
||||||
|
this._attributeMapping = mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _metadata = '';
|
||||||
|
|
||||||
|
public get metadata(): string {
|
||||||
|
return this._metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set metadata(metadata: string) {
|
||||||
|
this._metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadSamlPreferences()
|
||||||
|
.then(() => {
|
||||||
|
LoggerProxy.debug('Initializing SAML service');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
LoggerProxy.error('Error initializing SAML service');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): SamlService {
|
||||||
|
if (!SamlService.instance) {
|
||||||
|
SamlService.instance = new SamlService();
|
||||||
|
}
|
||||||
|
return SamlService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await this.loadSamlPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
|
||||||
|
if (this.identityProviderInstance === undefined || forceRecreate) {
|
||||||
|
this.identityProviderInstance = IdentityProvider({
|
||||||
|
metadata: this.metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.identityProviderInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRedirectLoginRequestUrl(): string {
|
||||||
|
const loginRequest = getServiceProviderInstance().createLoginRequest(
|
||||||
|
this.getIdentityProviderInstance(),
|
||||||
|
'redirect',
|
||||||
|
);
|
||||||
|
//TODO:SAML: debug logging
|
||||||
|
LoggerProxy.debug(loginRequest.context);
|
||||||
|
return loginRequest.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSamlLogin(
|
||||||
|
req: express.Request,
|
||||||
|
binding: 'post' | 'redirect',
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
authenticatedUser: User | undefined;
|
||||||
|
attributes: SamlUserAttributes;
|
||||||
|
onboardingRequired: boolean;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
const attributes = await this.getAttributesFromLoginResponse(req, binding);
|
||||||
|
if (attributes.email) {
|
||||||
|
const user = await Db.collections.User.findOne({
|
||||||
|
where: { email: attributes.email },
|
||||||
|
relations: ['globalRole', 'authIdentities'],
|
||||||
|
});
|
||||||
|
if (user) {
|
||||||
|
// Login path for existing users that are fully set up
|
||||||
|
if (
|
||||||
|
user.authIdentities.find(
|
||||||
|
(e) => e.providerType === 'saml' && e.providerId === attributes.userPrincipalName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
authenticatedUser: user,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Login path for existing users that are NOT fully set up for SAML
|
||||||
|
const updatedUser = await updateUserFromSamlAttributes(user, attributes);
|
||||||
|
return {
|
||||||
|
authenticatedUser: updatedUser,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New users to be created JIT based on SAML attributes
|
||||||
|
if (isSsoJustInTimeProvisioningEnabled()) {
|
||||||
|
const newUser = await createUserFromSamlAttributes(attributes);
|
||||||
|
return {
|
||||||
|
authenticatedUser: newUser,
|
||||||
|
attributes,
|
||||||
|
onboardingRequired: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSamlPreferences(): Promise<SamlPreferences> {
|
||||||
|
return {
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSamlPreferences(prefs: SamlPreferences): Promise<void> {
|
||||||
|
this.attributeMapping = prefs.mapping;
|
||||||
|
this.metadata = prefs.metadata;
|
||||||
|
this.getIdentityProviderInstance(true);
|
||||||
|
await this.saveSamlPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSamlPreferences(): Promise<SamlPreferences | undefined> {
|
||||||
|
const samlPreferences = await Db.collections.Settings.findOne({
|
||||||
|
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||||
|
});
|
||||||
|
if (samlPreferences) {
|
||||||
|
const prefs = jsonParse<SamlPreferences>(samlPreferences.value);
|
||||||
|
if (prefs) {
|
||||||
|
this.attributeMapping = prefs.mapping;
|
||||||
|
this.metadata = prefs.metadata;
|
||||||
|
}
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSamlPreferences(): Promise<void> {
|
||||||
|
const samlPreferences = await Db.collections.Settings.findOne({
|
||||||
|
where: { key: SAML_PREFERENCES_DB_KEY },
|
||||||
|
});
|
||||||
|
if (samlPreferences) {
|
||||||
|
samlPreferences.value = JSON.stringify({
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
});
|
||||||
|
samlPreferences.loadOnStartup = true;
|
||||||
|
await Db.collections.Settings.save(samlPreferences);
|
||||||
|
} else {
|
||||||
|
await Db.collections.Settings.save({
|
||||||
|
key: SAML_PREFERENCES_DB_KEY,
|
||||||
|
value: JSON.stringify({
|
||||||
|
mapping: this.attributeMapping,
|
||||||
|
metadata: this.metadata,
|
||||||
|
}),
|
||||||
|
loadOnStartup: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAttributesFromLoginResponse(
|
||||||
|
req: express.Request,
|
||||||
|
binding: 'post' | 'redirect',
|
||||||
|
): Promise<SamlUserAttributes> {
|
||||||
|
let parsedSamlResponse;
|
||||||
|
try {
|
||||||
|
parsedSamlResponse = await getServiceProviderInstance().parseLoginResponse(
|
||||||
|
this.getIdentityProviderInstance(),
|
||||||
|
binding,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
// throw new AuthError('SAML Authentication failed. Could not parse SAML response.');
|
||||||
|
}
|
||||||
|
const { attributes, missingAttributes } = getMappedSamlAttributesFromFlowResult(
|
||||||
|
parsedSamlResponse,
|
||||||
|
this.attributeMapping,
|
||||||
|
);
|
||||||
|
if (!attributes) {
|
||||||
|
throw new AuthError('SAML Authentication failed. Invalid SAML response.');
|
||||||
|
}
|
||||||
|
if (!attributes.email && missingAttributes.length > 0) {
|
||||||
|
throw new AuthError(
|
||||||
|
`SAML Authentication failed. Invalid SAML response (missing attributes: ${missingAttributes.join(
|
||||||
|
', ',
|
||||||
|
)}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
136
packages/cli/src/sso/saml/samlHelpers.ts
Normal file
136
packages/cli/src/sso/saml/samlHelpers.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import config from '@/config';
|
||||||
|
import * as Db from '@/Db';
|
||||||
|
import { AuthIdentity } from '../../databases/entities/AuthIdentity';
|
||||||
|
import { User } from '../../databases/entities/User';
|
||||||
|
import { getLicense } from '../../License';
|
||||||
|
import { AuthError } from '../../ResponseHelper';
|
||||||
|
import { hashPassword, isUserManagementEnabled } from '../../UserManagement/UserManagementHelper';
|
||||||
|
import type { SamlPreferences } from './types/samlPreferences';
|
||||||
|
import type { SamlUserAttributes } from './types/samlUserAttributes';
|
||||||
|
import type { FlowResult } from 'samlify/types/src/flow';
|
||||||
|
import type { SamlAttributeMapping } from './types/samlAttributeMapping';
|
||||||
|
/**
|
||||||
|
* Check whether the SAML feature is licensed and enabled in the instance
|
||||||
|
*/
|
||||||
|
export function isSamlEnabled(): boolean {
|
||||||
|
return config.getEnv('sso.saml.enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSamlLicensed(): boolean {
|
||||||
|
const license = getLicense();
|
||||||
|
return (
|
||||||
|
isUserManagementEnabled() &&
|
||||||
|
(license.isSamlEnabled() || config.getEnv('enterprise.features.saml'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSamlPreferences = (candidate: unknown): candidate is SamlPreferences => {
|
||||||
|
const o = candidate as SamlPreferences;
|
||||||
|
return typeof o === 'object' && typeof o.metadata === 'string' && typeof o.mapping === 'object';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generatePassword(): string {
|
||||||
|
const length = 18;
|
||||||
|
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
const charsetNoNumbers = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
const randomNumber = Math.floor(Math.random() * 10);
|
||||||
|
const randomUpper = charset.charAt(Math.floor(Math.random() * charsetNoNumbers.length));
|
||||||
|
const randomNumberPosition = Math.floor(Math.random() * length);
|
||||||
|
const randomUpperPosition = Math.floor(Math.random() * length);
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0, n = charset.length; i < length; ++i) {
|
||||||
|
password += charset.charAt(Math.floor(Math.random() * n));
|
||||||
|
}
|
||||||
|
password =
|
||||||
|
password.substring(0, randomNumberPosition) +
|
||||||
|
randomNumber.toString() +
|
||||||
|
password.substring(randomNumberPosition);
|
||||||
|
password =
|
||||||
|
password.substring(0, randomUpperPosition) +
|
||||||
|
randomUpper +
|
||||||
|
password.substring(randomUpperPosition);
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUserFromSamlAttributes(attributes: SamlUserAttributes): Promise<User> {
|
||||||
|
const user = new User();
|
||||||
|
const authIdentity = new AuthIdentity();
|
||||||
|
user.email = attributes.email;
|
||||||
|
user.firstName = attributes.firstName;
|
||||||
|
user.lastName = attributes.lastName;
|
||||||
|
user.globalRole = await Db.collections.Role.findOneOrFail({
|
||||||
|
where: { name: 'member', scope: 'global' },
|
||||||
|
});
|
||||||
|
// generates a password that is not used or known to the user
|
||||||
|
user.password = await hashPassword(generatePassword());
|
||||||
|
authIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
authIdentity.providerType = 'saml';
|
||||||
|
authIdentity.user = user;
|
||||||
|
const resultAuthIdentity = await Db.collections.AuthIdentity.save(authIdentity);
|
||||||
|
if (!resultAuthIdentity) throw new AuthError('Could not create AuthIdentity');
|
||||||
|
user.authIdentities = [authIdentity];
|
||||||
|
const resultUser = await Db.collections.User.save(user);
|
||||||
|
if (!resultUser) throw new AuthError('Could not create User');
|
||||||
|
return resultUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserFromSamlAttributes(
|
||||||
|
user: User,
|
||||||
|
attributes: SamlUserAttributes,
|
||||||
|
): Promise<User> {
|
||||||
|
if (!attributes.email) throw new AuthError('Email is required to update user');
|
||||||
|
if (!user) throw new AuthError('User not found');
|
||||||
|
let samlAuthIdentity = user?.authIdentities.find((e) => e.providerType === 'saml');
|
||||||
|
if (!samlAuthIdentity) {
|
||||||
|
samlAuthIdentity = new AuthIdentity();
|
||||||
|
samlAuthIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
samlAuthIdentity.providerType = 'saml';
|
||||||
|
samlAuthIdentity.user = user;
|
||||||
|
user.authIdentities.push(samlAuthIdentity);
|
||||||
|
} else {
|
||||||
|
samlAuthIdentity.providerId = attributes.userPrincipalName;
|
||||||
|
}
|
||||||
|
await Db.collections.AuthIdentity.save(samlAuthIdentity);
|
||||||
|
user.firstName = attributes.firstName;
|
||||||
|
user.lastName = attributes.lastName;
|
||||||
|
const resultUser = await Db.collections.User.save(user);
|
||||||
|
if (!resultUser) throw new AuthError('Could not create User');
|
||||||
|
return resultUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMappedSamlReturn = {
|
||||||
|
attributes: SamlUserAttributes | undefined;
|
||||||
|
missingAttributes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMappedSamlAttributesFromFlowResult(
|
||||||
|
flowResult: FlowResult,
|
||||||
|
attributeMapping: SamlAttributeMapping,
|
||||||
|
): GetMappedSamlReturn {
|
||||||
|
const result: GetMappedSamlReturn = {
|
||||||
|
attributes: undefined,
|
||||||
|
missingAttributes: [] as string[],
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
if (flowResult?.extract?.attributes) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
const attributes = flowResult.extract.attributes as { [key: string]: string };
|
||||||
|
// TODO:SAML: fetch mapped attributes from flowResult.extract.attributes and create or login user
|
||||||
|
const email = attributes[attributeMapping.email];
|
||||||
|
const firstName = attributes[attributeMapping.firstName];
|
||||||
|
const lastName = attributes[attributeMapping.lastName];
|
||||||
|
const userPrincipalName = attributes[attributeMapping.userPrincipalName];
|
||||||
|
|
||||||
|
result.attributes = {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
userPrincipalName,
|
||||||
|
};
|
||||||
|
if (!email) result.missingAttributes.push(attributeMapping.email);
|
||||||
|
if (!userPrincipalName) result.missingAttributes.push(attributeMapping.userPrincipalName);
|
||||||
|
if (!firstName) result.missingAttributes.push(attributeMapping.firstName);
|
||||||
|
if (!lastName) result.missingAttributes.push(attributeMapping.lastName);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
39
packages/cli/src/sso/saml/serviceProvider.ee.ts
Normal file
39
packages/cli/src/sso/saml/serviceProvider.ee.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
|
||||||
|
import type { ServiceProviderInstance } from 'samlify';
|
||||||
|
import { ServiceProvider, setSchemaValidator } from 'samlify';
|
||||||
|
import { SamlUrls } from './constants';
|
||||||
|
|
||||||
|
let serviceProviderInstance: ServiceProviderInstance | undefined;
|
||||||
|
|
||||||
|
setSchemaValidator({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
validate: async (response: string) => {
|
||||||
|
// TODO:SAML: implment validation
|
||||||
|
return Promise.resolve('skipped');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = `
|
||||||
|
<EntityDescriptor
|
||||||
|
xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
|
||||||
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
||||||
|
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
|
||||||
|
entityID="${getInstanceBaseUrl() + SamlUrls.restMetadata}">
|
||||||
|
<SPSSODescriptor WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
|
||||||
|
<AssertionConsumerService isDefault="true" index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="${
|
||||||
|
getInstanceBaseUrl() + SamlUrls.restAcs
|
||||||
|
}"/>
|
||||||
|
</SPSSODescriptor>
|
||||||
|
</EntityDescriptor>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function getServiceProviderInstance(): ServiceProviderInstance {
|
||||||
|
if (serviceProviderInstance === undefined) {
|
||||||
|
serviceProviderInstance = ServiceProvider({
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceProviderInstance;
|
||||||
|
}
|
7
packages/cli/src/sso/saml/types/requests.ts
Normal file
7
packages/cli/src/sso/saml/types/requests.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { AuthenticatedRequest } from '../../../requests';
|
||||||
|
import type { SamlPreferences } from './samlPreferences';
|
||||||
|
|
||||||
|
export declare namespace SamlConfiguration {
|
||||||
|
type Read = AuthenticatedRequest<{}, {}, {}, {}>;
|
||||||
|
type Update = AuthenticatedRequest<{}, {}, SamlPreferences, {}>;
|
||||||
|
}
|
6
packages/cli/src/sso/saml/types/samlAttributeMapping.ts
Normal file
6
packages/cli/src/sso/saml/types/samlAttributeMapping.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface SamlAttributeMapping {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
userPrincipalName: string;
|
||||||
|
}
|
7
packages/cli/src/sso/saml/types/samlPreferences.ts
Normal file
7
packages/cli/src/sso/saml/types/samlPreferences.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { SamlAttributeMapping } from './samlAttributeMapping';
|
||||||
|
|
||||||
|
export interface SamlPreferences {
|
||||||
|
mapping: SamlAttributeMapping;
|
||||||
|
metadata: string;
|
||||||
|
//TODO:SAML: add fields for separate SAML settins to generate metadata from
|
||||||
|
}
|
6
packages/cli/src/sso/saml/types/samlUserAttributes.ts
Normal file
6
packages/cli/src/sso/saml/types/samlUserAttributes.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface SamlUserAttributes {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
userPrincipalName: string;
|
||||||
|
}
|
13
packages/cli/src/sso/ssoHelpers.ts
Normal file
13
packages/cli/src/sso/ssoHelpers.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import config from '@/config';
|
||||||
|
|
||||||
|
export function isSamlCurrentAuthenticationMethod(): boolean {
|
||||||
|
return config.getEnv('userManagement.authenticationMethod') === 'saml';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSsoJustInTimeProvisioningEnabled(): boolean {
|
||||||
|
return config.getEnv('sso.justInTimeProvisioning');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doRedirectUsersFromLoginToSsoFlow(): boolean {
|
||||||
|
return config.getEnv('sso.redirectLoginToSso');
|
||||||
|
}
|
|
@ -164,7 +164,7 @@ export class WorkflowsService {
|
||||||
|
|
||||||
if (!config.getEnv('workflowTagsDisabled')) {
|
if (!config.getEnv('workflowTagsDisabled')) {
|
||||||
relations.push('tags');
|
relations.push('tags');
|
||||||
select.tags = { name: true };
|
select.tags = { id: true, name: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSharingEnabled()) {
|
if (isSharingEnabled()) {
|
||||||
|
|
|
@ -401,6 +401,7 @@ export async function createManyWorkflows(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a workflow in the DB (without a trigger) and optionally assign it to a user.
|
* Store a workflow in the DB (without a trigger) and optionally assign it to a user.
|
||||||
|
* @param attributes workflow attributes
|
||||||
* @param user user to assign the workflow to
|
* @param user user to assign the workflow to
|
||||||
*/
|
*/
|
||||||
export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, user?: User) {
|
export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, user?: User) {
|
||||||
|
|
|
@ -154,6 +154,7 @@ describe('GET /workflows', () => {
|
||||||
test('should return workflows without nodes, sharing and credential usage details', async () => {
|
test('should return workflows without nodes, sharing and credential usage details', async () => {
|
||||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||||
|
const tag = await testDb.createTag({ name: 'test' });
|
||||||
|
|
||||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||||
|
|
||||||
|
@ -175,6 +176,7 @@ describe('GET /workflows', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
tags: [tag],
|
||||||
},
|
},
|
||||||
owner,
|
owner,
|
||||||
);
|
);
|
||||||
|
@ -193,6 +195,14 @@ describe('GET /workflows', () => {
|
||||||
expect(fetchedWorkflow.sharedWith).not.toBeDefined()
|
expect(fetchedWorkflow.sharedWith).not.toBeDefined()
|
||||||
expect(fetchedWorkflow.usedCredentials).not.toBeDefined()
|
expect(fetchedWorkflow.usedCredentials).not.toBeDefined()
|
||||||
expect(fetchedWorkflow.nodes).not.toBeDefined()
|
expect(fetchedWorkflow.nodes).not.toBeDefined()
|
||||||
|
expect(fetchedWorkflow.tags).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: expect.any(String)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "0.155.1",
|
"version": "0.156.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-design-system",
|
"name": "n8n-design-system",
|
||||||
"version": "0.54.0",
|
"version": "0.55.0",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
"author": {
|
"author": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.182.1",
|
"version": "0.183.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -56,11 +56,6 @@
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"jsonpath": "^1.1.1",
|
"jsonpath": "^1.1.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lodash.camelcase": "^4.3.0",
|
|
||||||
"lodash.debounce": "^4.0.8",
|
|
||||||
"lodash.get": "^4.4.2",
|
|
||||||
"lodash.orderby": "^4.6.0",
|
|
||||||
"lodash.set": "^4.3.2",
|
|
||||||
"luxon": "^3.1.0",
|
"luxon": "^3.1.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"n8n-design-system": "workspace:*",
|
"n8n-design-system": "workspace:*",
|
||||||
|
|
|
@ -52,7 +52,7 @@ import { deepCopy, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
||||||
|
|
||||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { Component } from 'vue';
|
import { Component } from 'vue';
|
||||||
|
|
|
@ -265,7 +265,7 @@ import {
|
||||||
IWorkflowShortResponse,
|
IWorkflowShortResponse,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { ExecutionStatus, IDataObject } from 'n8n-workflow';
|
import type { ExecutionStatus, IDataObject } from 'n8n-workflow';
|
||||||
import { range as _range } from 'lodash';
|
import { range as _range } from 'lodash-es';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
|
|
|
@ -48,7 +48,7 @@ import { showMessage } from '@/mixins/showMessage';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { Route } from 'vue-router';
|
import { Route } from 'vue-router';
|
||||||
import { executionHelpers } from '@/mixins/executionsHelpers';
|
import { executionHelpers } from '@/mixins/executionsHelpers';
|
||||||
import { range as _range } from 'lodash';
|
import { range as _range } from 'lodash-es';
|
||||||
import { debounceHelper } from '@/mixins/debounce';
|
import { debounceHelper } from '@/mixins/debounce';
|
||||||
import { getNodeViewTab, NO_NETWORK_ERROR_CODE } from '@/utils';
|
import { getNodeViewTab, NO_NETWORK_ERROR_CODE } from '@/utils';
|
||||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
|
|
|
@ -124,7 +124,7 @@ import {
|
||||||
isINodePropertyCollectionList,
|
isINodePropertyCollectionList,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'FixedCollectionParameter',
|
name: 'FixedCollectionParameter',
|
||||||
|
|
|
@ -90,7 +90,7 @@ import { deepCopy, INodeParameters, INodeProperties } from 'n8n-workflow';
|
||||||
import CollectionParameter from '@/components/CollectionParameter.vue';
|
import CollectionParameter from '@/components/CollectionParameter.vue';
|
||||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'MultipleParameter',
|
name: 'MultipleParameter',
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue, { PropType } from 'vue';
|
import Vue, { PropType } from 'vue';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import PanelDragButton from './PanelDragButton.vue';
|
import PanelDragButton from './PanelDragButton.vue';
|
||||||
|
|
|
@ -189,7 +189,7 @@ import TitledList from '@/components/TitledList.vue';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
import { getStyleTokenValue, getTriggerNodeServiceName } from '@/utils';
|
import { getStyleTokenValue, getTriggerNodeServiceName } from '@/utils';
|
||||||
import {
|
import {
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
|
|
|
@ -95,7 +95,7 @@ import {
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
nextTick,
|
nextTick,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import camelcase from 'lodash.camelcase';
|
import { camelCase } from 'lodash-es';
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import ItemIterator from './ItemIterator.vue';
|
import ItemIterator from './ItemIterator.vue';
|
||||||
|
@ -184,7 +184,7 @@ const activeSubcategoryTitle = computed<string>(() => {
|
||||||
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
|
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
|
||||||
|
|
||||||
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
|
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
|
||||||
const subcategoryName = camelcase(subcategory);
|
const subcategoryName = camelCase(subcategory);
|
||||||
|
|
||||||
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
|
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
|
||||||
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
|
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
|
||||||
|
|
|
@ -14,14 +14,14 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ISubcategoryItemProps } from '@/Interface';
|
import { ISubcategoryItemProps } from '@/Interface';
|
||||||
import camelcase from 'lodash.camelcase';
|
import { camelCase } from 'lodash-es';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
export interface Props {
|
export interface Props {
|
||||||
item: ISubcategoryItemProps;
|
item: ISubcategoryItemProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
const subcategoryName = computed(() => camelcase(props.item.subcategory));
|
const subcategoryName = computed(() => camelCase(props.item.subcategory));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -188,7 +188,7 @@ import ParameterInputList from '@/components/ParameterInputList.vue';
|
||||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||||
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
|
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
|
||||||
import NodeWebhooks from '@/components/NodeWebhooks.vue';
|
import NodeWebhooks from '@/components/NodeWebhooks.vue';
|
||||||
import { get, set, unset } from 'lodash';
|
import { get, set, unset } from 'lodash-es';
|
||||||
|
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||||
|
|
|
@ -319,7 +319,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/* eslint-disable prefer-spread */
|
/* eslint-disable prefer-spread */
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
import { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
import { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<n8n-text size="small" color="text-base" tag="div" v-if="hint">
|
<n8n-text size="small" color="text-base" tag="div" v-if="hint">
|
||||||
<div v-if="!renderHTML" :class="{ [$style.hint]: true, [$style.highlight]: highlight }">
|
<div
|
||||||
|
v-if="!renderHTML"
|
||||||
|
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
|
||||||
|
>
|
||||||
{{ hint }}
|
{{ hint }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
ref="hint"
|
ref="hint"
|
||||||
:class="{ [$style.hint]: true, [$style.highlight]: highlight }"
|
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
|
||||||
v-html="sanitizeHtml(hint)"
|
v-html="sanitizeHtml(hint)"
|
||||||
></div>
|
></div>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
|
@ -25,6 +28,9 @@ export default Vue.extend({
|
||||||
highlight: {
|
highlight: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
singleLine: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
renderHTML: {
|
renderHTML: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -42,12 +48,11 @@ export default Vue.extend({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.hint {
|
.singleline {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ImportParameter from '@/components/ImportParameter.vue';
|
import ImportParameter from '@/components/ImportParameter.vue';
|
||||||
|
|
||||||
import { get, set } from 'lodash';
|
import { get, set } from 'lodash-es';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { Component, PropType } from 'vue';
|
import { Component, PropType } from 'vue';
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
data-test-id="parameter-expression-preview"
|
data-test-id="parameter-expression-preview"
|
||||||
:highlight="!!(expressionOutput && targetItem)"
|
:highlight="!!(expressionOutput && targetItem)"
|
||||||
:hint="expressionOutput"
|
:hint="expressionOutput"
|
||||||
|
:singleLine="true"
|
||||||
/>
|
/>
|
||||||
<input-hint
|
<input-hint
|
||||||
v-else-if="parameterHint"
|
v-else-if="parameterHint"
|
||||||
|
|
|
@ -172,7 +172,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { get, set, unset } from 'lodash';
|
import { get, set, unset } from 'lodash-es';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { useLogStreamingStore } from '../../stores/logStreamingStore';
|
import { useLogStreamingStore } from '../../stores/logStreamingStore';
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* defined on the component which uses this mixin
|
* defined on the component which uses this mixin
|
||||||
*/
|
*/
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
export const copyPaste = Vue.extend({
|
export const copyPaste = Vue.extend({
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash-es';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export const debounceHelper = Vue.extend({
|
export const debounceHelper = Vue.extend({
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
|
|
||||||
import { restApi } from '@/mixins/restApi';
|
import { restApi } from '@/mixins/restApi';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { isObjectLiteral } from '@/utils';
|
import { isObjectLiteral } from '@/utils';
|
||||||
|
|
|
@ -48,7 +48,7 @@ import { restApi } from '@/mixins/restApi';
|
||||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||||
import { showMessage } from '@/mixins/showMessage';
|
import { showMessage } from '@/mixins/showMessage';
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from '@jsplumb/core';
|
} from '@jsplumb/core';
|
||||||
import { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common';
|
import { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common';
|
||||||
import { BezierSegment } from '@jsplumb/connector-bezier';
|
import { BezierSegment } from '@jsplumb/connector-bezier';
|
||||||
import { isArray } from 'lodash';
|
import { isArray } from 'lodash-es';
|
||||||
import { deepCopy } from 'n8n-workflow';
|
import { deepCopy } from 'n8n-workflow';
|
||||||
|
|
||||||
export interface N8nConnectorOptions extends ConnectorOptions {}
|
export interface N8nConnectorOptions extends ConnectorOptions {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { startCase } from 'lodash';
|
import { startCase } from 'lodash-es';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import {
|
import {
|
||||||
INodePropertyCollection,
|
INodePropertyCollection,
|
||||||
|
|
|
@ -16,11 +16,6 @@ const ignoreChunks = [
|
||||||
'@fontsource/open-sans',
|
'@fontsource/open-sans',
|
||||||
'normalize-wheel',
|
'normalize-wheel',
|
||||||
'stream-browserify',
|
'stream-browserify',
|
||||||
'lodash.camelcase',
|
|
||||||
'lodash.debounce',
|
|
||||||
'lodash.get',
|
|
||||||
'lodash.orderby',
|
|
||||||
'lodash.set',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const isScopedPackageToIgnore = (str: string) => /@codemirror\//.test(str);
|
const isScopedPackageToIgnore = (str: string) => /@codemirror\//.test(str);
|
||||||
|
@ -44,11 +39,6 @@ function renderChunks() {
|
||||||
|
|
||||||
const publicPath = process.env.VUE_APP_PUBLIC_PATH || '/';
|
const publicPath = process.env.VUE_APP_PUBLIC_PATH || '/';
|
||||||
|
|
||||||
const lodashAliases = ['orderBy', 'camelCase', 'cloneDeep', 'isEqual', 'startCase'].map((name) => ({
|
|
||||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
|
||||||
replacement: require.resolve(`lodash-es/${name}`),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { NODE_ENV } = process.env;
|
const { NODE_ENV } = process.env;
|
||||||
|
|
||||||
export default mergeConfig(
|
export default mergeConfig(
|
||||||
|
@ -78,9 +68,12 @@ export default mergeConfig(
|
||||||
find: /^n8n-design-system\//,
|
find: /^n8n-design-system\//,
|
||||||
replacement: resolve(__dirname, '..', 'design-system', 'src') + '/',
|
replacement: resolve(__dirname, '..', 'design-system', 'src') + '/',
|
||||||
},
|
},
|
||||||
...lodashAliases,
|
...['orderBy', 'camelCase', 'cloneDeep', 'isEqual', 'startCase'].map((name) => ({
|
||||||
|
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||||
|
replacement: require.resolve(`lodash-es/${name}`),
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
find: /^lodash.(.+)$/,
|
find: /^lodash\.(.+)$/,
|
||||||
replacement: 'lodash-es/$1',
|
replacement: 'lodash-es/$1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-node-dev",
|
"name": "n8n-node-dev",
|
||||||
"version": "0.94.1",
|
"version": "0.95.0",
|
||||||
"description": "CLI to simplify n8n credentials/node development",
|
"description": "CLI to simplify n8n credentials/node development",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
|
|
@ -4,7 +4,8 @@ import type { IDataObject, ILoadOptionsFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
import type { OptionsWithUri } from 'request';
|
import type { OptionsWithUri } from 'request';
|
||||||
|
|
||||||
import { flow, omit } from 'lodash';
|
import flow from 'lodash.flow';
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AllFieldsUi,
|
AllFieldsUi,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
INodePropertyOptions,
|
INodePropertyOptions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an API request to Asana
|
* Make an API request to Asana
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { jsonParse, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { awsApiRequestSOAP } from './GenericFunctions';
|
import { awsApiRequestSOAP } from './GenericFunctions';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
export class AwsSnsTrigger implements INodeType {
|
export class AwsSnsTrigger implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import { parseString } from 'xml2js';
|
import { parseString } from 'xml2js';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import { parseString } from 'xml2js';
|
import { parseString } from 'xml2js';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import { parseString } from 'xml2js';
|
import { parseString } from 'xml2js';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
||||||
import type { IDataObject, IHttpRequestOptions } from 'n8n-workflow';
|
import type { IDataObject, IHttpRequestOptions } from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
export async function awsApiRequest(
|
export async function awsApiRequest(
|
||||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import type {
|
||||||
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
function getEndpointForService(
|
function getEndpointForService(
|
||||||
service: string,
|
service: string,
|
||||||
|
|
|
@ -33,7 +33,8 @@ import type {
|
||||||
} from './descriptions/MemberDescription';
|
} from './descriptions/MemberDescription';
|
||||||
import { memberFields, memberOperations } from './descriptions/MemberDescription';
|
import { memberFields, memberOperations } from './descriptions/MemberDescription';
|
||||||
|
|
||||||
import { isEmpty, partialRight } from 'lodash';
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import partialRight from 'lodash.partialright';
|
||||||
|
|
||||||
export class Bitwarden implements INodeType {
|
export class Bitwarden implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { upperFirst } from 'lodash';
|
import upperFirst from 'lodash.upperfirst';
|
||||||
|
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||||
|
|
||||||
import { difference, get, intersection, isEmpty, omit, set, union } from 'lodash';
|
import difference from 'lodash.difference';
|
||||||
|
import get from 'lodash.get';
|
||||||
|
import intersection from 'lodash.intersection';
|
||||||
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
import set from 'lodash.set';
|
||||||
|
import union from 'lodash.union';
|
||||||
import { fuzzyCompare } from '../../utils/utilities';
|
import { fuzzyCompare } from '../../utils/utilities';
|
||||||
|
|
||||||
type PairToMatch = {
|
type PairToMatch = {
|
||||||
|
|
|
@ -13,7 +13,8 @@ import type {
|
||||||
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { flow, omit } from 'lodash';
|
import flow from 'lodash.flow';
|
||||||
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AddressFixedCollection,
|
AddressFixedCollection,
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { responderFields, respondersOperations } from './ResponderDescription';
|
||||||
|
|
||||||
import { jobFields, jobOperations } from './JobDescription';
|
import { jobFields, jobOperations } from './JobDescription';
|
||||||
|
|
||||||
import { upperFirst } from 'lodash';
|
import upperFirst from 'lodash.upperfirst';
|
||||||
|
|
||||||
import type { IJob } from './AnalyzerInterface';
|
import type { IJob } from './AnalyzerInterface';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { set } from 'lodash';
|
import set from 'lodash.set';
|
||||||
|
|
||||||
import type { IExecuteFunctions } from 'n8n-core';
|
import type { IExecuteFunctions } from 'n8n-core';
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions } from 'n
|
||||||
|
|
||||||
import type { IDataObject, IHttpRequestMethods, IHttpRequestOptions } from 'n8n-workflow';
|
import type { IDataObject, IHttpRequestMethods, IHttpRequestOptions } from 'n8n-workflow';
|
||||||
|
|
||||||
import { get } from 'lodash';
|
import get from 'lodash.get';
|
||||||
|
|
||||||
export async function customerIoApiRequest(
|
export async function customerIoApiRequest(
|
||||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { deepCopy, NodeOperationError } from 'n8n-workflow';
|
import { deepCopy, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { set } from 'lodash';
|
import set from 'lodash.set';
|
||||||
|
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { flow, sortBy, uniqBy } from 'lodash';
|
import flow from 'lodash.flow';
|
||||||
|
import sortBy from 'lodash.sortby';
|
||||||
|
import uniqBy from 'lodash.uniqby';
|
||||||
|
|
||||||
export type DocumentProperties = {
|
export type DocumentProperties = {
|
||||||
customProperty: Array<{ field: string; value: string }>;
|
customProperty: Array<{ field: string; value: string }>;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { documentFields, documentOperations, indexFields, indexOperations } from
|
||||||
|
|
||||||
import type { DocumentGetAllOptions, FieldsUiValues } from './types';
|
import type { DocumentGetAllOptions, FieldsUiValues } from './types';
|
||||||
|
|
||||||
import { omit } from 'lodash';
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
export class Elasticsearch implements INodeType {
|
export class Elasticsearch implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
|
@ -22,7 +22,8 @@ import { connect as imapConnect, getParts } from 'imap-simple';
|
||||||
import type { Source as ParserSource } from 'mailparser';
|
import type { Source as ParserSource } from 'mailparser';
|
||||||
import { simpleParser } from 'mailparser';
|
import { simpleParser } from 'mailparser';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import find from 'lodash.find';
|
||||||
|
|
||||||
export async function parseRawEmail(
|
export async function parseRawEmail(
|
||||||
this: ITriggerFunctions,
|
this: ITriggerFunctions,
|
||||||
|
@ -241,7 +242,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||||
if (credentials.secure) {
|
if (credentials.secure) {
|
||||||
tlsOptions.servername = credentials.host as string;
|
tlsOptions.servername = credentials.host as string;
|
||||||
}
|
}
|
||||||
if (!_.isEmpty(tlsOptions)) {
|
if (!isEmpty(tlsOptions)) {
|
||||||
config.imap.tlsOptions = tlsOptions;
|
config.imap.tlsOptions = tlsOptions;
|
||||||
}
|
}
|
||||||
const conn = imapConnect(config).then(async (entry) => {
|
const conn = imapConnect(config).then(async (entry) => {
|
||||||
|
@ -385,7 +386,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||||
) {
|
) {
|
||||||
staticData.lastMessageUid = message.attributes.uid;
|
staticData.lastMessageUid = message.attributes.uid;
|
||||||
}
|
}
|
||||||
const part = _.find(message.parts, { which: '' });
|
const part = find(message.parts, { which: '' });
|
||||||
|
|
||||||
if (part === undefined) {
|
if (part === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
||||||
|
@ -474,7 +475,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||||
) {
|
) {
|
||||||
staticData.lastMessageUid = message.attributes.uid;
|
staticData.lastMessageUid = message.attributes.uid;
|
||||||
}
|
}
|
||||||
const part = _.find(message.parts, { which: 'TEXT' });
|
const part = find(message.parts, { which: 'TEXT' });
|
||||||
|
|
||||||
if (part === undefined) {
|
if (part === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
||||||
|
@ -569,7 +570,7 @@ export class EmailReadImapV1 implements INodeType {
|
||||||
tlsOptions.servername = credentials.host as string;
|
tlsOptions.servername = credentials.host as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(tlsOptions)) {
|
if (!isEmpty(tlsOptions)) {
|
||||||
config.imap.tlsOptions = tlsOptions;
|
config.imap.tlsOptions = tlsOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ import { connect as imapConnect, getParts } from 'imap-simple';
|
||||||
import type { Source as ParserSource } from 'mailparser';
|
import type { Source as ParserSource } from 'mailparser';
|
||||||
import { simpleParser } from 'mailparser';
|
import { simpleParser } from 'mailparser';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import isEmpty from 'lodash.isempty';
|
||||||
|
import find from 'lodash.find';
|
||||||
import type { ICredentialsDataImap } from '../../../credentials/Imap.credentials';
|
import type { ICredentialsDataImap } from '../../../credentials/Imap.credentials';
|
||||||
import { isCredentialsDataImap } from '../../../credentials/Imap.credentials';
|
import { isCredentialsDataImap } from '../../../credentials/Imap.credentials';
|
||||||
|
|
||||||
|
@ -240,7 +241,7 @@ export class EmailReadImapV2 implements INodeType {
|
||||||
if (credentials.secure) {
|
if (credentials.secure) {
|
||||||
tlsOptions.servername = credentials.host;
|
tlsOptions.servername = credentials.host;
|
||||||
}
|
}
|
||||||
if (!_.isEmpty(tlsOptions)) {
|
if (!isEmpty(tlsOptions)) {
|
||||||
config.imap.tlsOptions = tlsOptions;
|
config.imap.tlsOptions = tlsOptions;
|
||||||
}
|
}
|
||||||
const connection = await imapConnect(config);
|
const connection = await imapConnect(config);
|
||||||
|
@ -393,7 +394,7 @@ export class EmailReadImapV2 implements INodeType {
|
||||||
) {
|
) {
|
||||||
staticData.lastMessageUid = message.attributes.uid;
|
staticData.lastMessageUid = message.attributes.uid;
|
||||||
}
|
}
|
||||||
const part = _.find(message.parts, { which: '' });
|
const part = find(message.parts, { which: '' });
|
||||||
|
|
||||||
if (part === undefined) {
|
if (part === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
||||||
|
@ -482,7 +483,7 @@ export class EmailReadImapV2 implements INodeType {
|
||||||
) {
|
) {
|
||||||
staticData.lastMessageUid = message.attributes.uid;
|
staticData.lastMessageUid = message.attributes.uid;
|
||||||
}
|
}
|
||||||
const part = _.find(message.parts, { which: 'TEXT' });
|
const part = find(message.parts, { which: 'TEXT' });
|
||||||
|
|
||||||
if (part === undefined) {
|
if (part === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
||||||
|
@ -580,7 +581,7 @@ export class EmailReadImapV2 implements INodeType {
|
||||||
tlsOptions.servername = credentials.host;
|
tlsOptions.servername = credentials.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(tlsOptions)) {
|
if (!isEmpty(tlsOptions)) {
|
||||||
config.imap.tlsOptions = tlsOptions;
|
config.imap.tlsOptions = tlsOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { campaignFields, campaignOperations } from './CampaignDescription';
|
||||||
|
|
||||||
import { contactListFields, contactListOperations } from './ContactListDescription';
|
import { contactListFields, contactListOperations } from './ContactListDescription';
|
||||||
|
|
||||||
import { isEmpty } from 'lodash';
|
import isEmpty from 'lodash.isempty';
|
||||||
|
|
||||||
export class Emelia implements INodeType {
|
export class Emelia implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
||||||
|
|
||||||
import type { OptionsWithUri } from 'request';
|
import type { OptionsWithUri } from 'request';
|
||||||
|
|
||||||
import { omit } from 'lodash';
|
import omit from 'lodash.omit';
|
||||||
|
|
||||||
export async function freshserviceApiRequest(
|
export async function freshserviceApiRequest(
|
||||||
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
|
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue