mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Azure Cosmos DB Node): New node (#14156)
Co-authored-by: feelgood-interface <feelgood.interface@gmail.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { createHmac } from 'crypto';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialType,
|
||||
ICredentialTestRequest,
|
||||
IHttpRequestOptions,
|
||||
INodeProperties,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
CURRENT_VERSION,
|
||||
HeaderConstants,
|
||||
RESOURCE_TYPES,
|
||||
} from '../nodes/Microsoft/AzureCosmosDb/helpers/constants';
|
||||
|
||||
export class MicrosoftAzureCosmosDbSharedKeyApi implements ICredentialType {
|
||||
name = 'microsoftAzureCosmosDbSharedKeyApi';
|
||||
|
||||
displayName = 'Microsoft Azure Cosmos DB API';
|
||||
|
||||
documentationUrl = 'microsoftAzureCosmosdb';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Account',
|
||||
name: 'account',
|
||||
default: '',
|
||||
description: 'Account name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
default: '',
|
||||
description: 'Account key',
|
||||
required: true,
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Database',
|
||||
name: 'database',
|
||||
default: '',
|
||||
description: 'Database name',
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
];
|
||||
|
||||
async authenticate(
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const date = new Date().toUTCString();
|
||||
|
||||
requestOptions.headers ??= {};
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers,
|
||||
'x-ms-date': date,
|
||||
'x-ms-version': CURRENT_VERSION,
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
// HttpRequest node uses IRequestOptions.uri
|
||||
const url = new URL(
|
||||
(requestOptions as IRequestOptions).uri ?? requestOptions.baseURL + requestOptions.url,
|
||||
);
|
||||
|
||||
const pathSegments = url.pathname.split('/').filter(Boolean);
|
||||
|
||||
const foundResource = RESOURCE_TYPES.map((type) => ({
|
||||
type,
|
||||
index: pathSegments.lastIndexOf(type),
|
||||
}))
|
||||
.filter(({ index }) => index !== -1)
|
||||
.sort((a, b) => b.index - a.index)
|
||||
.shift();
|
||||
|
||||
if (!foundResource) {
|
||||
throw new OperationalError('Unable to determine the resource type from the URL');
|
||||
}
|
||||
|
||||
const { type, index } = foundResource;
|
||||
const resourceId =
|
||||
pathSegments[index + 1] !== undefined
|
||||
? `${pathSegments.slice(0, index).join('/')}/${type}/${pathSegments[index + 1]}`
|
||||
: pathSegments.slice(0, index).join('/');
|
||||
|
||||
const key = Buffer.from(credentials.key as string, 'base64');
|
||||
const payload = `${(requestOptions.method ?? 'GET').toLowerCase()}\n${type.toLowerCase()}\n${resourceId}\n${date.toLowerCase()}\n\n`;
|
||||
const hmacSha256 = createHmac('sha256', key);
|
||||
const signature = hmacSha256.update(payload, 'utf8').digest('base64');
|
||||
|
||||
requestOptions.headers[HeaderConstants.AUTHORIZATION] = encodeURIComponent(
|
||||
`type=master&ver=1.0&sig=${signature}`,
|
||||
);
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL:
|
||||
'=https://{{ $credentials.account }}.documents.azure.com/dbs/{{ $credentials.database }}',
|
||||
url: '/colls',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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/microsoft/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.azureCosmosDb/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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',
|
||||
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 |
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,94 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
getWorkflowFilenames,
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Create Container', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('create.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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/',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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,38 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Delete Container', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('delete.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
|
||||
.delete('/colls/container1')
|
||||
.reply(204, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos DB",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Azure Cosmos DB": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos DB": [
|
||||
{
|
||||
"json": {
|
||||
"deleted": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Get Container', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('get.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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/',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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,178 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Get All Containers', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('getAll.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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,132 @@
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
IHttpRequestOptions,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { MicrosoftAzureCosmosDbSharedKeyApi } from '../../../../../credentials/MicrosoftAzureCosmosDbSharedKeyApi.credentials';
|
||||
import { FAKE_CREDENTIALS_DATA } from '../../../../../test/nodes/FakeCredentialsMap';
|
||||
|
||||
jest.mock('crypto', () => ({
|
||||
createHmac: jest.fn(() => ({
|
||||
update: jest.fn(() => ({
|
||||
digest: jest.fn(() => 'fake-signature'),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('Azure Cosmos DB', () => {
|
||||
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 credentials: ICredentialDataDecryptedObject = {
|
||||
account: FAKE_CREDENTIALS_DATA.microsoftAzureCosmosDbSharedKeyApi.account,
|
||||
key: FAKE_CREDENTIALS_DATA.microsoftAzureCosmosDbSharedKeyApi.key,
|
||||
};
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: `${FAKE_CREDENTIALS_DATA.microsoftAzureCosmosDbSharedKeyApi.baseUrl}/colls/container1/docs/item1`,
|
||||
method: 'GET',
|
||||
};
|
||||
const result = await azureCosmosDbSharedKeyApi.authenticate(credentials, 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 credentials: ICredentialDataDecryptedObject = {
|
||||
account: FAKE_CREDENTIALS_DATA.microsoftAzureCosmosDbSharedKeyApi.account,
|
||||
key: FAKE_CREDENTIALS_DATA.microsoftAzureCosmosDbSharedKeyApi.key,
|
||||
};
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/dbs/mydb/colls/mycoll/docs/mydoc',
|
||||
method: 'GET',
|
||||
};
|
||||
const result = await azureCosmosDbSharedKeyApi.authenticate(credentials, 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,390 @@
|
||||
import type { IDataObject, IHttpRequestOptions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError, OperationalError } from 'n8n-workflow';
|
||||
|
||||
import { ErrorMap } from '../../helpers/errorHandler';
|
||||
import {
|
||||
getPartitionKey,
|
||||
simplifyData,
|
||||
validateQueryParameters,
|
||||
processJsonInput,
|
||||
validatePartitionKey,
|
||||
validateCustomProperties,
|
||||
} from '../../helpers/utils';
|
||||
import { azureCosmosDbApiRequest } from '../../transport';
|
||||
|
||||
interface RequestBodyWithParameters extends IDataObject {
|
||||
parameters: Array<{ name: string; value: string }>;
|
||||
}
|
||||
|
||||
jest.mock('n8n-workflow', () => ({
|
||||
...jest.requireActual('n8n-workflow'),
|
||||
azureCosmosDbApiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../transport', () => ({
|
||||
azureCosmosDbApiRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getPartitionKey', () => {
|
||||
let mockExecuteSingleFunctions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteSingleFunctions = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
};
|
||||
});
|
||||
|
||||
test('should return partition key when found', async () => {
|
||||
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
|
||||
const mockApiResponse = {
|
||||
partitionKey: {
|
||||
paths: ['/partitionKeyPath'],
|
||||
},
|
||||
};
|
||||
(azureCosmosDbApiRequest as jest.Mock).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 as jest.Mock).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 as jest.Mock).mockRejectedValue(mockError);
|
||||
|
||||
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
|
||||
new NodeApiError(
|
||||
mockExecuteSingleFunctions.getNode(),
|
||||
{},
|
||||
{
|
||||
message: errorMessage,
|
||||
description: ErrorMap.Container.NotFound.description,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatePartitionKey', () => {
|
||||
let mockExecuteSingleFunctions: any;
|
||||
let requestOptions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteSingleFunctions = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
};
|
||||
requestOptions = { body: {}, headers: {} };
|
||||
|
||||
(azureCosmosDbApiRequest as jest.Mock).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 as jest.Mock).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 as jest.Mock).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 as jest.Mock).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 as jest.Mock).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', () => {
|
||||
let mockExecuteSingleFunctions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteSingleFunctions = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
};
|
||||
});
|
||||
|
||||
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 mockExecuteSingleFunctions: any;
|
||||
let requestOptions: IHttpRequestOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteSingleFunctions = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
};
|
||||
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 mockExecuteSingleFunctions: any;
|
||||
let requestOptions: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteSingleFunctions = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getNode: jest.fn(() => ({ name: 'MockNode' })),
|
||||
};
|
||||
requestOptions = { body: {}, headers: {}, url: 'http://mock.url' };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
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,76 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Create Item', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('create.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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,69 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Delete Item', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('delete.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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, '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"deleted": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Get Item', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('get.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Azure Cosmos Db",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"Azure Cosmos Db": [
|
||||
{
|
||||
"json": {
|
||||
"id": "item1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Get All Items', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('getAll.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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,53 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Query Items', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('query.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "queryItems",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"queryItems": [
|
||||
{
|
||||
"json": {
|
||||
"id": "User1",
|
||||
"key1": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import nock from 'nock';
|
||||
|
||||
import {
|
||||
initBinaryDataService,
|
||||
testWorkflows,
|
||||
getWorkflowFilenames,
|
||||
} from '../../../../../test/nodes/Helpers';
|
||||
|
||||
describe('Azure Cosmos DB - Update Item', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
|
||||
filename.includes('update.workflow.json'),
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await initBinaryDataService();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
const baseUrl = 'https://n8n-us-east-account.documents.azure.com/dbs/database_1';
|
||||
|
||||
nock.cleanAll();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
testWorkflows(workflows);
|
||||
});
|
||||
@@ -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 ‘Test 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 ‘Test 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 { FAKE_CREDENTIALS_DATA } from '../../../../../test/nodes/FakeCredentialsMap';
|
||||
import { AzureCosmosDb } from '../../AzureCosmosDb.node';
|
||||
import { HeaderConstants } from '../../helpers/constants';
|
||||
|
||||
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 FAKE_CREDENTIALS_DATA.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 FAKE_CREDENTIALS_DATA.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);
|
||||
}
|
||||
@@ -225,6 +225,7 @@
|
||||
"dist/credentials/MetabaseApi.credentials.js",
|
||||
"dist/credentials/MessageBirdApi.credentials.js",
|
||||
"dist/credentials/MetabaseApi.credentials.js",
|
||||
"dist/credentials/MicrosoftAzureCosmosDbSharedKeyApi.credentials.js",
|
||||
"dist/credentials/MicrosoftAzureMonitorOAuth2Api.credentials.js",
|
||||
"dist/credentials/MicrosoftDynamicsOAuth2Api.credentials.js",
|
||||
"dist/credentials/MicrosoftEntraOAuth2Api.credentials.js",
|
||||
@@ -638,6 +639,7 @@
|
||||
"dist/nodes/Merge/Merge.node.js",
|
||||
"dist/nodes/MessageBird/MessageBird.node.js",
|
||||
"dist/nodes/Metabase/Metabase.node.js",
|
||||
"dist/nodes/Microsoft/AzureCosmosDb/AzureCosmosDb.node.js",
|
||||
"dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js",
|
||||
"dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js",
|
||||
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
|
||||
|
||||
@@ -107,6 +107,12 @@ BQIDAQAB
|
||||
},
|
||||
baseUrl: 'https://api.gong.io',
|
||||
},
|
||||
microsoftAzureCosmosDbSharedKeyApi: {
|
||||
account: 'n8n-us-east-account',
|
||||
key: 'I3rwpzP0XoFpNzJ7hRIUXjwgpD1qaVKi71NZBbk7oOHUXrbd80WAoIAAoRaT47W9hHO3b6us1yABACDbVdilag==',
|
||||
database: 'database_1',
|
||||
baseUrl: 'https://n8n-us-east-account.documents.azure.com/dbs/database_1',
|
||||
},
|
||||
microsoftEntraOAuth2Api: {
|
||||
grantType: 'authorizationCode',
|
||||
authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
|
||||
Reference in New Issue
Block a user