🔀 Merge branch 'master' into Improve-Zulip-Node

This commit is contained in:
Jan Oberhauser 2020-05-31 10:59:37 +02:00
commit e320ace9a0
21 changed files with 2064 additions and 732 deletions

View file

@ -8,9 +8,16 @@ fi
if [ "$#" -gt 0 ]; then if [ "$#" -gt 0 ]; then
# Got started with arguments # Got started with arguments
shift COMMAND=$1;
exec su-exec node ./packages/cli/bin/n8n "$@"
if [[ "$COMMAND" == "n8n" ]]; then
shift
exec su-exec node ./packages/cli/bin/n8n "$@"
else
exec su-exec node "$@"
fi
else else
# Got started without arguments # Got started without arguments
exec su-exec node ./packages/cli/bin/n8n exec su-exec node ./packages/cli/bin/n8n
fi fi

View file

@ -17,6 +17,7 @@ n8n is a free and open [fair-code](http://faircode.io) licensed node based Workf
- [Securing n8n](#securing-n8n) - [Securing n8n](#securing-n8n)
- [Persist data](#persist-data) - [Persist data](#persist-data)
- [Passing Sensitive Data via File](#passing-sensitive-data-via-file) - [Passing Sensitive Data via File](#passing-sensitive-data-via-file)
- [Updating a Running docker-compose Instance](#updating-a-running-docker-compose-instance)
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt) - [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt)
- [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it) - [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it)
- [Support](#support) - [Support](#support)
@ -226,6 +227,18 @@ The following environment variables support file input:
A basic step by step example setup of n8n with docker-compose and Lets Encrypt is available on the A basic step by step example setup of n8n with docker-compose and Lets Encrypt is available on the
[Server Setup](https://docs.n8n.io/#/server-setup) page. [Server Setup](https://docs.n8n.io/#/server-setup) page.
## Updating a running docker-compose instance
```
# Pull down the latest version from dockerhub
docker pull n8nio/n8n
# Stop current setup
sudo docker-compose stop
# Delete it (will only delete the docker-containers, data is stored separately)
sudo docker-compose rm
# Then start it again
sudo docker-compose up -d
```
## Setting Timezone ## Setting Timezone

View file

@ -192,7 +192,7 @@ class LoadNodesAndCredentialsClass {
* @memberof N8nPackagesInformationClass * @memberof N8nPackagesInformationClass
*/ */
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> { async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
const files = await glob(path.join(directory, '*\.@(node|credentials)\.js')); const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js'));
let fileName: string; let fileName: string;
let type: string; let type: string;

View file

@ -19,7 +19,16 @@
<div class="header"> <div class="header">
<div class="title-text"> <div class="title-text">
<strong>Results: {{ dataCount }}</strong>&nbsp; <strong v-if="dataCount < this.MAX_DISPLAY_ITEMS_AUTO_ALL && dataSize < MAX_DISPLAY_DATA_SIZE">
Results: {{ dataCount }}
</strong>
<strong v-else>Results:
<el-select v-model="maxDisplayItems" @click.stop>
<el-option v-for="option in maxDisplayItemsOptions" :label="option" :value="option" :key="option" />
</el-select>&nbsp;/
{{ dataCount }}
</strong>
&nbsp;
<el-popover <el-popover
v-if="runMetadata" v-if="runMetadata"
placement="right" placement="right"
@ -184,6 +193,11 @@ import {
ITableData, ITableData,
} from '@/Interface'; } from '@/Interface';
import {
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_ITEMS_AUTO_ALL,
} from '@/constants';
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue'; import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
@ -211,8 +225,12 @@ export default mixins(
runIndex: 0, runIndex: 0,
showData: false, showData: false,
outputIndex: 0, outputIndex: 0,
maxDisplayItems: 25 as number | null,
binaryDataDisplayVisible: false, binaryDataDisplayVisible: false,
binaryDataDisplayData: null as IBinaryDisplayData | null, binaryDataDisplayData: null as IBinaryDisplayData | null,
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_ITEMS_AUTO_ALL,
}; };
}, },
computed: { computed: {
@ -229,6 +247,9 @@ export default mixins(
const executionData: IRunExecutionData = this.workflowExecution.data; const executionData: IRunExecutionData = this.workflowExecution.data;
return executionData.resultData.runData; return executionData.resultData.runData;
}, },
maxDisplayItemsOptions (): number[] {
return [25, 50, 100, 250, 500, 1000, this.dataCount].filter(option => option <= this.dataCount);
},
node (): INodeUi | null { node (): INodeUi | null {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;
}, },
@ -323,19 +344,27 @@ export default mixins(
return 0; return 0;
}, },
jsonData (): IDataObject[] { jsonData (): IDataObject[] {
const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
if (inputData.length === 0 || !Array.isArray(inputData)) { if (inputData.length === 0 || !Array.isArray(inputData)) {
return []; return [];
} }
if (this.maxDisplayItems !== null) {
inputData = inputData.slice(0, this.maxDisplayItems);
}
return this.convertToJson(inputData); return this.convertToJson(inputData);
}, },
tableData (): ITableData | undefined { tableData (): ITableData | undefined {
const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
if (inputData.length === 0) { if (inputData.length === 0) {
return undefined; return undefined;
} }
if (this.maxDisplayItems !== null) {
inputData = inputData.slice(0,this.maxDisplayItems);
}
return this.convertToTable(inputData); return this.convertToTable(inputData);
}, },
binaryData (): IBinaryKeyData[] { binaryData (): IBinaryKeyData[] {
@ -450,9 +479,12 @@ export default mixins(
// Check how much data there is to display // Check how much data there is to display
const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex);
this.dataSize = JSON.stringify(inputData).length;
if (this.dataSize < 204800) { const jsonItems = inputData.slice(0, this.maxDisplayItems || inputData.length).map(item => item.json);
this.dataSize = JSON.stringify(jsonItems).length;
if (this.dataSize < this.MAX_DISPLAY_DATA_SIZE) {
// Data is reasonable small (< 200kb) so display it directly // Data is reasonable small (< 200kb) so display it directly
this.showData = true; this.showData = true;
} }
@ -466,6 +498,7 @@ export default mixins(
node (newNode, oldNode) { node (newNode, oldNode) {
// Reset the selected output index every time another node gets selected // Reset the selected output index every time another node gets selected
this.outputIndex = 0; this.outputIndex = 0;
this.maxDisplayItems = 25;
this.refreshDataSize(); this.refreshDataSize();
}, },
jsonData () { jsonData () {

View file

@ -65,6 +65,7 @@ export const nodeBase = mixins(nodeIndex).extend({
'name', 'name',
'nodeId', 'nodeId',
'instance', 'instance',
'isReadOnly',
], ],
methods: { methods: {
__addNode (node: INodeUi) { __addNode (node: INodeUi) {
@ -182,7 +183,7 @@ export const nodeBase = mixins(nodeIndex).extend({
endpoint: inputData.endpoint, endpoint: inputData.endpoint,
endpointStyle: inputData.endpointStyle, endpointStyle: inputData.endpointStyle,
isSource: false, isSource: false,
isTarget: true, isTarget: !this.isReadOnly,
parameters: { parameters: {
nodeIndex: this.nodeIndex, nodeIndex: this.nodeIndex,
type: inputName, type: inputName,
@ -246,7 +247,7 @@ export const nodeBase = mixins(nodeIndex).extend({
maxConnections: inputData.maxConnections, maxConnections: inputData.maxConnections,
endpoint: inputData.endpoint, endpoint: inputData.endpoint,
endpointStyle: inputData.endpointStyle, endpointStyle: inputData.endpointStyle,
isSource: true, isSource: !this.isReadOnly,
isTarget: false, isTarget: false,
parameters: { parameters: {
nodeIndex: this.nodeIndex, nodeIndex: this.nodeIndex,
@ -275,61 +276,63 @@ export const nodeBase = mixins(nodeIndex).extend({
this.instance.addEndpoint(this.nodeName, newEndpointData); this.instance.addEndpoint(this.nodeName, newEndpointData);
}); });
// Make nodes draggable if (this.isReadOnly === false) {
this.instance.draggable(this.nodeName, { // Make nodes draggable
grid: [10, 10], this.instance.draggable(this.nodeName, {
start: (params: { e: MouseEvent }) => { grid: [10, 10],
if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { start: (params: { e: MouseEvent }) => {
// Only the node which gets dragged directly gets an event, for all others it is if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) {
// undefined. So check if the currently dragged node is selected and if not clear // Only the node which gets dragged directly gets an event, for all others it is
// the drag-selection. // undefined. So check if the currently dragged node is selected and if not clear
this.instance.clearDragSelection(); // the drag-selection.
this.$store.commit('resetSelectedNodes'); this.instance.clearDragSelection();
} this.$store.commit('resetSelectedNodes');
this.$store.commit('addActiveAction', 'dragActive');
},
stop: (params: { e: MouseEvent}) => {
if (this.$store.getters.isActionActive('dragActive')) {
const moveNodes = this.$store.getters.getSelectedNodes.slice();
const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
if (!selectedNodeNames.includes(this.data.name)) {
// If the current node is not in selected add it to the nodes which
// got moved manually
moveNodes.push(this.data);
} }
// This does for some reason just get called once for the node that got clicked this.$store.commit('addActiveAction', 'dragActive');
// even though "start" and "drag" gets called for all. So lets do for now },
// some dirty DOM query to get the new positions till I have more time to stop: (params: { e: MouseEvent }) => {
// create a proper solution if (this.$store.getters.isActionActive('dragActive')) {
let newNodePositon: XYPositon; const moveNodes = this.$store.getters.getSelectedNodes.slice();
moveNodes.forEach((node: INodeUi) => { const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
const nodeElement = `node-${this.getNodeIndex(node.name)}`; if (!selectedNodeNames.includes(this.data.name)) {
const element = document.getElementById(nodeElement); // If the current node is not in selected add it to the nodes which
if (element === null) { // got moved manually
return; moveNodes.push(this.data);
} }
newNodePositon = [ // This does for some reason just get called once for the node that got clicked
parseInt(element.style.left!.slice(0, -2), 10), // even though "start" and "drag" gets called for all. So lets do for now
parseInt(element.style.top!.slice(0, -2), 10), // some dirty DOM query to get the new positions till I have more time to
]; // create a proper solution
let newNodePositon: XYPositon;
moveNodes.forEach((node: INodeUi) => {
const nodeElement = `node-${this.getNodeIndex(node.name)}`;
const element = document.getElementById(nodeElement);
if (element === null) {
return;
}
const updateInformation = { newNodePositon = [
name: node.name, parseInt(element.style.left!.slice(0, -2), 10),
properties: { parseInt(element.style.top!.slice(0, -2), 10),
// @ts-ignore, draggable does not have definitions ];
position: newNodePositon,
},
};
this.$store.commit('updateNodeProperties', updateInformation); const updateInformation = {
}); name: node.name,
} properties: {
}, // @ts-ignore, draggable does not have definitions
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', position: newNodePositon,
}); },
};
this.$store.commit('updateNodeProperties', updateInformation);
});
}
},
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
});
}
}, },
isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {

View file

@ -1,2 +1,4 @@
export const MAX_DISPLAY_DATA_SIZE = 204800;
export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250;
export const NODE_NAME_PREFIX = 'node-'; export const NODE_NAME_PREFIX = 'node-';
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';

View file

@ -20,6 +20,7 @@
:id="'node-' + getNodeIndex(nodeData.name)" :id="'node-' + getNodeIndex(nodeData.name)"
:key="getNodeIndex(nodeData.name)" :key="getNodeIndex(nodeData.name)"
:name="nodeData.name" :name="nodeData.name"
:isReadOnly="isReadOnly"
:instance="instance" :instance="instance"
></node> ></node>
</div> </div>
@ -102,6 +103,9 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import {
OverlaySpec,
} from 'jsplumb';
import { MessageBoxInputData } from 'element-ui/types/message-box'; import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb'; import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
@ -1013,21 +1017,9 @@ export default mixins(
} }
}, },
initNodeView () { initNodeView () {
this.instance.importDefaults({ const connectionOverlays: OverlaySpec[] = [];
// notice the 'curviness' argument to this Bezier curve. if (this.isReadOnly === false) {
// the curves on this page are far smoother connectionOverlays.push.apply(connectionOverlays, [
// than the curves on the first demo, which use the default curviness value.
// Connector: ["Bezier", { curviness: 80 }],
Connector: ['Bezier', { curviness: 40 }],
// @ts-ignore
Endpoint: ['Dot', { radius: 5 }],
DragOptions: { cursor: 'pointer', zIndex: 5000 },
PaintStyle: { strokeWidth: 2, stroke: '#334455' },
EndpointStyle: { radius: 9, fill: '#acd', stroke: 'red' },
// EndpointStyle: {},
HoverPaintStyle: { stroke: '#ff6d5a', lineWidth: 4 },
EndpointHoverStyle: { fill: '#ff6d5a', stroke: '#acd' },
ConnectionOverlays: [
[ [
'Arrow', 'Arrow',
{ {
@ -1045,7 +1037,24 @@ export default mixins(
location: 0.5, location: 0.5,
}, },
], ],
], ]);
}
this.instance.importDefaults({
// notice the 'curviness' argument to this Bezier curve.
// the curves on this page are far smoother
// than the curves on the first demo, which use the default curviness value.
// Connector: ["Bezier", { curviness: 80 }],
Connector: ['Bezier', { curviness: 40 }],
// @ts-ignore
Endpoint: ['Dot', { radius: 5 }],
DragOptions: { cursor: 'pointer', zIndex: 5000 },
PaintStyle: { strokeWidth: 2, stroke: '#334455' },
EndpointStyle: { radius: 9, fill: '#acd', stroke: 'red' },
// EndpointStyle: {},
HoverPaintStyle: { stroke: '#ff6d5a', lineWidth: 4 },
EndpointHoverStyle: { fill: '#ff6d5a', stroke: '#acd' },
ConnectionOverlays: connectionOverlays,
Container: '#node-view', Container: '#node-view',
}); });
@ -1100,41 +1109,43 @@ export default mixins(
info.connection.setConnector(['Straight']); info.connection.setConnector(['Straight']);
} }
// Display the connection-delete button only on hover
let timer: NodeJS.Timeout | undefined;
info.connection.bind('mouseover', (connection: IConnection) => {
if (timer !== undefined) {
clearTimeout(timer);
}
const overlay = info.connection.getOverlay('remove-connection');
overlay.setVisible(true);
});
info.connection.bind('mouseout', (connection: IConnection) => {
timer = setTimeout(() => {
const overlay = info.connection.getOverlay('remove-connection');
overlay.setVisible(false);
timer = undefined;
}, 500);
});
// @ts-ignore // @ts-ignore
info.connection.removeOverlay('drop-add-node'); info.connection.removeOverlay('drop-add-node');
// @ts-ignore if (this.isReadOnly === false) {
info.connection.addOverlay([ // Display the connection-delete button only on hover
'Label', let timer: NodeJS.Timeout | undefined;
{ info.connection.bind('mouseover', (connection: IConnection) => {
id: 'remove-connection', if (timer !== undefined) {
label: '<span class="delete-connection clickable" title="Delete Connection">x</span>', clearTimeout(timer);
cssClass: 'remove-connection-label', }
visible: false, const overlay = info.connection.getOverlay('remove-connection');
events: { overlay.setVisible(true);
mousedown: () => { });
this.__removeConnectionByConnectionInfo(info, true); info.connection.bind('mouseout', (connection: IConnection) => {
timer = setTimeout(() => {
const overlay = info.connection.getOverlay('remove-connection');
overlay.setVisible(false);
timer = undefined;
}, 500);
});
// @ts-ignore
info.connection.addOverlay([
'Label',
{
id: 'remove-connection',
label: '<span class="delete-connection clickable" title="Delete Connection">x</span>',
cssClass: 'remove-connection-label',
visible: false,
events: {
mousedown: () => {
this.__removeConnectionByConnectionInfo(info, true);
},
}, },
}, },
}, ]);
]); }
// Display input names if they exist on connection // Display input names if they exist on connection
const targetNodeTypeData: INodeTypeDescription = this.$store.getters.nodeType(targetNode.type); const targetNodeTypeData: INodeTypeDescription = this.$store.getters.nodeType(targetNode.type);
@ -1329,6 +1340,7 @@ export default mixins(
// @ts-ignore // @ts-ignore
this.instance.connect({ this.instance.connect({
uuids: uuid, uuids: uuid,
detachable: !this.isReadOnly,
}); });
} else { } else {
// When nodes get connected it gets saved automatically to the storage // When nodes get connected it gets saved automatically to the storage

View file

@ -12,12 +12,20 @@ export class AgileCrmApi implements ICredentialType {
name: 'email', name: 'email',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
}, },
{ {
displayName: 'API Key', displayName: 'API Key',
name: 'apiKey', name: 'apiKey',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
}, },
{
displayName: 'Subdomain',
name: 'subdomain',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'example',
description: 'If the domain is https://example.agilecrm.com "example" would have to be entered.',
},
]; ];
} }

View file

@ -27,7 +27,7 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction
username: credentials!.email as string, username: credentials!.email as string,
password: credentials!.apiKey as string password: credentials!.apiKey as string
}, },
uri: uri || `https://n8nio.agilecrm.com/dev/${endpoint}`, uri: uri || `https://${credentials!.subdomain}.agilecrm.com/dev/${endpoint}`,
json: true, json: true,
}; };
@ -45,8 +45,9 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction
} }
export async function agileCrmApiRequestUpdate(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method = 'PUT', endpoint?: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise<any> { // tslint:disable-line:no-any export async function agileCrmApiRequestUpdate(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method = 'PUT', endpoint?: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise<any> { // tslint:disable-line:no-any
const baseUri = 'https://n8nio.agilecrm.com/dev/';
const credentials = this.getCredentials('agileCrmApi'); const credentials = this.getCredentials('agileCrmApi');
const baseUri = `https://${credentials!.subdomain}.agilecrm.com/dev/`;
const options: OptionsWithUri = { const options: OptionsWithUri = {
method, method,
headers: { headers: {

View file

@ -432,6 +432,80 @@ export class EditImage implements INodeType {
}, },
description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..', description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..',
}, },
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
hide: {
operation: [
'information',
],
},
},
options: [
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'File name to set in binary data.',
},
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'bmp',
value: 'bmp',
},
{
name: 'gif',
value: 'gif',
},
{
name: 'jpeg',
value: 'jpeg',
},
{
name: 'png',
value: 'png',
},
{
name: 'tiff',
value: 'tiff',
},
],
default: 'jpeg',
description: 'Set the output image format.',
},
{
displayName: 'Quality',
name: 'quality',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 100,
},
default: 100,
displayOptions: {
show: {
format: [
'jpeg',
'png',
'tiff',
],
},
},
description: 'Sets the jpeg|png|tiff compression level from 0 to 100 (best).',
},
],
},
] ]
}; };
@ -442,6 +516,8 @@ export class EditImage implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
const dataPropertyName = this.getNodeParameter('dataPropertyName') as string; const dataPropertyName = this.getNodeParameter('dataPropertyName') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
// TODO: Later should make so that it sends directly a valid buffer and the buffer.from stuff is not needed anymore // TODO: Later should make so that it sends directly a valid buffer and the buffer.from stuff is not needed anymore
if (item.binary === undefined) { if (item.binary === undefined) {
return item; return item;
@ -550,6 +626,24 @@ export class EditImage implements INodeType {
Object.assign(newItem.binary, item.binary); Object.assign(newItem.binary, item.binary);
} }
if (options.quality !== undefined) {
gmInstance = gmInstance.quality(options.quality as number);
}
if (options.format !== undefined) {
gmInstance = gmInstance.setFormat(options.format as string);
newItem.binary![dataPropertyName as string].fileExtension = options.format as string;
newItem.binary![dataPropertyName as string].mimeType = `image/${options.format}`;
const fileName = newItem.binary![dataPropertyName as string].fileName;
if (fileName && fileName.includes('.')) {
newItem.binary![dataPropertyName as string].fileName = fileName.split('.').slice(0, -1).join('.') + '.' + options.format;
}
}
if (options.fileName !== undefined) {
newItem.binary![dataPropertyName as string].fileName = options.fileName as string;
}
return new Promise<INodeExecutionData>((resolve, reject) => { return new Promise<INodeExecutionData>((resolve, reject) => {
gmInstance gmInstance
.toBuffer((error: Error | null, buffer: Buffer) => { .toBuffer((error: Error | null, buffer: Buffer) => {

View file

@ -1,13 +1,19 @@
import { OptionsWithUri } from 'request'; import {
OptionsWithUri,
} from 'request';
import { import {
IExecuteFunctions, IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions, IHookFunctions,
ILoadOptionsFunctions, ILoadOptionsFunctions,
IExecuteSingleFunctions
} from 'n8n-core'; } from 'n8n-core';
export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, headers?: object): Promise<any> { // tslint:disable-line:no-any import {
IDataObject,
} from 'n8n-workflow';
export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {} ,headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('mailchimpApi'); const credentials = this.getCredentials('mailchimpApi');
if (credentials === undefined) { if (credentials === undefined) {
@ -27,6 +33,7 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio
const options: OptionsWithUri = { const options: OptionsWithUri = {
headers: headerWithAuthentication, headers: headerWithAuthentication,
method, method,
qs,
uri: `https://${datacenter}.${host}${endpoint}`, uri: `https://${datacenter}.${host}${endpoint}`,
json: true, json: true,
}; };
@ -34,19 +41,36 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio
if (Object.keys(body).length !== 0) { if (Object.keys(body).length !== 0) {
options.body = body; options.body = body;
} }
try { try {
return await this.helpers.request!(options); return await this.helpers.request!(options);
} catch (error) { } catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message; if (error.response.body && error.response.body.detail) {
throw new Error(`Mailchimp Error response [${error.statusCode}]: ${error.response.body.detail}`);
if (errorMessage !== undefined) {
throw errorMessage;
} }
throw error.response.body; throw error;
} }
} }
export async function mailchimpApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, endpoint: string, method: string, propertyName: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.offset = 0;
query.count = 500;
do {
responseData = await mailchimpApiRequest.call(this, endpoint, method, body, query);
returnData.push.apply(returnData, responseData[propertyName]);
query.offset += query.count;
} while (
responseData[propertyName] && responseData[propertyName].length !== 0
);
return returnData;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result; let result;
try { try {

File diff suppressed because it is too large Load diff

View file

@ -98,9 +98,9 @@ export class Mattermost implements INodeType {
description: 'Soft-deletes a channel', description: 'Soft-deletes a channel',
}, },
{ {
name: 'Members', name: 'Member',
value: 'members', value: 'members',
description: 'Returns the members of a channel.', description: 'Get a page of members for a channel.',
}, },
{ {
name: 'Restore', name: 'Restore',
@ -317,6 +317,23 @@ export class Mattermost implements INodeType {
}, },
description: 'The Mattermost Team.', description: 'The Mattermost Team.',
}, },
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
displayOptions: {
show: {
resource: [
'channel',
],
operation: [
'members',
],
},
},
default: true,
description: 'By default the response only contain the ID of the user.<br />If this option gets activated it will resolve the user automatically.',
},
{ {
displayName: 'Return All', displayName: 'Return All',
name: 'returnAll', name: 'returnAll',
@ -950,6 +967,11 @@ export class Mattermost implements INodeType {
value: 'getByEmail', value: 'getByEmail',
description: 'Get a user by email', description: 'Get a user by email',
}, },
{
name: 'Get By ID',
value: 'getById',
description: 'Get a user by id',
},
], ],
default: '', default: '',
description: 'The operation to perform.', description: 'The operation to perform.',
@ -1113,6 +1135,54 @@ export class Mattermost implements INodeType {
default: '', default: '',
description: `User's email`, description: `User's email`,
}, },
// ----------------------------------
// user:getById
// ----------------------------------
{
displayName: 'User IDs',
name: 'userIds',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getById',
],
},
},
default: '',
description: `User's ID`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getById',
],
},
},
default: {},
options: [
{
displayName: 'Since',
name: 'since',
type: 'dateTime',
default: '',
description: 'Only return users that have been modified since the given Unix timestamp (in milliseconds).',
},
],
},
], ],
}; };
@ -1245,6 +1315,10 @@ export class Mattermost implements INodeType {
let resource: string; let resource: string;
let requestMethod = 'POST'; let requestMethod = 'POST';
let returnAll = false; let returnAll = false;
let userIds: string[] = [];
resource = this.getNodeParameter('resource', 0) as string;
operation = this.getNodeParameter('operation', 0) as string;
// For Post // For Post
let body: IDataObject; let body: IDataObject;
@ -1256,9 +1330,6 @@ export class Mattermost implements INodeType {
body = {}; body = {};
qs = {}; qs = {};
resource = this.getNodeParameter('resource', i) as string;
operation = this.getNodeParameter('operation', i) as string;
if (resource === 'channel') { if (resource === 'channel') {
if (operation === 'create') { if (operation === 'create') {
// ---------------------------------- // ----------------------------------
@ -1509,6 +1580,25 @@ export class Mattermost implements INodeType {
endpoint = `users/email/${email}`; endpoint = `users/email/${email}`;
} }
if (operation === 'getById') {
// ----------------------------------
// user:getById
// ----------------------------------
userIds = (this.getNodeParameter('userIds', i) as string).split(',') as string[];
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.since) {
qs.since = new Date(additionalFields.since as string).getTime();
}
requestMethod = 'POST';
endpoint = 'users/ids';
//@ts-ignore
body = userIds;
}
} }
else { else {
throw new Error(`The resource "${resource}" is not known!`); throw new Error(`The resource "${resource}" is not known!`);
@ -1519,6 +1609,18 @@ export class Mattermost implements INodeType {
responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs); responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else { } else {
responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs);
if (resource === 'channel' && operation === 'members') {
const resolveData = this.getNodeParameter('resolveData', i) as boolean;
if (resolveData) {
const userIds: string[] = [];
for (const data of responseData) {
userIds.push(data.user_id);
}
if (userIds.length > 0) {
responseData = await apiRequest.call(this, 'POST', 'users/ids', userIds , qs);
}
}
}
} }
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData); returnData.push.apply(returnData, responseData);

View file

@ -40,7 +40,7 @@ export class TrelloTrigger implements INodeType {
webhooks: [ webhooks: [
{ {
name: 'setup', name: 'setup',
httpMethod: 'GET', httpMethod: 'HEAD',
responseMode: 'onReceived', responseMode: 'onReceived',
path: 'webhook', path: 'webhook',
}, },

View file

@ -62,4 +62,3 @@ export function validateJSON(json: string | undefined): any { // tslint:disable-
} }
return result; return result;
} }

View file

@ -51,9 +51,9 @@ export const messageOperations = [
export const messageFields = [ export const messageFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:sendPrivate */ /* message:sendPrivate */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'To', displayName: 'To',
name: 'to', name: 'to',
@ -96,9 +96,9 @@ export const messageFields = [
}, },
description: 'The content of the message.', description: 'The content of the message.',
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:sendStream */ /* message:sendStream */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Stream', displayName: 'Stream',
name: 'stream', name: 'stream',
@ -163,9 +163,9 @@ export const messageFields = [
}, },
description: 'The content of the message.', description: 'The content of the message.',
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:update */ /* message:update */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Message ID', displayName: 'Message ID',
name: 'messageId', name: 'messageId',
@ -241,9 +241,9 @@ export const messageFields = [
}, },
] ]
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:get */ /* message:get */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Message ID', displayName: 'Message ID',
name: 'messageId', name: 'messageId',
@ -262,9 +262,9 @@ export const messageFields = [
}, },
description: 'Unique identifier for the message.', description: 'Unique identifier for the message.',
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:delete */ /* message:delete */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Message ID', displayName: 'Message ID',
name: 'messageId', name: 'messageId',
@ -283,9 +283,9 @@ export const messageFields = [
}, },
description: 'Unique identifier for the message.', description: 'Unique identifier for the message.',
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* message:updateFile */ /* message:updateFile */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Binary Property', displayName: 'Binary Property',
name: 'dataBinaryProperty', name: 'dataBinaryProperty',

