Initial commit to release

This commit is contained in:
Jan Oberhauser
2019-06-23 12:35:23 +02:00
commit 9cb9804eee
257 changed files with 42436 additions and 0 deletions

230
packages/workflow/LICENSE Normal file
View File

@@ -0,0 +1,230 @@
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant
of rights under the License will not include, and the License
does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required by
the License must also include this Commons Clause License
Condition notice.
Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
---------------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,13 @@
# n8n-workflow
![n8n.io - Workflow Automation](https://n8n.io/n8n-logo.png)
Workflow base code for n8n
```
npm install n8n-workflow
```
## License
[Apache 2.0 with Commons Clause](LICENSE)

View File

@@ -0,0 +1,52 @@
{
"name": "n8n-workflow",
"version": "0.1.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE",
"author": {
"name": "Jan Oberhauser",
"email": "jan@n8n.io"
},
"main": "dist/src/index",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "tsc",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"watch": "tsc --watch",
"test": "jest"
},
"files": [
"dist"
],
"devDependencies": {
"@types/express": "^4.16.1",
"@types/jest": "^23.3.2",
"@types/lodash.get": "^4.4.5",
"@types/node": "^10.10.1",
"jest": "^23.6.0",
"ts-jest": "^23.10.1",
"tslint": "^5.11.0",
"typescript": "~3.3.0"
},
"dependencies": {
"lodash.get": "^4.4.2",
"riot-tmpl": "^3.0.8"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testURL": "http://localhost/",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
]
}
}

View File

