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)
|
||||
|
||||
|
||||
|
|
|
@ -10,9 +10,12 @@ const WorkflowPage = new WorkflowPageClass();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Undo/Redo', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
});
|
||||
|
@ -38,7 +41,11 @@ describe('Undo/Redo', () => {
|
|||
it('should undo/redo adding node in the middle', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_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.hitUndo();
|
||||
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';
|
||||
|
||||
describe('Canvas Actions', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
});
|
||||
|
@ -46,14 +49,16 @@ describe('Canvas Actions', () => {
|
|||
// Change connection from Set to Set1
|
||||
cy.draganddrop(
|
||||
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);
|
||||
// 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);
|
||||
});
|
||||
|
||||
|
@ -67,7 +72,10 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
||||
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.actions.addNodeToCanvas(IF_NODE_NAME, false);
|
||||
|
@ -79,18 +87,20 @@ describe('Canvas Actions', () => {
|
|||
|
||||
// Switch has 4 output endpoints
|
||||
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.actions.addNodeToCanvas(SET_NODE_NAME, false);
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
}
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload()
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
// Make sure all connections are there after reload
|
||||
for (let i = 0; i < 4; 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
|
||||
cy.draganddrop(
|
||||
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);
|
||||
|
||||
// Connect Set1 and Set2 to merge
|
||||
cy.draganddrop(
|
||||
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(
|
||||
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);
|
||||
|
||||
// Make sure all connections are there after save & reload
|
||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||
cy.reload()
|
||||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
|
||||
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('.jtk-connector.success').should('have.length', 3);
|
||||
cy.get('.jtk-connector').should('have.length', 4);
|
||||
})
|
||||
});
|
||||
|
||||
it('should add a connected node using plus endpoint', () => {
|
||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
|
|
|
@ -4,15 +4,18 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Data pinning', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
});
|
||||
|
||||
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.pinDataButton().should('not.exist');
|
||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||
|
@ -43,7 +46,7 @@ describe('Data pinning', () => {
|
|||
});
|
||||
|
||||
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.pinDataButton().should('not.exist');
|
||||
ndv.getters.editPinnedDataButton().should('be.visible');
|
||||
|
|
|
@ -4,15 +4,20 @@ const wf = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Data transformation expressions', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wf.actions.visit();
|
||||
cy.waitForLoad();
|
||||
|
||||
cy.window()
|
||||
// @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', () => {
|
||||
|
@ -26,7 +31,7 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||
ndv.actions.execute();
|
||||
ndv.getters.outputDataContainer().should('be.visible')
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputDataContainer().contains(output);
|
||||
});
|
||||
|
||||
|
@ -41,7 +46,7 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||
ndv.actions.execute();
|
||||
ndv.getters.outputDataContainer().should('be.visible')
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputDataContainer().contains(output);
|
||||
});
|
||||
|
||||
|
@ -56,7 +61,7 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||
ndv.actions.execute();
|
||||
ndv.getters.outputDataContainer().should('be.visible')
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputDataContainer().contains(output);
|
||||
});
|
||||
|
||||
|
@ -71,7 +76,7 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||
ndv.actions.execute();
|
||||
ndv.getters.outputDataContainer().should('be.visible')
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputDataContainer().contains(output);
|
||||
});
|
||||
|
||||
|
@ -86,7 +91,7 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
ndv.getters.inlineExpressionEditorInput().clear().type(input);
|
||||
ndv.actions.execute();
|
||||
ndv.getters.outputDataContainer().should('be.visible')
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
ndv.getters.outputDataContainer().contains(output);
|
||||
});
|
||||
|
||||
|
|
|
@ -9,15 +9,20 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Data mapping', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
|
||||
cy.window()
|
||||
// @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', () => {
|
||||
|
@ -30,15 +35,29 @@ describe('Data mapping', () => {
|
|||
ndv.getters.inputDataContainer().get('table', { timeout: 10000 }).should('exist');
|
||||
|
||||
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.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.getters
|
||||
.nodeParameters()
|
||||
.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.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
|
||||
|
||||
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', () => {
|
||||
|
@ -50,23 +69,37 @@ describe('Data mapping', () => {
|
|||
ndv.actions.switchInputMode('Table');
|
||||
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.parameterInput('value').should('have.length', 1).find('input').should('have.value', '');
|
||||
ndv.getters
|
||||
.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.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.parameterExpressionPreview('value')
|
||||
ndv.getters
|
||||
.parameterExpressionPreview('value')
|
||||
.should('include.text', '0')
|
||||
.invoke('css', 'color')
|
||||
.should('equal', 'rgb(125, 125, 135)');
|
||||
|
||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
ndv.getters
|
||||
.parameterExpressionPreview('value')
|
||||
.should('include.text', '1')
|
||||
.invoke('css', 'color')
|
||||
.should('equal', 'rgb(125, 125, 135)');
|
||||
|
@ -74,13 +107,15 @@ describe('Data mapping', () => {
|
|||
ndv.actions.execute();
|
||||
|
||||
ndv.getters.outputTbodyCell(1, 0).realHover();
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
ndv.getters
|
||||
.parameterExpressionPreview('value')
|
||||
.should('include.text', '0')
|
||||
.invoke('css', 'color')
|
||||
.should('equal', 'rgb(125, 125, 135)'); // todo update color
|
||||
|
||||
ndv.getters.outputTbodyCell(2, 0).realHover();
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
ndv.getters
|
||||
.parameterExpressionPreview('value')
|
||||
.should('include.text', '1')
|
||||
.invoke('css', 'color')
|
||||
.should('equal', 'rgb(125, 125, 135)');
|
||||
|
@ -94,24 +129,34 @@ describe('Data mapping', () => {
|
|||
workflowPage.actions.openNode('Set');
|
||||
ndv.actions.switchInputMode('JSON');
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('.json-data')
|
||||
.should('have.text', '[{"input":[{"count":0,"with space":"!!","with.dot":"!!","with"quotes":"!!"}]},{"input":[{"count":1}]}]')
|
||||
.find('span').contains('"count"')
|
||||
ndv.getters
|
||||
.inputDataContainer()
|
||||
.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();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
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.inputDataContainer().find('.json-data')
|
||||
.find('span').contains('"input"')
|
||||
ndv.getters
|
||||
.inputDataContainer()
|
||||
.find('.json-data')
|
||||
.find('span')
|
||||
.contains('"input"')
|
||||
.realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
.should('include.text', '0 [object Object]');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.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', () => {
|
||||
|
@ -123,25 +168,19 @@ describe('Data mapping', () => {
|
|||
ndv.actions.clearParameterInput('value');
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
ndv.getters.inputDataContainer()
|
||||
.should('exist')
|
||||
.find('span').contains('count')
|
||||
.realMouseDown();
|
||||
|
||||
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
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.inputDataContainer()
|
||||
.find('span').contains('input')
|
||||
.realMouseDown();
|
||||
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }} {{ $json.input }}');
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
.should('include.text', '0 [object Object]');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.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', () => {
|
||||
|
@ -150,32 +189,33 @@ describe('Data mapping', () => {
|
|||
|
||||
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
|
||||
ndv.getters.inputDataContainer()
|
||||
.find('span').contains('count')
|
||||
.realMouseDown();
|
||||
ndv.getters.inputDataContainer().find('span').contains('count').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('value');
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.text', `{{ $node["${SCHEDULE_TRIGGER_NODE_NAME}"].json.input[0].count }}`);
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
.should('not.exist');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.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.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.parameterExpressionPreview('value')
|
||||
.should('not.exist');
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.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.executePrevious();
|
||||
ndv.getters.executingLoader().should('not.exist');
|
||||
ndv.getters.inputDataContainer().should('exist');
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
.should('include.text', '0 [object Object]');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '0 [object Object]');
|
||||
|
||||
ndv.getters.inputTbodyCell(2, 0).realHover();
|
||||
ndv.getters.parameterExpressionPreview('value')
|
||||
.should('include.text', '1 [object Object]');
|
||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1 [object Object]');
|
||||
});
|
||||
|
||||
it('maps keys to path', () => {
|
||||
|
@ -186,20 +226,20 @@ describe('Data mapping', () => {
|
|||
{
|
||||
input: [
|
||||
{
|
||||
"hello.world": {
|
||||
"my count": 0,
|
||||
'hello.world': {
|
||||
'my count': 0,
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
input: [
|
||||
{
|
||||
"hello.world": {
|
||||
"my count": 1,
|
||||
}
|
||||
}
|
||||
]
|
||||
'hello.world': {
|
||||
'my count': 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -208,21 +248,18 @@ describe('Data mapping', () => {
|
|||
workflowPage.actions.addNodeToCanvas('Item Lists');
|
||||
workflowPage.actions.openNode('Item Lists');
|
||||
|
||||
ndv.getters.parameterInput('operation')
|
||||
.click()
|
||||
.find('li').contains('Sort')
|
||||
.click();
|
||||
ndv.getters.parameterInput('operation').click().find('li').contains('Sort').click();
|
||||
|
||||
ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click();
|
||||
|
||||
ndv.getters.inputDataContainer()
|
||||
.find('span').contains('my count')
|
||||
.realMouseDown();
|
||||
ndv.getters.inputDataContainer().find('span').contains('my count').realMouseDown();
|
||||
|
||||
ndv.actions.mapToParameter('fieldName');
|
||||
|
||||
ndv.getters.inlineExpressionEditorInput().should('have.length', 0);
|
||||
ndv.getters.parameterInput('fieldName')
|
||||
.find('input').should('have.value', 'input[0]["hello.world"]["my count"]');
|
||||
ndv.getters
|
||||
.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();
|
||||
|
||||
describe('Schedule Trigger node', async () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
@ -42,28 +42,38 @@ describe('Schedule Trigger node', async () => {
|
|||
workflowPage.actions.activateWorkflow();
|
||||
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
|
||||
|
||||
cy.request("GET", '/rest/workflows').then((response) => {
|
||||
cy.request('GET', '/rest/workflows')
|
||||
.then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
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) => {
|
||||
})
|
||||
.then((workflowId) => {
|
||||
cy.wait(1200);
|
||||
cy.request("GET", '/rest/executions').then((response) => {
|
||||
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);
|
||||
const matchingExecutions = response.body.data.results.filter(
|
||||
(execution: any) => execution.workflowId === workflowId,
|
||||
);
|
||||
expect(matchingExecutions).to.have.length(1);
|
||||
return workflowId;
|
||||
}).then((workflowId) => {
|
||||
})
|
||||
.then((workflowId) => {
|
||||
cy.wait(1200);
|
||||
cy.request("GET", '/rest/executions').then((response) => {
|
||||
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);
|
||||
const matchingExecutions = response.body.data.results.filter(
|
||||
(execution: any) => execution.workflowId === workflowId,
|
||||
);
|
||||
expect(matchingExecutions).to.have.length(2);
|
||||
}).then(()=>{
|
||||
})
|
||||
.then(() => {
|
||||
workflowPage.actions.activateWorkflow();
|
||||
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
|
||||
cy.visit(workflowsPage.url);
|
||||
|
|
|
@ -83,7 +83,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
|||
ndv.actions.execute();
|
||||
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);
|
||||
ndv.getters.outputPanel().contains('headers');
|
||||
});
|
||||
|
@ -91,36 +91,41 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
|
|||
};
|
||||
|
||||
describe('Webhook Trigger node', async () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
|
||||
cy.window()
|
||||
// @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', () => {
|
||||
simpleWebhookCall({method: 'GET', webhookPath: uuid(), executeNow: true});
|
||||
simpleWebhookCall({ method: 'GET', webhookPath: uuid(), executeNow: true });
|
||||
});
|
||||
|
||||
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', () => {
|
||||
simpleWebhookCall({method: 'DELETE', webhookPath: uuid(), executeNow: true});
|
||||
simpleWebhookCall({ method: 'DELETE', webhookPath: uuid(), executeNow: true });
|
||||
});
|
||||
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', () => {
|
||||
simpleWebhookCall({method: 'PATCH', webhookPath: uuid(), executeNow: true});
|
||||
simpleWebhookCall({ method: 'PATCH', webhookPath: uuid(), executeNow: true });
|
||||
});
|
||||
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', () => {
|
||||
|
@ -138,7 +143,10 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.openNode('Set');
|
||||
cy.get('.add-option').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');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
|
@ -147,7 +155,7 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
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.body.MyValue).to.eq(1234);
|
||||
});
|
||||
|
@ -165,7 +173,7 @@ describe('Webhook Trigger node', async () => {
|
|||
ndv.actions.execute();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -184,14 +192,17 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.openNode('Set');
|
||||
cy.get('.add-option').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');
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
workflowPage.actions.executeWorkflow();
|
||||
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.body.MyValue).to.eq(1234);
|
||||
});
|
||||
|
@ -213,10 +224,14 @@ describe('Webhook Trigger node', async () => {
|
|||
cy.get('.add-option').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-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();
|
||||
|
||||
|
||||
workflowPage.actions.addNodeToCanvas('Move Binary Data');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
|
@ -232,7 +247,7 @@ describe('Webhook Trigger node', async () => {
|
|||
workflowPage.actions.executeWorkflow();
|
||||
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(Object.keys(response.body).includes('data')).to.be.true;
|
||||
});
|
||||
|
@ -249,7 +264,7 @@ describe('Webhook Trigger node', async () => {
|
|||
});
|
||||
ndv.actions.execute();
|
||||
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.body.MyValue).to.be.undefined;
|
||||
});
|
||||
|
@ -273,22 +288,23 @@ describe('Webhook Trigger node', async () => {
|
|||
cy.wait(waitForWebhook);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/webhook-test/'+ webhookPath,
|
||||
url: '/webhook-test/' + webhookPath,
|
||||
auth: {
|
||||
'user': 'username',
|
||||
'pass': 'password',
|
||||
user: 'username',
|
||||
pass: 'password',
|
||||
},
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.then((response) => {
|
||||
expect(response.status).to.eq(403);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/webhook-test/'+ webhookPath,
|
||||
url: '/webhook-test/' + webhookPath,
|
||||
auth: {
|
||||
'user': 'test',
|
||||
'pass': 'test',
|
||||
user: 'test',
|
||||
pass: 'test',
|
||||
},
|
||||
failOnStatusCode: true,
|
||||
}).then((response) => {
|
||||
|
@ -315,7 +331,7 @@ describe('Webhook Trigger node', async () => {
|
|||
cy.wait(waitForWebhook);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/webhook-test/'+ webhookPath,
|
||||
url: '/webhook-test/' + webhookPath,
|
||||
headers: {
|
||||
test: 'wrong',
|
||||
},
|
||||
|
@ -323,10 +339,11 @@ describe('Webhook Trigger node', async () => {
|
|||
})
|
||||
.then((response) => {
|
||||
expect(response.status).to.eq(403);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: '/webhook-test/'+ webhookPath,
|
||||
url: '/webhook-test/' + webhookPath,
|
||||
headers: {
|
||||
test: 'test',
|
||||
},
|
||||
|
|
|
@ -5,9 +5,12 @@ const wf = new WorkflowPage();
|
|||
const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3'];
|
||||
|
||||
describe('Workflow tags', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wf.actions.visit();
|
||||
cy.waitForLoad();
|
||||
});
|
||||
|
|
|
@ -42,8 +42,8 @@ const updatedPersonalData = {
|
|||
newLastName: 'Else',
|
||||
newEmail: 'something_else@acme.corp',
|
||||
newPassword: 'Keybo4rd',
|
||||
invalidPasswords: ['abc', 'longEnough', 'longenough123']
|
||||
}
|
||||
invalidPasswords: ['abc', 'longEnough', 'longenough123'],
|
||||
};
|
||||
|
||||
const usersSettingsPage = new SettingsUsersPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
@ -67,7 +67,7 @@ describe('User Management', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
|
@ -79,7 +79,10 @@ describe('User Management', () => {
|
|||
// All items in user list should be there
|
||||
usersSettingsPage.getters.userListItems().should('have.length', 3);
|
||||
// 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
|
||||
usersSettingsPage.getters.userActionsToggle(users[0].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`, () => {
|
||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
||||
personalSettingsPage.actions.updateFirstAndLastName(updatedPersonalData.newFirstName, updatedPersonalData.newLastName);
|
||||
personalSettingsPage.getters.currentUserName().should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
||||
personalSettingsPage.actions.updateFirstAndLastName(
|
||||
updatedPersonalData.newFirstName,
|
||||
updatedPersonalData.newLastName,
|
||||
);
|
||||
personalSettingsPage.getters
|
||||
.currentUserName()
|
||||
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
||||
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`, () => {
|
||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
|
||||
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`, () => {
|
||||
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');
|
||||
personalSettingsPage.actions.loginWithNewData(instanceOwner.email, updatedPersonalData.newPassword);
|
||||
personalSettingsPage.actions.loginWithNewData(
|
||||
instanceOwner.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('@')[0]);
|
||||
// try without domain
|
||||
|
@ -140,9 +160,15 @@ describe('User Management', () => {
|
|||
});
|
||||
|
||||
it(`should change user email`, () => {
|
||||
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, updatedPersonalData.newPassword);
|
||||
personalSettingsPage.actions.loginAndVisit(
|
||||
instanceOwner.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);
|
||||
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 ndv = new NDV();
|
||||
|
||||
describe('Execution',() => {
|
||||
beforeEach(() => {
|
||||
describe('Execution', () => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
|
@ -34,17 +37,36 @@ describe('Execution',() => {
|
|||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
||||
|
||||
// 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.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'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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);
|
||||
|
||||
// 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.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check')).should('be.visible');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.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
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -77,20 +99,39 @@ describe('Execution',() => {
|
|||
workflowPage.getters.stopExecutionWaitingForWebhookButton().should('not.exist');
|
||||
|
||||
// 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.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'));
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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);
|
||||
workflowPage.getters.stopExecutionButton().click();
|
||||
|
||||
// Check canvas nodes after workflow stopped
|
||||
workflowPage.getters.canvasNodeByName('Manual').within(() => cy.get('.fa-check')).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'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Manual')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -140,17 +181,36 @@ describe('Execution',() => {
|
|||
});
|
||||
|
||||
// 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.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'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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);
|
||||
|
||||
// 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.canvasNodeByName('Wait').within(() => cy.get('.fa-check')).should('be.visible');
|
||||
workflowPage.getters.canvasNodeByName('Set').within(() => cy.get('.fa-check')).should('be.visible');
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.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
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
@ -200,19 +260,39 @@ describe('Execution',() => {
|
|||
});
|
||||
|
||||
// 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.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'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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);
|
||||
workflowPage.getters.stopExecutionWaitingForWebhookButton().click();
|
||||
|
||||
// Check canvas nodes after workflow stopped
|
||||
workflowPage.getters.canvasNodeByName('Webhook').within(() => cy.get('.fa-check')).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'));
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('Webhook')
|
||||
.within(() => cy.get('.fa-check'))
|
||||
.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
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
|
|
@ -259,7 +259,7 @@ describe('Credentials', () => {
|
|||
cy.contains('Create New Credential').click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Notion API');
|
||||
})
|
||||
});
|
||||
|
||||
it('should render custom node with custom credential', () => {
|
||||
workflowPage.actions.visit();
|
||||
|
@ -269,5 +269,5 @@ describe('Credentials', () => {
|
|||
cy.contains('Create New Credential').click();
|
||||
credentialsModal.getters.editCredentialModal().should('be.visible');
|
||||
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { WorkflowPage } from "../pages";
|
||||
import { WorkflowExecutionsTab } from "../pages/workflow-executions-tab";
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
|
@ -9,6 +9,9 @@ describe('Current Workflow Executions', () => {
|
|||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
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.successfulExecutionListItems().should('have.length', 9);
|
||||
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 = () => {
|
||||
workflowPage.actions.turnOnManualExecutionSaving();
|
||||
executionsTab.actions.createManualExecutions(5);
|
||||
|
@ -37,4 +42,4 @@ const createMockExecutions = () => {
|
|||
executionsTab.actions.createManualExecutions(4);
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.waitForLoad();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,11 +28,7 @@ describe('Node Creator', () => {
|
|||
it('should open node creator on trigger tab if no trigger is on canvas', () => {
|
||||
nodeCreatorFeature.getters.canvasAddButton().click();
|
||||
|
||||
nodeCreatorFeature.getters
|
||||
.nodeCreator()
|
||||
.contains('Select a trigger')
|
||||
.should('be.visible');
|
||||
|
||||
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
||||
});
|
||||
|
||||
it('should navigate subcategory', () => {
|
||||
|
@ -86,20 +82,14 @@ describe('Node Creator', () => {
|
|||
|
||||
// TODO: Replace once we have canvas feature utils
|
||||
cy.get('div').contains('Add first step').should('be.hidden');
|
||||
nodeCreatorFeature.actions.openNodeCreator()
|
||||
nodeCreatorFeature.getters
|
||||
.nodeCreator()
|
||||
.contains('What happens next?')
|
||||
.should('be.visible');
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
|
||||
|
||||
nodeCreatorFeature.getters.getCreatorItem('Add another trigger').click();
|
||||
nodeCreatorFeature.getters.nodeCreator().contains('Select a trigger').should('be.visible');
|
||||
nodeCreatorFeature.getters.activeSubcategory().find('button').should('exist');
|
||||
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
|
||||
nodeCreatorFeature.getters
|
||||
.nodeCreator()
|
||||
.contains('What happens next?')
|
||||
.should('be.visible');
|
||||
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
|
||||
});
|
||||
|
||||
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.getCreatorItem('Crop Image').click();
|
||||
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop');
|
||||
})
|
||||
});
|
||||
|
||||
it('should search through actions and confirm added action', () => {
|
||||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
|
|
|
@ -6,16 +6,17 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('NDV', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.renameWorkflow(uuid());
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
});
|
||||
|
||||
|
||||
it('should show up when double clicked on a node and close when Back to canvas clicked', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
|
|
|
@ -5,7 +5,7 @@ const WorkflowPage = new WorkflowPageClass();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('Code node', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
|
|
@ -14,9 +14,12 @@ const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
|
|||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('Workflow Actions', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
cy.waitForLoad();
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ const workflowPage = new WorkflowPage();
|
|||
const ndv = new NDV();
|
||||
|
||||
describe('HTTP Request node', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.resetAll();
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.216.1",
|
||||
"version": "0.217.1",
|
||||
"private": true,
|
||||
"homepage": "https://n8n.io",
|
||||
"engines": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.216.1",
|
||||
"version": "0.217.1",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -76,8 +76,8 @@
|
|||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^9.0.1",
|
||||
"@types/localtunnel": "^1.9.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.intersection": "^4.4.7",
|
||||
"@types/lodash.iteratee": "^4.7.7",
|
||||
"@types/lodash.merge": "^4.6.6",
|
||||
|
@ -191,6 +191,7 @@
|
|||
"psl": "^1.8.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"replacestream": "^4.0.3",
|
||||
"samlify": "^2.8.9",
|
||||
"semver": "^7.3.8",
|
||||
"shelljs": "^0.8.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
|
|
|
@ -190,7 +190,7 @@ export function send<T, R extends Request, S extends Response>(
|
|||
try {
|
||||
const data = await processFunction(req, res);
|
||||
|
||||
sendSuccessResponse(res, data, raw);
|
||||
if (!res.headersSent) sendSuccessResponse(res, data, raw);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
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 { PostHogClient } from './posthog';
|
||||
import { eventBus } from './eventbus';
|
||||
import { isSamlEnabled } from './Saml/helpers';
|
||||
import { Container } from 'typedi';
|
||||
import { InternalHooks } from './InternalHooks';
|
||||
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);
|
||||
|
||||
|
@ -318,7 +321,7 @@ class Server extends AbstractServer {
|
|||
sharing: isSharingEnabled(),
|
||||
logStreaming: isLogStreamingEnabled(),
|
||||
ldap: isLdapEnabled(),
|
||||
saml: isSamlEnabled(),
|
||||
saml: isSamlLicensed(),
|
||||
});
|
||||
|
||||
if (isLdapEnabled()) {
|
||||
|
@ -495,6 +498,19 @@ class Server extends AbstractServer {
|
|||
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
|
||||
// get generated dynamically
|
||||
this.app.get(
|
||||
|
|
|
@ -44,7 +44,7 @@ import {
|
|||
|
||||
import pick from 'lodash.pick';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
import { LessThanOrEqual } from 'typeorm';
|
||||
import { LessThanOrEqual, In } from 'typeorm';
|
||||
import { DateUtils } from 'typeorm/util/DateUtils';
|
||||
import config from '@/config';
|
||||
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
|
||||
const utcDate = DateUtils.mixedDateToUtcDatetimeString(date);
|
||||
|
||||
const toPrune: FindOptionsWhere<IExecutionFlattedDb> = { stoppedAt: LessThanOrEqual(utcDate) };
|
||||
const toPrune: Array<FindOptionsWhere<IExecutionFlattedDb>> = [
|
||||
{ stoppedAt: LessThanOrEqual(utcDate) },
|
||||
];
|
||||
|
||||
if (maxCount > 0) {
|
||||
const executions = await Db.collections.Execution.find({
|
||||
|
@ -223,27 +225,29 @@ async function pruneExecutionData(this: WorkflowHooks): Promise<void> {
|
|||
});
|
||||
|
||||
if (executions[0]) {
|
||||
toPrune.id = LessThanOrEqual(executions[0].id);
|
||||
toPrune.push({ id: LessThanOrEqual(executions[0].id) });
|
||||
}
|
||||
}
|
||||
|
||||
const isBinaryModeDefaultMode = config.getEnv('binaryDataManager.mode') === 'default';
|
||||
try {
|
||||
const executions = isBinaryModeDefaultMode
|
||||
? []
|
||||
: await Db.collections.Execution.find({
|
||||
select: ['id'],
|
||||
where: toPrune,
|
||||
});
|
||||
await Db.collections.Execution.delete(toPrune);
|
||||
setTimeout(() => {
|
||||
throttling = false;
|
||||
}, timeout * 1000);
|
||||
let executionIds: Array<IExecutionFlattedDb['id']>;
|
||||
do {
|
||||
executionIds = (
|
||||
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(
|
||||
executions.map(({ id }) => id),
|
||||
);
|
||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionIds(executionIds);
|
||||
} while (executionIds.length > 0);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
throttling = false;
|
||||
|
@ -472,7 +476,6 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
|
|||
fullExecutionData.status = 'running';
|
||||
|
||||
const flattenedExecutionData = ResponseHelper.flattenExecutionData(fullExecutionData);
|
||||
|
||||
await Db.collections.Execution.update(
|
||||
this.executionId,
|
||||
flattenedExecutionData as IExecutionFlattedDb,
|
||||
|
@ -578,7 +581,11 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
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 (
|
||||
(workflowDidSucceed && saveDataSuccessExecution === 'none') ||
|
||||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
||||
|
@ -626,7 +633,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
stoppedAt: fullRunData.stoppedAt,
|
||||
workflowData: pristineWorkflowData,
|
||||
waitTill: fullRunData.waitTill,
|
||||
status: fullRunData.status,
|
||||
status: workflowStatusFinal,
|
||||
};
|
||||
|
||||
if (this.retryOf !== undefined) {
|
||||
|
|
|
@ -237,7 +237,7 @@ export class Start extends BaseCommand {
|
|||
// Load settings from database and set them to config.
|
||||
const databaseSettings = await Db.collections.Settings.findBy({ loadOnStartup: true });
|
||||
databaseSettings.forEach((setting) => {
|
||||
config.set(setting.key, jsonParse(setting.value));
|
||||
config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value }));
|
||||
});
|
||||
|
||||
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: {
|
||||
|
@ -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: {
|
||||
loginEnabled: {
|
||||
format: Boolean,
|
||||
|
|
|
@ -19,6 +19,8 @@ import type {
|
|||
} from '@/Interfaces';
|
||||
import { handleEmailLogin, handleLdapLogin } from '@/auth';
|
||||
import type { PostHogClient } from '@/posthog';
|
||||
import { isSamlCurrentAuthenticationMethod } from '../sso/ssoHelpers';
|
||||
import { SamlUrls } from '../sso/saml/constants';
|
||||
|
||||
@RestController()
|
||||
export class AuthController {
|
||||
|
@ -57,14 +59,34 @@ export class AuthController {
|
|||
* Authless endpoint.
|
||||
*/
|
||||
@Post('/login')
|
||||
async login(req: LoginRequest, res: Response): Promise<PublicUser> {
|
||||
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
|
||||
const { email, password } = req.body;
|
||||
if (!email) throw new Error('Email is required to log in');
|
||||
if (!password) throw new Error('Password is required to log in');
|
||||
|
||||
const user =
|
||||
(await handleLdapLogin(email, password)) ?? (await handleEmailLogin(email, password));
|
||||
let user: User | undefined;
|
||||
|
||||
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) {
|
||||
await issueCookie(res, user);
|
||||
return withFeatureFlags(this.postHog, sanitizeUser(user));
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Column, Entity, ManyToOne, PrimaryColumn, Unique } from 'typeorm';
|
|||
import { AbstractEntity } from './AbstractEntity';
|
||||
import { User } from './User';
|
||||
|
||||
export type AuthProviderType = 'ldap' | 'email'; //| 'saml' | 'google';
|
||||
export type AuthProviderType = 'ldap' | 'email' | 'saml'; // | 'google';
|
||||
|
||||
@Entity()
|
||||
@Unique(['providerId', 'providerType'])
|
||||
|
|
|
@ -111,9 +111,6 @@ export class User extends AbstractEntity implements IUser {
|
|||
@AfterLoad()
|
||||
@AfterUpdate()
|
||||
computeIsPending(): void {
|
||||
this.isPending =
|
||||
this.globalRole?.name === 'owner' && this.globalRole.scope === 'global'
|
||||
? false
|
||||
: this.password === null;
|
||||
this.isPending = 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 { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||
import { UpdateRunningExecutionStatus1677236788851 } from './1677236788851-UpdateRunningExecutionStatus';
|
||||
import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
|
@ -71,6 +72,7 @@ export const mysqlMigrations = [
|
|||
PurgeInvalidWorkflowConnections1675940580449,
|
||||
AddStatusToExecutions1674138566000,
|
||||
MigrateExecutionStatus1676996103000,
|
||||
UpdateRunningExecutionStatus1677236788851,
|
||||
PurgeInvalidWorkflowConnections1675940580449,
|
||||
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 { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||
import { UpdateRunningExecutionStatus1677236854063 } from './1677236854063-UpdateRunningExecutionStatus';
|
||||
import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable';
|
||||
|
||||
export const postgresMigrations = [
|
||||
|
@ -67,6 +68,7 @@ export const postgresMigrations = [
|
|||
PurgeInvalidWorkflowConnections1675940580449,
|
||||
AddStatusToExecutions1674138566000,
|
||||
MigrateExecutionStatus1676996103000,
|
||||
UpdateRunningExecutionStatus1677236854063,
|
||||
PurgeInvalidWorkflowConnections1675940580449,
|
||||
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 { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
|
||||
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
|
||||
import { UpdateRunningExecutionStatus1677237073720 } from './1677237073720-UpdateRunningExecutionStatus';
|
||||
import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable';
|
||||
|
||||
const sqliteMigrations = [
|
||||
|
@ -65,6 +66,7 @@ const sqliteMigrations = [
|
|||
PurgeInvalidWorkflowConnections1675940580449,
|
||||
AddStatusToExecutions1674138566000,
|
||||
MigrateExecutionStatus1676996103000,
|
||||
UpdateRunningExecutionStatus1677237073720,
|
||||
CreateExecutionMetadataTable1674133106777,
|
||||
];
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@/UserManagement/UserManagementHelper';
|
||||
import type { Repository } from 'typeorm';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { SamlUrls } from '../sso/saml/constants';
|
||||
|
||||
const jwtFromRequest = (req: Request) => {
|
||||
// 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}/oauth2-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)
|
||||
) {
|
||||
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')) {
|
||||
relations.push('tags');
|
||||
select.tags = { name: true };
|
||||
select.tags = { id: true, name: true };
|
||||
}
|
||||
|
||||
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.
|
||||
* @param attributes workflow attributes
|
||||
* @param user user to assign the workflow to
|
||||
*/
|
||||
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 () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const tag = await testDb.createTag({ name: 'test' });
|
||||
|
||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||
|
||||
|
@ -175,6 +176,7 @@ describe('GET /workflows', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
tags: [tag],
|
||||
},
|
||||
owner,
|
||||
);
|
||||
|
@ -193,6 +195,14 @@ describe('GET /workflows', () => {
|
|||
expect(fetchedWorkflow.sharedWith).not.toBeDefined()
|
||||
expect(fetchedWorkflow.usedCredentials).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",
|
||||
"version": "0.155.1",
|
||||
"version": "0.156.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "0.54.0",
|
||||
"version": "0.55.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.182.1",
|
||||
"version": "0.183.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -56,11 +56,6 @@
|
|||
"jquery": "^3.4.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
"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",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"n8n-design-system": "workspace:*",
|
||||
|
|
|
@ -52,7 +52,7 @@ import { deepCopy, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
|||
|
||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { Component } from 'vue';
|
||||
|
|
|
@ -265,7 +265,7 @@ import {
|
|||
IWorkflowShortResponse,
|
||||
} from '@/Interface';
|
||||
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 { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui';
|
||||
|
|
|
@ -48,7 +48,7 @@ import { showMessage } from '@/mixins/showMessage';
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { Route } from 'vue-router';
|
||||
import { executionHelpers } from '@/mixins/executionsHelpers';
|
||||
import { range as _range } from 'lodash';
|
||||
import { range as _range } from 'lodash-es';
|
||||
import { debounceHelper } from '@/mixins/debounce';
|
||||
import { getNodeViewTab, NO_NETWORK_ERROR_CODE } from '@/utils';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
|
|
@ -124,7 +124,7 @@ import {
|
|||
isINodePropertyCollectionList,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FixedCollectionParameter',
|
||||
|
|
|
@ -90,7 +90,7 @@ import { deepCopy, INodeParameters, INodeProperties } from 'n8n-workflow';
|
|||
import CollectionParameter from '@/components/CollectionParameter.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'MultipleParameter',
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import PanelDragButton from './PanelDragButton.vue';
|
||||
|
|
|
@ -189,7 +189,7 @@ import TitledList from '@/components/TitledList.vue';
|
|||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
import { getStyleTokenValue, getTriggerNodeServiceName } from '@/utils';
|
||||
import {
|
||||
IExecutionsSummary,
|
||||
|
|
|
@ -95,7 +95,7 @@ import {
|
|||
onUnmounted,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import ItemIterator from './ItemIterator.vue';
|
||||
|
@ -184,7 +184,7 @@ const activeSubcategoryTitle = computed<string>(() => {
|
|||
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
|
||||
|
||||
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
|
||||
const subcategoryName = camelcase(subcategory);
|
||||
const subcategoryName = camelCase(subcategory);
|
||||
|
||||
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
|
||||
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ISubcategoryItemProps } from '@/Interface';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { computed } from 'vue';
|
||||
export interface Props {
|
||||
item: ISubcategoryItemProps;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const subcategoryName = computed(() => camelcase(props.item.subcategory));
|
||||
const subcategoryName = computed(() => camelCase(props.item.subcategory));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -188,7 +188,7 @@ import ParameterInputList from '@/components/ParameterInputList.vue';
|
|||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
import NodeSettingsTabs from '@/components/NodeSettingsTabs.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 { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
|
|
|
@ -319,7 +319,7 @@
|
|||
<script lang="ts">
|
||||
/* eslint-disable prefer-spread */
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
||||
import {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<template>
|
||||
<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 }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="hint"
|
||||
:class="{ [$style.hint]: true, [$style.highlight]: highlight }"
|
||||
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
|
||||
v-html="sanitizeHtml(hint)"
|
||||
></div>
|
||||
</n8n-text>
|
||||
|
@ -25,6 +28,9 @@ export default Vue.extend({
|
|||
highlight: {
|
||||
type: Boolean,
|
||||
},
|
||||
singleLine: {
|
||||
type: Boolean,
|
||||
},
|
||||
renderHTML: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -42,12 +48,11 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.hint {
|
||||
.singleline {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
|
|||
import ParameterInputFull from '@/components/ParameterInputFull.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 { Component, PropType } from 'vue';
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
data-test-id="parameter-expression-preview"
|
||||
:highlight="!!(expressionOutput && targetItem)"
|
||||
:hint="expressionOutput"
|
||||
:singleLine="true"
|
||||
/>
|
||||
<input-hint
|
||||
v-else-if="parameterHint"
|
||||
|
|
|
@ -172,7 +172,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { get, set, unset } from 'lodash';
|
||||
import { get, set, unset } from 'lodash-es';
|
||||
import { mapStores } from 'pinia';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { useLogStreamingStore } from '../../stores/logStreamingStore';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* defined on the component which uses this mixin
|
||||
*/
|
||||
import Vue from 'vue';
|
||||
import { debounce } from 'lodash';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
export const copyPaste = Vue.extend({
|
||||
data() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { debounce } from 'lodash';
|
||||
import { debounce } from 'lodash-es';
|
||||
import Vue from 'vue';
|
||||
|
||||
export const debounceHelper = Vue.extend({
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
|
||||
import { restApi } from '@/mixins/restApi';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { isObjectLiteral } from '@/utils';
|
||||
|
|
|
@ -48,7 +48,7 @@ import { restApi } from '@/mixins/restApi';
|
|||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/mixins/showMessage';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '@jsplumb/core';
|
||||
import { AnchorPlacement, ConnectorOptions, Geometry, PaintAxis } from '@jsplumb/common';
|
||||
import { BezierSegment } from '@jsplumb/connector-bezier';
|
||||
import { isArray } from 'lodash';
|
||||
import { isArray } from 'lodash-es';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
|
||||
export interface N8nConnectorOptions extends ConnectorOptions {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { startCase } from 'lodash';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { defineStore } from 'pinia';
|
||||
import {
|
||||
INodePropertyCollection,
|
||||
|
|
|
@ -16,11 +16,6 @@ const ignoreChunks = [
|
|||
'@fontsource/open-sans',
|
||||
'normalize-wheel',
|
||||
'stream-browserify',
|
||||
'lodash.camelcase',
|
||||
'lodash.debounce',
|
||||
'lodash.get',
|
||||
'lodash.orderby',
|
||||
'lodash.set',
|
||||
];
|
||||
|
||||
const isScopedPackageToIgnore = (str: string) => /@codemirror\//.test(str);
|
||||
|
@ -44,11 +39,6 @@ function renderChunks() {
|
|||
|
||||
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;
|
||||
|
||||
export default mergeConfig(
|
||||
|
@ -78,9 +68,12 @@ export default mergeConfig(
|
|||
find: /^n8n-design-system\//,
|
||||
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',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-node-dev",
|
||||
"version": "0.94.1",
|
||||
"version": "0.95.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -4,7 +4,8 @@ import type { IDataObject, ILoadOptionsFunctions } from 'n8n-workflow';
|
|||
|
||||
import type { OptionsWithUri } from 'request';
|
||||
|
||||
import { flow, omit } from 'lodash';
|
||||
import flow from 'lodash.flow';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import type {
|
||||
AllFieldsUi,
|
||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
|||
INodePropertyOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
/**
|
||||
* Make an API request to Asana
|
||||
|
|
|
@ -12,7 +12,7 @@ import { jsonParse, NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import { awsApiRequestSOAP } from './GenericFunctions';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
export class AwsSnsTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { parseString } from 'xml2js';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { parseString } from 'xml2js';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
import { parseString } from 'xml2js';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
import type { IDataObject, IHttpRequestOptions } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
export async function awsApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
|
|
|
@ -15,7 +15,7 @@ import type {
|
|||
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
function getEndpointForService(
|
||||
service: string,
|
||||
|
|
|
@ -33,7 +33,8 @@ import type {
|
|||
} 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 {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { upperFirst } from 'lodash';
|
||||
import upperFirst from 'lodash.upperfirst';
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
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';
|
||||
|
||||
type PairToMatch = {
|
||||
|
|
|
@ -13,7 +13,8 @@ import type {
|
|||
import type { ICredentialDataDecryptedObject, IDataObject } 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 {
|
||||
AddressFixedCollection,
|
||||
|
|
|
@ -18,7 +18,7 @@ import { responderFields, respondersOperations } from './ResponderDescription';
|
|||
|
||||
import { jobFields, jobOperations } from './JobDescription';
|
||||
|
||||
import { upperFirst } from 'lodash';
|
||||
import upperFirst from 'lodash.upperfirst';
|
||||
|
||||
import type { IJob } from './AnalyzerInterface';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { set } from 'lodash';
|
||||
import set from 'lodash.set';
|
||||
|
||||
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 { get } from 'lodash';
|
||||
import get from 'lodash.get';
|
||||
|
||||
export async function customerIoApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
|
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { deepCopy, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { set } from 'lodash';
|
||||
import set from 'lodash.set';
|
||||
|
||||
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 = {
|
||||
customProperty: Array<{ field: string; value: string }>;
|
||||
|
|
|
@ -14,7 +14,7 @@ import { documentFields, documentOperations, indexFields, indexOperations } from
|
|||
|
||||
import type { DocumentGetAllOptions, FieldsUiValues } from './types';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
export class Elasticsearch implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
|
@ -22,7 +22,8 @@ import { connect as imapConnect, getParts } from 'imap-simple';
|
|||
import type { Source as ParserSource } from 'mailparser';
|
||||
import { simpleParser } from 'mailparser';
|
||||
|
||||
import _ from 'lodash';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import find from 'lodash.find';
|
||||
|
||||
export async function parseRawEmail(
|
||||
this: ITriggerFunctions,
|
||||
|
@ -241,7 +242,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||
if (credentials.secure) {
|
||||
tlsOptions.servername = credentials.host as string;
|
||||
}
|
||||
if (!_.isEmpty(tlsOptions)) {
|
||||
if (!isEmpty(tlsOptions)) {
|
||||
config.imap.tlsOptions = tlsOptions;
|
||||
}
|
||||
const conn = imapConnect(config).then(async (entry) => {
|
||||
|
@ -385,7 +386,7 @@ export class EmailReadImapV1 implements INodeType {
|
|||
) {
|
||||
staticData.lastMessageUid = message.attributes.uid;
|
||||
}
|
||||
const part = _.find(message.parts, { which: '' });
|
||||
const part = find(message.parts, { which: '' });
|
||||
|
||||
if (part === undefined) {
|
||||
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;
|
||||
}
|
||||
const part = _.find(message.parts, { which: 'TEXT' });
|
||||
const part = find(message.parts, { which: 'TEXT' });
|
||||
|
||||
if (part === undefined) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(tlsOptions)) {
|
||||
if (!isEmpty(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 { simpleParser } from 'mailparser';
|
||||
|
||||
import _ from 'lodash';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import find from 'lodash.find';
|
||||
import type { ICredentialsDataImap } from '../../../credentials/Imap.credentials';
|
||||
import { isCredentialsDataImap } from '../../../credentials/Imap.credentials';
|
||||
|
||||
|
@ -240,7 +241,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
if (credentials.secure) {
|
||||
tlsOptions.servername = credentials.host;
|
||||
}
|
||||
if (!_.isEmpty(tlsOptions)) {
|
||||
if (!isEmpty(tlsOptions)) {
|
||||
config.imap.tlsOptions = tlsOptions;
|
||||
}
|
||||
const connection = await imapConnect(config);
|
||||
|
@ -393,7 +394,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
) {
|
||||
staticData.lastMessageUid = message.attributes.uid;
|
||||
}
|
||||
const part = _.find(message.parts, { which: '' });
|
||||
const part = find(message.parts, { which: '' });
|
||||
|
||||
if (part === undefined) {
|
||||
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;
|
||||
}
|
||||
const part = _.find(message.parts, { which: 'TEXT' });
|
||||
const part = find(message.parts, { which: 'TEXT' });
|
||||
|
||||
if (part === undefined) {
|
||||
throw new NodeOperationError(this.getNode(), 'Email part could not be parsed.');
|
||||
|
@ -580,7 +581,7 @@ export class EmailReadImapV2 implements INodeType {
|
|||
tlsOptions.servername = credentials.host;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(tlsOptions)) {
|
||||
if (!isEmpty(tlsOptions)) {
|
||||
config.imap.tlsOptions = tlsOptions;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { campaignFields, campaignOperations } from './CampaignDescription';
|
|||
|
||||
import { contactListFields, contactListOperations } from './ContactListDescription';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
export class Emelia implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
|||
|
||||
import type { OptionsWithUri } from 'request';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
export async function freshserviceApiRequest(
|
||||
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