View file

@ -13,7 +13,7 @@ export const streamOperations = [
}, },
}, },
options: [ options: [
{ {
name: 'Create', name: 'Create',
value: 'create', value: 'create',
description: 'Create a stream.', description: 'Create a stream.',
@ -27,17 +27,17 @@ export const streamOperations = [
name: 'Get All', name: 'Get All',
value: 'getAll', value: 'getAll',
description: 'Get all streams.', description: 'Get all streams.',
}, },
{ {
name: 'Get Subscribed', name: 'Get Subscribed',
value: 'getSubscribed', value: 'getSubscribed',
description: 'Get subscribed streams.', description: 'Get subscribed streams.',
},
{
name: 'Update',
value: 'update',
description: 'Update a stream.',
}, },
// {
// name: 'Update',
// value: 'update',
// description: 'Update a stream.',
// },
], ],
default: 'create', default: 'create',
description: 'The operation to perform.', description: 'The operation to perform.',
@ -45,17 +45,17 @@ export const streamOperations = [
] as INodeProperties[]; ] as INodeProperties[];
export const streamFields = [ export const streamFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* stream:create */ /* stream:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Subscriptions', displayName: 'Subscriptions',
name: 'subscriptions', name: 'subscriptions',
type: 'fixedCollection', type: 'fixedCollection',
required: true, required: true,
default: '', default: '',
description: 'A list of dictionaries containing the the key name and value specifying the name of the stream to subscribe. If the stream does not exist a new stream is created.', description: 'A list of dictionaries containing the the key name and value specifying the name of the stream to subscribe. If the stream does not exist a new stream is created.',
displayOptions: { displayOptions: {
show: { show: {
resource: [ resource: [
'stream', 'stream',
@ -65,35 +65,35 @@ export const streamFields = [
], ],
}, },
}, },
typeOptions: { typeOptions: {
multipleValues: true, multipleValues: true,
}, },
options: [ options: [
{ {
displayName: 'Subscription Properties', displayName: 'Subscription Properties',
name: 'properties', name: 'properties',
values: [ values: [
{ {
displayName: 'Name', displayName: 'Name',
name: 'name', name: 'name',
type: 'string', type: 'string',
required: true, required: true,
default: '', default: '',
description: 'Name of Subscription.', description: 'Name of Subscription.',
}, },
{ {
displayName: 'Description', displayName: 'Description',
name: 'description', name: 'description',
type: 'string', type: 'string',
required: true, required: true,
default: '', default: '',
description: 'Description of Subscription.', description: 'Description of Subscription.',
} }
], ],
}, },
], ],
}, },
{ {
displayName: 'JSON Parameters', displayName: 'JSON Parameters',
name: 'jsonParameters', name: 'jsonParameters',
type: 'boolean', type: 'boolean',
@ -131,9 +131,56 @@ export const streamFields = [
], ],
}, },
}, },
description: `JSON format parameters for stream creation.`, description: `JSON format parameters for stream creation.`,
}, },
{
displayName: 'Subscriptions',
name: 'subscriptions',
type: 'fixedCollection',
default: {},
displayOptions: {
show: {
resource: [
'stream',
],
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
required: true,
description: 'A list of dictionaries containing the the key name and value specifying the name of the stream to subscribe. If the stream does not exist a new stream is created.',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Subscription Properties',
name: 'properties',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
default: '',
description: 'Name of Subscription.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
required: true,
default: '',
description: 'Description of Subscription.',
}
],
},
],
},
{ {
displayName: 'Additional Fields', displayName: 'Additional Fields',
name: 'additionalFields', name: 'additionalFields',
@ -154,39 +201,39 @@ export const streamFields = [
}, },
}, },
options: [ options: [
{ {
displayName: 'Announce', displayName: 'Announce',
name: 'announce', name: 'announce',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'If announce is True and one of the streams specified in subscriptions has to be created (i.e. doesnt exist to begin with), an announcement will be made notifying that a new stream was created.', description: 'If announce is True and one of the streams specified in subscriptions has to be created (i.e. doesnt exist to begin with), an announcement will be made notifying that a new stream was created.',
}, },
{ {
displayName: 'Authorization Errors Fatal', displayName: 'Authorization Errors Fatal',
name: 'authorizationErrorsFatal', name: 'authorizationErrorsFatal',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'A boolean specifying whether authorization errors (such as when the requesting user is not authorized to access a private stream) should be considered fatal or not. When True, an authorization error is reported as such. When set to False, the returned JSON payload indicates that there was an authorization error, but the response is still considered a successful one.', description: 'A boolean specifying whether authorization errors (such as when the requesting user is not authorized to access a private stream) should be considered fatal or not. When True, an authorization error is reported as such. When set to False, the returned JSON payload indicates that there was an authorization error, but the response is still considered a successful one.',
}, },
{ {
displayName: 'History Public to Subscribers', displayName: 'History Public to Subscribers',
name: 'historyPublicToSubscribers', name: 'historyPublicToSubscribers',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.',
}, },
{ {
displayName: 'Invite Only', displayName: 'Invite Only',
name: 'inviteOnly', name: 'inviteOnly',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'A boolean specifying whether the streams specified in subscriptions are invite-only or not.', description: 'A boolean specifying whether the streams specified in subscriptions are invite-only or not.',
}, },
{ {
displayName: 'Principals', displayName: 'Principals',
name: 'principals', name: 'principals',
type: 'fixedCollection', type: 'fixedCollection',
default: '', default: {},
description: 'A list of email addresses of the users that will be subscribed/unsubscribed to the streams specified in the subscriptions argument. If not provided, then the requesting user/bot is subscribed.', description: 'A list of email addresses of the users that will be subscribed/unsubscribed to the streams specified in the subscriptions argument. If not provided, then the requesting user/bot is subscribed.',
typeOptions: { typeOptions: {
multipleValues: true, multipleValues: true,
@ -207,141 +254,144 @@ export const streamFields = [
], ],
}, },
], ],
}, },
{ {
displayName: 'Stream Post Policy', displayName: 'Stream Post Policy',
name: 'streamPostPolicy', name: 'streamPostPolicy',
type: 'options', type: 'options',
default: '', default: '',
description: 'Policy for which users can post messages to the stream.', description: 'Policy for which users can post messages to the stream.',
options: [ options: [
{ {
name: '1', name: '1',
value: 1, value: 1,
description: 'Any user can post.' description: 'Any user can post.'
}, },
{ {
name: '2', name: '2',
value: 2, value: 2,
description: 'Only administrators can post.' description: 'Only administrators can post.'
}, },
{ {
name: '3', name: '3',
value: 3, value: 3,
description: 'Only new members can post.' description: 'Only new members can post.'
}, },
], ],
} }
] ]
}, },
/* -------------------------------------------------------------------------- */
/* stream:get all */ /* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */ /* stream:get all */
{ /* -------------------------------------------------------------------------- */
displayName: 'Additional Fields', {
name: 'additionalFields', displayName: 'Additional Fields',
type: 'collection', name: 'additionalFields',
placeholder: 'Add Field', type: 'collection',
default: {}, placeholder: 'Add Field',
displayOptions: { default: {},
show: { displayOptions: {
resource: [ show: {
'stream', resource: [
], 'stream',
operation: [ ],
'getAll', operation: [
], 'getAll',
}, ],
}, },
options: [ },
{ options: [
displayName: 'Include All Active', {
name: 'includeAllActive', displayName: 'Include All Active',
type: 'boolean', name: 'includeAllActive',
default: true, type: 'boolean',
description: 'Include all active streams. The user must have administrative privileges to use this parameter.', default: true,
}, description: 'Include all active streams. The user must have administrative privileges to use this parameter.',
{ },
displayName: 'Include Default', {
name: 'includeDefault', displayName: 'Include Default',
type: 'boolean', name: 'includeDefault',
default: true, type: 'boolean',
description: 'Include all default streams for the users realm.', default: true,
}, description: 'Include all default streams for the users realm.',
{ },
displayName: 'Include Owner Subscribed', {
name: 'includeOwnersubscribed', displayName: 'Include Owner Subscribed',
type: 'boolean', name: 'includeOwnersubscribed',
default: true, type: 'boolean',
description: 'If the user is a bot, include all streams that the bots owner is subscribed to.', default: true,
}, description: 'If the user is a bot, include all streams that the bots owner is subscribed to.',
{ },
displayName: 'Include Public', {
name: 'includePublic', displayName: 'Include Public',
type: 'boolean', name: 'includePublic',
default: true, type: 'boolean',
description: 'Include all public streams.', default: true,
}, description: 'Include all public streams.',
{ },
displayName: 'Include Subscribed', {
name: 'includeSubscribed', displayName: 'Include Subscribed',
type: 'boolean', name: 'includeSubscribed',
default: true, type: 'boolean',
description: 'Include all streams that the user is subscribed to.', default: true,
}, description: 'Include all streams that the user is subscribed to.',
] },
}, ]
/* -------------------------------------------------------------------------- */ },
/* stream:get subscribed */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ /* stream:get subscribed */
displayName: 'Additional Fields', /* -------------------------------------------------------------------------- */
name: 'additionalFields', {
type: 'collection', displayName: 'Additional Fields',
placeholder: 'Add Field', name: 'additionalFields',
default: {}, type: 'collection',
displayOptions: { placeholder: 'Add Field',
show: { default: {},
resource: [ displayOptions: {
'stream', show: {
], resource: [
operation: [ 'stream',
'getSubscribed', ],
], operation: [
}, 'getSubscribed',
}, ],
options: [ },
{ },
displayName: 'Include Subscribers', options: [
name: 'includeSubscribers', {
type: 'boolean', displayName: 'Include Subscribers',
default: true, name: 'includeSubscribers',
description: 'Whether each returned stream object should include a subscribers field containing a list of the user IDs of its subscribers.', type: 'boolean',
} default: true,
] description: 'Whether each returned stream object should include a subscribers field containing a list of the user IDs of its subscribers.',
}, }
/* -------------------------------------------------------------------------- */ ]
/* stream:update */ },
/* -------------------------------------------------------------------------- */
{ /* -------------------------------------------------------------------------- */
displayName: 'Stream ID', /* stream:update */
name: 'streamId', /* -------------------------------------------------------------------------- */
type: 'string', {
required: true, displayName: 'Stream ID',
default: '', name: 'streamId',
displayOptions: { type: 'string',
show: { required: true,
resource: [ default: '',
'stream', displayOptions: {
], show: {
operation: [ resource: [
'update', 'stream',
], ],
}, operation: [
}, 'update',
description: 'ID of stream to update.', ],
}, },
{ },
description: 'ID of stream to update.',
},
{
displayName: 'JSON Parameters', displayName: 'JSON Parameters',
name: 'jsonParameters', name: 'jsonParameters',
type: 'boolean', type: 'boolean',
@ -403,89 +453,89 @@ export const streamFields = [
}, },
}, },
options: [ options: [
{ {
displayName: 'Announcement Only', displayName: 'Announcement Only',
name: 'isAnnouncementOnly', name: 'isAnnouncementOnly',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the stream is limited to announcements.', description: 'Whether the stream is limited to announcements.',
}, },
{ {
displayName: 'Description', displayName: 'Description',
name: 'description', name: 'description',
type: 'string', type: 'string',
default: '', default: '',
description: 'The new description for the stream.', description: 'The new description for the stream.',
placeholder: 'Place of discussion' placeholder: 'Place of discussion'
}, },
{ {
displayName: 'Is Private', displayName: 'Is Private',
name: 'isPrivate', name: 'isPrivate',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Change whether the stream is a private stream.', description: 'Change whether the stream is a private stream.',
}, },
{ {
displayName: 'History Public to Subscribers', displayName: 'History Public to Subscribers',
name: 'historyPublicToSubscribers', name: 'historyPublicToSubscribers',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.',
}, },
{ {
displayName: 'New Name', displayName: 'New Name',
name: 'newName', name: 'newName',
type: 'string', type: 'string',
default: '', default: '',
description: 'The new name for the stream.', description: 'The new name for the stream.',
placeholder: 'Italy' placeholder: 'Italy'
}, },
{ {
displayName: 'Stream Post Policy', displayName: 'Stream Post Policy',
name: 'streamPostPolicy', name: 'streamPostPolicy',
type: 'options', type: 'options',
default: '', default: '',
description: 'Policy for which users can post messages to the stream.', description: 'Policy for which users can post messages to the stream.',
options: [ options: [
{ {
name: '1', name: '1',
value: 1, value: 1,
description: 'Any user can post.' description: 'Any user can post.'
}, },
{ {
name: '2', name: '2',
value: 2, value: 2,
description: 'Only administrators can post.' description: 'Only administrators can post.'
}, },
{ {
name: '3', name: '3',
value: 3, value: 3,
description: 'Only new members can post.' description: 'Only new members can post.'
}, },
], ],
}, },
] ]
}, },
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* stream:delete */ /* stream:delete */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Stream ID', displayName: 'Stream ID',
name: 'streamId', name: 'streamId',
type: 'string', type: 'string',
required: true, required: true,
default: '', default: '',
displayOptions: { displayOptions: {
show: { show: {
resource: [ resource: [
'stream', 'stream',
], ],
operation: [ operation: [
'delete', 'delete',
], ],
}, },
}, },
description: 'ID of stream to delete.', description: 'ID of stream to delete.',
}, },
] as INodeProperties[]; ] as INodeProperties[];