@@ -0,0 +1,552 @@
import { Workflow } from './Workflow';
import * as express from "express";
export interface IBinaryData {
[key: string]: string | undefined;
data: string;
mimeType: string;
fileName?: string;
fileExtension?: string;
}
export interface IConnection {
// The node the connection is to
node: string;
// The type of the input on destination node (for example "main")
type: string;
// The output/input-index of destination node (if node has multiple inputs/outputs of the same type)
index: number;
}
export interface IExecutionError {
message: string;
node?: string;
stack?: string;
}
// Get used to gives nodes access to credentials
export interface IGetCredentials {
get(type: string, name: string): Promise<ICredentialsEncrypted>;
}
// Defines which nodes are allowed to access the credentials and
// when that access got grented from which user
export interface ICredentialNodeAccess {
nodeType: string;
user?: string;
date?: number;
}
export interface ICredentialsDecrypted {
name: string;
type: string;
nodesAccess: ICredentialNodeAccess[];
data?: ICredentialDataDecryptedObject;
}
export interface ICredentialsEncrypted {
name: string;
type: string;
nodesAccess: ICredentialNodeAccess[];
data?: string;
}
export interface ICredentialType {
name: string;
displayName: string;
properties: INodeProperties[];
}
export interface ICredentialTypes {
credentialTypes?: {
[key: string]: ICredentialType
};
init(credentialTypes?: { [key: string]: ICredentialType }): Promise<void>;
getAll(): ICredentialType[];
getByName(credentialType: string): ICredentialType;
}
// The way the credentials get saved in the database (data encrypted)
export interface ICredentialData {
name: string;
data: string; // Contains the access data as encrypted JSON string
nodesAccess: ICredentialNodeAccess[];
}
// The encrypted credentials which the nodes can access
export type CredentialInformation = string | number | boolean;
// The encrypted credentials which the nodes can access
export interface ICredentialDataDecryptedObject {
[key: string]: CredentialInformation;
}
// First array index: The output/input-index (if node has multiple inputs/outputs of the same type)
// Second array index: The different connections (if one node is connected to multiple nodes)
export type NodeInputConnections = IConnection[][];
export interface INodeConnections {
// Input name
[key: string]: NodeInputConnections;
}
export interface IConnections {
// Node name
[key: string]: INodeConnections;
}
export type GenericValue = string | object | number | boolean | undefined | null;
export interface IDataObject {
[key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[];
}
export interface IExecuteData {
data: ITaskDataConnections;
node: INode;
}
export type IContextObject = {
[key: string]: any; // tslint:disable-line:no-any
};
export interface IExecuteContextData {
// Keys are: "flow" | "node:<NODE_NAME>"
[key: string]: IContextObject;
}
export interface IExecuteFunctions {
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData[];
getMode(): WorkflowExecuteMode;
getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getWorkflowStaticData(type: string): IDataObject;
getTimezone(): string;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
};
}
export interface IExecuteSingleFunctions {
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData;
getMode(): WorkflowExecuteMode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getTimezone(): string;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
};
}
export interface ILoadOptionsFunctions {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getTimezone(): string;
helpers: {
[key: string]: ((...args: any[]) => any) | undefined; //tslint:disable-line:no-any
};
}
export interface IHookFunctions {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getMode(): WorkflowExecuteMode;
getNodeWebhookUrl: (name: string) => string | undefined;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getTimezone(): string;
getWebhookDescription(name: string): IWebhookDescription | undefined;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
};
}
export interface ITriggerFunctions {
emit(data: INodeExecutionData[][]): void;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getMode(): WorkflowExecuteMode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getTimezone(): string;
getWorkflowStaticData(type: string): IDataObject;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
};
}
export interface IWebhookFunctions {
getBodyData(): IDataObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getHeaderData(): object;
getMode(): WorkflowExecuteMode;
getNodeParameter(parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; //tslint:disable-line:no-any
getQueryData(): object;
getRequestObject(): express.Request;
getResponseObject(): express.Response;
getTimezone(): string;
getWorkflowStaticData(type: string): IDataObject;
prepareOutputData(outputData: INodeExecutionData[], outputIndex?: number): Promise<INodeExecutionData[][]>;
helpers: {
[key: string]: (...args: any[]) => any //tslint:disable-line:no-any
};
}
export interface INodeCredentials {
[key: string]: string;
}
export interface INode {
name: string;
typeVersion: number;
type: string;
position: [number, number];
disabled?: boolean;
continueOnFail?: boolean;
parameters: INodeParameters;
credentials?: INodeCredentials;
}
export interface INodes {
[key: string]: INode;
}
export interface IObservableObject {
[key: string]: any; // tslint:disable-line:no-any
__dataChanged: boolean;
}
export interface IBinaryKeyData {
[key: string]: IBinaryData;
}
export interface INodeExecutionData {
[key: string]: IDataObject | IBinaryKeyData | undefined;
// TODO: Rename this one as json does not really fit as it is not json (which is a string) it is actually a JS object
json: IDataObject;
// json: object;
// json?: object;
binary?: IBinaryKeyData;
}
export interface INodeExecuteFunctions {
getExecuteTriggerFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): ITriggerFunctions;
getExecuteFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteFunctions;
getExecuteSingleFunctions(workflow: Workflow, runExecutionData: IRunExecutionData, runIndex: number, connectionInputData: INodeExecutionData[], inputData: ITaskDataConnections, node: INode, itemIndex: number, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IExecuteSingleFunctions;
getExecuteHookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean): IHookFunctions;
getExecuteWebhookFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode): IWebhookFunctions;
}
// The values a node property can have
export type NodeParameterValue = string | number | boolean;
export interface INodeParameters {
// TODO: Later also has to be possible to add multiple ones with the name name. So array has to be possible
[key: string]: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
}
export type NodePropertyTypes = 'boolean' | 'collection' | 'color' | 'dateTime' | 'fixedCollection' | 'json' | 'multiOptions' | 'number' | 'options' | 'string';
export interface INodePropertyTypeOptions {
alwaysOpenEditWindow?: boolean; // Supported by: string
loadOptionsMethod?: string; // Supported by: options
maxValue?: number; // Supported by: number
minValue?: number; // Supported by: number
multipleValues?: boolean; // Supported by: <All>
multipleValueButtonText?: string; // Supported when "multipleValues" set to true
numberPrecision?: number; // Supported by: number
numberStepSize?: number; // Supported by: number
password?: boolean; // Supported by: string
rows?: number; // Supported by: string
[key: string]: boolean | number | string | undefined;
}
export interface IDisplayOptions {
hide?: {
[key: string]: NodeParameterValue[];
};
show?: {
[key: string]: NodeParameterValue[];
};
}
export interface INodeProperties {
displayName: string;
name: string;
type: NodePropertyTypes;
typeOptions?: INodePropertyTypeOptions;
default: NodeParameterValue | INodeParameters | INodeParameters[] | NodeParameterValue[];
description?: string;
displayOptions?: IDisplayOptions;
options?: Array<INodePropertyOptions | INodeProperties | INodePropertyCollection >;
placeholder?: string;
isNodeSetting?: boolean;
noDataExpression?: boolean;
required?: boolean;
}
export interface INodePropertyOptions {
name: string;
value: string;
description?: string;
}
export interface INodePropertyCollection {
displayName: string;
name: string;
values: INodeProperties[];
}
export interface ITriggerResponse {
closeFunction?: () => Promise<void>;
// To manually trigger the run
manualTriggerFunction?: () => Promise<void>;
// Gets added automatically at manual workflow runs resolves with
// the first emitted data
manualTriggerResponse?: Promise<INodeExecutionData[][]>;
}
export interface INodeType {
description: INodeTypeDescription;
execute?(this: IExecuteFunctions): Promise<INodeExecutionData[][] | null>;
executeSingle?(this: IExecuteSingleFunctions): Promise<INodeExecutionData>;
trigger?(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>;
webhook?(this: IWebhookFunctions): Promise<IWebhookResonseData>;
hooks?: {
[key: string]: (this: IHookFunctions) => Promise<boolean>;
};
methods?: {
loadOptions?: {
[key: string]: (this: ILoadOptionsFunctions) => Promise<INodePropertyOptions[]>;
}
};
webhookMethods?: {
[key: string]: IWebhookSetupMethods;
};
}
export type WebhookSetupMethodNames = 'checkExists' | 'create' | 'delete';
export interface IWebhookSetupMethods {
[key: string]: ((this: IHookFunctions) => Promise<boolean>) | undefined;
checkExists?: (this: IHookFunctions) => Promise<boolean>;
create?: (this: IHookFunctions) => Promise<boolean>;
delete?: (this: IHookFunctions) => Promise<boolean>;
}
export interface INodeCredentialDescription {
name: string;
required?: boolean;
displayOptions?: IDisplayOptions;
}
export type INodeIssueTypes = 'credentials' | 'execution' | 'parameters' | 'typeUnknown';
export interface INodeIssueObjectProperty {
[key: string]: string[];
}
export interface INodeIssueData {
node: string;
type: INodeIssueTypes;
value: boolean | string | string[] | INodeIssueObjectProperty;
}
export interface INodeIssues {
execution?: boolean;
credentials?: INodeIssueObjectProperty;
parameters?: INodeIssueObjectProperty;
typeUnknown?: boolean;
[key: string]: undefined | boolean | INodeIssueObjectProperty;
}
export interface IWorfklowIssues {
[key: string]: INodeIssues;
}
export interface INodeTypeDescription {
displayName: string;
name: string;
icon?: string;
group: string[];
version: number;
description: string;
defaults: INodeParameters;
inputs: string[];
outputs: string[];
outputNames?: string[];
properties: INodeProperties[];
credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow
hooks?: {
[key: string]: INodeHookDescription[] | undefined;
activate?: INodeHookDescription[];
deactivate?: INodeHookDescription[];
};
webhooks?: IWebhookDescription[];
}
export interface INodeHookDescription {
method: string;
}
export interface IWebhookData {
httpMethod: WebhookHttpMethod;
node: string;
path: string;
webhookDescription: IWebhookDescription;
workflow: Workflow;
workflowExecuteAdditionalData: IWorkflowExecuteAdditionalData;
}
export interface IWebhookDescription {
[key: string]: WebhookHttpMethod | WebhookResponseMode | string | undefined;
httpMethod: WebhookHttpMethod | string;
name: string;
path: string;
responseBinaryPropertyName?: string;
reponseMode?: WebhookResponseMode | string;
reponseData?: WebhookResponseData | string;
}
export type WebhookHttpMethod = 'GET' | 'POST';
export interface IWebhookResonseData {
workflowData?: INodeExecutionData[][];
webhookResponse?: any; // tslint:disable-line:no-any
noWebhookResponse?: boolean;
}
export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary';
export type WebhookResponseMode = 'onReceived' | 'lastNode';
export interface INodeTypesObject {
[key: string]: INodeType;
}
export interface INodeTypes {
init(nodeTypes?: INodeTypesObject): Promise<void>;
getAll(): INodeType[];
getByName(nodeType: string): INodeType | undefined;
}
export interface IRun {
data: IRunExecutionData;
finished?: boolean;
mode: WorkflowExecuteMode;
startedAt: number;
stoppedAt: number;
}
// Contains all the data which is needed to execute a workflow and so also to
// start restart it again after it did fail.
// The RunData, ExecuteData and WaitForExecution contain often the same data.
export interface IRunExecutionData {
startData?: {
destinationNode?: string;
runNodeFilter?: string[];
};
resultData: {
error?: IExecutionError;
runData: IRunData;
lastNodeExecuted?: string;
};
executionData?: {
contextData: IExecuteContextData;
nodeExecutionStack: IExecuteData[];
waitingExecution: IWaitingForExecution;
};
}
export interface IRunData {
// node-name: result-data
[key: string]: ITaskData[];
}
// The data that gets returned when a node runs
export interface ITaskData {
startTime: number;
executionTime: number;
data?: ITaskDataConnections;
error?: IExecutionError;
}
// The data for al the different kind of connectons (like main) and all the indexes
export interface ITaskDataConnections {
// Key for each input type and because there can be multiple inputs of the same type it is an array
// null is also allowed because if we still need data for a later while executing the workflow set teompoary to null
// the nodes get as input TaskDataConnections which is identical to this one except that no null is allowed.
[key: string]: Array<INodeExecutionData[] | null>;
}
// Keeps data while workflow gets executed and allows when provided to restart execution
export interface IWaitingForExecution {
// Node name
[key: string]: {
// Run index
[key: number]: ITaskDataConnections
};
}
export interface IWorkflowCredentials {
// Credential type
[key: string]: {
// Name
[key: string]: ICredentialsEncrypted;
};
}
export interface IWorkflowExecuteHooks {
afterExecute? (data: IRun, waitingExecutionData: IWaitingForExecution): Promise<void>;
}
export interface IWorkflowExecuteAdditionalData {
credentials: IWorkflowCredentials;
encryptionKey: string;
hooks?: {
[key: string]: Array<((...args: any[]) => Promise<void>)> | undefined; // tslint:disable-line:no-any
nodeExecuteAfter?: Array<((executionId: string, nodeName: string, data: ITaskData) => Promise<void>)>;
nodeExecuteBefore?: Array<((nodeName: string, executionId: string) => Promise<void>)>;
workflowExecuteAfter?: Array<((data: IRun, executionId: string) => Promise<void>)>;
workflowExecuteBefore?: Array<((executionId: string) => Promise<void>)>;
};
httpResponse?: express.Response;
httpRequest?: express.Request;
timezone: string;
webhookBaseUrl: string;
webhookTestBaseUrl: string;
}
export type WorkflowExecuteMode = 'cli' | 'error' | 'internal' | 'manual' | 'retry' | 'trigger' | 'webhook';
export interface IWorkflowSettings {
[key: string]: IDataObject | string | number | boolean | undefined;
}

View File

@@ -0,0 +1,703 @@
import {
IContextObject,
INodeCredentialDescription,
INode,
INodeExecutionData,
INodeIssues,
INodeIssueObjectProperty,
INodeParameters,
INodeProperties,
INodePropertyCollection,
INodeType,
IRunExecutionData,
IWebhookData,
NodeParameterValue,
WebhookHttpMethod,
IWorkflowExecuteAdditionalData,
} from './Interfaces';
import {
Workflow
} from './Workflow';
import { get } from 'lodash';
/**
* Returns if the parameter should be displayed or not
*
* @export
* @param {INodeParameters} nodeValues The data on the node which decides if the parameter
* should be displayed
* @param {(INodeProperties | INodeCredentialDescription)} parameter The parameter to check if it should be displayed
* @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data
* @returns
*/
export function displayParameter(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, nodeValuesRoot?: INodeParameters) {
if (!parameter.displayOptions) {
return true;
}
nodeValuesRoot = nodeValuesRoot || nodeValues;
let value;
if (parameter.displayOptions.show) {
// All the defined rules have to match to display parameter
for (const propertyName of Object.keys(parameter.displayOptions.show)) {
if (propertyName.charAt(0) === '/') {
// Get the value from the root of the node
value = nodeValuesRoot[propertyName.slice(1)];
} else {
// Get the value from current level
value = nodeValues[propertyName];
}
if (value === undefined || !parameter.displayOptions.show[propertyName].includes(value as string)) {
return false;
}
}
return true;
}
if (parameter.displayOptions.hide) {
// Any of the defined hide rules have to match to hide the paramter
for (const propertyName of Object.keys(parameter.displayOptions.hide)) {
if (propertyName.charAt(0) === '/') {
// Get the value from the root of the node
value = nodeValuesRoot[propertyName.slice(1)];
} else {
// Get the value from current level
value = nodeValues[propertyName];
}
if (value !== undefined && parameter.displayOptions.hide[propertyName].includes(value as string)) {
return false;
}
}
}
return true;
}
/**
* Returns if the given parameter should be displayed or not considering the path
* to the properties
*
* @export
* @param {INodeParameters} nodeValues The data on the node which decides if the parameter
* should be displayed
* @param {(INodeProperties | INodeCredentialDescription)} parameter The parameter to check if it should be displayed
* @param {string} path The path to the property
* @returns
*/
export function displayParameterPath(nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string) {
let resolvedNodeValues = nodeValues;
if (path !== '') {
resolvedNodeValues = get(
nodeValues,
path,
) as INodeParameters;
}
// Get the root parameter data
let nodeValuesRoot = nodeValues;
if (path && path.split('.').indexOf('parameters') === 0) {
nodeValuesRoot = get(
nodeValues,
'parameters',
) as INodeParameters;
}
return displayParameter(resolvedNodeValues, parameter, nodeValuesRoot);
}
/**
* Returns the context data
*
* @export
* @param {IRunExecutionData} runExecutionData The run execution data
* @param {string} type The data type. "node"/"flow"
* @param {INode} [node] If type "node" is set the node to return the context of has to be supplied
* @returns {IContextObject}
*/
export function getContext(runExecutionData: IRunExecutionData, type: string, node?: INode): IContextObject {
if (runExecutionData.executionData === undefined) {
// TODO: Should not happen leave it for test now
throw new Error('The "executionData" is not initialized!');
}
let key: string;
if (type === 'flow') {
key = 'flow';
} else if (type === 'node') {
if (node === undefined) {
throw new Error(`The request data of context type "node" the node parameter has to be set!`);
}
key = `node:${node.name}`;
} else {
throw new Error(`The context type "${type}" is not know. Only "flow" and node" are supported!`);
}
if (runExecutionData.executionData.contextData[key] === undefined) {
runExecutionData.executionData.contextData[key] = {};
}
return runExecutionData.executionData.contextData[key];
}
/**
* Returns the node parameter values. Depending on the settings it either just returns the none
* default values or it applies all the default values.
*
* @export
* @param {INodeProperties[]} nodePropertiesArray The properties which exist and their settings
* @param {INodeParameters} nodeValues The node parameter data
* @param {boolean} returnDefaults If default values get added or only none default values returned
* @param {boolean} returnNoneDisplayed If also values which should not be displayed should be returned
* @param {boolean} [onlySimpleTypes=false] If only simple types should be resolved
* @param {boolean} [dataIsResolved=false] If nodeValues are already fully resolved (so that all default values got added already)
* @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data
* @returns {(INodeParameters | null)}
*/
export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeValues: INodeParameters, returnDefaults: boolean, returnNoneDisplayed: boolean, onlySimpleTypes = false, dataIsResolved = false, nodeValuesRoot?: INodeParameters, parentType?: string): INodeParameters | null {
const nodeParameters: INodeParameters = {};
let nodeValuesDisplayCheck = nodeValues;
if (dataIsResolved !== true && returnNoneDisplayed === false) {
nodeValuesDisplayCheck = getNodeParameters(nodePropertiesArray, nodeValues, true, true, true, true, nodeValuesRoot, parentType) as INodeParameters;
}
nodeValuesRoot = nodeValuesRoot || nodeValuesDisplayCheck;
for (const nodeProperties of nodePropertiesArray) {
if (nodeValues[nodeProperties.name] === undefined && (returnDefaults === false || parentType === 'collection')) {
// The value is not defined so go to the next
continue;
}
if (returnNoneDisplayed === false && !displayParameter(nodeValuesDisplayCheck, nodeProperties, nodeValuesRoot)) {
if (returnNoneDisplayed === false) {
continue;
}
if (returnDefaults === false) {
continue;
}
}
if (!['collection', 'fixedCollection'].includes(nodeProperties.type)) {
// Is a simple property so can be set as it is
if (returnDefaults === true) {
// Set also when it has the default value
if (['boolean', 'number'].includes(nodeProperties.type)) {
// Boolean and numbers are special as false and 0 are valid values
// and should not be replaced with default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] !== undefined ? nodeValues[nodeProperties.name] : nodeProperties.default;
} else {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] || nodeProperties.default;
}
} else if (nodeValues[nodeProperties.name] !== nodeProperties.default || (nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')) {
// Set only if it is different to the default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
continue;
}
}
if (onlySimpleTypes === true) {
// It is only supposed to resolve the simple types. So continue.
continue;
}
// Is a complex property so check lower levels
let tempValue: INodeParameters | null;
if (nodeProperties.type === 'collection') {
// Is collection
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues === true) {
// Multiple can be set so will be an array
// Return directly the values like they are
if (nodeValues[nodeProperties.name] !== undefined) {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
} else if (returnDefaults === true) {
// Does not have values defined but defaults should be returned which is in the
// case of a collection with multipleValues always an empty array
nodeParameters[nodeProperties.name] = [];
}
} else {
if (nodeValues[nodeProperties.name] !== undefined) {
// Has values defined so get them
const tempNodeParameters = getNodeParameters(nodeProperties.options as INodeProperties[], nodeValues[nodeProperties.name] as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type);
if (tempNodeParameters !== null) {
nodeParameters[nodeProperties.name] = tempNodeParameters;
}
} else if (returnDefaults === true) {
// Does not have values defined but defaults should be returned
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
}
}
} else if (nodeProperties.type === 'fixedCollection') {
// Is fixedCollection
const collectionValues: INodeParameters = {};
let tempNodeParameters: INodeParameters;
let tempNodePropertiesArray: INodeProperties[];
let nodePropertyOptions: INodePropertyCollection | undefined;
let propertyValues = nodeValues[nodeProperties.name];
if (returnDefaults === true) {
if (propertyValues === undefined) {
propertyValues = JSON.parse(JSON.stringify(nodeProperties.default));
}
}
// Itterate over all collections
for (const itemName of Object.keys(propertyValues)) {
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues === true) {
// Multiple can be set so will be an array
const tempArrayValue: INodeParameters[] = [];
// Itterate over all items as it contains multiple ones
for (const nodeValue of (propertyValues as INodeParameters)[itemName] as INodeParameters[]) {
nodePropertyOptions = nodeProperties!.options!.find((nodePropertyOptions) => nodePropertyOptions.name === itemName) as INodePropertyCollection;
if (nodePropertyOptions === undefined) {
throw new Error(`Could not find property option "${itemName}" for "${nodeProperties.name}"`);
}
tempNodePropertiesArray = (nodePropertyOptions as INodePropertyCollection).values!;
tempValue = getNodeParameters(tempNodePropertiesArray, nodeValue as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type);
if (tempValue !== null) {
tempArrayValue.push(tempValue);
}
}
collectionValues[itemName] = tempArrayValue;
} else {
// Only one can be set so is an object of objects
tempNodeParameters = {};
// Get the options of the current item
const nodePropertyOptions = nodeProperties!.options!.find((data) => data.name === itemName);
if (nodePropertyOptions !== undefined) {
tempNodePropertiesArray = (nodePropertyOptions as INodePropertyCollection).values!;
tempValue = getNodeParameters(tempNodePropertiesArray, (nodeValues[nodeProperties.name] as INodeParameters)[itemName] as INodeParameters, returnDefaults, returnNoneDisplayed, false, false, nodeValuesRoot, nodeProperties.type);
if (tempValue !== null) {
Object.assign(tempNodeParameters, tempValue);
}
}
if (Object.keys(tempNodeParameters).length !== 0) {
collectionValues[itemName] = tempNodeParameters;
}
}
}
if (Object.keys(collectionValues).length !== 0 || returnDefaults === true) {
// Set only if value got found
if (returnDefaults === true) {
// Set also when it has the default value
if (collectionValues === undefined) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
} else {
nodeParameters[nodeProperties.name] = collectionValues;
}
} else if (collectionValues !== nodeProperties.default) {
// Set only if values got found and it is not the default
nodeParameters[nodeProperties.name] = collectionValues;
}
}
}
}
return nodeParameters;
}
/**
* Brings the output data in a format that can be returned from a node
*
* @export
* @param {INodeExecutionData[]} outputData
* @param {number} [outputIndex=0]
* @returns {Promise<INodeExecutionData[][]>}
*/
export async function prepareOutputData(outputData: INodeExecutionData[], outputIndex = 0): Promise<INodeExecutionData[][]> {
// TODO: Check if node has output with that index
const returnData = [];
for (let i = 0; i < outputIndex; i++) {
returnData.push([]);
}
returnData.push(outputData);
return returnData;
}
/**
* Returns all the webhooks which should be created for the give node
*
* @export
*
* @param {INode} node
* @returns {IWebhookData[]}
*/
export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData): IWebhookData[] {
if (node.disabled === true) {
// Node is disabled so webhooks will also not be enabled
return [];
}
if (workflow.id === undefined) {
// Workflow has no id which means it is not saved and so webhooks
// will not be enabled
return [];
}
const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.description.webhooks === undefined) {
// Node does not have any webhooks so return
return [];
}
const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) {
let nodeWebhookPath = workflow.getWebhookParameterValue(node, webhookDescription, 'path', 'GET');
if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflow.id}".`);
continue;
}
if (nodeWebhookPath.charAt(0) === '/') {
nodeWebhookPath = nodeWebhookPath.slice(1);
}
const path = getNodeWebhookPath(workflow.id, node, nodeWebhookPath);
const httpMethod = workflow.getWebhookParameterValue(node, webhookDescription, 'httpMethod', 'GET');
if (httpMethod === undefined) {
// TODO: Use a proper logger
console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflow.id}" could not be added because the httpMethod is not defined.`);
continue;
}
returnData.push({
httpMethod: httpMethod as WebhookHttpMethod,
node: node.name,
path,
webhookDescription,
workflow,
workflowExecuteAdditionalData: additionalData,
});
}
return returnData;
}
/**
* Returns the webhook path
*
* @export
* @param {string} workflowId
* @param {string} nodeTypeName
* @param {string} path
* @returns {string}
*/
export function getNodeWebhookPath(workflowId: string, node: INode, path: string): string {
return `${workflowId}/${encodeURIComponent(node.name.toLowerCase())}/${path}`;
}
/**
* Returns the webhook URL
*
* @export
* @param {string} baseUrl
* @param {string} workflowId
* @param {string} nodeTypeName
* @param {string} path
* @returns {string}
*/
export function getNodeWebhookUrl(baseUrl: string, workflowId: string, node: INode, path: string): string {
// return `${baseUrl}/${workflowId}/${nodeTypeName}/${path}`;
return `${baseUrl}/${getNodeWebhookPath(workflowId, node, path)}`;
}
/**
* Returns all the parameter-issues of the node
*
* @export
* @param {INodeProperties[]} nodePropertiesArray The properties of the node
* @param {INode} node The data of the node
* @returns {(INodeIssues | null)}
*/
export function getNodeParametersIssues(nodePropertiesArray: INodeProperties[], node: INode): INodeIssues | null {
const foundIssues: INodeIssues = {};
let propertyIssues: INodeIssues;
for (const nodeProperty of nodePropertiesArray) {
propertyIssues = getParameterIssues(nodeProperty, node.parameters, '');
mergeIssues(foundIssues, propertyIssues);
}
if (Object.keys(foundIssues).length === 0) {
return null;
}
return foundIssues;
}
/**
* Returns the issues of the node as string
*
* @export
* @param {INodeIssues} issues The issues of the node
* @param {INode} node The node
* @returns {string[]}
*/
export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[] {
const nodeIssues = [];
if (issues.execution !== undefined) {
nodeIssues.push(`Execution Error.`);
}
const objectProperties = [
'parameters',
'credentials',
];
let issueText: string, parameterName: string;
for (const propertyName of objectProperties) {
if (issues[propertyName] !== undefined) {
for (parameterName of Object.keys(issues[propertyName] as object)) {
for (issueText of (issues[propertyName] as INodeIssueObjectProperty)[parameterName]) {
nodeIssues.push(issueText);
}
}
}
}
if (issues.typeUnknown !== undefined) {
if (node !== undefined) {
nodeIssues.push(`Node Type "${node.type}" is not known.`);
} else {
nodeIssues.push(`Node Type is not known.`);
}
}
return nodeIssues;
}
/**
* Adds an issue if the parameter is not defined
*
* @export
* @param {INodeIssues} foundIssues The already found issues
* @param {INodeProperties} nodeProperties The properties of the node
* @param {NodeParameterValue} value The value of the parameter
*/
export function addToIssuesIfMissing(foundIssues: INodeIssues, nodeProperties: INodeProperties, value: NodeParameterValue) {
// TODO: Check what it really has when undefined
if ((nodeProperties.type === 'string' && (value === '' || value === undefined)) ||
(nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) ||
(nodeProperties.type === 'dateTime' && value === undefined)) {
// Parameter is requried but empty
if (foundIssues.parameters === undefined) {
foundIssues.parameters = {};
}
if (foundIssues.parameters[nodeProperties.name] === undefined) {
foundIssues.parameters[nodeProperties.name] = [];
}
foundIssues.parameters[nodeProperties.name].push(`Parameter "${nodeProperties.displayName}" is required.`);
}
}
/**
* Returns the parameter value
*
* @export
* @param {INodeParameters} nodeValues The values of the node
* @param {string} parameterName The name of the parameter to return the value of
* @param {string} path The path to the properties
* @returns
*/
export function getParameterValueByPath(nodeValues: INodeParameters, parameterName: string, path: string) {
return get(
nodeValues,
path ? path + '.' + parameterName : parameterName,
);
}
/**
* Returns all the issues with the given node-values
*
* @export
* @param {INodeProperties} nodeProperties The properties of the node
* @param {INodeParameters} nodeValues The values of the node
* @param {string} path The path to the properties
* @returns {INodeIssues}
*/
export function getParameterIssues(nodeProperties: INodeProperties, nodeValues: INodeParameters, path: string): INodeIssues {
const foundIssues: INodeIssues = {};
let value;
if (nodeProperties.required === true) {
if (displayParameterPath(nodeValues, nodeProperties, path)) {
value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues !== undefined) {
// Multiple can be set so will be an array
if (Array.isArray(value)) {
for (const singleValue of value as NodeParameterValue[]) {
addToIssuesIfMissing(foundIssues, nodeProperties, singleValue as NodeParameterValue);
}
}
} else {
// Only one can be set so will be a single value
addToIssuesIfMissing(foundIssues, nodeProperties, value as NodeParameterValue);
}
}
}
// Check if there are any child parameters
if (nodeProperties.options === undefined) {
// There are none so nothing else to check
return foundIssues;
}
// Check the child parameters
// Important:
// Checks the child properties only if the property is defined on current level.
// That means that the required flag works only for the current level only. If
// it is set on a lower level it means that the property is only required in case
// the parent property got set.
let basePath = path ? `${path}.` : '';
const checkChildNodeProperties: Array<{
basePath: string;
data: INodeProperties;
}> = [];
// Collect all the properties to check
if (nodeProperties.type === 'collection') {
for (const option of nodeProperties.options) {
checkChildNodeProperties.push({
basePath,
data: option as INodeProperties,
});
}
} else if (nodeProperties.type === 'fixedCollection') {
basePath = basePath ? `${basePath}.` : '' + nodeProperties.name + '.';
let propertyOptions: INodePropertyCollection;
for (propertyOptions of nodeProperties.options as INodePropertyCollection[]) {
// Check if the option got set and if not skip it
value = getParameterValueByPath(nodeValues, propertyOptions.name, basePath.slice(0, -1));
if (value === undefined) {
continue;
}
if (nodeProperties.typeOptions !== undefined && nodeProperties.typeOptions.multipleValues !== undefined) {
// Multiple can be set so will be an array of objects
if (Array.isArray(value)) {
for (let i = 0; i < (value as INodeParameters[]).length; i++) {
for (const option of propertyOptions.values) {
checkChildNodeProperties.push({
basePath: `${basePath}${propertyOptions.name}[${i}]`,
data: option as INodeProperties,
});
}
}
}
} else {
// Only one can be set so will be an object
for (const option of propertyOptions.values) {
checkChildNodeProperties.push({
basePath: basePath + propertyOptions.name,
data: option as INodeProperties,
});
}
}
}
} else {
// For all other types there is nothing to check so return
return foundIssues;
}
let propertyIssues;
for (const optionData of checkChildNodeProperties) {
propertyIssues = getParameterIssues(optionData.data as INodeProperties, nodeValues, optionData.basePath);
mergeIssues(foundIssues, propertyIssues);
}
return foundIssues;
}
/**
* Merges multiple NodeIssues together
*
* @export
* @param {INodeIssues} destination The issues to merge into
* @param {(INodeIssues | null)} source The issues to merge
* @returns
*/
export function mergeIssues(destination: INodeIssues, source: INodeIssues | null) {
if (source === null) {
// Nothing to merge
return;
}
if (source.execution === true) {
destination.execution = true;
}
const objectProperties = [
'parameters',
'credentials',
];
let destinationProperty: INodeIssueObjectProperty;
for (const propertyName of objectProperties) {
if (source[propertyName] !== undefined) {
if (destination[propertyName] === undefined) {
destination[propertyName] = {};
}
let parameterName: string;
for (parameterName of Object.keys(source[propertyName] as INodeIssueObjectProperty)) {
destinationProperty = destination[propertyName] as INodeIssueObjectProperty;
if (destinationProperty[parameterName] === undefined) {
destinationProperty[parameterName] = [];
}
destinationProperty[parameterName].push.apply(destinationProperty[parameterName], (source[propertyName] as INodeIssueObjectProperty)[parameterName]);
}
}
}
if (source.typeUnknown === true) {
destination.typeUnknown = true;
}
}

