first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.azureCosmosDb",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Data & Storage"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/azurecosmosdb/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.azurecosmosdb/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { container, item } from './descriptions';
|
||||
import { listSearch } from './methods';
|
||||
|
||||
export class AzureCosmosDb implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Azure Cosmos DB',
|
||||
name: 'azureCosmosDb',
|
||||
icon: {
|
||||
light: 'file:AzureCosmosDb.svg',
|
||||
dark: 'file:AzureCosmosDb.svg',
|
||||
},
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
|
||||
description: 'Interact with Azure Cosmos DB API',
|
||||
schemaPath: 'Microsoft/AzureCosmosDb',
|
||||
defaults: {
|
||||
name: 'Azure Cosmos DB',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'microsoftAzureCosmosDbSharedKeyApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
baseURL:
|
||||
'=https://{{ $credentials.account }}.documents.azure.com/dbs/{{ $credentials.database }}',
|
||||
json: true,
|
||||
ignoreHttpStatusErrors: true,
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Container',
|
||||
value: 'container',
|
||||
},
|
||||
{
|
||||
name: 'Item',
|
||||
value: 'item',
|
||||
},
|
||||
],
|
||||
default: 'container',
|
||||
},
|
||||
|
||||
...container.description,
|
||||
...item.description,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M105.825 58.635c5.491 22.449-8.458 45.049-31.156 50.48-22.695 5.429-45.547-8.365-51.036-30.815-5.492-22.449 8.458-45.049 31.153-50.48l.078-.018c22.588-5.46 45.374 8.223 50.896 30.565l.065.268Z" fill="#59B3D8"/><path fill-rule="evenodd" clip-rule="evenodd" d="M58.747 85.194c-.013-6.137-5.055-11.1-11.259-11.085h-1.701c1.442-5.932-2.248-11.895-8.246-13.322a11.182 11.182 0 0 0-2.703-.306H23.162c-2.525 12.851 1.246 26.127 10.174 35.796h14.149c6.205.015 11.247-4.948 11.262-11.083ZM72.69 39.233c0 .649.085 1.296.255 1.925h-4.861c-6.445 0-11.667 5.168-11.667 11.543 0 6.372 5.222 11.54 11.667 11.54h38.645c-1.258-13.787-9.486-26.01-21.862-32.477h-4.605c-4.177-.002-7.562 3.339-7.572 7.469Zm34.043 33.587H83.679c-5.259-.013-9.531 4.187-9.552 9.385a9.241 9.241 0 0 0 1.148 4.471c-5.003 1.546-7.792 6.814-6.228 11.765 1.242 3.934 4.938 6.607 9.106 6.589h6.427c12.314-6.454 20.607-18.51 22.153-32.21Z" fill="#B6DEEC"/><path fill-rule="evenodd" clip-rule="evenodd" d="M17.382 40.624a1.286 1.286 0 0 1-1.3-1.275v-.003c-.021-8.064-6.637-14.587-14.79-14.579A1.286 1.286 0 0 1 0 23.489c0-.703.579-1.275 1.292-1.275 8.143.007 14.756-6.503 14.79-14.554a1.29 1.29 0 0 1 1.356-1.227c.672.028 1.21.56 1.241 1.227.021 8.061 6.639 14.584 14.792 14.577.713 0 1.292.572 1.292 1.277 0 .706-.579 1.279-1.292 1.279-8.148-.011-14.766 6.507-14.792 14.566-.01.7-.589 1.265-1.297 1.265Z" fill="#B7D332"/><path fill-rule="evenodd" clip-rule="evenodd" d="M108.6 122.793a.764.764 0 0 1-.768-.759c-.018-4.821-3.98-8.719-8.854-8.709a.762.762 0 0 1-.77-.756c0-.419.342-.759.765-.759h.005c4.872.002 8.826-3.893 8.844-8.711a.77.77 0 0 1 .778-.767.77.77 0 0 1 .775.767c.018 4.818 3.972 8.713 8.843 8.711a.761.761 0 0 1 .77.756.759.759 0 0 1-.765.759h-.005c-4.871-.002-8.828 3.893-8.843 8.714a.764.764 0 0 1-.773.754h-.002Z" fill="#0072C5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M126.317 30.84c-4.035-6.539-14.175-8.049-29.306-4.384a121.688 121.688 0 0 0-13.893 4.384 42.829 42.829 0 0 1 8.187 5.173c2.574-.836 5.101-1.59 7.512-2.173a53.33 53.33 0 0 1 12.335-1.727c4.957 0 7.691 1.211 8.606 2.686 1.496 2.423.119 8.816-8.681 18.871-1.566 1.789-3.326 3.601-5.179 5.423a175.936 175.936 0 0 1-31.843 24.149 176.032 176.032 0 0 1-36.329 17.105c-15.317 4.936-25.773 4.836-28.119 1.048-2.342-3.788 2.344-13.048 13.776-24.29a41.005 41.005 0 0 1-.938-9.735c-18.2 16.271-24.09 30.365-19.387 37.981 2.463 3.985 7.844 6.229 15.705 6.229a80.772 80.772 0 0 0 27.183-5.932 194.648 194.648 0 0 0 32.11-15.926 193.405 193.405 0 0 0 28.884-21.148 118.565 118.565 0 0 0 9.947-9.941c10.207-11.655 13.466-21.268 9.43-27.793Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_attachments": {
|
||||
"type": "string"
|
||||
},
|
||||
"_etag": {
|
||||
"type": "string"
|
||||
},
|
||||
"_rid": {
|
||||
"type": "string"
|
||||
},
|
||||
"_self": {
|
||||
"type": "string"
|
||||
},
|
||||
"_ts": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"documentType": {
|
||||
"type": "string"
|
||||
},
|
||||
"estadoManifiesto": {
|
||||
"type": "string"
|
||||
},
|
||||
"estadoValidacion": {
|
||||
"type": "string"
|
||||
},
|
||||
"eTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"fechaExpedicionManifiesto": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"ingresoIdManifiesto": {
|
||||
"type": "integer"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastModifiedDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdateDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "null"
|
||||
},
|
||||
"numNitEmpresaTransporte": {
|
||||
"type": "string"
|
||||
},
|
||||
"numPlaca": {
|
||||
"type": "string"
|
||||
},
|
||||
"partitionKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"procesado": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"puntosControl": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"codPuntoControl": {
|
||||
"type": "integer"
|
||||
},
|
||||
"estadoPunto": {
|
||||
"type": "string"
|
||||
},
|
||||
"fechaCita": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"selfLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeStamp": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"documentType": {
|
||||
"type": "string"
|
||||
},
|
||||
"eTag": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastModifiedDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"partitionKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"resourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"selfLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeStamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { HeaderConstants } from '../helpers/constants';
|
||||
import { untilContainerSelected } from '../helpers/utils';
|
||||
|
||||
export const containerResourceLocator: INodeProperties = {
|
||||
displayName: 'Container',
|
||||
name: 'container',
|
||||
default: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
},
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From list',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchContainers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
hint: 'Enter the container ID',
|
||||
placeholder: 'e.g. AndersenFamily',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^[\\w+=,.@-]+$',
|
||||
errorMessage: 'The container ID must follow the allowed pattern',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
type: 'resourceLocator',
|
||||
};
|
||||
|
||||
export const itemResourceLocator: INodeProperties = {
|
||||
displayName: 'Item',
|
||||
name: 'item',
|
||||
default: {
|
||||
mode: 'list',
|
||||
value: '',
|
||||
},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
},
|
||||
},
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From list',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchItems',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
hint: 'Enter the item ID',
|
||||
placeholder: 'e.g. AndersenFamily',
|
||||
type: 'string',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^[\\w+=,.@-]+$',
|
||||
errorMessage: 'The item ID must follow the allowed pattern',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
type: 'resourceLocator',
|
||||
};
|
||||
|
||||
export const paginationParameters: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
routing: {
|
||||
send: {
|
||||
paginate: '={{ $value }}',
|
||||
},
|
||||
operations: {
|
||||
pagination: {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
continue: `={{ !!$response.headers?.["${HeaderConstants.X_MS_CONTINUATION}"] }}`,
|
||||
request: {
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_CONTINUATION]: `={{ $response.headers?.["${HeaderConstants.X_MS_CONTINUATION}"] }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
default: 50,
|
||||
description: 'Max number of results to return',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_MAX_ITEM_COUNT]: '={{ $value || undefined }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
validateType: 'number',
|
||||
},
|
||||
];
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as del from './delete.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as getAll from './getAll.operation';
|
||||
import { handleError } from '../../helpers/errorHandler';
|
||||
import { simplifyData } from '../../helpers/utils';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['container'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a container',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/colls',
|
||||
},
|
||||
output: {
|
||||
postReceive: [handleError],
|
||||
},
|
||||
},
|
||||
action: 'Create container',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a container',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/colls/{{ $parameter["container"] }}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
handleError,
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "deleted": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete container',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Retrieve a container',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/colls/{{ $parameter["container"] }}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [handleError, simplifyData],
|
||||
},
|
||||
},
|
||||
action: 'Get container',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Retrieve a list of containers',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/colls',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
handleError,
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'DocumentCollections',
|
||||
},
|
||||
},
|
||||
simplifyData,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get many containers',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
},
|
||||
|
||||
...create.description,
|
||||
...del.description,
|
||||
...get.description,
|
||||
...getAll.description,
|
||||
];
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteSingleFunctions,
|
||||
IHttpRequestOptions,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { OperationalError, updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { HeaderConstants } from '../../helpers/constants';
|
||||
import { processJsonInput } from '../../helpers/utils';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'containerCreate',
|
||||
default: '',
|
||||
description: 'Unique identifier for the new container',
|
||||
placeholder: 'e.g. Container1',
|
||||
required: true,
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [
|
||||
async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const id = this.getNodeParameter('containerCreate') as string;
|
||||
|
||||
if (/\s/.test(id)) {
|
||||
throw new OperationalError('The container ID must not contain spaces.');
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(id)) {
|
||||
throw new OperationalError(
|
||||
'The container ID may only contain letters, numbers, hyphens, and underscores.',
|
||||
);
|
||||
}
|
||||
|
||||
(requestOptions.body as IDataObject).id = id;
|
||||
|
||||
return requestOptions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
displayName: 'Partition Key',
|
||||
name: 'partitionKey',
|
||||
default: '{\n\t"paths": [\n\t\t"/id"\n\t],\n\t"kind": "Hash",\n\t"version": 2\n}',
|
||||
description:
|
||||
'The partition key is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.',
|
||||
required: true,
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [
|
||||
async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const rawPartitionKey = this.getNodeParameter('partitionKey') as IDataObject;
|
||||
const partitionKey = processJsonInput(rawPartitionKey, 'Partition Key', {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
});
|
||||
(requestOptions.body as IDataObject).partitionKey = partitionKey;
|
||||
return requestOptions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Indexing Policy',
|
||||
name: 'indexingPolicy',
|
||||
default:
|
||||
'{\n\t"indexingMode": "consistent",\n\t"automatic": true,\n\t"includedPaths": [\n\t\t{\n\t\t\t"path": "/*"\n\t\t}\n\t],\n\t"excludedPaths": []\n}',
|
||||
description: 'This value is used to configure indexing policy',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [
|
||||
async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const rawIndexingPolicy = this.getNodeParameter(
|
||||
'additionalFields.indexingPolicy',
|
||||
) as IDataObject;
|
||||
const indexPolicy = processJsonInput(rawIndexingPolicy, 'Indexing Policy');
|
||||
(requestOptions.body as IDataObject).indexingPolicy = indexPolicy;
|
||||
return requestOptions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
displayName: 'Max RU/s (for Autoscale)',
|
||||
name: 'maxThroughput',
|
||||
default: 1000,
|
||||
description: 'The user specified autoscale max RU/s',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/additionalFields.offerThroughput': [{ _cnd: { exists: true } }],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_COSMOS_OFFER_AUTOPILOT_SETTING]: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Manual Throughput RU/s',
|
||||
name: 'offerThroughput',
|
||||
default: 400,
|
||||
description:
|
||||
'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/additionalFields.maxThroughput': [{ _cnd: { exists: true } }],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
request: {
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_OFFER_THROUGHPUT]: '={{ $value }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 400,
|
||||
},
|
||||
},
|
||||
],
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['container'],
|
||||
operation: ['create'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { containerResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to delete' },
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['container'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { containerResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to retrieve' },
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['container'],
|
||||
operation: ['get'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { paginationParameters } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
...paginationParameters,
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['container'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as container from './container/Container.resource';
|
||||
export * as item from './item/Item.resource';
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as create from './create.operation';
|
||||
import * as del from './delete.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as getAll from './getAll.operation';
|
||||
import * as query from './query.operation';
|
||||
import * as update from './update.operation';
|
||||
import { HeaderConstants } from '../../helpers/constants';
|
||||
import { handleError } from '../../helpers/errorHandler';
|
||||
import { simplifyData, validatePartitionKey } from '../../helpers/utils';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new item',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validatePartitionKey],
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/colls/{{ $parameter["container"] }}/docs',
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_DOCUMENTDB_IS_UPSERT]: 'True',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
postReceive: [handleError],
|
||||
},
|
||||
},
|
||||
action: 'Create item',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete an existing item',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validatePartitionKey],
|
||||
},
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/colls/{{ $parameter["container"] }}/docs/{{ $parameter["item"] }}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
handleError,
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "deleted": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete item',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Retrieve an item',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validatePartitionKey],
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/colls/{{ $parameter["container"]}}/docs/{{$parameter["item"]}}',
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_DOCUMENTDB_IS_UPSERT]: 'True',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
postReceive: [handleError, simplifyData],
|
||||
},
|
||||
},
|
||||
action: 'Get item',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
description: 'Retrieve a list of items',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/colls/{{ $parameter["container"] }}/docs',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
handleError,
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'Documents',
|
||||
},
|
||||
},
|
||||
simplifyData,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get many items',
|
||||
},
|
||||
{
|
||||
name: 'Execute Query',
|
||||
value: 'query',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/colls/{{ $parameter["container"] }}/docs',
|
||||
headers: {
|
||||
'Content-Type': 'application/query+json',
|
||||
'x-ms-documentdb-isquery': 'True',
|
||||
'x-ms-documentdb-query-enablecrosspartition': 'True',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
handleError,
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'Documents',
|
||||
},
|
||||
},
|
||||
simplifyData,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Query items',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update an existing item',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validatePartitionKey],
|
||||
},
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/colls/{{ $parameter["container"] }}/docs/{{ $parameter["item"] }}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
postReceive: [handleError],
|
||||
},
|
||||
},
|
||||
action: 'Update item',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
},
|
||||
|
||||
...create.description,
|
||||
...del.description,
|
||||
...get.description,
|
||||
...getAll.description,
|
||||
...query.description,
|
||||
...update.description,
|
||||
];
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteSingleFunctions,
|
||||
IHttpRequestOptions,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { processJsonInput, untilContainerSelected } from '../../helpers/utils';
|
||||
import { containerResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
{
|
||||
displayName: 'Item Contents',
|
||||
name: 'customProperties',
|
||||
default: '{\n\t"id": "replace_with_new_document_id"\n}',
|
||||
description: 'The item contents as a JSON object',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
},
|
||||
},
|
||||
hint: 'The item requires an ID and partition key value if a custom key is set',
|
||||
required: true,
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [
|
||||
async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const rawCustomProperties = this.getNodeParameter('customProperties') as IDataObject;
|
||||
const customProperties = processJsonInput(
|
||||
rawCustomProperties,
|
||||
'Item Contents',
|
||||
undefined,
|
||||
['id'],
|
||||
);
|
||||
requestOptions.body = customProperties;
|
||||
return requestOptions;
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: 'json',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['create'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { untilContainerSelected, untilItemSelected } from '../../helpers/utils';
|
||||
import { containerResourceLocator, itemResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
{ ...itemResourceLocator, description: 'Select the item to be deleted' },
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
...untilItemSelected,
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Partition Key',
|
||||
name: 'partitionKey',
|
||||
default: '',
|
||||
hint: 'Only required if a custom partition key is set for the container',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
placeholder: 'Add Partition Key',
|
||||
type: 'collection',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { untilContainerSelected, untilItemSelected } from '../../helpers/utils';
|
||||
import { containerResourceLocator, itemResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
{ ...itemResourceLocator, description: 'Select the item you want to retrieve' },
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
...untilItemSelected,
|
||||
},
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
...untilItemSelected,
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Partition Key',
|
||||
name: 'partitionKey',
|
||||
default: '',
|
||||
hint: 'Only required if a custom partition key is set for the container',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
placeholder: 'Add Partition Key',
|
||||
type: 'collection',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['get'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { containerResourceLocator, paginationParameters } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
...paginationParameters,
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { updateDisplayOptions, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { validateQueryParameters } from '../../helpers/utils';
|
||||
import { containerResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
default: '',
|
||||
description:
|
||||
"The SQL query to execute. Use $1, $2, $3, etc., to reference the 'Query Parameters' set in the options below.",
|
||||
hint: 'Consider using query parameters to prevent SQL injection attacks. Add them in the options below.',
|
||||
noDataExpression: true,
|
||||
placeholder: 'e.g. SELECT id, name FROM c WHERE c.name = $1',
|
||||
required: true,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'query',
|
||||
value: "={{ $value.replace(/\\$(\\d+)/g, '@Param$1') }}",
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
editor: 'sqlEditor',
|
||||
sqlDialect: 'StandardSQL',
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
name: 'simple',
|
||||
default: true,
|
||||
description: 'Whether to return a simplified version of the response instead of the raw data',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query Options',
|
||||
name: 'queryOptions',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Query Parameters',
|
||||
name: 'queryParameters',
|
||||
default: '',
|
||||
description:
|
||||
'Comma-separated list of values used as query parameters. Use $1, $2, $3, etc., in your query.',
|
||||
hint: 'Reference them in your query as $1, $2, $3…',
|
||||
placeholder: 'e.g. value1,value2,value3',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validateQueryParameters],
|
||||
},
|
||||
},
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
placeholder: 'Add options',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['query'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
untilContainerSelected,
|
||||
untilItemSelected,
|
||||
validateCustomProperties,
|
||||
} from '../../helpers/utils';
|
||||
import { containerResourceLocator, itemResourceLocator } from '../common';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{ ...containerResourceLocator, description: 'Select the container you want to use' },
|
||||
{ ...itemResourceLocator, description: 'Select the item to be updated' },
|
||||
{
|
||||
displayName: 'Item Contents',
|
||||
name: 'customProperties',
|
||||
default: '{}',
|
||||
description: 'The item contents as a JSON object',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
...untilItemSelected,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [validateCustomProperties],
|
||||
},
|
||||
},
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
...untilContainerSelected,
|
||||
...untilItemSelected,
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Partition Key',
|
||||
name: 'partitionKey',
|
||||
type: 'string',
|
||||
hint: 'Only required if a custom partition key is set for the container',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
placeholder: 'Add Partition Key',
|
||||
type: 'collection',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['item'],
|
||||
operation: ['update'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
@@ -0,0 +1,22 @@
|
||||
export const RESOURCE_TYPES = [
|
||||
'dbs',
|
||||
'colls',
|
||||
'sprocs',
|
||||
'udfs',
|
||||
'triggers',
|
||||
'users',
|
||||
'permissions',
|
||||
'docs',
|
||||
];
|
||||
|
||||
export const CURRENT_VERSION = '2018-12-31';
|
||||
|
||||
export const HeaderConstants = {
|
||||
AUTHORIZATION: 'authorization',
|
||||
X_MS_CONTINUATION: 'x-ms-continuation',
|
||||
X_MS_COSMOS_OFFER_AUTOPILOT_SETTING: 'x-ms-cosmos-offer-autopilot-setting',
|
||||
X_MS_DOCUMENTDB_IS_UPSERT: 'x-ms-documentdb-is-upsert',
|
||||
X_MS_DOCUMENTDB_PARTITIONKEY: 'x-ms-documentdb-partitionkey',
|
||||
X_MS_MAX_ITEM_COUNT: 'x-ms-max-item-count',
|
||||
X_MS_OFFER_THROUGHPUT: 'x-ms-offer-throughput',
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
IExecuteSingleFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import type { IErrorResponse } from './interfaces';
|
||||
|
||||
export const ErrorMap = {
|
||||
Container: {
|
||||
Conflict: {
|
||||
getMessage: (id: string) => `Container "${id}" already exists.`,
|
||||
description: "Use a unique value for 'ID' and try again.",
|
||||
},
|
||||
NotFound: {
|
||||
getMessage: (id: string) => `Container "${id}" was not found.`,
|
||||
description: "Double-check the value in the parameter 'Container' and try again.",
|
||||
},
|
||||
},
|
||||
Item: {
|
||||
NotFound: {
|
||||
getMessage: (id: string) => `Item "${id}" was not found.`,
|
||||
description:
|
||||
"Double-check the values in the parameter 'Item' and 'Partition Key' (if applicable) and try again.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function handleError(
|
||||
this: IExecuteSingleFunctions,
|
||||
data: INodeExecutionData[],
|
||||
response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const error = response.body as IErrorResponse;
|
||||
let errorMessage = error.message;
|
||||
|
||||
let errorDetails: string[] | undefined = undefined;
|
||||
|
||||
if (resource === 'container') {
|
||||
if (error.code === 'Conflict') {
|
||||
const newContainerValue = this.getNodeParameter('containerCreate') as string;
|
||||
throw new NodeApiError(this.getNode(), error as unknown as JsonObject, {
|
||||
message: ErrorMap.Container.Conflict.getMessage(newContainerValue ?? 'Unknown'),
|
||||
description: ErrorMap.Container.Conflict.description,
|
||||
});
|
||||
}
|
||||
if (error.code === 'NotFound') {
|
||||
const containerValue = this.getNodeParameter('container', undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
throw new NodeApiError(this.getNode(), error as unknown as JsonObject, {
|
||||
message: ErrorMap.Container.NotFound.getMessage(containerValue ?? 'Unknown'),
|
||||
description: ErrorMap.Container.NotFound.description,
|
||||
});
|
||||
}
|
||||
} else if (resource === 'item') {
|
||||
if (error.code === 'NotFound') {
|
||||
const itemValue = this.getNodeParameter('item', undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
throw new NodeApiError(this.getNode(), error as unknown as JsonObject, {
|
||||
message: ErrorMap.Item.NotFound.getMessage(itemValue ?? 'Unknown'),
|
||||
description: ErrorMap.Item.NotFound.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Certain error responses have nested Message
|
||||
errorMessage = jsonParse<{
|
||||
message: string;
|
||||
}>(errorMessage).message;
|
||||
} catch {}
|
||||
|
||||
const match = errorMessage.match(/Message: ({.*?})/);
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
errorDetails = jsonParse<{
|
||||
Errors: string[];
|
||||
}>(match[1]).Errors;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (errorDetails && errorDetails.length > 0) {
|
||||
throw new NodeApiError(this.getNode(), error as unknown as JsonObject, {
|
||||
message: error.code,
|
||||
description: errorDetails.join('\n'),
|
||||
});
|
||||
} else {
|
||||
throw new NodeApiError(this.getNode(), error as unknown as JsonObject, {
|
||||
message: error.code,
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface ICosmosDbCredentials {
|
||||
account: string;
|
||||
key: string;
|
||||
database: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export interface IErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IContainer {
|
||||
partitionKey: {
|
||||
paths: string[];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteSingleFunctions,
|
||||
IHttpRequestOptions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
NodeApiError,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeOperationError, OperationalError } from 'n8n-workflow';
|
||||
|
||||
import { HeaderConstants } from './constants';
|
||||
import { ErrorMap } from './errorHandler';
|
||||
import type { IContainer } from './interfaces';
|
||||
import { azureCosmosDbApiRequest } from '../transport';
|
||||
|
||||
export async function getPartitionKey(this: IExecuteSingleFunctions): Promise<string> {
|
||||
const container = this.getNodeParameter('container', undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
let partitionKeyField: string | undefined = undefined;
|
||||
try {
|
||||
const responseData = (await azureCosmosDbApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/colls/${container}`,
|
||||
)) as IContainer;
|
||||
partitionKeyField = responseData.partitionKey?.paths[0]?.replace('/', '');
|
||||
} catch (error) {
|
||||
const err = error as NodeApiError;
|
||||
if (err.httpCode === '404') {
|
||||
err.message = ErrorMap.Container.NotFound.getMessage(container);
|
||||
err.description = ErrorMap.Container.NotFound.description;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!partitionKeyField) {
|
||||
throw new NodeOperationError(this.getNode(), 'Partition key not found', {
|
||||
description: 'Failed to determine the partition key for this collection',
|
||||
});
|
||||
}
|
||||
|
||||
return partitionKeyField;
|
||||
}
|
||||
|
||||
export async function simplifyData(
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
_response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
|
||||
if (!simple) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const simplifyFields = (data: IDataObject): IDataObject => {
|
||||
const simplifiedData = Object.keys(data)
|
||||
.filter((key) => !key.startsWith('_'))
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = data[key];
|
||||
return acc;
|
||||
}, {} as IDataObject);
|
||||
|
||||
return simplifiedData;
|
||||
};
|
||||
|
||||
return items.map((item) => {
|
||||
const simplifiedData = simplifyFields(item.json);
|
||||
return { json: simplifiedData } as INodeExecutionData;
|
||||
});
|
||||
}
|
||||
|
||||
export async function validateQueryParameters(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const query = this.getNodeParameter('query', '') as string;
|
||||
const queryOptions = this.getNodeParameter('options.queryOptions') as IDataObject;
|
||||
|
||||
const parameterNames = query.replace(/\$(\d+)/g, '@Param$1').match(/@\w+/g) ?? [];
|
||||
|
||||
const queryParamsString = queryOptions?.queryParameters as string;
|
||||
const parameterValues = queryParamsString
|
||||
? queryParamsString.split(',').map((param) => param.trim())
|
||||
: [];
|
||||
|
||||
if (parameterNames.length !== parameterValues.length) {
|
||||
throw new NodeOperationError(this.getNode(), 'Empty parameter value provided', {
|
||||
description: 'Please provide non-empty values for the query parameters',
|
||||
});
|
||||
}
|
||||
|
||||
requestOptions.body = {
|
||||
...(requestOptions.body as IDataObject),
|
||||
parameters: parameterNames.map((name, index) => ({
|
||||
name,
|
||||
value: parameterValues[index],
|
||||
})),
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export function processJsonInput<T>(
|
||||
jsonData: T,
|
||||
inputName?: string,
|
||||
fallbackValue: T | undefined = undefined,
|
||||
disallowSpacesIn?: string[],
|
||||
): Record<string, unknown> {
|
||||
let values: Record<string, unknown> = {};
|
||||
|
||||
const input = inputName ? `'${inputName}' ` : '';
|
||||
|
||||
if (typeof jsonData === 'string') {
|
||||
try {
|
||||
values = jsonParse(jsonData, { fallbackValue }) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
throw new OperationalError(`Input ${input}must contain a valid JSON`, { level: 'warning' });
|
||||
}
|
||||
} else if (jsonData && typeof jsonData === 'object') {
|
||||
values = jsonData as Record<string, unknown>;
|
||||
} else {
|
||||
throw new OperationalError(`Input ${input}must contain a valid JSON`, { level: 'warning' });
|
||||
}
|
||||
|
||||
disallowSpacesIn?.forEach((key) => {
|
||||
const value = values[key];
|
||||
if (typeof value === 'string' && value.includes(' ')) {
|
||||
throw new OperationalError(
|
||||
`${inputName ? `'${inputName}'` : ''} property '${key}' should not contain spaces (received "${value}")`,
|
||||
{ level: 'warning' },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export async function validatePartitionKey(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
let customProperties = this.getNodeParameter('customProperties', {}) as IDataObject;
|
||||
|
||||
const partitionKey = await getPartitionKey.call(this);
|
||||
|
||||
if (typeof customProperties === 'string') {
|
||||
try {
|
||||
customProperties = jsonParse(customProperties);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Invalid JSON format in "Item Contents"', {
|
||||
description: 'Ensure the "Item Contents" field contains a valid JSON object',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let partitionKeyValue: string = '';
|
||||
const needsPartitionKey = ['update', 'delete', 'get'].includes(operation);
|
||||
|
||||
if (operation === 'create') {
|
||||
if (!(partitionKey in customProperties) || !customProperties[partitionKey]) {
|
||||
throw new NodeOperationError(this.getNode(), "Partition key not found in 'Item Contents'", {
|
||||
description: `Partition key '${partitionKey}' must be present and have a valid, non-empty value in 'Item Contents'.`,
|
||||
});
|
||||
}
|
||||
partitionKeyValue = customProperties[partitionKey] as string;
|
||||
} else if (needsPartitionKey) {
|
||||
try {
|
||||
partitionKeyValue =
|
||||
partitionKey === 'id'
|
||||
? String(this.getNodeParameter('item', undefined, { extractValue: true }) ?? '')
|
||||
: String(this.getNodeParameter('additionalFields.partitionKey', undefined) ?? '');
|
||||
|
||||
if (!partitionKeyValue) {
|
||||
throw new NodeOperationError(this.getNode(), 'Partition key is empty', {
|
||||
description: 'Ensure the "Partition Key" field has a valid, non-empty value.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), 'Partition key is missing or empty', {
|
||||
description: 'Ensure the "Partition Key" field exists and has a valid, non-empty value.',
|
||||
});
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
const idValue = String(
|
||||
this.getNodeParameter('item', undefined, { extractValue: true }) ?? '',
|
||||
);
|
||||
|
||||
(requestOptions.body as IDataObject).id = idValue;
|
||||
(requestOptions.body as IDataObject)[partitionKey] = partitionKeyValue;
|
||||
}
|
||||
}
|
||||
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers,
|
||||
[HeaderConstants.X_MS_DOCUMENTDB_PARTITIONKEY]: `["${partitionKeyValue}"]`,
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function validateCustomProperties(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const rawCustomProperties = this.getNodeParameter('customProperties') as IDataObject;
|
||||
const customProperties = processJsonInput(rawCustomProperties, 'Item Contents');
|
||||
if (
|
||||
Object.keys(customProperties).length === 0 ||
|
||||
Object.values(customProperties).every((val) => val === undefined || val === null || val === '')
|
||||
) {
|
||||
throw new NodeOperationError(this.getNode(), 'Item contents are empty', {
|
||||
description: 'Ensure the "Item Contents" field contains at least one valid property.',
|
||||
});
|
||||
}
|
||||
requestOptions.body = {
|
||||
...(requestOptions.body as IDataObject),
|
||||
...customProperties,
|
||||
};
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export const untilContainerSelected = { container: [''] };
|
||||
|
||||
export const untilItemSelected = { item: [''] };
|
||||
@@ -0,0 +1 @@
|
||||
export * as listSearch from './listSearch';
|
||||
@@ -0,0 +1,76 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchResult,
|
||||
INodeListSearchItems,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { HeaderConstants } from '../helpers/constants';
|
||||
import { azureCosmosDbApiRequest } from '../transport';
|
||||
|
||||
function formatResults(items: IDataObject[], filter?: string): INodeListSearchItems[] {
|
||||
return items
|
||||
.map(({ id }) => ({
|
||||
name: String(id).replace(/ /g, ''),
|
||||
value: String(id),
|
||||
}))
|
||||
.filter(({ name }) => !filter || name.includes(filter))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export async function searchContainers(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const headers = paginationToken ? { [HeaderConstants.X_MS_CONTINUATION]: paginationToken } : {};
|
||||
const responseData = (await azureCosmosDbApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/colls',
|
||||
{},
|
||||
{},
|
||||
headers,
|
||||
true,
|
||||
)) as {
|
||||
body: IDataObject;
|
||||
headers: IDataObject;
|
||||
};
|
||||
|
||||
const containers = responseData.body.DocumentCollections as IDataObject[];
|
||||
|
||||
return {
|
||||
results: formatResults(containers, filter),
|
||||
paginationToken: responseData.headers[HeaderConstants.X_MS_CONTINUATION],
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchItems(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const container = this.getCurrentNodeParameter('container', {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
const headers = paginationToken ? { [HeaderConstants.X_MS_CONTINUATION]: paginationToken } : {};
|
||||
const responseData = (await azureCosmosDbApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/colls/${container}/docs`,
|
||||
{},
|
||||
{},
|
||||
headers,
|
||||
true,
|
||||
)) as {
|
||||
body: IDataObject;
|
||||
headers: IDataObject;
|
||||
};
|
||||
|
||||
const items = responseData.body.Documents as IDataObject[];
|
||||
|
||||
return {
|
||||
results: formatResults(items, filter),
|
||||
paginationToken: responseData.headers[HeaderConstants.X_MS_CONTINUATION],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Create Container', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.post('/colls', {
|
||||
id: 'container1',
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*',
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
},
|
||||
})
|
||||
.matchHeader('x-ms-offer-throughput', '400')
|
||||
.reply(201, {
|
||||
id: 'container1',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*',
|
||||
},
|
||||
],
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?',
|
||||
},
|
||||
],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAOKH+8U=',
|
||||
_ts: 1742313299,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAOKH+8U=/',
|
||||
_etag: '"00005702-0000-0300-0000-67d997530000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['create.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "create",
|
||||
"containerCreate": "container1",
|
||||
"additionalFields": {
|
||||
"partitionKey": "{\n\t\"paths\": [\n\t\t\"/id\"\n\t],\n\t\"kind\": \"Hash\",\n\t\"version\": 2\n}",
|
||||
"indexingPolicy": "{\n\t\"indexingMode\": \"consistent\",\n\t\"automatic\": true,\n\t\"includedPaths\": [\n\t\t{\n\t\t\t\"path\": \"/*\"\n\t\t}\n\t],\n\t\"excludedPaths\": []\n}",
|
||||
"offerThroughput": 400
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
|
||||
"name": "Azure Cosmos DB",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos DB",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Azure Cosmos DB": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos DB": [
|
||||
{
|
||||
"json": {
|
||||
"id": "container1",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/id"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
},
|
||||
"_rid": "4PVyAOKH+8U=",
|
||||
"_ts": 1742313299,
|
||||
"_self": "dbs/4PVyAA==/colls/4PVyAOKH+8U=/",
|
||||
"_etag": "\"00005702-0000-0300-0000-67d997530000\"",
|
||||
"_docs": "docs/",
|
||||
"_sprocs": "sprocs/",
|
||||
"_triggers": "triggers/",
|
||||
"_udfs": "udfs/",
|
||||
"_conflicts": "conflicts/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Delete Container', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.delete('/colls/container1')
|
||||
.reply(204, {});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['delete.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "delete",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
|
||||
"name": "Azure Cosmos DB",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos DB",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Azure Cosmos DB": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos DB": [
|
||||
{
|
||||
"json": {
|
||||
"deleted": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Get Container', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/container1')
|
||||
.reply(200, {
|
||||
id: 'container1',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*',
|
||||
},
|
||||
],
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?',
|
||||
},
|
||||
],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAMPuBto=',
|
||||
_ts: 1742298418,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/',
|
||||
_etag: '"00004402-0000-0300-0000-67d95d320000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['get.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "fabb9c53-ca91-4b11-84e3-3b172b10b83a",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "get",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "134e2bbd-937a-4ee8-9ad7-7a745cf0905f",
|
||||
"name": "Azure Cosmos DB",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos DB",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Azure Cosmos DB": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos DB": [
|
||||
{
|
||||
"json": {
|
||||
"id": "container1",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/id"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Get All Containers', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls')
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
_rid: '4PVyAA==',
|
||||
DocumentCollections: [
|
||||
{
|
||||
id: 'newOne3',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
fullTextIndexes: [],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: { type: 'Geography' },
|
||||
},
|
||||
{
|
||||
id: 'newId',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
fullTextIndexes: [],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
uniqueKeyPolicy: {
|
||||
uniqueKeys: [],
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: { type: 'Geography' },
|
||||
fullTextPolicy: {
|
||||
defaultLanguage: 'en-US',
|
||||
fullTextPaths: [],
|
||||
},
|
||||
computedProperties: [],
|
||||
},
|
||||
{
|
||||
id: 'ContainerWithNameAsKey',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
fullTextIndexes: [],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/Name'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
uniqueKeyPolicy: {
|
||||
uniqueKeys: [],
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: { type: 'Geography' },
|
||||
fullTextPolicy: {
|
||||
defaultLanguage: 'en-US',
|
||||
fullTextPaths: [],
|
||||
},
|
||||
computedProperties: [],
|
||||
},
|
||||
{
|
||||
id: 'ContainerWithPhoneNrAsKey',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
fullTextIndexes: [],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/PhoneNumber'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: { type: 'Geography' },
|
||||
},
|
||||
],
|
||||
_count: 4,
|
||||
},
|
||||
{
|
||||
'x-ms-continuation': '4PVyAKoVaBQ=',
|
||||
},
|
||||
)
|
||||
.get('/colls')
|
||||
.matchHeader('x-ms-continuation', '4PVyAKoVaBQ=')
|
||||
.reply(200, {
|
||||
_rid: '4PVyAA==',
|
||||
DocumentCollections: [
|
||||
{
|
||||
id: 'ContainerWithPhoneNrAsKey',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
fullTextIndexes: [],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/PhoneNumber'],
|
||||
kind: 'Hash',
|
||||
version: 2,
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: { type: 'Geography' },
|
||||
},
|
||||
],
|
||||
_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['getAll.workflow.json'],
|
||||
});
|
||||
});
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-180, -520],
|
||||
"id": "8218dacc-3b5f-460a-a773-817faf012ba9",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [20, -520],
|
||||
"id": "4568cad5-4e55-4370-9fbe-7f19e03faa67",
|
||||
"name": "getAllContainers",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "iTUZNEdLdLW0RpF9",
|
||||
"name": "Azure Cosmos DB account "
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "getAllContainers",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"getAllContainers": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"getAllContainers": [
|
||||
{
|
||||
"json": {
|
||||
"id": "newOne3",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
],
|
||||
"fullTextIndexes": []
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/id"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "newId",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
],
|
||||
"fullTextIndexes": []
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/id"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": []
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
},
|
||||
"fullTextPolicy": {
|
||||
"defaultLanguage": "en-US",
|
||||
"fullTextPaths": []
|
||||
},
|
||||
"computedProperties": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "ContainerWithNameAsKey",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
],
|
||||
"fullTextIndexes": []
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/Name"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"uniqueKeyPolicy": {
|
||||
"uniqueKeys": []
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
},
|
||||
"fullTextPolicy": {
|
||||
"defaultLanguage": "en-US",
|
||||
"fullTextPaths": []
|
||||
},
|
||||
"computedProperties": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "ContainerWithPhoneNrAsKey",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
],
|
||||
"fullTextIndexes": []
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": ["/PhoneNumber"],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
},
|
||||
"conflictResolutionPolicy": {
|
||||
"mode": "LastWriterWins",
|
||||
"conflictResolutionPath": "/_ts",
|
||||
"conflictResolutionProcedure": ""
|
||||
},
|
||||
"geospatialConfig": {
|
||||
"type": "Geography"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const credentials = {
|
||||
microsoftAzureCosmosDbSharedKeyApi: {
|
||||
account: 'n8n-us-east-account',
|
||||
key: 'I3rwpzP0XoFpNzJ7hRIUXjwgpD1qaVKi71NZBbk7oOHUXrbd80WAoIAAoRaT47W9hHO3b6us1yABACDbVdilag==',
|
||||
database: 'database_1',
|
||||
baseUrl: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import type { IHttpRequestOptions, IRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { MicrosoftAzureCosmosDbSharedKeyApi } from '@credentials/MicrosoftAzureCosmosDbSharedKeyApi.credentials';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
createHmac: jest.fn(() => ({
|
||||
update: jest.fn(() => ({
|
||||
digest: jest.fn(() => 'fake-signature'),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Azure Cosmos DB', () => {
|
||||
const { account, key, baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
describe('authenticate', () => {
|
||||
const azureCosmosDbSharedKeyApi = new MicrosoftAzureCosmosDbSharedKeyApi();
|
||||
|
||||
it('should generate a valid authorization header', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: `${baseUrl}/colls/container1/docs/item1`,
|
||||
method: 'GET',
|
||||
};
|
||||
const result = await azureCosmosDbSharedKeyApi.authenticate({ account, key }, requestOptions);
|
||||
|
||||
expect(result.headers?.authorization).toBe(
|
||||
'type%3Dmaster%26ver%3D1.0%26sig%3Dfake-signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when unable to determine the resource type from the URL', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
uri: 'https://invalid-url.com/',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
const urlString =
|
||||
requestOptions.uri ??
|
||||
(requestOptions.baseURL && requestOptions.url
|
||||
? requestOptions.baseURL + requestOptions.url
|
||||
: '');
|
||||
|
||||
if (!urlString) {
|
||||
throw new OperationalError('Invalid URL: Both uri and baseURL+url are missing');
|
||||
}
|
||||
|
||||
const url = new URL(urlString);
|
||||
const pathSegments = url.pathname.split('/').filter(Boolean);
|
||||
|
||||
const RESOURCE_TYPES = ['dbs', 'colls', 'docs', 'sprocs', 'udfs', 'triggers'];
|
||||
|
||||
const foundResource = RESOURCE_TYPES.map((type) => ({
|
||||
type,
|
||||
index: pathSegments.lastIndexOf(type),
|
||||
}))
|
||||
.filter(({ index }) => index !== -1)
|
||||
.sort((a, b) => b.index - a.index)
|
||||
.shift();
|
||||
|
||||
expect(foundResource).toBeUndefined();
|
||||
|
||||
expect(() => {
|
||||
if (!foundResource) {
|
||||
throw new OperationalError('Unable to determine the resource type from the URL');
|
||||
}
|
||||
}).toThrowError(new OperationalError('Unable to determine the resource type from the URL'));
|
||||
});
|
||||
|
||||
it('should throw OperationalError if no resource type found in URL path', async () => {
|
||||
const requestOptions: IRequestOptions = {
|
||||
uri: 'https://example.com/invalidpath',
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
const urlString =
|
||||
requestOptions.uri ??
|
||||
(requestOptions.baseURL && requestOptions.url
|
||||
? requestOptions.baseURL + requestOptions.url
|
||||
: '');
|
||||
|
||||
if (!urlString) {
|
||||
throw new OperationalError('Invalid URL: Both uri and baseURL+url are missing');
|
||||
}
|
||||
|
||||
const url = new URL(urlString);
|
||||
const pathSegments = url.pathname.split('/').filter(Boolean);
|
||||
|
||||
const RESOURCE_TYPES = ['dbs', 'colls', 'docs', 'sprocs', 'udfs', 'triggers'];
|
||||
const foundResource = RESOURCE_TYPES.map((type) => ({
|
||||
type,
|
||||
index: pathSegments.lastIndexOf(type),
|
||||
}))
|
||||
.filter(({ index }) => index !== -1)
|
||||
.sort((a, b) => b.index - a.index)
|
||||
.shift();
|
||||
|
||||
expect(foundResource).toBeUndefined();
|
||||
|
||||
expect(() => {
|
||||
if (!foundResource) {
|
||||
throw new OperationalError('Unable to determine the resource type from the URL');
|
||||
}
|
||||
}).toThrowError(new OperationalError('Unable to determine the resource type from the URL'));
|
||||
});
|
||||
|
||||
it('should properly construct the resourceId and payload', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01T00:00:00Z'));
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/dbs/mydb/colls/mycoll/docs/mydoc',
|
||||
method: 'GET',
|
||||
};
|
||||
const result = await azureCosmosDbSharedKeyApi.authenticate({ account, key }, requestOptions);
|
||||
|
||||
expect(result.headers?.authorization).toBe(
|
||||
'type%3Dmaster%26ver%3D1.0%26sig%3Dfake-signature',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { IN8nHttpFullResponse, INodeExecutionData, JsonObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { handleError, ErrorMap } from '../../helpers/errorHandler';
|
||||
|
||||
const mockExecuteSingleFunctions = {
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
getNodeParameter: jest.fn(),
|
||||
} as any;
|
||||
|
||||
describe('handleError', () => {
|
||||
let response: IN8nHttpFullResponse;
|
||||
let data: INodeExecutionData[];
|
||||
|
||||
beforeEach(() => {
|
||||
data = [{}] as INodeExecutionData[];
|
||||
response = { statusCode: 200, body: {} } as IN8nHttpFullResponse;
|
||||
});
|
||||
|
||||
test('should return data when no error occurs', async () => {
|
||||
const result = await handleError.call(mockExecuteSingleFunctions, data, response);
|
||||
expect(result).toBe(data);
|
||||
});
|
||||
|
||||
test('should throw NodeApiError for container conflict', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
|
||||
|
||||
response.statusCode = 409;
|
||||
response.body = { code: 'Conflict', message: 'Container already exists' } as JsonObject;
|
||||
|
||||
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
|
||||
message: ErrorMap.Container.Conflict.getMessage('container'),
|
||||
description: ErrorMap.Container.Conflict.description,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeApiError for container not found', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
|
||||
|
||||
response.statusCode = 404;
|
||||
response.body = { code: 'NotFound', message: 'Container not found' } as JsonObject;
|
||||
|
||||
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
|
||||
message: ErrorMap.Container.NotFound.getMessage('container'),
|
||||
description: ErrorMap.Container.NotFound.description,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeApiError for item not found', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('item');
|
||||
|
||||
response.statusCode = 404;
|
||||
response.body = { code: 'NotFound', message: 'Item not found' } as JsonObject;
|
||||
|
||||
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
|
||||
message: ErrorMap.Item.NotFound.getMessage('item'),
|
||||
description: ErrorMap.Item.NotFound.description,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw generic error if no specific mapping exists', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
|
||||
|
||||
response.statusCode = 400;
|
||||
response.body = { code: 'BadRequest', message: 'Invalid request' } as JsonObject;
|
||||
|
||||
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
|
||||
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
|
||||
message: 'BadRequest',
|
||||
description: 'Invalid request',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle error details correctly when match is successful', async () => {
|
||||
const errorMessage = 'Message: {"Errors":["Error 1", "Error 2"]}';
|
||||
const match = errorMessage.match(/Message: ({.*?})/);
|
||||
let errorDetails: string[] = [];
|
||||
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
errorDetails = JSON.parse(match[1]).Errors;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
expect(errorDetails).toEqual(['Error 1', 'Error 2']);
|
||||
});
|
||||
|
||||
test('should handle error when match does not return expected format', async () => {
|
||||
const errorMessage = 'Message: Invalid format';
|
||||
|
||||
const match = errorMessage.match(/Message: ({.*?})/);
|
||||
let errorDetails: string[] = [];
|
||||
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
errorDetails = JSON.parse(match[1]).Errors;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
expect(errorDetails).toEqual([]);
|
||||
});
|
||||
|
||||
test('should throw NodeApiError with proper details if error details are present', async () => {
|
||||
const errorMessage = 'Message: {"Errors":["Specific error occurred"]}';
|
||||
const match = errorMessage.match(/Message: ({.*?})/);
|
||||
let errorDetails: string[] = [];
|
||||
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
errorDetails = JSON.parse(match[1]).Errors;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (errorDetails && errorDetails.length > 0) {
|
||||
await expect(
|
||||
handleError.call(mockExecuteSingleFunctions, data, {
|
||||
statusCode: 500,
|
||||
body: { code: 'InternalServerError', message: errorMessage },
|
||||
headers: {},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new NodeApiError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
{
|
||||
code: 'InternalServerError',
|
||||
message: errorMessage,
|
||||
} as JsonObject,
|
||||
{
|
||||
message: 'InternalServerError',
|
||||
description: errorDetails.join('\n'),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should throw NodeApiError with fallback message if no details found', async () => {
|
||||
const errorMessage = 'Message: {"Errors":[] }';
|
||||
const match = errorMessage.match(/Message: ({.*?})/);
|
||||
let errorDetails: string[] = [];
|
||||
|
||||
if (match?.[1]) {
|
||||
try {
|
||||
errorDetails = JSON.parse(match[1]).Errors;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (errorDetails && errorDetails.length > 0) {
|
||||
await expect(
|
||||
handleError.call(mockExecuteSingleFunctions, data, {
|
||||
statusCode: 500,
|
||||
body: { code: 'InternalServerError', message: errorMessage },
|
||||
headers: {},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new NodeApiError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
{
|
||||
code: 'InternalServerError',
|
||||
message: errorMessage,
|
||||
} as JsonObject,
|
||||
{
|
||||
message: 'InternalServerError',
|
||||
description: 'Internal Server Error',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteSingleFunctions,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError, OperationalError } from 'n8n-workflow';
|
||||
|
||||
const azureCosmosDbApiRequest = jest.fn();
|
||||
jest.mock('../../transport', () => ({ azureCosmosDbApiRequest }));
|
||||
|
||||
import { ErrorMap } from '../../helpers/errorHandler';
|
||||
import {
|
||||
getPartitionKey,
|
||||
simplifyData,
|
||||
validateQueryParameters,
|
||||
processJsonInput,
|
||||
validatePartitionKey,
|
||||
validateCustomProperties,
|
||||
} from '../../helpers/utils';
|
||||
|
||||
interface RequestBodyWithParameters extends IDataObject {
|
||||
parameters: Array<{ name: string; value: string }>;
|
||||
}
|
||||
|
||||
const mockExecuteSingleFunctions = mock<IExecuteSingleFunctions>();
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
mockExecuteSingleFunctions.getNode.mockReturnValue({ name: 'MockNode' } as INode);
|
||||
});
|
||||
|
||||
describe('getPartitionKey', () => {
|
||||
test('should return partition key when found', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
const result = await getPartitionKey.call(mockExecuteSingleFunctions);
|
||||
|
||||
expect(result).toBe('partitionKeyPath');
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError if partition key is not found', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
|
||||
const mockApiResponse = {};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
|
||||
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Partition key not found', {
|
||||
description: 'Failed to determine the partition key for this collection',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeApiError for 404 error', async () => {
|
||||
const containerName = 'containerName';
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(containerName);
|
||||
|
||||
const errorMessage = ErrorMap.Container.NotFound.getMessage(containerName);
|
||||
|
||||
const mockError = new NodeApiError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
{},
|
||||
{
|
||||
httpCode: '404',
|
||||
message: errorMessage,
|
||||
description: ErrorMap.Container.NotFound.description,
|
||||
},
|
||||
);
|
||||
|
||||
azureCosmosDbApiRequest.mockRejectedValue(mockError);
|
||||
|
||||
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
|
||||
new NodeApiError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
{},
|
||||
{
|
||||
message: errorMessage,
|
||||
description: ErrorMap.Container.NotFound.description,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePartitionKey', () => {
|
||||
let requestOptions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
requestOptions = { body: {}, headers: {} };
|
||||
|
||||
azureCosmosDbApiRequest.mockClear();
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when partition key is missing for "create" operation', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({});
|
||||
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
await expect(
|
||||
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
"Partition key not found in 'Item Contents'",
|
||||
{
|
||||
description:
|
||||
"Partition key 'partitionKey' must be present and have a valid, non-empty value in 'Item Contents'.",
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when partition key is missing for "update" operation', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('update');
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({ partitionKey: '' });
|
||||
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
await expect(
|
||||
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
'Partition key is missing or empty',
|
||||
{
|
||||
description: 'Ensure the "Partition Key" field has a valid, non-empty value.',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when partition key is missing for "get" operation', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('get');
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce(undefined);
|
||||
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
await expect(
|
||||
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
'Partition key is missing or empty',
|
||||
{
|
||||
description: 'Ensure the "Partition Key" field exists and has a valid, non-empty value.',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when invalid JSON is provided for customProperties', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('invalidJson');
|
||||
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
|
||||
|
||||
await expect(
|
||||
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
'Invalid JSON format in "Item Contents"',
|
||||
{
|
||||
description: 'Ensure the "Item Contents" field contains a valid JSON object',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplifyData', () => {
|
||||
test('should return the same data when "simple" parameter is false', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(false);
|
||||
const items = [{ json: { foo: 'bar' } }] as INodeExecutionData[];
|
||||
|
||||
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
|
||||
|
||||
expect(result).toEqual(items);
|
||||
});
|
||||
|
||||
test('should simplify the data when "simple" parameter is true', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(true);
|
||||
const items = [{ json: { _internalKey: 'value', foo: 'bar' } }] as INodeExecutionData[];
|
||||
|
||||
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
|
||||
|
||||
expect(result).toEqual([{ json: { foo: 'bar' } }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateQueryParameters', () => {
|
||||
let requestOptions: IHttpRequestOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
requestOptions = { body: {}, headers: {} } as IHttpRequestOptions;
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when parameter values do not match', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('$1')
|
||||
.mockReturnValueOnce({ queryParameters: 'param1, param2' });
|
||||
|
||||
await expect(
|
||||
validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
'Empty parameter value provided',
|
||||
{
|
||||
description: 'Please provide non-empty values for the query parameters',
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should successfully map parameters when they match', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('$1, $2')
|
||||
.mockReturnValueOnce({ queryParameters: 'value1, value2' });
|
||||
|
||||
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
|
||||
|
||||
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
|
||||
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
|
||||
{ name: '@Param1', value: 'value1' },
|
||||
{ name: '@Param2', value: 'value2' },
|
||||
]);
|
||||
} else {
|
||||
throw new OperationalError('Expected result.body to contain a parameters array');
|
||||
}
|
||||
});
|
||||
|
||||
test('should correctly map parameters when query contains multiple dynamic values', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter
|
||||
.mockReturnValueOnce('$1, $2, $3')
|
||||
.mockReturnValueOnce({ queryParameters: 'firstValue, secondValue, thirdValue' });
|
||||
|
||||
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
|
||||
|
||||
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
|
||||
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
|
||||
{ name: '@Param1', value: 'firstValue' },
|
||||
{ name: '@Param2', value: 'secondValue' },
|
||||
{ name: '@Param3', value: 'thirdValue' },
|
||||
]);
|
||||
} else {
|
||||
throw new OperationalError('Expected result.body to contain a parameters array');
|
||||
}
|
||||
});
|
||||
|
||||
test('should extract and map parameter names correctly using regex', async () => {
|
||||
const query = '$1, $2, $3';
|
||||
const queryParamsString = 'value1, value2, value3';
|
||||
|
||||
const parameterNames = query.replace(/\$(\d+)/g, '@param$1').match(/@\w+/g) ?? [];
|
||||
|
||||
const parameterValues = queryParamsString.split(',').map((val) => val.trim());
|
||||
|
||||
expect(parameterNames).toEqual(['@param1', '@param2', '@param3']);
|
||||
expect(parameterValues).toEqual(['value1', 'value2', 'value3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processJsonInput', () => {
|
||||
test('should return parsed JSON when input is a valid JSON string', () => {
|
||||
const result = processJsonInput('{"key": "value"}');
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
test('should return input data when it is already an object', () => {
|
||||
const result = processJsonInput({ key: 'value' });
|
||||
|
||||
expect(result).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
test('should throw OperationalError for invalid JSON string', () => {
|
||||
const invalidJson = '{key: value}';
|
||||
expect(() => processJsonInput(invalidJson)).toThrowError(
|
||||
new OperationalError('Input must contain a valid JSON', { level: 'warning' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw OperationalError for invalid non-string and non-object input', () => {
|
||||
const invalidInput = 123;
|
||||
expect(() => processJsonInput(invalidInput, 'testInput')).toThrowError(
|
||||
new OperationalError("Input 'testInput' must contain a valid JSON", { level: 'warning' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCustomProperties', () => {
|
||||
let requestOptions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
requestOptions = { body: {}, headers: {}, url: 'http://mock.url' };
|
||||
});
|
||||
|
||||
test('should merge custom properties into requestOptions.body for valid input', async () => {
|
||||
const validCustomProperties = { property1: 'value1', property2: 'value2' };
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(validCustomProperties);
|
||||
|
||||
const result = await validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions);
|
||||
|
||||
expect(result.body).toEqual({ property1: 'value1', property2: 'value2' });
|
||||
});
|
||||
|
||||
test('should throw NodeOperationError when customProperties are empty, undefined, null, or contain only invalid values', async () => {
|
||||
const emptyCustomProperties = {};
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(emptyCustomProperties);
|
||||
|
||||
await expect(
|
||||
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
|
||||
description: 'Ensure the "Item Contents" field contains at least one valid property.',
|
||||
}),
|
||||
);
|
||||
|
||||
const invalidValues = { property1: null, property2: '' };
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(invalidValues);
|
||||
|
||||
await expect(
|
||||
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
|
||||
).rejects.toThrowError(
|
||||
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
|
||||
description: 'Ensure the "Item Contents" field contains at least one valid property.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Create Item', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/container1')
|
||||
.reply(200, {
|
||||
id: 'containerFromSDK',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAMLVz0c=',
|
||||
_ts: 1739178329,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
|
||||
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
})
|
||||
.post('/colls/container1/docs')
|
||||
.reply(201, {
|
||||
id: 'item1',
|
||||
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
|
||||
_etag: '"bb000143-0000-0300-0000-67d9a2430000"',
|
||||
_attachments: 'attachments/',
|
||||
_ts: 1742316099,
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['create.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"operation": "create",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"customProperties": "{\n\t\"id\": \"item1\"\n}",
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
|
||||
"name": "Azure Cosmos Db",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"id": "item1",
|
||||
"_rid": "4PVyAMPuBtoEAAAAAAAAAA==",
|
||||
"_self": "dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/",
|
||||
"_etag": "\"bb000143-0000-0300-0000-67d9a2430000\"",
|
||||
"_attachments": "attachments/",
|
||||
"_ts": 1742316099
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Delete Item', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/container1')
|
||||
.reply(200, {
|
||||
id: 'containerFromSDK',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAMLVz0c=',
|
||||
_ts: 1739178329,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
|
||||
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
})
|
||||
.delete('/colls/container1/docs/item1')
|
||||
.reply(204, '');
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['delete.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"operation": "delete",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"item": {
|
||||
"__rl": true,
|
||||
"value": "item1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "item1"
|
||||
},
|
||||
"additionalFields": {},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
|
||||
"name": "Azure Cosmos Db",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"deleted": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Get Item', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/container1')
|
||||
.reply(200, {
|
||||
id: 'containerFromSDK',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAMLVz0c=',
|
||||
_ts: 1739178329,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
|
||||
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
})
|
||||
.get('/colls/container1/docs/item1')
|
||||
.matchHeader('x-ms-documentdb-partitionkey', '["item1"]')
|
||||
.reply(200, {
|
||||
id: 'item1',
|
||||
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
|
||||
_etag: '"bb000143-0000-0300-0000-67d9a2430000"',
|
||||
_attachments: 'attachments/',
|
||||
_ts: 1742316099,
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['get.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"operation": "get",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"item": {
|
||||
"__rl": true,
|
||||
"value": "item1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "item1"
|
||||
},
|
||||
"additionalFields": {
|
||||
"partitionKey": "item1"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
|
||||
"name": "Azure Cosmos Db",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"id": "item1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Get All Items', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/newOne3/docs')
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
_rid: '4PVyAMPuBto=',
|
||||
Documents: [
|
||||
{
|
||||
id: 'John',
|
||||
FamilyName: 'NewNameAdded',
|
||||
Parents: [
|
||||
88,
|
||||
{ FirstName: 'Thomas', FamilyName: 'Bob' },
|
||||
{ FamilyName: null, FirstName: 'Mary Kay' },
|
||||
],
|
||||
ExtraField: 'nothing serious',
|
||||
Otherdetails: 'male',
|
||||
This: 'male',
|
||||
},
|
||||
{
|
||||
FamilyName: 'NewName',
|
||||
id: 'NewId',
|
||||
},
|
||||
{
|
||||
id: 'this',
|
||||
},
|
||||
],
|
||||
_count: 3,
|
||||
},
|
||||
{
|
||||
'x-ms-continuation': '4PVyAKoVaBQ=',
|
||||
},
|
||||
)
|
||||
.get('/colls/newOne3/docs')
|
||||
.matchHeader('x-ms-continuation', '4PVyAKoVaBQ=')
|
||||
.reply(200, {
|
||||
_rid: '4PVyAMPuBto=',
|
||||
Documents: [
|
||||
{
|
||||
id: 'John',
|
||||
FamilyName: 'NewNameAdded',
|
||||
Parents: [
|
||||
88,
|
||||
{ FirstName: 'Thomas', FamilyName: 'Bob' },
|
||||
{ FamilyName: null, FirstName: 'Mary Kay' },
|
||||
],
|
||||
ExtraField: 'nothing serious',
|
||||
Otherdetails: 'male',
|
||||
This: 'male',
|
||||
},
|
||||
],
|
||||
_count: 1,
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['getAll.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-180, -520],
|
||||
"id": "8218dacc-3b5f-460a-a773-817faf012ba9",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "newOne3",
|
||||
"mode": "list",
|
||||
"cachedResultName": "newOne3"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -360],
|
||||
"id": "4f60d430-3c90-4838-b9d1-8c9637e745b6",
|
||||
"name": "getAllItems",
|
||||
"alwaysOutputData": true,
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account "
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "getAllItems",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"getAllItems": [
|
||||
{
|
||||
"json": {
|
||||
"id": "John",
|
||||
"FamilyName": "NewNameAdded",
|
||||
"Parents": [
|
||||
88,
|
||||
{
|
||||
"FirstName": "Thomas",
|
||||
"FamilyName": "Bob"
|
||||
},
|
||||
{
|
||||
"FamilyName": null,
|
||||
"FirstName": "Mary Kay"
|
||||
}
|
||||
],
|
||||
"ExtraField": "nothing serious",
|
||||
"Otherdetails": "male",
|
||||
"This": "male"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"FamilyName": "NewName",
|
||||
"id": "NewId"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "this"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Query Items', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.post('/colls/newId/docs', {
|
||||
query: 'SELECT * FROM c WHERE c.id = @Param1',
|
||||
parameters: [
|
||||
{
|
||||
name: '@Param1',
|
||||
value: 'User1',
|
||||
},
|
||||
],
|
||||
})
|
||||
.reply(200, {
|
||||
Documents: [
|
||||
{
|
||||
id: 'User1',
|
||||
key1: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['query.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-80, -100],
|
||||
"id": "7da2ce49-9a9d-4240-b082-ff1b12d101b1",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"operation": "query",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "newId",
|
||||
"mode": "list",
|
||||
"cachedResultName": "newId"
|
||||
},
|
||||
"query": "SELECT * FROM c WHERE c.id = $1",
|
||||
"options": {
|
||||
"queryOptions": {
|
||||
"queryParameters": "User1"
|
||||
}
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [160, -100],
|
||||
"id": "0dc90797-8211-457c-a673-b7df28139de8",
|
||||
"name": "queryItems",
|
||||
"retryOnFail": false,
|
||||
"alwaysOutputData": true,
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account "
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "queryItems",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"queryItems": [
|
||||
{
|
||||
"json": {
|
||||
"id": "User1",
|
||||
"key1": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB - Update Item', () => {
|
||||
beforeEach(() => {
|
||||
const { baseUrl } = credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.get('/colls/container1')
|
||||
.reply(200, {
|
||||
id: 'containerFromSDK',
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent',
|
||||
automatic: true,
|
||||
includedPaths: [{ path: '/*' }],
|
||||
excludedPaths: [{ path: '/"_etag"/?' }],
|
||||
},
|
||||
partitionKey: {
|
||||
paths: ['/id'],
|
||||
kind: 'Hash',
|
||||
},
|
||||
conflictResolutionPolicy: {
|
||||
mode: 'LastWriterWins',
|
||||
conflictResolutionPath: '/_ts',
|
||||
conflictResolutionProcedure: '',
|
||||
},
|
||||
geospatialConfig: {
|
||||
type: 'Geography',
|
||||
},
|
||||
_rid: '4PVyAMLVz0c=',
|
||||
_ts: 1739178329,
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMLVz0c=/',
|
||||
_etag: '"0000f905-0000-0300-0000-67a9c1590000"',
|
||||
_docs: 'docs/',
|
||||
_sprocs: 'sprocs/',
|
||||
_triggers: 'triggers/',
|
||||
_udfs: 'udfs/',
|
||||
_conflicts: 'conflicts/',
|
||||
})
|
||||
.put('/colls/container1/docs/item1', {
|
||||
id: 'item1',
|
||||
key1: 'value1',
|
||||
})
|
||||
.matchHeader('x-ms-documentdb-partitionkey', '["item1"]')
|
||||
.reply(200, {
|
||||
id: 'item1',
|
||||
key1: 'value1',
|
||||
_rid: '4PVyAMPuBtoEAAAAAAAAAA==',
|
||||
_self: 'dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/',
|
||||
_etag: '"bb002b70-0000-0300-0000-67d9a3c70000"',
|
||||
_attachments: 'attachments/',
|
||||
_ts: 1742316487,
|
||||
});
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['update.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "49466d78-738a-4dee-a2fb-2143f7800b45",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "item",
|
||||
"operation": "update",
|
||||
"container": {
|
||||
"__rl": true,
|
||||
"value": "container1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "container1"
|
||||
},
|
||||
"item": {
|
||||
"__rl": true,
|
||||
"value": "item1",
|
||||
"mode": "list",
|
||||
"cachedResultName": "item1"
|
||||
},
|
||||
"customProperties": "{\n \"key1\": \"value1\"\n}",
|
||||
"additionalFields": {},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.azureCosmosDb",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 0],
|
||||
"id": "00d6b986-6b8a-43ee-a6a5-b6a7c165c991",
|
||||
"name": "Azure Cosmos Db",
|
||||
"credentials": {
|
||||
"microsoftAzureCosmosDbSharedKeyApi": {
|
||||
"id": "exampleId",
|
||||
"name": "Azure Cosmos DB account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"id": "item1",
|
||||
"key1": "value1",
|
||||
"_rid": "4PVyAMPuBtoEAAAAAAAAAA==",
|
||||
"_self": "dbs/4PVyAA==/colls/4PVyAMPuBto=/docs/4PVyAMPuBtoEAAAAAAAAAA==/",
|
||||
"_etag": "\"bb002b70-0000-0300-0000-67d9a3c70000\"",
|
||||
"_attachments": "attachments/",
|
||||
"_ts": 1742316487
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "7047d2700c0010a2580ba6b2649861ef0a567bfcd1671650a6ca242150e686b8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
OperationalError,
|
||||
type IGetNodeParameterOptions,
|
||||
type ILoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { AzureCosmosDb } from '../../AzureCosmosDb.node';
|
||||
import { HeaderConstants } from '../../helpers/constants';
|
||||
import { credentials } from '../credentials';
|
||||
|
||||
describe('Azure Cosmos DB', () => {
|
||||
describe('List search', () => {
|
||||
it('should list search containers', async () => {
|
||||
const mockResponse = {
|
||||
body: {
|
||||
DocumentCollections: [
|
||||
{
|
||||
id: 'Container2',
|
||||
},
|
||||
{
|
||||
id: 'Container1',
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
'x-ms-continuation': '4PVyAKoVaBQ=',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
|
||||
|
||||
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
|
||||
if (type === 'microsoftAzureCosmosDbSharedKeyApi') {
|
||||
return credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
}
|
||||
throw new OperationalError('Unknown credentials');
|
||||
});
|
||||
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockRequestWithAuthentication,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
const node = new AzureCosmosDb();
|
||||
|
||||
const paginationToken = '4PVyAKoVaBQ=';
|
||||
|
||||
const listSearchResult = await node.methods.listSearch.searchContainers.call(
|
||||
mockContext,
|
||||
'',
|
||||
paginationToken,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftAzureCosmosDbSharedKeyApi',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1/colls',
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_CONTINUATION]: paginationToken,
|
||||
},
|
||||
qs: {},
|
||||
body: {},
|
||||
json: true,
|
||||
returnFullResponse: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(listSearchResult).toEqual({
|
||||
results: [
|
||||
{ name: 'Container1', value: 'Container1' },
|
||||
{ name: 'Container2', value: 'Container2' },
|
||||
],
|
||||
paginationToken: '4PVyAKoVaBQ=',
|
||||
});
|
||||
});
|
||||
|
||||
it('should list search items', async () => {
|
||||
const mockResponse = {
|
||||
body: {
|
||||
Documents: [{ id: 'Item2' }, { id: 'Item1' }],
|
||||
},
|
||||
headers: {
|
||||
'x-ms-continuation': '4PVyAKoVaBQ=',
|
||||
},
|
||||
};
|
||||
|
||||
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
|
||||
|
||||
const mockGetCurrentNodeParameter = jest.fn(
|
||||
(parameterName, options: IGetNodeParameterOptions) => {
|
||||
if (parameterName === 'container' && options.extractValue) {
|
||||
return 'Container1';
|
||||
}
|
||||
throw new OperationalError('Unknown parameter');
|
||||
},
|
||||
);
|
||||
|
||||
const mockGetCredentials = jest.fn(async (type: string, _itemIndex?: number) => {
|
||||
if (type === 'microsoftAzureCosmosDbSharedKeyApi') {
|
||||
return credentials.microsoftAzureCosmosDbSharedKeyApi;
|
||||
}
|
||||
throw new OperationalError('Unknown credentials');
|
||||
});
|
||||
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
getCurrentNodeParameter: mockGetCurrentNodeParameter,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockRequestWithAuthentication,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
const node = new AzureCosmosDb();
|
||||
|
||||
const paginationToken = '4PVyAKoVaBQ=';
|
||||
const listSearchResult = await node.methods.listSearch.searchItems.call(
|
||||
mockContext,
|
||||
'',
|
||||
paginationToken,
|
||||
);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftAzureCosmosDbSharedKeyApi',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1/colls/Container1/docs',
|
||||
headers: {
|
||||
[HeaderConstants.X_MS_CONTINUATION]: paginationToken,
|
||||
},
|
||||
qs: {},
|
||||
body: {},
|
||||
json: true,
|
||||
returnFullResponse: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(listSearchResult).toEqual({
|
||||
results: [
|
||||
{ name: 'Item1', value: 'Item1' },
|
||||
{ name: 'Item2', value: 'Item2' },
|
||||
],
|
||||
paginationToken: '4PVyAKoVaBQ=',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IHttpRequestOptions,
|
||||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { ICosmosDbCredentials } from '../helpers/interfaces';
|
||||
|
||||
export async function azureCosmosDbApiRequest(
|
||||
this: IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
qs?: IDataObject,
|
||||
headers?: IDataObject,
|
||||
returnFullResponse: boolean = false,
|
||||
): Promise<any> {
|
||||
const credentialsType = 'microsoftAzureCosmosDbSharedKeyApi';
|
||||
const credentials = await this.getCredentials<ICosmosDbCredentials>(credentialsType);
|
||||
const baseUrl = `https://${credentials.account}.documents.azure.com/dbs/${credentials.database}`;
|
||||
|
||||
const options: IHttpRequestOptions = {
|
||||
method,
|
||||
url: `${baseUrl}${endpoint}`,
|
||||
json: true,
|
||||
headers,
|
||||
body,
|
||||
qs,
|
||||
returnFullResponse,
|
||||
};
|
||||
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, credentialsType, options);
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IDataObject,
|
||||
INodeProperties,
|
||||
INodePropertyOptions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
export async function microsoftApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const credentials = await this.getCredentials<{
|
||||
subdomain: string;
|
||||
region: string;
|
||||
}>('microsoftDynamicsOAuth2Api');
|
||||
|
||||
let options: IRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
accept: 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://${credentials.subdomain}.${credentials.region}/api/data/v9.2${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
if (Object.keys(option).length !== 0) {
|
||||
options = Object.assign({}, options, option);
|
||||
}
|
||||
return await this.helpers.requestOAuth2.call(this, 'microsoftDynamicsOAuth2Api', options, {
|
||||
property: 'id_token',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function microsoftApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
propertyName: string,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
let uri: string | undefined;
|
||||
query.$top = 100;
|
||||
|
||||
do {
|
||||
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
|
||||
uri = responseData['@odata.nextLink'];
|
||||
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
|
||||
} while (responseData['@odata.nextLink'] !== undefined);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getPicklistOptions(
|
||||
this: ILoadOptionsFunctions,
|
||||
entityName: string,
|
||||
attributeName: string,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const endpoint = `/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')/Microsoft.Dynamics.CRM.PicklistAttributeMetadata?$select=LogicalName&$expand=OptionSet($select=Options),GlobalOptionSet($select=Options)`;
|
||||
const {
|
||||
OptionSet: { Options: options },
|
||||
} = await microsoftApiRequest.call(this, 'GET', endpoint);
|
||||
for (const option of options) {
|
||||
returnData.push({
|
||||
name: option.Label.UserLocalizedLabel.Label,
|
||||
value: option.Value,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getEntityFields(
|
||||
this: ILoadOptionsFunctions,
|
||||
entityName: string,
|
||||
): Promise<IField[]> {
|
||||
const endpoint = `/EntityDefinitions(LogicalName='${entityName}')/Attributes`;
|
||||
const { value } = await microsoftApiRequest.call(this, 'GET', endpoint);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function adjustAddresses(addresses: [{ [key: string]: string }]) {
|
||||
const results: { [key: string]: any } = {};
|
||||
for (const [index, address] of addresses.entries()) {
|
||||
for (const key of Object.keys(address)) {
|
||||
if (address[key] !== '') {
|
||||
results[`address${index + 1}_${key}`] = address[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getAccountFields(): INodeProperties[] {
|
||||
return [
|
||||
{
|
||||
displayName: 'Account Category Name or ID',
|
||||
name: 'accountcategorycode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountCategories',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Category to indicate whether the customer account is standard or preferred. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Account Rating Name or ID',
|
||||
name: 'accountratingcode',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountRatingCodes',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'addresses',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
placeholder: 'Add Address Field',
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address Fields',
|
||||
name: 'address',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Address Type Name or ID',
|
||||
name: 'addresstypecode',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAddressTypes',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line1',
|
||||
name: 'line1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line2',
|
||||
name: 'line2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Line3',
|
||||
name: 'line3',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State or Province',
|
||||
name: 'stateorprovince',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Postalcode',
|
||||
name: 'postalcode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Primary Contact Name',
|
||||
name: 'primarycontactname',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Telephone1',
|
||||
name: 'telephone1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Telephone2',
|
||||
name: 'telephone2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Fax',
|
||||
name: 'fax',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Business Type Name or ID',
|
||||
name: 'businesstypecode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getBusinessTypes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The legal designation or other business type of the account for contracts or reporting purposes. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Customer Size Name or ID',
|
||||
name: 'customersizecode',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCustomerSizeCodes',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Customer Type Name or ID',
|
||||
name: 'customertypecode',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCustomerTypeCodes',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Additional information to describe the account, such as an excerpt from the company’s website',
|
||||
},
|
||||
{
|
||||
displayName: 'Email Address 1',
|
||||
name: 'emailaddress1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The primary email address for the account',
|
||||
},
|
||||
{
|
||||
displayName: 'Email Address 2',
|
||||
name: 'emailaddress2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The secondary email address for the account',
|
||||
},
|
||||
{
|
||||
displayName: 'Email Address 3',
|
||||
name: 'emailaddress3',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Alternate email address for the account',
|
||||
},
|
||||
{
|
||||
displayName: 'Fax',
|
||||
name: 'fax',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'FTP site URL',
|
||||
name: 'ftpsiteurl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'URL for the account’s FTP site to enable users to access data and share documents',
|
||||
},
|
||||
{
|
||||
displayName: 'Industry Name or ID',
|
||||
name: 'industrycode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getIndustryCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The account’s primary industry for use in marketing segmentation and demographic analysis. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/resource': ['account'],
|
||||
'/operation': ['update'],
|
||||
},
|
||||
},
|
||||
description: 'Company o business name',
|
||||
},
|
||||
{
|
||||
displayName: 'Credit Limit',
|
||||
name: 'creditlimit',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description:
|
||||
'Credit limit of the account. This is a useful reference when you address invoice and accounting issues with the customer.',
|
||||
},
|
||||
{
|
||||
displayName: 'Number Of Employees',
|
||||
name: 'numberofemployees',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
'Number of employees that work at the account for use in marketing segmentation and demographic analysis',
|
||||
},
|
||||
{
|
||||
displayName: 'Payment Terms Name or ID',
|
||||
name: 'paymenttermscode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPaymentTermsCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The payment terms to indicate when the customer needs to pay the total amount. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Preferred Appointment Day Name or ID',
|
||||
name: 'preferredappointmentdaycode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPreferredAppointmentDayCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Preferred Appointment Time Name or ID',
|
||||
name: 'preferredappointmenttimecode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPreferredAppointmentTimeCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Preferred Contact Method Name or ID',
|
||||
name: 'preferredcontactmethodcode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPreferredContactMethodCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'Primary Satori ID',
|
||||
name: 'primarysatoriid',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Primary Twitter ID',
|
||||
name: 'primarytwitterid',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Revenue',
|
||||
name: 'revenue',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description:
|
||||
'The annual revenue for the account, used as an indicator in financial performance analysis',
|
||||
},
|
||||
{
|
||||
displayName: 'Shares Outstanding',
|
||||
name: 'sharesoutstanding',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description:
|
||||
'The number of shares available to the public for the account. This number is used as an indicator in financial performance analysis.',
|
||||
},
|
||||
{
|
||||
displayName: 'Shipping Method Name or ID',
|
||||
name: 'shippingmethodcode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getShippingMethodCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Shipping method for deliveries sent to the account’s address to designate the preferred carrier or other delivery option. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'SIC',
|
||||
name: 'sic',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The Standard Industrial Classification (SIC) code that indicates the account’s primary industry of business, for use in marketing segmentation and demographic analysis',
|
||||
},
|
||||
{
|
||||
displayName: 'Stage ID',
|
||||
name: 'stageid',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Stock Exchange',
|
||||
name: 'stockexchange',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The stock exchange at which the account is listed to track their stock and financial performance of the company',
|
||||
},
|
||||
{
|
||||
displayName: 'Telephone 1',
|
||||
name: 'telephone1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The main phone number for this account',
|
||||
},
|
||||
{
|
||||
displayName: 'Telephone 2',
|
||||
name: 'telephone2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The second phone number for this account',
|
||||
},
|
||||
{
|
||||
displayName: 'Telephone 3',
|
||||
name: 'telephone3',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The third phone number for this account',
|
||||
},
|
||||
{
|
||||
displayName: 'Territory Name or ID',
|
||||
name: 'territorycode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTerritoryCodes',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Region or territory for the account for use in segmentation and analysis. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Ticker Symbol',
|
||||
name: 'tickersymbol',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Type the stock exchange symbol for the account to track financial performance of the company. You can click the code entered in this field to access the latest trading information from MSN Money.',
|
||||
},
|
||||
{
|
||||
displayName: 'Website URL',
|
||||
name: 'websiteurl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The account’s website URL to get quick details about the company profile',
|
||||
},
|
||||
{
|
||||
displayName: 'Yomi Name',
|
||||
name: 'yominame',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The phonetic spelling of the company name, if specified in Japanese, to make sure the name is pronounced correctly in phone calls and other communications',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const sort = (a: { name: string }, b: { name: string }) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export interface IField {
|
||||
IsRetrievable: boolean;
|
||||
LogicalName: string;
|
||||
IsSearchable: string;
|
||||
IsValidODataAttribute: string;
|
||||
IsValidForRead: string;
|
||||
CanBeSecuredForRead: string;
|
||||
AttributeType: string;
|
||||
IsSortableEnabled: {
|
||||
Value: boolean;
|
||||
};
|
||||
DisplayName: {
|
||||
UserLocalizedLabel: {
|
||||
Label: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.microsoftDynamicsCrm",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Marketing", "Sales"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftdynamicscrm/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { accountFields, accountOperations } from './descriptions';
|
||||
import type { IField } from './GenericFunctions';
|
||||
import {
|
||||
adjustAddresses,
|
||||
getEntityFields,
|
||||
getPicklistOptions,
|
||||
microsoftApiRequest,
|
||||
microsoftApiRequestAllItems,
|
||||
sort,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class MicrosoftDynamicsCrm implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Microsoft Dynamics CRM',
|
||||
name: 'microsoftDynamicsCrm',
|
||||
icon: { light: 'file:microsoftDynamicsCrm.svg', dark: 'file:microsoftDynamicsCrm.dark.svg' },
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Microsoft Dynamics CRM API',
|
||||
defaults: {
|
||||
name: 'Microsoft Dynamics CRM',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'microsoftDynamicsOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Account',
|
||||
value: 'account',
|
||||
},
|
||||
],
|
||||
default: 'account',
|
||||
},
|
||||
...accountOperations,
|
||||
...accountFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getAccountCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'accountcategorycode');
|
||||
},
|
||||
async getAccountRatingCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'accountratingcode');
|
||||
},
|
||||
async getAddressTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'address1_addresstypecode');
|
||||
},
|
||||
async getBusinessTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'businesstypecode');
|
||||
},
|
||||
async getCustomerSizeCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'customersizecode');
|
||||
},
|
||||
async getCustomerTypeCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'customertypecode');
|
||||
},
|
||||
async getIndustryCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'industrycode');
|
||||
},
|
||||
async getPaymentTermsCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'paymenttermscode');
|
||||
},
|
||||
async getPreferredAppointmentDayCodes(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'preferredappointmentdaycode');
|
||||
},
|
||||
async getPreferredAppointmentTimeCodes(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'preferredappointmenttimecode');
|
||||
},
|
||||
async getPreferredContactMethodCodes(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'preferredcontactmethodcode');
|
||||
},
|
||||
async getShippingMethodCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'shippingmethodcode');
|
||||
},
|
||||
async getTerritoryCodes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getPicklistOptions.call(this, 'account', 'territorycode');
|
||||
},
|
||||
async getAccountFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const fields = await getEntityFields.call(this, 'account');
|
||||
const isSelectable = (field: IField) =>
|
||||
field.IsValidForRead &&
|
||||
field.CanBeSecuredForRead &&
|
||||
field.IsValidODataAttribute &&
|
||||
field.LogicalName !== 'slaid';
|
||||
return fields
|
||||
.filter(isSelectable)
|
||||
.filter((field) => field.DisplayName.UserLocalizedLabel?.Label)
|
||||
.map((field) => ({
|
||||
name: field.DisplayName.UserLocalizedLabel.Label,
|
||||
value: field.LogicalName,
|
||||
}))
|
||||
.sort(sort);
|
||||
},
|
||||
async getExpandableAccountFields(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const fields = await getEntityFields.call(this, 'account');
|
||||
const isSelectable = (field: IField) =>
|
||||
field.IsValidForRead &&
|
||||
field.CanBeSecuredForRead &&
|
||||
field.IsValidODataAttribute &&
|
||||
field.AttributeType === 'Lookup' &&
|
||||
field.LogicalName !== 'slaid';
|
||||
return fields
|
||||
.filter(isSelectable)
|
||||
.map((field) => ({
|
||||
name: field.DisplayName.UserLocalizedLabel.Label,
|
||||
value: field.LogicalName,
|
||||
}))
|
||||
.sort(sort);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const length = items.length;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (resource === 'account') {
|
||||
//https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/create-entity-web-api
|
||||
if (operation === 'create') {
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as {
|
||||
addresses: { address: [{ [key: string]: any }] };
|
||||
};
|
||||
const options = this.getNodeParameter('options', i) as { returnFields: string[] };
|
||||
|
||||
const body = {
|
||||
name,
|
||||
...additionalFields,
|
||||
};
|
||||
|
||||
if (body?.addresses?.address) {
|
||||
Object.assign(body, adjustAddresses(body.addresses.address));
|
||||
//@ts-ignore
|
||||
delete body?.addresses;
|
||||
}
|
||||
|
||||
if (options.returnFields) {
|
||||
options.returnFields.push('accountid');
|
||||
qs.$select = options.returnFields.join(',');
|
||||
} else {
|
||||
qs.$select = 'accountid';
|
||||
}
|
||||
|
||||
responseData = await microsoftApiRequest.call(this, 'POST', '/accounts', body, qs);
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
//https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/update-delete-entities-using-web-api#basic-delete
|
||||
const accountId = this.getNodeParameter('accountId', i) as string;
|
||||
await microsoftApiRequest.call(this, 'DELETE', `/accounts(${accountId})`, {}, qs);
|
||||
responseData = { success: true };
|
||||
}
|
||||
|
||||
if (operation === 'get') {
|
||||
//https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/retrieve-entity-using-web-api
|
||||
const accountId = this.getNodeParameter('accountId', i) as string;
|
||||
const options = this.getNodeParameter('options', i);
|
||||
if (options.returnFields) {
|
||||
qs.$select = (options.returnFields as string[]).join(',');
|
||||
}
|
||||
if (options.expandFields) {
|
||||
qs.$expand = (options.expandFields as string[]).join(',');
|
||||
}
|
||||
responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/accounts(${accountId})`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
//https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/query-data-web-api
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
if (options.returnFields) {
|
||||
qs.$select = (options.returnFields as string[]).join(',');
|
||||
}
|
||||
if (options.expandFields) {
|
||||
qs.$expand = (options.expandFields as string[]).join(',');
|
||||
}
|
||||
if (filters.query) {
|
||||
qs.$filter = filters.query as string;
|
||||
}
|
||||
if (returnAll) {
|
||||
responseData = await microsoftApiRequestAllItems.call(
|
||||
this,
|
||||
'value',
|
||||
'GET',
|
||||
'/accounts',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.$top = this.getNodeParameter('limit', 0);
|
||||
responseData = await microsoftApiRequest.call(this, 'GET', '/accounts', {}, qs);
|
||||
responseData = responseData.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'update') {
|
||||
const accountId = this.getNodeParameter('accountId', i) as string;
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as {
|
||||
addresses: { address: [{ [key: string]: any }] };
|
||||
};
|
||||
const options = this.getNodeParameter('options', i) as { returnFields: string[] };
|
||||
|
||||
const body = {
|
||||
...updateFields,
|
||||
};
|
||||
|
||||
if (body?.addresses?.address) {
|
||||
Object.assign(body, adjustAddresses(body.addresses.address));
|
||||
//@ts-ignore
|
||||
delete body?.addresses;
|
||||
}
|
||||
|
||||
if (options.returnFields) {
|
||||
options.returnFields.push('accountid');
|
||||
qs.$select = options.returnFields.join(',');
|
||||
} else {
|
||||
qs.$select = 'accountid';
|
||||
}
|
||||
|
||||
responseData = await microsoftApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/accounts(${accountId})`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { getAccountFields } from '../GenericFunctions';
|
||||
|
||||
export const accountOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
action: 'Create an account',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
action: 'Delete an account',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
action: 'Get an account',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
action: 'Get many accounts',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
action: 'Update an account',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export const accountFields: INodeProperties[] = [
|
||||
// ----------------------------------------
|
||||
// account:create
|
||||
// ----------------------------------------
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
description: 'Company or business name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [...getAccountFields()],
|
||||
},
|
||||
// ----------------------------------------
|
||||
// account:get
|
||||
// ----------------------------------------
|
||||
{
|
||||
displayName: 'Account ID',
|
||||
name: 'accountId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['delete', 'get', 'update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
// ----------------------------------------
|
||||
// account:getAll
|
||||
// ----------------------------------------
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
default: 5,
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['get', 'getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Return Field Names or IDs',
|
||||
name: 'returnFields',
|
||||
type: 'multiOptions',
|
||||
description:
|
||||
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountFields',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
displayName: 'Expand Field Names or IDs',
|
||||
name: 'expandFields',
|
||||
type: 'multiOptions',
|
||||
description:
|
||||
'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getExpandableAccountFields',
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query to filter the results. Check <a href="https://docs.microsoft.com/en-us/powerapps/developer/data-platform/webapi/query-data-web-api#filter-results" target="_blank">filters</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------------
|
||||
// account:update
|
||||
// ----------------------------------------
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [...getAccountFields()],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['account'],
|
||||
operation: ['create', 'update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Return Field Names or IDs',
|
||||
name: 'returnFields',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountFields',
|
||||
},
|
||||
default: [],
|
||||
description:
|
||||
'Fields the response will include. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AccountDescription';
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.7888 5.90441e-05C8.57858 0.00300251 8.37796 0.0885859 8.23034 0.238298C8.08273 0.38801 7.99998 0.589814 8 0.800062V10.6833C7.99984 10.834 8.04225 10.9817 8.12236 11.1094C8.20247 11.2371 8.31702 11.3395 8.4528 11.4049L22.8529 18.3201C22.9781 18.3806 23.1169 18.4074 23.2557 18.3979C23.3944 18.3883 23.5283 18.3428 23.6441 18.2657L30.8441 13.4657C30.9586 13.3899 31.0515 13.2859 31.114 13.1636C31.1765 13.0414 31.2064 12.9051 31.2007 12.7679C31.1951 12.6307 31.1542 12.4973 31.0819 12.3806C31.0096 12.2639 30.9084 12.1678 30.7881 12.1017L9.18801 0.101659C9.06585 0.0333917 8.92793 -0.00164085 8.788 5.90441e-05M8.82 13.6001C8.67738 13.597 8.53651 13.6321 8.412 13.7017C8.28733 13.7709 8.1834 13.8721 8.11096 13.9948C8.03853 14.1176 8.00022 14.2576 8 14.4001V39.1986C7.99976 39.3376 8.03574 39.4743 8.10438 39.5951C8.17303 39.716 8.27198 39.8169 8.39148 39.8878C8.51099 39.9588 8.64692 39.9974 8.78589 39.9999C8.92486 40.0023 9.06207 39.9685 9.18401 39.9018L31.5969 27.0938C31.7192 27.0239 31.8209 26.9229 31.8917 26.8012C31.9625 26.6794 31.9999 26.541 32.0001 26.4002V16.0001C32.0003 15.8406 31.9527 15.6846 31.8636 15.5523C31.7744 15.42 31.6477 15.3173 31.4998 15.2576C31.3519 15.1978 31.1895 15.1836 31.0334 15.2169C30.8774 15.2501 30.7348 15.3292 30.6241 15.4441L9.60001 37.6002L15.964 18.6361C16.0161 18.4675 16.0108 18.2863 15.9492 18.121C15.8876 17.9556 15.773 17.8152 15.6232 17.7217L9.22321 13.7217C9.10208 13.6459 8.96285 13.6039 8.82 13.6001Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.7888 5.90441e-05C8.57858 0.00300251 8.37796 0.0885859 8.23034 0.238298C8.08273 0.38801 7.99998 0.589814 8 0.800062V10.6833C7.99984 10.834 8.04225 10.9817 8.12236 11.1094C8.20247 11.2371 8.31702 11.3395 8.4528 11.4049L22.8529 18.3201C22.9781 18.3806 23.1169 18.4074 23.2557 18.3979C23.3944 18.3883 23.5283 18.3428 23.6441 18.2657L30.8441 13.4657C30.9586 13.3899 31.0515 13.2859 31.114 13.1636C31.1765 13.0414 31.2064 12.9051 31.2007 12.7679C31.1951 12.6307 31.1542 12.4973 31.0819 12.3806C31.0096 12.2639 30.9084 12.1678 30.7881 12.1017L9.18801 0.101659C9.06585 0.0333917 8.92793 -0.00164085 8.788 5.90441e-05M8.82 13.6001C8.67738 13.597 8.53651 13.6321 8.412 13.7017C8.28733 13.7709 8.1834 13.8721 8.11096 13.9948C8.03853 14.1176 8.00022 14.2576 8 14.4001V39.1986C7.99976 39.3376 8.03574 39.4743 8.10438 39.5951C8.17303 39.716 8.27198 39.8169 8.39148 39.8878C8.51099 39.9588 8.64692 39.9974 8.78589 39.9999C8.92486 40.0023 9.06207 39.9685 9.18401 39.9018L31.5969 27.0938C31.7192 27.0239 31.8209 26.9229 31.8917 26.8012C31.9625 26.6794 31.9999 26.541 32.0001 26.4002V16.0001C32.0003 15.8406 31.9527 15.6846 31.8636 15.5523C31.7744 15.42 31.6477 15.3173 31.4998 15.2576C31.3519 15.1978 31.1895 15.1836 31.0334 15.2169C30.8774 15.2501 30.7348 15.3292 30.6241 15.4441L9.60001 37.6002L15.964 18.6361C16.0161 18.4675 16.0108 18.2863 15.9492 18.121C15.8876 17.9556 15.773 17.8152 15.6232 17.7217L9.22321 13.7217C9.10208 13.6459 8.96285 13.6039 8.82 13.6001Z" fill="#00255A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,424 @@
|
||||
import type {
|
||||
JsonObject,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
IRequestOptions,
|
||||
INodeExecutionData,
|
||||
IN8nHttpFullResponse,
|
||||
INodePropertyOptions,
|
||||
INodeListSearchResult,
|
||||
INodeListSearchItems,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
|
||||
export async function microsoftApiRequest(
|
||||
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
qs?: IDataObject,
|
||||
headers?: IDataObject,
|
||||
url?: string,
|
||||
): Promise<any> {
|
||||
const credentials = await this.getCredentials('microsoftEntraOAuth2Api');
|
||||
const baseUrl = (
|
||||
typeof credentials.graphApiBaseUrl === 'string' && credentials.graphApiBaseUrl !== ''
|
||||
? credentials.graphApiBaseUrl
|
||||
: 'https://graph.microsoft.com'
|
||||
).replace(/\/+$/, '');
|
||||
const options: IHttpRequestOptions = {
|
||||
method,
|
||||
url: url ?? `${baseUrl}/v1.0${endpoint}`,
|
||||
json: true,
|
||||
headers,
|
||||
body,
|
||||
qs,
|
||||
};
|
||||
|
||||
return await this.helpers.requestWithAuthentication.call(
|
||||
this,
|
||||
'microsoftEntraOAuth2Api',
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function microsoftApiPaginateRequest(
|
||||
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
qs?: IDataObject,
|
||||
headers?: IDataObject,
|
||||
url?: string,
|
||||
itemIndex: number = 0,
|
||||
): Promise<IDataObject[]> {
|
||||
const credentials = await this.getCredentials('microsoftEntraOAuth2Api');
|
||||
const baseUrl = (
|
||||
typeof credentials.graphApiBaseUrl === 'string' && credentials.graphApiBaseUrl !== ''
|
||||
? credentials.graphApiBaseUrl
|
||||
: 'https://graph.microsoft.com'
|
||||
).replace(/\/+$/, '');
|
||||
// Todo: IHttpRequestOptions doesn't have uri property which is required for requestWithAuthenticationPaginated
|
||||
const options: IRequestOptions = {
|
||||
method,
|
||||
uri: url ?? `${baseUrl}/v1.0${endpoint}`,
|
||||
json: true,
|
||||
headers,
|
||||
body,
|
||||
qs,
|
||||
};
|
||||
|
||||
const pages = await this.helpers.requestWithAuthenticationPaginated.call(
|
||||
this,
|
||||
options,
|
||||
itemIndex,
|
||||
{
|
||||
continue: '={{ !!$response.body?.["@odata.nextLink"] }}',
|
||||
request: {
|
||||
url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}',
|
||||
},
|
||||
requestInterval: 0,
|
||||
},
|
||||
'microsoftEntraOAuth2Api',
|
||||
);
|
||||
|
||||
let results: IDataObject[] = [];
|
||||
for (const page of pages) {
|
||||
const items = page.body.value as IDataObject[];
|
||||
if (items) {
|
||||
results = results.concat(items);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function handleErrorPostReceive(
|
||||
this: IExecuteSingleFunctions,
|
||||
data: INodeExecutionData[],
|
||||
response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
const {
|
||||
code: errorCode,
|
||||
message: errorMessage,
|
||||
details: errorDetails,
|
||||
} = (response.body as IDataObject)?.error as {
|
||||
code: string;
|
||||
message: string;
|
||||
innerError?: {
|
||||
code: string;
|
||||
'request-id'?: string;
|
||||
date?: string;
|
||||
};
|
||||
details?: Array<{
|
||||
code: string;
|
||||
message: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Operation specific errors
|
||||
if (resource === 'group') {
|
||||
if (operation === 'create') {
|
||||
} else if (operation === 'delete') {
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required group doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'Group to Delete' and try again",
|
||||
});
|
||||
}
|
||||
} else if (operation === 'get') {
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required group doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'Group to Get' and try again",
|
||||
});
|
||||
}
|
||||
} else if (operation === 'getAll') {
|
||||
} else if (operation === 'update') {
|
||||
if (
|
||||
errorCode === 'BadRequest' &&
|
||||
errorMessage === 'Empty Payload. JSON content expected.'
|
||||
) {
|
||||
// Ignore empty payload error. Currently n8n deletes the empty body object from the request.
|
||||
return data;
|
||||
}
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required group doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'Group to Update' and try again",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (resource === 'user') {
|
||||
if (operation === 'addGroup') {
|
||||
if (
|
||||
errorCode === 'Request_BadRequest' &&
|
||||
errorMessage ===
|
||||
"One or more added object references already exist for the following modified properties: 'members'."
|
||||
) {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: 'The user is already in the group',
|
||||
description:
|
||||
'The specified user cannot be added to the group because they are already a member',
|
||||
});
|
||||
} else if (errorCode === 'Request_ResourceNotFound') {
|
||||
const group = this.getNodeParameter('group.value') as string;
|
||||
if (errorMessage.includes(group)) {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required group doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'Group' and try again",
|
||||
});
|
||||
} else {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required user doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'User to Add' and try again",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (operation === 'create') {
|
||||
} else if (operation === 'delete') {
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required user doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'User to Delete' and try again",
|
||||
});
|
||||
}
|
||||
} else if (operation === 'get') {
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required user doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'User to Get' and try again",
|
||||
});
|
||||
}
|
||||
} else if (operation === 'getAll') {
|
||||
} else if (operation === 'removeGroup') {
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: 'The user is not in the group',
|
||||
description:
|
||||
'The specified user cannot be removed from the group because they are not a member of the group',
|
||||
});
|
||||
} else if (
|
||||
errorCode === 'Request_UnsupportedQuery' &&
|
||||
errorMessage ===
|
||||
"Unsupported referenced-object resource identifier for link property 'members'."
|
||||
) {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: 'The user ID is invalid',
|
||||
description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
|
||||
});
|
||||
}
|
||||
} else if (operation === 'update') {
|
||||
if (
|
||||
errorCode === 'BadRequest' &&
|
||||
errorMessage === 'Empty Payload. JSON content expected.'
|
||||
) {
|
||||
// Ignore empty payload error. Currently n8n deletes the empty body object from the request.
|
||||
return data;
|
||||
}
|
||||
if (errorCode === 'Request_ResourceNotFound') {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: "The required user doesn't match any existing one",
|
||||
description: "Double-check the value in the parameter 'User to Update' and try again",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic errors
|
||||
if (
|
||||
errorCode === 'Request_BadRequest' &&
|
||||
errorMessage.startsWith('Invalid object identifier')
|
||||
) {
|
||||
const group = this.getNodeParameter('group.value', '') as string;
|
||||
const parameterResource =
|
||||
resource === 'group' || errorMessage.includes(group) ? 'group' : 'user';
|
||||
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: `The ${parameterResource} ID is invalid`,
|
||||
description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
|
||||
});
|
||||
}
|
||||
if (errorDetails?.some((x) => x.code === 'ObjectConflict' || x.code === 'ConflictingObjects')) {
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
|
||||
message: `The ${resource} already exists`,
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getGroupProperties(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#groups');
|
||||
const metadata = await parseStringPromise(response as string, {
|
||||
explicitArray: false,
|
||||
});
|
||||
|
||||
/* eslint-disable */
|
||||
const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema']
|
||||
.find((x: any) => x['$']['Namespace'] === 'microsoft.graph')
|
||||
['EntityType'].filter((x: any) =>
|
||||
['entity', 'directoryObject', 'group'].includes(x['$']['Name']),
|
||||
);
|
||||
let properties = entities
|
||||
.flatMap((x: any) => x['Property'])
|
||||
.map((x: any) => x['$']['Name']) as string[];
|
||||
/* eslint-enable */
|
||||
|
||||
properties = properties.filter(
|
||||
(x) => !['id', 'isArchived', 'hasMembersWithLicenseErrors'].includes(x),
|
||||
);
|
||||
|
||||
properties = properties.sort();
|
||||
|
||||
for (const property of properties) {
|
||||
returnData.push({
|
||||
name: property,
|
||||
value: property,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getUserProperties(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#users');
|
||||
const metadata = await parseStringPromise(response as string, {
|
||||
explicitArray: false,
|
||||
});
|
||||
|
||||
/* eslint-disable */
|
||||
const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema']
|
||||
.find((x: any) => x['$']['Namespace'] === 'microsoft.graph')
|
||||
['EntityType'].filter((x: any) =>
|
||||
['entity', 'directoryObject', 'user'].includes(x['$']['Name']),
|
||||
);
|
||||
let properties = entities
|
||||
.flatMap((x: any) => x['Property'])
|
||||
.map((x: any) => x['$']['Name']) as string[];
|
||||
/* eslint-enable */
|
||||
|
||||
// signInActivity requires AuditLog.Read.All
|
||||
// mailboxSettings MailboxSettings.Read
|
||||
properties = properties.filter(
|
||||
(x) =>
|
||||
!['id', 'deviceEnrollmentLimit', 'mailboxSettings', 'print', 'signInActivity'].includes(x),
|
||||
);
|
||||
|
||||
properties = properties.sort();
|
||||
|
||||
for (const property of properties) {
|
||||
returnData.push({
|
||||
name: property,
|
||||
value: property,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getGroups(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
let response: any;
|
||||
if (paginationToken) {
|
||||
response = await microsoftApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/groups',
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
paginationToken,
|
||||
);
|
||||
} else {
|
||||
const qs: IDataObject = {
|
||||
$select: 'id,displayName',
|
||||
};
|
||||
const headers: IDataObject = {};
|
||||
if (filter) {
|
||||
headers.ConsistencyLevel = 'eventual';
|
||||
qs.$search = `"displayName:${filter}"`;
|
||||
}
|
||||
response = await microsoftApiRequest.call(this, 'GET', '/groups', {}, qs, headers);
|
||||
}
|
||||
|
||||
const groups: Array<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
}> = response.value;
|
||||
|
||||
const results: INodeListSearchItems[] = groups
|
||||
.map((g) => ({
|
||||
name: g.displayName,
|
||||
value: g.id,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
|
||||
return { results, paginationToken: response['@odata.nextLink'] };
|
||||
}
|
||||
|
||||
export async function getUsers(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
let response: any;
|
||||
if (paginationToken) {
|
||||
response = await microsoftApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/users',
|
||||
{},
|
||||
undefined,
|
||||
undefined,
|
||||
paginationToken,
|
||||
);
|
||||
} else {
|
||||
const qs: IDataObject = {
|
||||
$select: 'id,displayName',
|
||||
};
|
||||
const headers: IDataObject = {};
|
||||
if (filter) {
|
||||
qs.$filter = `startsWith(displayName, '${filter}') OR startsWith(userPrincipalName, '${filter}')`;
|
||||
}
|
||||
response = await microsoftApiRequest.call(this, 'GET', '/users', {}, qs, headers);
|
||||
}
|
||||
|
||||
const users: Array<{
|
||||
id: string;
|
||||
displayName: string;
|
||||
}> = response.value;
|
||||
|
||||
const results: INodeListSearchItems[] = users
|
||||
.map((u) => ({
|
||||
name: u.displayName,
|
||||
value: u.id,
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
);
|
||||
|
||||
return { results, paginationToken: response['@odata.nextLink'] };
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.microsoftEntra",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Development", "Developer Tools"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftentra/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import type {
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { groupFields, groupOperations, userFields, userOperations } from './descriptions';
|
||||
import { getGroupProperties, getGroups, getUserProperties, getUsers } from './GenericFunctions';
|
||||
|
||||
export class MicrosoftEntra implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Microsoft Entra ID',
|
||||
name: 'microsoftEntra',
|
||||
icon: {
|
||||
light: 'file:microsoftEntra.svg',
|
||||
dark: 'file:microsoftEntra.dark.svg',
|
||||
},
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Interact with Microsoft Entra ID API',
|
||||
schemaPath: 'Microsoft/Entra',
|
||||
defaults: {
|
||||
name: 'Microsoft Entra ID',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'microsoftEntraOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
baseURL:
|
||||
'={{ ($credentials.graphApiBaseUrl || "https://graph.microsoft.com").replace(/\\/+$/, "") }}/v1.0',
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Group',
|
||||
value: 'group',
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
},
|
||||
|
||||
...groupOperations,
|
||||
...groupFields,
|
||||
...userOperations,
|
||||
...userFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getGroupProperties,
|
||||
|
||||
async getGroupPropertiesGetAll(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
// Filter items not supported for list endpoint
|
||||
return (await getGroupProperties.call(this)).filter(
|
||||
(x) =>
|
||||
![
|
||||
'allowExternalSenders',
|
||||
'autoSubscribeNewMembers',
|
||||
'hideFromAddressLists',
|
||||
'hideFromOutlookClients',
|
||||
'isSubscribedByMail',
|
||||
'unseenCount',
|
||||
].includes(x.value as string),
|
||||
);
|
||||
},
|
||||
|
||||
getUserProperties,
|
||||
|
||||
async getUserPropertiesGetAll(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
// Filter items not supported for list endpoint
|
||||
return (await getUserProperties.call(this)).filter(
|
||||
(x) =>
|
||||
![
|
||||
'aboutMe',
|
||||
'birthday',
|
||||
'hireDate',
|
||||
'interests',
|
||||
'mySite',
|
||||
'pastProjects',
|
||||
'preferredName',
|
||||
'responsibilities',
|
||||
'schools',
|
||||
'skills',
|
||||
'mailboxSettings',
|
||||
].includes(x.value as string),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
listSearch: {
|
||||
getGroups,
|
||||
|
||||
getUsers,
|
||||
},
|
||||
};
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mailEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mailNickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"securityEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"securityIdentifier": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@odata.context": {
|
||||
"type": "string"
|
||||
},
|
||||
"businessPhones": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"department": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"preferredLanguage": {
|
||||
"type": "null"
|
||||
},
|
||||
"userPrincipalName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@odata.context": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdDateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mailNickname": {
|
||||
"type": "string"
|
||||
},
|
||||
"securityIdentifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"userPrincipalName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDateTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"userPrincipalName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 6
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updated": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
export * from './GroupDescription';
|
||||
export * from './UserDescription';
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="42" height="40" viewBox="0 0 42 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M40.2158 22.1487C41.5024 23.6021 41.1669 25.7976 39.498 26.8398V26.8376L22.8313 37.2554C22.33 37.5682 21.7635 37.7399 21.189 37.7704L21.1882 37.7906C20.5527 37.7906 19.7243 37.6044 19.1687 37.2577L9.44873 31.1821C10.311 31.7199 11.7443 32.3177 13.2598 32.3177C14.6398 32.3177 15.9221 31.9177 16.9843 31.2355C16.9843 31.2355 16.9865 31.2355 16.9887 31.2332L21.5727 28.3589L21.5723 28.3681L30.7024 22.6621L21.0047 11.7198L21.2469 11.4465C22.4691 10.0532 24.3069 9.1665 26.3624 9.1665C27.4091 9.1665 28.398 9.40206 29.2802 9.81317L40.2158 22.1487Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M21.0022 2.22439V2.22217L21 2.22439L21.0022 2.22439ZM21.0022 2.22439C21.9415 2.22498 22.8827 2.60054 23.5467 3.35106L29.2778 9.8155C28.3956 9.40661 27.4089 9.16883 26.36 9.16883C24.3044 9.16883 22.4667 10.0555 21.2444 11.4488L21.0703 11.6453L21.0703 11.6484L11.3022 22.6644L21 28.7266L16.9889 31.2333C16.9867 31.2355 16.9844 31.2355 16.9844 31.2355C15.9222 31.9177 14.64 32.3177 13.26 32.3177C11.7444 32.3177 10.3111 31.7199 9.44888 31.1822C8.6711 30.6955 2.50221 26.8399 2.50221 26.8399C0.833325 25.7977 0.49777 23.6022 1.78444 22.1488L18.4511 3.34883C19.1091 2.60751 20.0714 2.2363 21.0022 2.22675V2.22439Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.44824 30.9595C9.31047 31.4972 10.7438 32.095 12.2594 32.095C13.6394 32.095 14.9216 31.695 15.9838 31.0128C15.9838 31.0128 15.986 31.0128 15.9883 31.0106L19.9994 28.5039V37.555C19.3638 37.555 18.7238 37.3817 18.1683 37.035L8.44824 30.9595Z" fill="#225086"/>
|
||||
<path d="M17.4511 3.12667L0.784438 21.9267C-0.502231 23.38 -0.166675 25.5756 1.50222 26.6178C1.50222 26.6178 7.67112 30.4734 8.44889 30.96C9.31112 31.4978 10.7445 32.0956 12.26 32.0956C13.64 32.0956 14.9222 31.6956 15.9845 31.0134C15.9845 31.0134 15.9867 31.0134 15.9889 31.0112L20 28.5045L10.3022 22.4423L20.0022 11.5V2C19.06 2 18.1178 2.37556 17.4511 3.12667Z" fill="#66DDFF"/>
|
||||
<path d="M10.3027 22.4422L10.4183 22.5133L20.0005 28.5045H20.0027V11.5022L20.0005 11.5L10.3027 22.4422Z" fill="#CBF8FF"/>
|
||||
<path d="M38.498 26.6177C40.1669 25.5755 40.5025 23.3799 39.2158 21.9266L28.2802 9.591C27.398 9.17989 26.4091 8.94434 25.3624 8.94434C23.3069 8.94434 21.4691 9.831 20.2469 11.2243L20.0047 11.4977L29.7025 22.4399L20.0024 28.5021V37.5533C20.6402 37.5533 21.2758 37.3799 21.8313 37.0333L38.498 26.6155V26.6177Z" fill="#074793"/>
|
||||
<path d="M20.0022 2V11.5L20.2444 11.2267C21.4667 9.83334 23.3044 8.94668 25.36 8.94668C26.4089 8.94668 27.3956 9.18445 28.2778 9.59334L22.5467 3.12889C21.8822 2.37778 20.94 2.00222 20 2.00222L20.0022 2Z" fill="#0294E4"/>
|
||||
<path d="M29.7002 22.442L20.0024 11.502V28.502L29.7002 22.442Z" fill="#96BCC2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,303 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import { microsoftApiRequest, microsoftApiPaginateRequest } from '../GenericFunctions';
|
||||
|
||||
describe('Microsoft Entra GenericFunctions', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockNode: INode;
|
||||
let mockRequestWithAuthentication: jest.Mock;
|
||||
let mockRequestWithAuthenticationPaginated: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockRequestWithAuthentication = jest.fn();
|
||||
mockRequestWithAuthenticationPaginated = jest.fn();
|
||||
mockExecuteFunctions.helpers.requestWithAuthentication = mockRequestWithAuthentication;
|
||||
mockExecuteFunctions.helpers.requestWithAuthenticationPaginated =
|
||||
mockRequestWithAuthenticationPaginated;
|
||||
|
||||
mockNode = {
|
||||
id: 'test-node',
|
||||
name: 'Test Entra Node',
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockExecuteFunctions.getCredentials = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('microsoftApiRequest', () => {
|
||||
describe('graphApiBaseUrl from credentials', () => {
|
||||
it('should use base URL from credentials', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.us/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to default when credentials.graphApiBaseUrl is empty', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: '',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to default when credentials.graphApiBaseUrl is undefined', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip trailing slashes from base URL using regex', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.com/',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip multiple trailing slashes from base URL', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.com///',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use US Government cloud endpoint', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.us/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use US Government DOD cloud endpoint', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://dod-graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://dod-graph.microsoft.us/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use China cloud endpoint', async () => {
|
||||
const mockResponse = { data: 'test' };
|
||||
mockRequestWithAuthentication.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://microsoftgraph.chinacloudapi.cn',
|
||||
});
|
||||
|
||||
await microsoftApiRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'microsoftEntraOAuth2Api',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
url: 'https://microsoftgraph.chinacloudapi.cn/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('microsoftApiPaginateRequest', () => {
|
||||
describe('graphApiBaseUrl from credentials', () => {
|
||||
it('should use base URL from credentials', async () => {
|
||||
const mockResponse = [{ body: { value: [{ id: '1', name: 'Group 1' }] } }];
|
||||
mockRequestWithAuthenticationPaginated.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.us',
|
||||
});
|
||||
|
||||
await microsoftApiPaginateRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthenticationPaginated).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
uri: 'https://graph.microsoft.us/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
0,
|
||||
expect.objectContaining({
|
||||
continue: expect.any(String),
|
||||
request: expect.any(Object),
|
||||
requestInterval: 0,
|
||||
}),
|
||||
'microsoftEntraOAuth2Api',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to default when credentials.graphApiBaseUrl is empty', async () => {
|
||||
const mockResponse = [{ body: { value: [{ id: '1', name: 'Group 1' }] } }];
|
||||
mockRequestWithAuthenticationPaginated.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: '',
|
||||
});
|
||||
|
||||
await microsoftApiPaginateRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthenticationPaginated).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
uri: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
0,
|
||||
expect.objectContaining({
|
||||
continue: expect.any(String),
|
||||
request: expect.any(Object),
|
||||
requestInterval: 0,
|
||||
}),
|
||||
'microsoftEntraOAuth2Api',
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip trailing slashes from base URL using regex', async () => {
|
||||
const mockResponse = [{ body: { value: [{ id: '1', name: 'Group 1' }] } }];
|
||||
mockRequestWithAuthenticationPaginated.mockResolvedValue(mockResponse);
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
graphApiBaseUrl: 'https://graph.microsoft.com/',
|
||||
});
|
||||
|
||||
await microsoftApiPaginateRequest.call(mockExecuteFunctions, 'GET', '/groups');
|
||||
|
||||
expect(mockRequestWithAuthenticationPaginated).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
uri: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
}),
|
||||
0,
|
||||
expect.objectContaining({
|
||||
continue: expect.any(String),
|
||||
request: expect.any(Object),
|
||||
requestInterval: 0,
|
||||
}),
|
||||
'microsoftEntraOAuth2Api',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,749 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import { NodeConnectionTypes, type WorkflowTestData } from 'n8n-workflow';
|
||||
|
||||
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
|
||||
|
||||
describe('Microsoft Entra Node', () => {
|
||||
const baseUrl = 'https://graph.microsoft.com/v1.0';
|
||||
const testHarness = new NodeTestHarness();
|
||||
|
||||
describe('Group description', () => {
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'should create group',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'create',
|
||||
displayName: 'Group Display Name',
|
||||
groupType: 'Unified',
|
||||
mailEnabled: true,
|
||||
mailNickname: 'MailNickname',
|
||||
membershipType: 'DynamicMembership',
|
||||
securityEnabled: true,
|
||||
additionalFields: {
|
||||
isAssignableToRole: true,
|
||||
description: 'Group Description',
|
||||
membershipRule: 'department -eq "Marketing"',
|
||||
membershipRuleProcessingState: 'On',
|
||||
preferredDataLocation: 'Preferred Data Location',
|
||||
uniqueName: 'UniqueName',
|
||||
visibility: 'Public',
|
||||
},
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.createGroup],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'post',
|
||||
path: '/groups',
|
||||
statusCode: 201,
|
||||
requestBody: {
|
||||
displayName: 'Group Display Name',
|
||||
mailNickname: 'MailNickname',
|
||||
mailEnabled: true,
|
||||
membershipRule: 'department -eq "Marketing"',
|
||||
membershipRuleProcessingState: 'On',
|
||||
securityEnabled: true,
|
||||
groupTypes: ['Unified', 'DynamicMembership'],
|
||||
},
|
||||
responseBody: microsoftEntraApiResponse.postGroup,
|
||||
},
|
||||
{
|
||||
method: 'patch',
|
||||
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
|
||||
statusCode: 204,
|
||||
requestBody: {
|
||||
description: 'Group Description',
|
||||
preferredDataLocation: 'Preferred Data Location',
|
||||
uniqueName: 'UniqueName',
|
||||
visibility: 'Public',
|
||||
},
|
||||
responseBody: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should delete group',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'delete',
|
||||
group: {
|
||||
__rl: true,
|
||||
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
mode: 'id',
|
||||
},
|
||||
options: {},
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.deleteGroup],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'delete',
|
||||
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
statusCode: 204,
|
||||
responseBody: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should get group',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'get',
|
||||
group: {
|
||||
__rl: true,
|
||||
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
mode: 'id',
|
||||
},
|
||||
output: 'raw',
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.getGroup],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
statusCode: 200,
|
||||
responseBody: microsoftEntraApiResponse.getGroup,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should get group with fields output and members',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'get',
|
||||
group: {
|
||||
__rl: true,
|
||||
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
mode: 'id',
|
||||
},
|
||||
output: 'fields',
|
||||
fields: [
|
||||
'assignedLabels',
|
||||
'assignedLicenses',
|
||||
'createdDateTime',
|
||||
'classification',
|
||||
'deletedDateTime',
|
||||
'description',
|
||||
'displayName',
|
||||
'expirationDateTime',
|
||||
'groupTypes',
|
||||
'visibility',
|
||||
'unseenCount',
|
||||
'theme',
|
||||
'uniqueName',
|
||||
'serviceProvisioningErrors',
|
||||
'securityIdentifier',
|
||||
'renewedDateTime',
|
||||
'securityEnabled',
|
||||
'autoSubscribeNewMembers',
|
||||
'allowExternalSenders',
|
||||
'licenseProcessingState',
|
||||
'isManagementRestricted',
|
||||
'isSubscribedByMail',
|
||||
'isAssignableToRole',
|
||||
'id',
|
||||
'hideFromOutlookClients',
|
||||
'hideFromAddressLists',
|
||||
'onPremisesProvisioningErrors',
|
||||
'onPremisesSecurityIdentifier',
|
||||
'onPremisesSamAccountName',
|
||||
'onPremisesNetBiosName',
|
||||
'onPremisesSyncEnabled',
|
||||
'preferredDataLocation',
|
||||
'preferredLanguage',
|
||||
'proxyAddresses',
|
||||
'onPremisesLastSyncDateTime',
|
||||
'onPremisesDomainName',
|
||||
'membershipRuleProcessingState',
|
||||
'membershipRule',
|
||||
'mailNickname',
|
||||
'mailEnabled',
|
||||
'mail',
|
||||
],
|
||||
options: {
|
||||
includeMembers: true,
|
||||
},
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.getGroupWithProperties],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b?$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,unseenCount,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,autoSubscribeNewMembers,allowExternalSenders,licenseProcessingState,isManagementRestricted,isSubscribedByMail,isAssignableToRole,id,hideFromOutlookClients,hideFromAddressLists,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$expand=members($select=id,accountEnabled,createdDateTime,displayName,employeeId,mail,securityIdentifier,userPrincipalName,userType)',
|
||||
statusCode: 200,
|
||||
responseBody: microsoftEntraApiResponse.getGroupWithProperties,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should get all groups with simple output',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'getAll',
|
||||
returnAll: true,
|
||||
filter: '',
|
||||
output: 'simple',
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [new Array(102).fill(microsoftEntraNodeResponse.getGroup[0])],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility',
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
|
||||
'@odata.nextLink':
|
||||
'https://graph.microsoft.com/v1.0/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
|
||||
value: new Array(100).fill(microsoftEntraApiResponse.getGroup),
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
|
||||
value: new Array(2).fill(microsoftEntraApiResponse.getGroup),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should get limit 10 groups with raw output',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'getAll',
|
||||
limit: 10,
|
||||
filter: '',
|
||||
output: 'raw',
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [new Array(10).fill(microsoftEntraNodeResponse.getGroup[0])],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: '/groups?$top=10',
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
|
||||
'@odata.nextLink':
|
||||
'https://graph.microsoft.com/v1.0/groups?$top=10&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
|
||||
value: new Array(10).fill(microsoftEntraApiResponse.getGroup),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should get all groups with options and filter',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'getAll',
|
||||
returnAll: true,
|
||||
filter: "startswith(displayName,'group')",
|
||||
output: 'fields',
|
||||
fields: [
|
||||
'assignedLabels',
|
||||
'assignedLicenses',
|
||||
'createdDateTime',
|
||||
'classification',
|
||||
'deletedDateTime',
|
||||
'description',
|
||||
'displayName',
|
||||
'expirationDateTime',
|
||||
'groupTypes',
|
||||
'visibility',
|
||||
'theme',
|
||||
'uniqueName',
|
||||
'serviceProvisioningErrors',
|
||||
'securityIdentifier',
|
||||
'renewedDateTime',
|
||||
'securityEnabled',
|
||||
'licenseProcessingState',
|
||||
'isManagementRestricted',
|
||||
'isAssignableToRole',
|
||||
'onPremisesProvisioningErrors',
|
||||
'onPremisesSecurityIdentifier',
|
||||
'onPremisesSamAccountName',
|
||||
'onPremisesNetBiosName',
|
||||
'onPremisesSyncEnabled',
|
||||
'preferredDataLocation',
|
||||
'preferredLanguage',
|
||||
'proxyAddresses',
|
||||
'onPremisesLastSyncDateTime',
|
||||
'onPremisesDomainName',
|
||||
'membershipRuleProcessingState',
|
||||
'membershipRule',
|
||||
'mailNickname',
|
||||
'mailEnabled',
|
||||
'mail',
|
||||
],
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [
|
||||
new Array(102).fill(microsoftEntraNodeResponse.getGroupWithProperties[0]),
|
||||
],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id",
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
|
||||
'@odata.nextLink':
|
||||
"https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA",
|
||||
value: new Array(100).fill(microsoftEntraApiResponse.getGroupWithProperties),
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'get',
|
||||
path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA",
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
|
||||
value: new Array(2).fill(microsoftEntraApiResponse.getGroupWithProperties),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should update group',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
position: [820, 380],
|
||||
typeVersion: 1,
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'update',
|
||||
group: {
|
||||
__rl: true,
|
||||
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
mode: 'id',
|
||||
},
|
||||
updateFields: {
|
||||
allowExternalSenders: true,
|
||||
autoSubscribeNewMembers: true,
|
||||
description: 'Group Description',
|
||||
displayName: 'Group Display Name',
|
||||
mailNickname: 'MailNickname',
|
||||
membershipRule: 'department -eq "Marketing"',
|
||||
membershipRuleProcessingState: 'On',
|
||||
preferredDataLocation: 'Preferred Data Location',
|
||||
securityEnabled: true,
|
||||
uniqueName: 'UniqueName',
|
||||
visibility: 'Public',
|
||||
},
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.updateGroup],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'patch',
|
||||
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
|
||||
statusCode: 204,
|
||||
requestBody: {
|
||||
description: 'Group Description',
|
||||
displayName: 'Group Display Name',
|
||||
mailNickname: 'MailNickname',
|
||||
membershipRule: 'department -eq "Marketing"',
|
||||
membershipRuleProcessingState: 'On',
|
||||
preferredDataLocation: 'Preferred Data Location',
|
||||
securityEnabled: true,
|
||||
uniqueName: 'UniqueName',
|
||||
visibility: 'Public',
|
||||
},
|
||||
responseBody: {},
|
||||
},
|
||||
{
|
||||
method: 'patch',
|
||||
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
|
||||
statusCode: 204,
|
||||
requestBody: {
|
||||
allowExternalSenders: true,
|
||||
autoSubscribeNewMembers: true,
|
||||
},
|
||||
responseBody: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testData of tests) {
|
||||
testHarness.setupTest(testData);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import type { ILoadOptionsFunctions, WorkflowTestData } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
|
||||
import { MicrosoftEntra } from '../MicrosoftEntra.node';
|
||||
|
||||
describe('Microsoft Entra Node', () => {
|
||||
const testHarness = new NodeTestHarness();
|
||||
const baseUrl = 'https://graph.microsoft.com/v1.0';
|
||||
|
||||
describe('Credentials', () => {
|
||||
const credentials = {
|
||||
microsoftEntraOAuth2Api: {
|
||||
scope: '',
|
||||
oauthTokenData: {
|
||||
access_token: 'ACCESSTOKEN',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tests: WorkflowTestData[] = [
|
||||
{
|
||||
description: 'should use correct credentials',
|
||||
input: {
|
||||
workflowData: {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
type: 'n8n-nodes-base.manualTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
id: '1307e408-a8a5-464e-b858-494953e2f43b',
|
||||
name: 'When clicking ‘Execute workflow’',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resource: 'group',
|
||||
operation: 'get',
|
||||
group: {
|
||||
__rl: true,
|
||||
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
|
||||
mode: 'id',
|
||||
},
|
||||
filter: '',
|
||||
output: 'raw',
|
||||
requestOptions: {},
|
||||
},
|
||||
type: 'n8n-nodes-base.microsoftEntra',
|
||||
typeVersion: 1,
|
||||
position: [220, 0],
|
||||
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
|
||||
name: 'Microsoft Entra ID',
|
||||
credentials: {
|
||||
microsoftEntraOAuth2Api: {
|
||||
id: 'Hot2KwSMSoSmMVqd',
|
||||
name: 'Microsoft Entra ID (Azure Active Directory) account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
'When clicking ‘Execute workflow’': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Microsoft Entra ID',
|
||||
type: NodeConnectionTypes.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
nodeData: {
|
||||
'Microsoft Entra ID': [microsoftEntraNodeResponse.getGroup],
|
||||
},
|
||||
},
|
||||
nock: {
|
||||
baseUrl,
|
||||
mocks: [
|
||||
{
|
||||
method: 'get',
|
||||
path: `/groups/${microsoftEntraApiResponse.getGroup.id}`,
|
||||
statusCode: 200,
|
||||
responseBody: {
|
||||
...microsoftEntraApiResponse.getGroup,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testData of tests) {
|
||||
testHarness.setupTest(testData, { credentials });
|
||||
}
|
||||
});
|
||||
|
||||
describe('Load options', () => {
|
||||
it('should load group properties', async () => {
|
||||
const mockContext = {
|
||||
helpers: {
|
||||
requestWithAuthentication: jest
|
||||
.fn()
|
||||
.mockReturnValue(microsoftEntraApiResponse.metadata.groups),
|
||||
},
|
||||
getCurrentNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
const node = new MicrosoftEntra();
|
||||
|
||||
const properties = await node.methods.loadOptions.getGroupProperties.call(mockContext);
|
||||
|
||||
expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getGroupProperties);
|
||||
});
|
||||
|
||||
it('should load user properties', async () => {
|
||||
const mockContext = {
|
||||
helpers: {
|
||||
requestWithAuthentication: jest
|
||||
.fn()
|
||||
.mockReturnValue(microsoftEntraApiResponse.metadata.users),
|
||||
},
|
||||
getCurrentNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
const node = new MicrosoftEntra();
|
||||
|
||||
const properties = await node.methods.loadOptions.getUserProperties.call(mockContext);
|
||||
|
||||
expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getUserProperties);
|
||||
});
|
||||
});
|
||||
|
||||
describe('List search', () => {
|
||||
it('should list search groups', async () => {
|
||||
const mockResponse = {
|
||||
value: Array.from({ length: 2 }, (_, i) => ({
|
||||
id: (i + 1).toString(),
|
||||
displayName: `Group ${i + 1}`,
|
||||
})),
|
||||
'@odata.nextLink': '',
|
||||
};
|
||||
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
|
||||
const mockContext = {
|
||||
helpers: {
|
||||
requestWithAuthentication: mockRequestWithAuthentication,
|
||||
},
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
const node = new MicrosoftEntra();
|
||||
|
||||
const listSearchResult = await node.methods.listSearch.getGroups.call(mockContext);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', {
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/groups',
|
||||
json: true,
|
||||
headers: {},
|
||||
body: {},
|
||||
qs: {
|
||||
$select: 'id,displayName',
|
||||
},
|
||||
});
|
||||
expect(listSearchResult).toEqual({
|
||||
results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })),
|
||||
paginationToken: mockResponse['@odata.nextLink'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should list search users', async () => {
|
||||
const mockResponse = {
|
||||
value: Array.from({ length: 2 }, (_, i) => ({
|
||||
id: (i + 1).toString(),
|
||||
displayName: `User ${i + 1}`,
|
||||
})),
|
||||
'@odata.nextLink': '',
|
||||
};
|
||||
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
|
||||
const mockContext = {
|
||||
helpers: {
|
||||
requestWithAuthentication: mockRequestWithAuthentication,
|
||||
},
|
||||
getCredentials: jest.fn().mockResolvedValue({
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
const node = new MicrosoftEntra();
|
||||
|
||||
const listSearchResult = await node.methods.listSearch.getUsers.call(mockContext);
|
||||
|
||||
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', {
|
||||
method: 'GET',
|
||||
url: 'https://graph.microsoft.com/v1.0/users',
|
||||
json: true,
|
||||
headers: {},
|
||||
body: {},
|
||||
qs: {
|
||||
$select: 'id,displayName',
|
||||
},
|
||||
});
|
||||
expect(listSearchResult).toEqual({
|
||||
results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })),
|
||||
paginationToken: mockResponse['@odata.nextLink'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.microsoftExcel",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Data & Storage", "Productivity"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftexcel/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["_Excel", "Excel", "Sheet", "CSV", "Spreadsheet"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { MicrosoftExcelV1 } from './v1/MicrosoftExcelV1.node';
|
||||
import { MicrosoftExcelV2 } from './v2/MicrosoftExcelV2.node';
|
||||
|
||||
export class MicrosoftExcel extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Microsoft Excel 365',
|
||||
name: 'microsoftExcel',
|
||||
icon: 'file:excel.svg',
|
||||
group: ['input'],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Microsoft Excel API',
|
||||
defaultVersion: 2.2,
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new MicrosoftExcelV1(baseDescription),
|
||||
2: new MicrosoftExcelV2(baseDescription),
|
||||
2.1: new MicrosoftExcelV2(baseDescription),
|
||||
2.2: new MicrosoftExcelV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><g fill="#02723B" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path d="M43.1 20.227h9.512v4.692h-9.513zm0-7.034h9.512v4.69h-9.513zm0 14.227h9.512v4.691h-9.513zm0 7.036h9.512v4.691h-9.513z"/><path d="M43.1 41.649h9.512v4.69h-9.513zM58.07 6.78H35.146V.37L.37 6.467v47.22l34.776 5.942v-7.186H58.07c.935 0 1.559-.781 1.559-1.555V8.344c0-.781-.624-1.555-1.56-1.555V6.78zM20.018 40.554l-3.274-8.593h-.148c0 .149-3.275 8.13-3.275 8.13l-4.366-.312L14.1 29.616 9.422 19.453l4.367-.312 2.963 7.974h.148c0-.148 3.274-8.287 3.274-8.287l4.522-.312-5.457 11.101 5.614 11.102-4.834-.149zM57.6 50.717H35.146v-4.37h5.458v-4.691h-5.458v-2.352h5.458v-4.69h-5.458v-2.347h5.458v-4.69h-5.458V25.23h5.458v-4.69h-5.458v-2.345h5.458v-4.692h-5.458v-4.69h22.456v41.903h-.001z"/><path d="M42.729 19.857h9.512v4.691H42.73zm0-7.035h9.512v4.691H42.73zm0 14.228h9.512v4.69H42.73zm0 7.036h9.512v4.691H42.73z"/><path d="M42.729 41.279h9.512v4.69H42.73zM57.699 6.41H34.776V0L0 6.098v47.22l34.776 5.941v-7.185h22.923c.936 0 1.56-.781 1.56-1.555V7.974c0-.781-.624-1.555-1.56-1.555zM19.647 40.184l-3.274-8.593h-.148c0 .148-3.274 8.13-3.274 8.13l-4.367-.312 5.146-10.163-4.678-10.163 4.367-.313 2.962 7.974h.149c0-.148 3.274-8.286 3.274-8.286l4.522-.313-5.458 11.102 5.615 11.101-4.834-.148zM57.23 50.347H34.776v-4.37h5.457v-4.692h-5.457v-2.352h5.457v-4.69h-5.457v-2.347h5.457v-4.69h-5.457V24.86h5.457v-4.69h-5.457v-2.346h5.457v-4.692h-5.457v-4.69h22.456v41.904z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,8 @@
|
||||
export const credentials = {
|
||||
microsoftExcelOAuth2Api: {
|
||||
scope: 'openid',
|
||||
oauthTokenData: {
|
||||
access_token: 'token',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getWorksheetColumnRow } from '../../../v2/methods/loadOptions';
|
||||
import { microsoftApiRequest } from '../../../v2/transport';
|
||||
|
||||
// Mock the transport module
|
||||
jest.mock('../../../v2/transport', () => ({
|
||||
microsoftApiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Microsoft Excel V2 - loadOptions', () => {
|
||||
let mockContext: ILoadOptionsFunctions;
|
||||
const mockMicrosoftApiRequest = microsoftApiRequest as jest.MockedFunction<
|
||||
typeof microsoftApiRequest
|
||||
>;
|
||||
const mockGetNodeParameter = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = mock<ILoadOptionsFunctions>({
|
||||
getNodeParameter: mockGetNodeParameter,
|
||||
});
|
||||
mockMicrosoftApiRequest.mockReset();
|
||||
mockGetNodeParameter.mockReset();
|
||||
});
|
||||
|
||||
describe('getWorksheetColumnRow', () => {
|
||||
it('should get columns from usedRange when range is empty', async () => {
|
||||
// Arrange
|
||||
const workbookId = 'test-workbook-id';
|
||||
const worksheetId = 'test-worksheet-id';
|
||||
const range = '';
|
||||
|
||||
mockGetNodeParameter
|
||||
.mockReturnValueOnce(workbookId)
|
||||
.mockReturnValueOnce(worksheetId)
|
||||
.mockReturnValueOnce(range);
|
||||
|
||||
const mockResponse = {
|
||||
values: [['Column A', 'Column B', 'Column C']],
|
||||
};
|
||||
|
||||
mockMicrosoftApiRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getWorksheetColumnRow.call(mockContext);
|
||||
|
||||
expect(mockMicrosoftApiRequest).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
|
||||
undefined,
|
||||
{ select: 'values' },
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Column A', value: 'Column A' },
|
||||
{ name: 'Column B', value: 'Column B' },
|
||||
{ name: 'Column C', value: 'Column C' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get columns from specific range when range is provided', async () => {
|
||||
const workbookId = 'test-workbook-id';
|
||||
const worksheetId = 'test-worksheet-id';
|
||||
const range = 'A1:C1';
|
||||
|
||||
mockGetNodeParameter
|
||||
.mockReturnValueOnce(workbookId)
|
||||
.mockReturnValueOnce(worksheetId)
|
||||
.mockReturnValueOnce(range);
|
||||
|
||||
const mockResponse = {
|
||||
values: [['Header 1', 'Header 2', 'Header 3']],
|
||||
};
|
||||
|
||||
mockMicrosoftApiRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getWorksheetColumnRow.call(mockContext);
|
||||
|
||||
expect(mockMicrosoftApiRequest).toHaveBeenCalledWith(
|
||||
'PATCH',
|
||||
`/drive/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${range}')`,
|
||||
{ select: 'values' },
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Header 1', value: 'Header 1' },
|
||||
{ name: 'Header 2', value: 'Header 2' },
|
||||
{ name: 'Header 3', value: 'Header 3' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty columns array', async () => {
|
||||
const workbookId = 'test-workbook-id';
|
||||
const worksheetId = 'test-worksheet-id';
|
||||
const range = '';
|
||||
|
||||
mockGetNodeParameter
|
||||
.mockReturnValueOnce(workbookId)
|
||||
.mockReturnValueOnce(worksheetId)
|
||||
.mockReturnValueOnce(range);
|
||||
|
||||
const mockResponse = {
|
||||
values: [[]],
|
||||
};
|
||||
|
||||
mockMicrosoftApiRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getWorksheetColumnRow.call(mockContext);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single column', async () => {
|
||||
const workbookId = 'test-workbook-id';
|
||||
const worksheetId = 'test-worksheet-id';
|
||||
const range = 'A1:A1';
|
||||
|
||||
mockGetNodeParameter
|
||||
.mockReturnValueOnce(workbookId)
|
||||
.mockReturnValueOnce(worksheetId)
|
||||
.mockReturnValueOnce(range);
|
||||
|
||||
const mockResponse = {
|
||||
values: [['Single Column']],
|
||||
};
|
||||
|
||||
mockMicrosoftApiRequest.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await getWorksheetColumnRow.call(mockContext);
|
||||
|
||||
expect(result).toEqual([{ name: 'Single Column', value: 'Single Column' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => addTable', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.post(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/add',
|
||||
{ address: 'A1:D4', hasHeaders: true },
|
||||
)
|
||||
.reply(200, {
|
||||
style: 'TableStyleMedium2',
|
||||
name: 'Table3',
|
||||
showFilterButton: true,
|
||||
id: '{317CA469-7D1C-4A5D-9B0B-424444BF0336}',
|
||||
highlightLastColumn: false,
|
||||
highlightFirstColumn: false,
|
||||
legacyId: '3',
|
||||
showBandedColumns: false,
|
||||
showBandedRows: true,
|
||||
showHeaders: true,
|
||||
showTotals: false,
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['addTable.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [380, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "addTable",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet4"
|
||||
},
|
||||
"selectRange": "manual",
|
||||
"range": "A1:D4"
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"style": "TableStyleMedium2",
|
||||
"name": "Table3",
|
||||
"showFilterButton": true,
|
||||
"id": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}",
|
||||
"highlightLastColumn": false,
|
||||
"highlightFirstColumn": false,
|
||||
"legacyId": "3",
|
||||
"showBandedColumns": false,
|
||||
"showBandedRows": true,
|
||||
"showHeaders": true,
|
||||
"showTotals": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => append', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B317CA469-7D1C-4A5D-9B0B-424444BF0336%7D/columns',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [{ name: 'id' }, { name: 'name' }, { name: 'age' }, { name: 'data' }],
|
||||
})
|
||||
.post(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B317CA469-7D1C-4A5D-9B0B-424444BF0336%7D/rows/add',
|
||||
{ values: [['3', 'Donald', '99', 'data 5']] },
|
||||
)
|
||||
.reply(200, {
|
||||
index: 3,
|
||||
values: [[3, 'Donald', 99, 'data 5']],
|
||||
})
|
||||
.post('/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/createSession')
|
||||
.reply(200, { id: 12345 })
|
||||
.post('/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/closeSession')
|
||||
.reply(200);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['append.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [380, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія",
|
||||
"cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet4",
|
||||
"cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{317CA469-7D1C-4A5D-9B0B-424444BF0336}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table3",
|
||||
"cachedResultUrl": "https://5w1hb7-my.sharepoint.com/personal/michaeldevsandbox_5w1hb7_onmicrosoft_com/_layouts/15/Doc.aspx?sourcedoc=%7BECC4041C-3AB6-4CF7-B079-0926470A1388%7D&file=%D0%9F%D0%A0%D0%A0%D0%9E%20%D0%BA%D0%BE%D0%BF%D1%96%D1%8F.xlsx&action=default&mobileredirect=true&DefaultItemOpen=1&activeCell=Sheet4!A1:D4"
|
||||
},
|
||||
"fieldsUi": {
|
||||
"values": [
|
||||
{
|
||||
"column": "id",
|
||||
"fieldValue": "3"
|
||||
},
|
||||
{
|
||||
"column": "name",
|
||||
"fieldValue": "Donald"
|
||||
},
|
||||
{
|
||||
"column": "age",
|
||||
"fieldValue": "99"
|
||||
},
|
||||
{
|
||||
"column": "data",
|
||||
"fieldValue": "data 5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"name": "Donald",
|
||||
"age": 99,
|
||||
"data": "data 5"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "b9eda2d8-e1a5-4a54-aaa9-5e81adaae909",
|
||||
"id": "135",
|
||||
"meta": {
|
||||
"instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => convertToRange', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.post(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B6321EE4A-AC21-48AD-87D9-B527637D94B3%7D/convertToRange',
|
||||
)
|
||||
.reply(200, {
|
||||
address: 'Sheet4!A1:D5',
|
||||
values: [
|
||||
['id', 'name', 'age', 'data'],
|
||||
[1, 'Sam', 33, 'data 1'],
|
||||
[2, 'Jon', 44, 'data 2'],
|
||||
[3, 'Sam', 34, 'data 4'],
|
||||
[3, 'Donald', 99, 'data 5'],
|
||||
],
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['convertToRange.workflow.json'],
|
||||
});
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [580, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "convertToRange",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet4"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{6321EE4A-AC21-48AD-87D9-B527637D94B3}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table3"
|
||||
}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"address": "Sheet4!A1:D5",
|
||||
"values": [
|
||||
["id", "name", "age", "data"],
|
||||
[1, "Sam", 33, "data 1"],
|
||||
[2, "Jon", 44, "data 2"],
|
||||
[3, "Sam", 34, "data 4"],
|
||||
[3, "Donald", 99, "data 5"]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => deleteTable', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.delete(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7BA0883CFE-D27E-4ECC-B94B-981830AAD55B%7D/tables/%7B92FBE3F5-3180-47EE-8549-40892C38DA7F%7D',
|
||||
)
|
||||
.reply(200);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['deleteTable.workflow.json'],
|
||||
});
|
||||
});
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [580, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "deleteTable",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{A0883CFE-D27E-4ECC-B94B-981830AAD55B}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet4"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{92FBE3F5-3180-47EE-8549-40892C38DA7F}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table3"
|
||||
}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => getColumns', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24top=100&%24skip=0',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [
|
||||
{ name: 'country' },
|
||||
{ name: 'browser' },
|
||||
{ name: 'session_duration' },
|
||||
{ name: 'visits' },
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24top=100&%24skip=100',
|
||||
)
|
||||
.reply(200, { value: [] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['getColumns.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [580, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "getColumns",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{00000000-0001-0000-0000-000000000000}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet1"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table1"
|
||||
},
|
||||
"returnAll": true
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"name": "country"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"name": "browser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"name": "session_duration"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"name": "visits"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => getRows', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/rows?%24top=2',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [
|
||||
{ index: 0, values: [['uk', 'firefox', 1, 1]] },
|
||||
{ index: 1, values: [['us', 'chrome', 1, 12]] },
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24select=name&%24top=100&%24skip=0',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [
|
||||
{ name: 'country' },
|
||||
{ name: 'browser' },
|
||||
{ name: 'session_duration' },
|
||||
{ name: 'visits' },
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24select=name&%24top=100&%24skip=100',
|
||||
)
|
||||
.reply(200, { value: [] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['getRows.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [580, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "getRows",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{00000000-0001-0000-0000-000000000000}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet1"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table1"
|
||||
},
|
||||
"limit": 2,
|
||||
"filters": {}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"country": "uk",
|
||||
"browser": "firefox",
|
||||
"session_duration": 1,
|
||||
"visits": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"country": "us",
|
||||
"browser": "chrome",
|
||||
"session_duration": 1,
|
||||
"visits": 12
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, table => lookup', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/rows?%24top=100&%24skip=0',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [
|
||||
{ index: 0, values: [['uk', 'firefox', 1, 1]] },
|
||||
{ index: 1, values: [['us', 'chrome', 1, 12]] },
|
||||
{ index: 2, values: [['test', 'test', 55, 123]] },
|
||||
{ index: 3, values: [['ua', 'chrome', 1, 3]] },
|
||||
{ index: 4, values: [['ua', 'firefox', 1, 4]] },
|
||||
{ index: 5, values: [['uk', 'chrome', 1, 55]] },
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/rows?%24top=100&%24skip=100',
|
||||
)
|
||||
.reply(200, { value: [] })
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24select=name&%24top=100&%24skip=0',
|
||||
)
|
||||
.reply(200, {
|
||||
value: [
|
||||
{ name: 'country' },
|
||||
{ name: 'browser' },
|
||||
{ name: 'session_duration' },
|
||||
{ name: 'visits' },
|
||||
],
|
||||
})
|
||||
.get(
|
||||
'/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/%7B00000000-0001-0000-0000-000000000000%7D/tables/%7B613E8967-D581-44ED-81D3-82A01AA6A05C%7D/columns?%24select=name&%24top=100&%24skip=100',
|
||||
)
|
||||
.reply(200, { value: [] });
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['lookup.workflow.json'],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [580, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "table",
|
||||
"operation": "lookup",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"worksheet": {
|
||||
"__rl": true,
|
||||
"value": "{00000000-0001-0000-0000-000000000000}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Sheet1"
|
||||
},
|
||||
"table": {
|
||||
"__rl": true,
|
||||
"value": "{613E8967-D581-44ED-81D3-82A01AA6A05C}",
|
||||
"mode": "list",
|
||||
"cachedResultName": "Table1"
|
||||
},
|
||||
"lookupColumn": "country",
|
||||
"lookupValue": "uk",
|
||||
"options": {
|
||||
"returnAllMatches": true
|
||||
}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"country": "uk",
|
||||
"browser": "firefox",
|
||||
"session_duration": 1,
|
||||
"visits": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"country": "uk",
|
||||
"browser": "chrome",
|
||||
"session_duration": 1,
|
||||
"visits": 55
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, workbook => addWorksheet', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.post('/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/createSession', {
|
||||
persistChanges: true,
|
||||
})
|
||||
.reply(200, { id: 12345 })
|
||||
.post('/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/worksheets/add', {
|
||||
name: 'Sheet42',
|
||||
})
|
||||
.matchHeader('workbook-session-id', '12345')
|
||||
.reply(200, {
|
||||
id: '{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}',
|
||||
name: 'Sheet42',
|
||||
position: 8,
|
||||
visibility: 'Visible',
|
||||
})
|
||||
.post('/drive/items/01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I/workbook/closeSession')
|
||||
.reply(200);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['addWorksheet.workflow.json'],
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "My workflow 5",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "875e8784-eb59-40d8-ba45-129a5e29881c",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [380, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "addWorksheet",
|
||||
"workbook": {
|
||||
"__rl": true,
|
||||
"value": "01FUWX3BQ4ATCOZNR265GLA6IJEZDQUE4I",
|
||||
"mode": "list",
|
||||
"cachedResultName": "ПРРО копія"
|
||||
},
|
||||
"additionalFields": {
|
||||
"name": "Sheet42"
|
||||
}
|
||||
},
|
||||
"id": "0e0ac1d2-242c-486a-9287-c70307645acc",
|
||||
"name": "Microsoft Excel 365",
|
||||
"type": "n8n-nodes-base.microsoftExcel",
|
||||
"typeVersion": 2,
|
||||
"position": [860, 140],
|
||||
"credentials": {
|
||||
"microsoftExcelOAuth2Api": {
|
||||
"id": "70",
|
||||
"name": "Microsoft Excel account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Microsoft Excel 365": [
|
||||
{
|
||||
"json": {
|
||||
"id": "{266ADAB7-25B6-4F28-A2D1-FD5BFBD7A4F0}",
|
||||
"name": "Sheet42",
|
||||
"position": 8,
|
||||
"visibility": "Visible"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Microsoft Excel 365",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
import { credentials } from '../../../credentials';
|
||||
|
||||
describe('Test MicrosoftExcelV2, workbook => deleteWorkbook', () => {
|
||||
nock('https://graph.microsoft.com/v1.0/me')
|
||||
.delete('/drive/items/01FUWX3BXJLISGF2CFWBGYPHXFCXPXOJUK')
|
||||
.reply(200);
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['deleteWorkbook.workflow.json'],
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user