View file

@ -1,23 +1,23 @@
export interface IStream { export interface IStream {
subscriptions?: string; subscriptions?: string;
invite_only?: boolean; invite_only?: boolean;
principals?: string; principals?: string;
authorization_errors_fatal?: boolean; authorization_errors_fatal?: boolean;
history_public_to_subscribers?: boolean; history_public_to_subscribers?: boolean;
stream_post_policy?: number; stream_post_policy?: number;
announce?: boolean; announce?: boolean;
include_public?: boolean; include_public?: boolean;
include_subscribed?: boolean; include_subscribed?: boolean;
include_all_active?: boolean; include_all_active?: boolean;
include_default?: boolean; include_default?: boolean;
include_owner_subscribed?: boolean; include_owner_subscribed?: boolean;
include_subscribers?: boolean; include_subscribers?: boolean;
description?: string; description?: string;
new_name?: string; new_name?: string;
is_private?: boolean; is_private?: boolean;
is_announcement_only?: boolean; is_announcement_only?: boolean;
} }
export interface IPrincipal { export interface IPrincipal {
email: string; email: string;
} }

View file

@ -13,7 +13,7 @@ export const userOperations = [
}, },
}, },
options: [ options: [
{ {
name: 'Create', name: 'Create',
value: 'create', value: 'create',
description: 'Create a user.', description: 'Create a user.',
@ -22,22 +22,22 @@ export const userOperations = [
name: 'Deactivate', name: 'Deactivate',
value: 'deactivate', value: 'deactivate',
description: 'Deactivate a user.', description: 'Deactivate a user.',
}, },
{ {
name: 'Get', name: 'Get',
value: 'get', value: 'get',
description: 'Get a user.', description: 'Get a user.',
}, },
{ {
name: 'Get All', name: 'Get All',
value: 'getAll', value: 'getAll',
description: 'Get all users.', description: 'Get all users.',
},
{
name: 'Update',
value: 'update',
description: 'Update a user.',
}, },
// {
// name: 'Update',
// value: 'update',
// description: 'Update a user.',
// },
], ],
default: 'create', default: 'create',
description: 'The operation to perform.', description: 'The operation to perform.',
@ -45,9 +45,9 @@ export const userOperations = [
] as INodeProperties[]; ] as INodeProperties[];
export const userFields = [ export const userFields = [
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* user:create */ /* user:create */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
{ {
displayName: 'Email', displayName: 'Email',
name: 'email', name: 'email',
@ -65,8 +65,8 @@ export const userFields = [
}, },
default: '', default: '',
description: 'The email address of the new user.', description: 'The email address of the new user.',
}, },
{ {
displayName: 'Full Name', displayName: 'Full Name',
name: 'fullName', name: 'fullName',
type: 'string', type: 'string',
@ -83,8 +83,8 @@ export const userFields = [
}, },
default: '', default: '',
description: 'The full name of the new user.', description: 'The full name of the new user.',
}, },
{ {
displayName: 'Password', displayName: 'Password',
name: 'password', name: 'password',
type: 'string', type: 'string',
@ -101,8 +101,8 @@ export const userFields = [
}, },
default: '', default: '',
description: 'The password of the new user.', description: 'The password of the new user.',
}, },
{ {
displayName: 'Short Name', displayName: 'Short Name',
name: 'shortName', name: 'shortName',
type: 'string', type: 'string',
@ -119,10 +119,11 @@ export const userFields = [
}, },
default: '', default: '',
description: 'The short name of the new user. Not user-visible.', description: 'The short name of the new user. Not user-visible.',
}, },
/* -------------------------------------------------------------------------- */
/* user:get / getAll */ /* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */ /* user:get / getAll */
/* -------------------------------------------------------------------------- */
{ {
displayName: 'User ID', displayName: 'User ID',
name: 'userId', name: 'userId',
@ -140,104 +141,105 @@ export const userFields = [
}, },
default: '', default: '',
description: 'The ID of user to get.', description: 'The ID of user to get.',
}, },
{ {
displayName: 'Additional Fields', displayName: 'Additional Fields',
name: 'additionalFields', name: 'additionalFields',
type: 'collection', type: 'collection',
placeholder: 'Add Field', placeholder: 'Add Field',
default: {}, default: {},
displayOptions: { displayOptions: {
show: { show: {
resource: [ resource: [
'user', 'user',
], ],
operation: [ operation: [
'get', 'getAll' 'get', 'getAll'
], ],
}, },
}, },
options: [ options: [
{ {
displayName: 'Client Gravatar', displayName: 'Client Gravatar',
name: 'clientGravatar', name: 'clientGravatar',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the client supports computing gravatars URLs. If enabled, avatar_url will be included in the response only if there is a Zulip avatar, and will be null for users who are using gravatar as their avatar.', description: 'Whether the client supports computing gravatars URLs. If enabled, avatar_url will be included in the response only if there is a Zulip avatar, and will be null for users who are using gravatar as their avatar.',
}, },
{ {
displayName: 'Custom Profile Fields', displayName: 'Custom Profile Fields',
name: 'includeCustomProfileFields', name: 'includeCustomProfileFields',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether the client wants custom profile field data to be included in the response.', description: 'Whether the client wants custom profile field data to be included in the response.',
}, },
] ]
}, },
/* -------------------------------------------------------------------------- */
/* user:update */ /* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */ /* user:update */
{ /* -------------------------------------------------------------------------- */
displayName: 'User ID', {
name: 'userId', displayName: 'User ID',
type: 'string', name: 'userId',
required: true, type: 'string',
displayOptions: { required: true,
show: { displayOptions: {
resource: [ show: {
'user', resource: [
], 'user',
operation: [ ],
'update', operation: [
], 'update',
}, ],
}, },
default: '', },
description: 'The ID of user to update.', default: '',
}, description: 'The ID of user to update.',
{ },
displayName: 'Additional Fields', {
name: 'additionalFields', displayName: 'Additional Fields',
type: 'collection', name: 'additionalFields',
placeholder: 'Add Field', type: 'collection',
default: {}, placeholder: 'Add Field',
displayOptions: { default: {},
show: { displayOptions: {
resource: [ show: {
'user', resource: [
], 'user',
operation: [ ],
'update' operation: [
], 'update'
}, ],
}, },
options: [ },
{ options: [
displayName: 'Full Name', {
name: 'fullName', displayName: 'Full Name',
type: 'string', name: 'fullName',
default: '', type: 'string',
description: 'The users full name.', default: '',
}, description: 'The users full name.',
{ },
displayName: 'Is Admin', {
name: 'isAdmin', displayName: 'Is Admin',
type: 'boolean', name: 'isAdmin',
default: false, type: 'boolean',
description: 'Whether the target user is an administrator.', default: false,
}, description: 'Whether the target user is an administrator.',
{ },
displayName: 'Is Guest', {
name: 'isGuest', displayName: 'Is Guest',
type: 'boolean', name: 'isGuest',
default: false, type: 'boolean',
description: 'Whether the target user is a guest.', default: false,
}, description: 'Whether the target user is a guest.',
},
{ {
displayName: 'Profile Data', displayName: 'Profile Data',
name: 'profileData', name: 'profileData',
type: 'fixedCollection', type: 'fixedCollection',
default: '', default: {},
description: 'A dictionary containing the to be updated custom profile field data for the user.', description: 'A dictionary containing the to be updated custom profile field data for the user.',
typeOptions: { typeOptions: {
multipleValues: true, multipleValues: true,
@ -266,27 +268,28 @@ export const userFields = [
}, },
], ],
}, },
] ]
}, },
/* -------------------------------------------------------------------------- */
/* user:deactivate */ /* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */ /* user:deactivate */
{ /* -------------------------------------------------------------------------- */
displayName: 'User ID', {
name: 'userId', displayName: 'User ID',
type: 'string', name: 'userId',
required: true, type: 'string',
displayOptions: { required: true,
show: { displayOptions: {
resource: [ show: {
'user', resource: [
], 'user',
operation: [ ],
'deactivate', operation: [
], 'deactivate',
}, ],
}, },
default: '', },
description: 'The ID of user to deactivate.', default: '',
}, description: 'The ID of user to deactivate.',
] as INodeProperties[]; },
] as INodeProperties[];