View File

@@ -0,0 +1,54 @@
import {
IDataObject,
IObservableObject,
} from './';
export interface IObservableOptions {
ignoreEmptyOnFirstChild?: boolean;
}
export function create(target: IDataObject, parent?: IObservableObject, option?: IObservableOptions, depth?: number): IDataObject {
depth = depth || 0;
// Make all the children of target also observeable
for (const key in target) {
if (typeof target[key] === 'object') {
target[key] = create(target[key] as IDataObject, (parent || target) as IObservableObject, option, depth + 1);
}
}
Object.defineProperty(target, '__dataChanged', {
value: false,
writable: true,
});
return new Proxy(target, {
deleteProperty(target, name) {
if (parent === undefined) {
// If no parent is given mark current data as changed
(target as IObservableObject).__dataChanged = true;
} else {
// If parent is given mark the parent data as changed
parent.__dataChanged = true;
}
return Reflect.deleteProperty(target, name);
},
get(target, name, receiver) {
return Reflect.get(target, name, receiver);
},
set(target, name, value) {
if (parent === undefined) {
// If no parent is given mark current data as changed
if (option !== undefined && option.ignoreEmptyOnFirstChild === true && depth === 0
&& target[name.toString()] === undefined && typeof value === 'object' && Object.keys(value).length === 0) {
} else {
(target as IObservableObject).__dataChanged = true;
}
} else {
// If parent is given mark the parent data as changed
parent.__dataChanged = true;
}
return Reflect.set(target, name, value);
},
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
import {
IDataObject,
INodeExecutionData,
NodeHelpers,
IRunExecutionData,
Workflow,
} from './';
export class WorkflowDataProxy {
private workflow: Workflow;
private runExecutionData: IRunExecutionData | null;
private runIndex: number;
private itemIndex: number;
private activeNodeName: string;
private connectionInputData: INodeExecutionData[];
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[]) {
this.workflow = workflow;
this.runExecutionData = runExecutionData;
this.runIndex = runIndex;
this.itemIndex = itemIndex;
this.activeNodeName = activeNodeName;
this.connectionInputData = connectionInputData;
}
/**
* Returns a proxy which allows to query context data of a given node
*
* @private
* @param {string} nodeName The name of the node to get the context from
* @returns
* @memberof WorkflowDataProxy
*/
private nodeContextGetter(nodeName: string) {
const that = this;
const node = this.workflow.nodes[nodeName];
return new Proxy({}, {
ownKeys(target) {
if (Reflect.ownKeys(target).length === 0) {
// Target object did not get set yet
Object.assign(target, NodeHelpers.getContext(that.runExecutionData!, 'node', node));
}
return Reflect.ownKeys(target);
},
get(target, name, receiver) {
name = name.toString();
const contextData = NodeHelpers.getContext(that.runExecutionData!, 'node', node);
if (!contextData.hasOwnProperty(name)) {
// Parameter does not exist on node
throw new Error(`Could not find parameter "${name}" on context of node "${nodeName}"`);
}
return contextData[name];
}
});
}
/**
* Returns a proxy which allows to query parameter data of a given node
*
* @private
* @param {string} nodeName The name of the node to query data from
* @returns
* @memberof WorkflowDataGetter
*/
private nodeParameterGetter(nodeName: string) {
const that = this;
const node = this.workflow.nodes[nodeName];
return new Proxy(node.parameters, {
ownKeys(target) {
return Reflect.ownKeys(target);
},
get(target, name, receiver) {
name = name.toString();
if (!node.parameters.hasOwnProperty(name)) {
// Parameter does not exist on node
throw new Error(`Could not find parameter "${name}" on node "${nodeName}"`);
}
const returnValue = node.parameters[name];
if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
// The found value is an expression so resolve it
return that.workflow.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData);
}
return returnValue;
}
});
}
/**
* Returns a proxy which allows to query data of a given node
*
* @private
* @param {string} nodeName The name of the node query data from
* @param {boolean} [shortSyntax=false] If short syntax got used
* @returns
* @memberof WorkflowDataGetter
*/
private nodeDataGetter(nodeName: string, shortSyntax = false) {
const that = this;
const node = this.workflow.nodes[nodeName];
if (!node) {
throw new Error(`The node "${nodeName}" does not exist!`);
}
return new Proxy({}, {
get(target, name, receiver) {
name = name.toString();
if (['binary', 'data'].includes(name)) {
let executionData: INodeExecutionData[];
if (shortSyntax === false) {
// Long syntax got used to return data from node in path
if (that.runExecutionData === null) {
throw new Error(`Workflow did not run so do not have any execution-data.`);
}
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
throw new Error(`No execution data found for node "${nodeName}"`);
}
if (that.runExecutionData.resultData.runData[nodeName].length < that.runIndex) {
throw new Error(`No execution data found for run "${that.runIndex}" of node "${nodeName}"`);
}
const taskData = that.runExecutionData.resultData.runData[nodeName][that.runIndex].data!;
if (taskData.main === null || !taskData.main.length || taskData.main[0] === null) {
// throw new Error(`No data found for item-index: "${itemIndex}"`);
throw new Error(`No data found from "main" input.`);
}
// Currently it is only possible to reference data from the first input
// so we hardcode it.
executionData = taskData.main[0] as INodeExecutionData[];
} else {
// Short syntax got used to return data from active node
// TODO: Here have to generate connection Input data for the current node by itself
// Data needed:
// #- the run-index
// - node which did send data (has to be the one from last recent execution)
// - later also the name of the input and its index (currently not needed as it is always "main" and index "0")
executionData = that.connectionInputData;
}
if (executionData.length <= that.itemIndex) {
throw new Error(`No data found for item-index: "${that.itemIndex}"`);
}
if (name === 'data') {
// JSON-Data
return executionData[that.itemIndex].json;
} else if (name === 'binary') {
// Binary-Data
if (!executionData[that.itemIndex].binary) {
throw new Error(`No binary data for node "${nodeName}" has been found!`);
}
const returnData: IDataObject = {};
const binaryKeyData = executionData[that.itemIndex].binary!;
for (const keyName of Object.keys(binaryKeyData)) {
returnData[keyName] = {};
const binaryData = binaryKeyData[keyName];
for (const propertyName in binaryData) {
if (propertyName === 'data') {
// Skip the data property
continue;
}
(returnData[keyName] as IDataObject)[propertyName] = binaryData[propertyName];
}
}
return returnData;
}
} else if (name === 'context') {
return that.nodeContextGetter(nodeName);
} else if (name === 'parameter') {
// Get node parameter data
return that.nodeParameterGetter(nodeName);
}
return Reflect.get(target, name, receiver);
}
});
}
/**
* Returns a proxy to query data from the environment
*
* @private
* @returns
* @memberof WorkflowDataGetter
*/
private envGetter() {
return new Proxy({}, {
get(target, name, receiver) {
return process.env[name.toString()];
}
});
}
/**
* Returns a proxy to query data of all nodes
*
* @private
* @returns
* @memberof WorkflowDataGetter
*/
private nodeGetter() {
const that = this;
return new Proxy({}, {
get(target, name, receiver) {
return that.nodeDataGetter(name.toString());
}
});
}
/**
* Returns the data proxy object which allows to query data from current run
*
* @returns
* @memberof WorkflowDataGetter
*/
getDataProxy() {
const that = this;
const base = {
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$node: this.nodeGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
};
return new Proxy(base, {
get(target, name, receiver) {
if (name === '$data') {
// @ts-ignore
return that.nodeDataGetter(that.activeNodeName, true).data;
} else if (name === '$binary') {
// @ts-ignore
return that.nodeDataGetter(that.activeNodeName, true).binary;
}
return Reflect.get(target, name, receiver);
}
});
}
}

