mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
Initial commit to release
This commit is contained in:
230
packages/workflow/LICENSE
Normal file
230
packages/workflow/LICENSE
Normal 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.
|
||||
13
packages/workflow/README.md
Normal file
13
packages/workflow/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# n8n-workflow
|
||||
|
||||

|
||||
|
||||
Workflow base code for n8n
|
||||
|
||||
```
|
||||
npm install n8n-workflow
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0 with Commons Clause](LICENSE)
|
||||
52
packages/workflow/package.json
Normal file
52
packages/workflow/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
552
packages/workflow/src/Interfaces.ts
Normal file
552
packages/workflow/src/Interfaces.ts
Normal 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;
|
||||
}
|
||||
703
packages/workflow/src/NodeHelpers.ts
Normal file
703
packages/workflow/src/NodeHelpers.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
54
packages/workflow/src/ObservableObject.ts
Normal file
54
packages/workflow/src/ObservableObject.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
||||
1021
packages/workflow/src/Workflow.ts
Normal file
1021
packages/workflow/src/Workflow.ts
Normal file
File diff suppressed because it is too large
Load Diff
280
packages/workflow/src/WorkflowDataProxy.ts
Normal file
280
packages/workflow/src/WorkflowDataProxy.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
10
packages/workflow/src/index.ts
Normal file
10
packages/workflow/src/index.ts
Normal 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,
|
||||
};
|
||||
109
packages/workflow/test/Helpers.ts
Normal file
109
packages/workflow/test/Helpers.ts
Normal 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;
|
||||
}
|
||||
2240
packages/workflow/test/NodeHelpers.test.ts
Normal file
2240
packages/workflow/test/NodeHelpers.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
169
packages/workflow/test/ObservableObject.test.ts
Normal file
169
packages/workflow/test/ObservableObject.test.ts
Normal 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 });
|
||||
// });
|
||||
|
||||
});
|
||||
1245
packages/workflow/test/Workflow.test.ts
Normal file
1245
packages/workflow/test/Workflow.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
35
packages/workflow/tsconfig.json
Normal file
35
packages/workflow/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
103
packages/workflow/tslint.json
Normal file
103
packages/workflow/tslint.json
Normal 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": []
|
||||
}
|
||||
Reference in New Issue
Block a user