View file

@ -1,11 +1,11 @@
export interface IUser { export interface IUser {
client_gravatar?: boolean; client_gravatar?: boolean;
include_custom_profile_fields?: boolean; include_custom_profile_fields?: boolean;
full_name?: string; full_name?: string;
is_admin?: boolean; is_admin?: boolean;
is_guest?: boolean; is_guest?: boolean;
profile_data?: [{}]; profile_data?: [{}];
email?: string; email?: string;
password?: string; password?: string;
short_name?: string; short_name?: string;
} }

View file

@ -219,7 +219,7 @@ export class Zulip implements INodeType {
} }
} }
}; };
responseData = await zulipApiRequest.call(this, 'POST', '/user_uploads', {}, {}, undefined, { formData } ); responseData = await zulipApiRequest.call(this, 'POST', '/user_uploads', {}, {}, undefined, { formData });
responseData.uri = `${credentials!.url}${responseData.uri}`; responseData.uri = `${credentials!.url}${responseData.uri}`;
} }
} }
@ -247,6 +247,7 @@ export class Zulip implements INodeType {
} }
responseData = await zulipApiRequest.call(this, 'GET', `/streams`, body); responseData = await zulipApiRequest.call(this, 'GET', `/streams`, body);
responseData = responseData.streams;
} }
if (operation === 'getSubscribed') { if (operation === 'getSubscribed') {
@ -257,41 +258,40 @@ export class Zulip implements INodeType {
} }
responseData = await zulipApiRequest.call(this, 'GET', `/users/me/subscriptions`, body); responseData = await zulipApiRequest.call(this, 'GET', `/users/me/subscriptions`, body);
responseData = responseData.subscriptions;
} }
if (operation === 'create') { if (operation === 'create') {
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
const subscriptions = this.getNodeParameter('subscriptions', i) as IDataObject[]; const subscriptions = this.getNodeParameter('subscriptions', i) as IDataObject[];
//@ts-ignore //@ts-ignore
body.subscriptions = JSON.stringify(subscriptions.properties); body.subscriptions = JSON.stringify(subscriptions.properties);
if (jsonParameters) { if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string; const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') { if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) { if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson)); Object.assign(body, JSON.parse(additionalFieldsJson));
} else { } else {
throw new Error('Additional fields must be a valid JSON'); throw new Error('Additional fields must be a valid JSON');
} }
} }
} else { } else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const subscriptions = this.getNodeParameter('subscriptions', i) as IDataObject;
body.subscriptions = JSON.stringify(subscriptions.properties);
if (additionalFields.inviteOnly) { if (additionalFields.inviteOnly) {
body.invite_only = additionalFields.inviteOnly as boolean; body.invite_only = additionalFields.inviteOnly as boolean;
} }
if (additionalFields.principals) { if (additionalFields.principals) {
const principals : string[] = []; const principals: string[] = [];
//@ts-ignore //@ts-ignore
additionalFields.principals.properties.map((principal : IPrincipal) => { additionalFields.principals.properties.map((principal: IPrincipal) => {
principals.push(principal.email); principals.push(principal.email);
}); });
body.principals = JSON.stringify(principals); body.principals = JSON.stringify(principals);
@ -368,7 +368,7 @@ export class Zulip implements INodeType {
} }
if (resource === 'user') { if (resource === 'user') {
const body : IUser = {}; const body: IUser = {};
if (operation === 'get') { if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as string; const userId = this.getNodeParameter('userId', i) as string;
@ -396,6 +396,7 @@ export class Zulip implements INodeType {
} }
responseData = await zulipApiRequest.call(this, 'GET', `/users`, body); responseData = await zulipApiRequest.call(this, 'GET', `/users`, body);
responseData = responseData.members;
} }
if (operation === 'create') { if (operation === 'create') {
@ -407,7 +408,7 @@ export class Zulip implements INodeType {
responseData = await zulipApiRequest.call(this, 'POST', `/users`, body); responseData = await zulipApiRequest.call(this, 'POST', `/users`, body);
} }
if (operation === 'update') { if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as string; const userId = this.getNodeParameter('userId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
@ -434,19 +435,10 @@ export class Zulip implements INodeType {
responseData = await zulipApiRequest.call(this, 'DELETE', `/users/${userId}`, body); responseData = await zulipApiRequest.call(this, 'DELETE', `/users/${userId}`, body);
} }
} }
// Specific checks because API returns multiple objects within 1 object with each key name
if (responseData.members) {
returnData.push.apply(returnData, responseData.members as IDataObject[]);
}
else if (responseData.streams) { if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData.streams as IDataObject[]); returnData.push.apply(returnData, responseData as IDataObject[]);
} } else {
else if (responseData.subscriptions) {
returnData.push.apply(returnData, responseData.subscriptions as IDataObject[]);
}
else {
returnData.push(responseData as IDataObject); returnData.push(responseData as IDataObject);
} }
} }