View File

@@ -0,0 +1,10 @@
export * from './Interfaces';
export * from './Workflow';
export * from './WorkflowDataProxy';
import * as NodeHelpers from './NodeHelpers';
import * as ObservableObject from './ObservableObject';
export {
NodeHelpers,
ObservableObject,
};

View File

@@ -0,0 +1,109 @@
import {
INodeType,
INodeTypes,
INodeTypesObject,
} from '../src';
class NodeTypesClass implements INodeTypes {
nodeTypes: INodeTypesObject = {
'test.set': {
description: {
displayName: 'Set',
name: 'set',
group: ['input'],
version: 1,
description: 'Sets a value',
defaults: {
name: 'Set',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Value1',
name: 'value1',
type: 'string',
default: 'default-value1',
},
{
displayName: 'Value2',
name: 'value2',
type: 'string',
default: 'default-value2',
}
]
}
},
'test.setMulti': {
description: {
displayName: 'Set Multi',
name: 'setMulti',
group: ['input'],
version: 1,
description: 'Sets multiple values',
defaults: {
name: 'Set Multi',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Values',
name: 'values',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'string',
displayName: 'String',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: 'propertyName',
placeholder: 'Name of the property to write data to.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'The string value to write in the property.',
},
]
},
],
},
]
}
},
};
async init(nodeTypes: INodeTypesObject): Promise<void> { }
getAll(): INodeType[] {
return Object.values(this.nodeTypes);
}
getByName(nodeType: string): INodeType {
return this.nodeTypes[nodeType];
}
}
let nodeTypesInstance: NodeTypesClass | undefined;
export function NodeTypes(): NodeTypesClass {
if (nodeTypesInstance === undefined) {
nodeTypesInstance = new NodeTypesClass();
nodeTypesInstance.init({});
}
return nodeTypesInstance;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
import {
IDataObject,
ObservableObject,
} from '../src';
describe('ObservableObject', () => {
test('should recognize that item on parent level got added (init empty)', () => {
const testObject = ObservableObject.create({});
expect(testObject.__dataChanged).toBeFalsy();
testObject.a = {};
expect(testObject.__dataChanged).toBeTruthy();
// Make sure that "__dataChanged" does not returned as a key
expect(Object.keys(testObject)).toEqual(['a']);
});
test('should not recognize that item on parent level changed if it is empty object and option "ignoreEmptyOnFirstChild" === true (init empty)', () => {
const testObject = ObservableObject.create({}, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
testObject.a = {};
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a).toEqual({});
});
test('should recognize that item on parent level changed if it is not empty object and option "ignoreEmptyOnFirstChild" === true (init empty)', () => {
const testObject = ObservableObject.create({}, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
testObject.a = { b: 2 };
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual({ b: 2 });
});
test('should not recognize that item on parent level changed if it is empty array and option "ignoreEmptyOnFirstChild" === true (init empty)', () => {
const testObject = ObservableObject.create({}, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
testObject.a = [];
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a).toEqual([]);
});
test('should recognize that item on parent level changed if it is not empty []] and option "ignoreEmptyOnFirstChild" === true (init empty)', () => {
const testObject = ObservableObject.create({}, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
testObject.a = [1, 2];
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual([1, 2]);
});
test('should recognize that item on parent level changed (init data exists)', () => {
const testObject = ObservableObject.create({ a: 1 });
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a).toEqual(1);
testObject.a = 2;
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual(2);
});
test('should recognize that array on parent level changed (init data exists)', () => {
const testObject = ObservableObject.create({ a: [1, 2] });
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a).toEqual([1, 2]);
(testObject.a as number[]).push(3);
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual([1, 2, 3]);
});
test('should recognize that item on first child level changed (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: 1 } });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual(1);
(testObject.a! as IDataObject).b = 2;
expect(testObject.__dataChanged).toBeTruthy();
expect((testObject.a! as IDataObject).b).toEqual(2);
});
test('should recognize that item on first child level changed if it is now empty and option "ignoreEmptyOnFirstChild" === true (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: 1 } }, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual(1);
testObject.a = {};
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual({});
});
test('should recognize that item on first child level changed if it is now empty and option "ignoreEmptyOnFirstChild" === false (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: 1 } }, undefined, { ignoreEmptyOnFirstChild: false });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual(1);
testObject.a = {};
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a).toEqual({});
});
test('should recognize that array on first child level changed (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: [1, 2] } });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual([1, 2]);
((testObject.a! as IDataObject).b as number[]).push(3);
expect(testObject.__dataChanged).toBeTruthy();
expect((testObject.a! as IDataObject).b).toEqual([1, 2, 3]);
});
test('should recognize that item on second child level changed (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: { c: 1 } } });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual({c: 1});
expect(((testObject.a! as IDataObject).b! as IDataObject).c).toEqual(1);
((testObject.a! as IDataObject).b! as IDataObject).c = 2;
expect(testObject.__dataChanged).toBeTruthy();
expect((testObject.a! as IDataObject).b).toEqual({ c: 2 });
});
test('should recognize that item on parent level got deleted (init data exists)', () => {
const testObject = ObservableObject.create({ a: 1 });
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a!).toEqual(1);
delete testObject.a;
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a!).toEqual(undefined);
expect(testObject).toEqual({});
});
test('should recognize that item on parent level got deleted even with and option "ignoreEmptyOnFirstChild" === true (init data exists)', () => {
const testObject = ObservableObject.create({ a: 1 }, undefined, { ignoreEmptyOnFirstChild: true });
expect(testObject.__dataChanged).toBeFalsy();
expect(testObject.a!).toEqual(1);
delete testObject.a;
expect(testObject.__dataChanged).toBeTruthy();
expect(testObject.a!).toEqual(undefined);
expect(testObject).toEqual({});
});
test('should recognize that item on second child level got deleted (init data exists)', () => {
const testObject = ObservableObject.create({ a: { b: { c: 1 } } });
expect(testObject.__dataChanged).toBeFalsy();
expect((testObject.a! as IDataObject).b).toEqual({ c: 1 });
delete (testObject.a! as IDataObject).b;
expect(testObject.__dataChanged).toBeTruthy();
expect((testObject.a! as IDataObject).b).toEqual(undefined);
expect(testObject).toEqual({ a: {} });
});
// test('xxxxxx', () => {
// const testObject = ObservableObject.create({ a: { } }, undefined, { ignoreEmptyOnFirstChild: true });
// expect(testObject.__dataChanged).toBeFalsy();
// expect(testObject).toEqual({ a: { b: { c: 1 } } });
// ((testObject.a! as DataObject).b as DataObject).c = 2;
// // expect((testObject.a! as DataObject).b).toEqual({ c: 1 });
// expect(testObject.__dataChanged).toBeTruthy();
// // expect(testObject.a).toEqual({});
// // expect((testObject.a! as DataObject).b).toEqual({ c: 1 });
// // expect(((testObject.a! as DataObject).b! as DataObject).c).toEqual(1);
// // ((testObject.a! as DataObject).b! as DataObject).c = 2;
// // expect((testObject.a! as DataObject).b).toEqual({ c: 2 });
// });
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"lib": [
"dom",
"es2017"
],
"types": [
"node",
"jest"
],
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"strict": true,
"noUnusedLocals": true,
"preserveConstEnums": true,
"declaration": true,
"outDir": "./dist/",
"target": "es2017",
"sourceMap": true
},
"include": [
"**/*.d.ts",
"src/**/*",
"test/**/*",
],
"exclude": [
"dist/**/*",
"node_modules/**/*",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,103 @@
{
"linterOptions": {
"exclude": [
"node_modules/**/*"
]
},
"defaultSeverity": "error",
"jsRules": {},
"rules": {
"array-type": [
true,
"array-simple"
],
"arrow-return-shorthand": true,
"ban": [
true,
{
"name": "Array",
"message": "tsstyle#array-constructor"
}
],
"ban-types": [
true,
[
"Object",
"Use {} instead."
],
[
"String",
"Use 'string' instead."
],
[
"Number",
"Use 'number' instead."
],
[
"Boolean",
"Use 'boolean' instead."
]
],
"class-name": true,
"curly": [
true,
"ignore-same-line"
],
"forin": true,
"jsdoc-format": true,
"label-position": true,
"member-access": [
true,
"no-public"
],
"new-parens": true,
"no-angle-bracket-type-assertion": true,
"no-any": true,
"no-arg": true,
"no-conditional-assignment": true,
"no-construct": true,
"no-debugger": true,
"no-default-export": true,
"no-duplicate-variable": true,
"no-inferrable-types": true,
"no-namespace": [
true,
"allow-declarations"
],
"no-reference": true,
"no-string-throw": true,
"no-unused-expression": true,
"no-var-keyword": true,
"object-literal-shorthand": true,
"only-arrow-functions": [
true,
"allow-declarations",
"allow-named-functions"
],
"prefer-const": true,
"radix": true,
"semicolon": [
true,
"always",
"ignore-bound-class-methods"
],
"switch-default": true,
"triple-equals": [
true,
"allow-null-check"
],
"use-isnan": true,
"quotes": [
"error",
"single"
],
"variable-name": [
true,
"check-format",
"ban-keywords",
"allow-leading-underscore",
"allow-trailing-underscore"
]
},
"rulesDirectory": []
}