mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge 86775da09f
into d2dd1796a8
This commit is contained in:
commit
3151f6bfb6
|
@ -69,7 +69,7 @@
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.10",
|
||||||
"tsc-watch": "^6.2.0",
|
"tsc-watch": "^6.2.0",
|
||||||
"turbo": "2.3.3",
|
"turbo": "2.3.3",
|
||||||
"typescript": "*",
|
"typescript": "^5.6.2",
|
||||||
"zx": "^8.1.4"
|
"zx": "^8.1.4"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
import {
|
||||||
|
ApplicationError,
|
||||||
|
type ICredentialDataDecryptedObject,
|
||||||
|
type ICredentialType,
|
||||||
|
type IHttpRequestOptions,
|
||||||
|
type INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAuthorizationTokenUsingMasterKey,
|
||||||
|
HeaderConstants,
|
||||||
|
} from '../nodes/Microsoft/CosmosDB/GenericFunctions';
|
||||||
|
|
||||||
|
export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
|
||||||
|
name = 'microsoftCosmosDbSharedKeyApi';
|
||||||
|
|
||||||
|
displayName = 'Cosmos DB API';
|
||||||
|
|
||||||
|
documentationUrl = 'microsoftCosmosDb';
|
||||||
|
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Account',
|
||||||
|
name: 'account',
|
||||||
|
description: 'Account name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Key',
|
||||||
|
name: 'key',
|
||||||
|
description: 'Account key',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Database',
|
||||||
|
name: 'database',
|
||||||
|
description: 'Database name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Base URL',
|
||||||
|
name: 'baseUrl',
|
||||||
|
type: 'hidden',
|
||||||
|
default: '=https://{{ $self["account"] }}.documents.azure.com/dbs/{{ $self["database"] }}',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async authenticate(
|
||||||
|
credentials: ICredentialDataDecryptedObject,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
if (requestOptions.qs) {
|
||||||
|
for (const [key, value] of Object.entries(requestOptions.qs)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete requestOptions.qs[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions.headers ??= {};
|
||||||
|
|
||||||
|
const date = new Date().toUTCString().toLowerCase();
|
||||||
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-date': date,
|
||||||
|
'x-ms-version': '2018-12-31',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (credentials.sessionToken) {
|
||||||
|
requestOptions.headers['x-ms-session-token'] = credentials.sessionToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url;
|
||||||
|
if (requestOptions.url) {
|
||||||
|
url = new URL(requestOptions.baseURL + requestOptions.url);
|
||||||
|
//@ts-ignore
|
||||||
|
} else if (requestOptions.uri) {
|
||||||
|
//@ts-ignore
|
||||||
|
url = new URL(requestOptions.uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathSegments = url?.pathname.split('/').filter((segment) => segment);
|
||||||
|
let resourceType = '';
|
||||||
|
let resourceId = '';
|
||||||
|
|
||||||
|
if (pathSegments?.includes('docs')) {
|
||||||
|
const docsIndex = pathSegments.lastIndexOf('docs');
|
||||||
|
resourceType = 'docs';
|
||||||
|
if (pathSegments[docsIndex + 1]) {
|
||||||
|
const docsId = pathSegments[docsIndex + 1];
|
||||||
|
resourceId = pathSegments.slice(0, docsIndex).join('/') + `/docs/${docsId}`;
|
||||||
|
} else {
|
||||||
|
resourceId = pathSegments.slice(0, docsIndex).join('/');
|
||||||
|
}
|
||||||
|
} else if (pathSegments?.includes('colls')) {
|
||||||
|
const collsIndex = pathSegments.lastIndexOf('colls');
|
||||||
|
resourceType = 'colls';
|
||||||
|
if (pathSegments[collsIndex + 1]) {
|
||||||
|
const collId = pathSegments[collsIndex + 1];
|
||||||
|
resourceId = pathSegments.slice(0, collsIndex).join('/') + `/colls/${collId}`;
|
||||||
|
} else {
|
||||||
|
resourceId = pathSegments.slice(0, collsIndex).join('/');
|
||||||
|
}
|
||||||
|
} else if (pathSegments?.includes('dbs')) {
|
||||||
|
const dbsIndex = pathSegments.lastIndexOf('dbs');
|
||||||
|
resourceType = 'dbs';
|
||||||
|
resourceId = pathSegments.slice(0, dbsIndex + 2).join('/');
|
||||||
|
} else {
|
||||||
|
throw new ApplicationError('Unable to determine resourceType and resourceId from the URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestOptions.method) {
|
||||||
|
let authToken = '';
|
||||||
|
|
||||||
|
if (credentials.key) {
|
||||||
|
authToken = getAuthorizationTokenUsingMasterKey(
|
||||||
|
requestOptions.method,
|
||||||
|
resourceType,
|
||||||
|
resourceId,
|
||||||
|
credentials.key as string,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions.headers[HeaderConstants.AUTHORIZATION] = encodeURIComponent(authToken);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,70 @@
|
||||||
|
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { containerFields, containerOperations } from './descriptions/ContainerDescription';
|
||||||
|
import { itemFields, itemOperations } from './descriptions/ItemDescription';
|
||||||
|
import { getProperties, searchContainers, searchItems } from './GenericFunctions';
|
||||||
|
|
||||||
|
export class CosmosDb implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Cosmos DB',
|
||||||
|
name: 'cosmosDb',
|
||||||
|
icon: {
|
||||||
|
light: 'file:CosmosDB.svg',
|
||||||
|
dark: 'file:CosmosDB.svg',
|
||||||
|
},
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
|
description: 'Interact with Cosmos DB API',
|
||||||
|
defaults: {
|
||||||
|
name: 'Cosmos Db',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'microsoftCosmosDbSharedKeyApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requestDefaults: {
|
||||||
|
baseURL: '={{$credentials.baseUrl}}',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Container',
|
||||||
|
value: 'container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Item',
|
||||||
|
value: 'item',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'container',
|
||||||
|
},
|
||||||
|
...itemOperations,
|
||||||
|
...itemFields,
|
||||||
|
...containerOperations,
|
||||||
|
...containerFields,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
listSearch: {
|
||||||
|
searchContainers,
|
||||||
|
searchItems,
|
||||||
|
getProperties,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
932
packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts
Normal file
932
packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts
Normal file
|
@ -0,0 +1,932 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import type {
|
||||||
|
DeclarativeRestApiSettings,
|
||||||
|
IDataObject,
|
||||||
|
IExecutePaginationFunctions,
|
||||||
|
IExecuteSingleFunctions,
|
||||||
|
IHttpRequestOptions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
IN8nHttpFullResponse,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeListSearchItems,
|
||||||
|
INodeListSearchResult,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { ApplicationError, NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const HeaderConstants = {
|
||||||
|
AUTHORIZATION: 'Authorization',
|
||||||
|
CONTENT_TYPE: 'Content-Type',
|
||||||
|
X_MS_DATE: 'x-ms-date',
|
||||||
|
X_MS_VERSION: 'x-ms-version',
|
||||||
|
X_MS_SESSION_TOKEN: 'x-ms-session-token',
|
||||||
|
IF_MATCH: 'If-Match',
|
||||||
|
IF_NONE_MATCH: 'If-None-Match',
|
||||||
|
IF_MODIFIED_SINCE: 'If-Modified-Since',
|
||||||
|
USER_AGENT: 'User-Agent',
|
||||||
|
X_MS_ACTIVITY_ID: 'x-ms-activity-id',
|
||||||
|
X_MS_CONSISTENCY_LEVEL: 'x-ms-consistency-level',
|
||||||
|
X_MS_CONTINUATION: 'x-ms-continuation',
|
||||||
|
X_MS_MAX_ITEM_COUNT: 'x-ms-max-item-count',
|
||||||
|
X_MS_DOCUMENTDB_PARTITIONKEY: 'x-ms-documentdb-partitionkey',
|
||||||
|
X_MS_DOCUMENTDB_ISQUERY: 'x-ms-documentdb-isquery',
|
||||||
|
X_MS_DOCUMENTDB_QUERY_ENABLECROSSPARTITION: 'x-ms-documentdb-query-enablecrosspartition',
|
||||||
|
A_IM: 'A-IM',
|
||||||
|
X_MS_DOCUMENTDB_PARTITIONKEYRANGEID: 'x-ms-documentdb-partitionkeyrangeid',
|
||||||
|
X_MS_COSMOS_ALLOW_TENTATIVE_WRITES: 'x-ms-cosmos-allow-tentative-writes',
|
||||||
|
|
||||||
|
PREFIX_FOR_STORAGE: 'x-ms-',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getAuthorizationTokenUsingMasterKey(
|
||||||
|
verb: string,
|
||||||
|
resourceType: string,
|
||||||
|
resourceId: string,
|
||||||
|
masterKey: string,
|
||||||
|
): string {
|
||||||
|
const date = new Date().toUTCString().toLowerCase();
|
||||||
|
|
||||||
|
const key = Buffer.from(masterKey, 'base64');
|
||||||
|
const payload = `${verb.toLowerCase()}\n${resourceType.toLowerCase()}\n${resourceId}\n${date.toLowerCase()}\n\n`;
|
||||||
|
const hmacSha256 = crypto.createHmac('sha256', key);
|
||||||
|
const signature = hmacSha256.update(payload, 'utf8').digest('base64');
|
||||||
|
|
||||||
|
return `type=master&ver=1.0&sig=${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function microsoftCosmosDbRequest(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
opts: IHttpRequestOptions,
|
||||||
|
): Promise<IDataObject> {
|
||||||
|
const credentials = await this.getCredentials('microsoftCosmosDbSharedKeyApi');
|
||||||
|
const databaseAccount = credentials?.account;
|
||||||
|
|
||||||
|
if (!databaseAccount) {
|
||||||
|
throw new ApplicationError('Database account not found in credentials!', { level: 'error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions: IHttpRequestOptions = {
|
||||||
|
...opts,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
||||||
|
baseURL: `${credentials.baseUrl}`,
|
||||||
|
headers: {
|
||||||
|
...opts.headers,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorMapping: Record<number, Record<string, string>> = {
|
||||||
|
401: {
|
||||||
|
'The security token included in the request is invalid.':
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
'The request signature we calculated does not match the signature you provided':
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
'The security token included in the request is invalid.':
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
'The request signature we calculated does not match the signature you provided':
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
'The specified resource does not exist.': 'The requested resource was not found!',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (await this.helpers.requestWithAuthentication.call(
|
||||||
|
this,
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
requestOptions,
|
||||||
|
)) as IDataObject;
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode = (error.statusCode || error.cause?.statusCode) as number;
|
||||||
|
let errorMessage = (error.response?.body?.message ||
|
||||||
|
error.response?.body?.Message ||
|
||||||
|
error.message) as string;
|
||||||
|
|
||||||
|
if (statusCode in errorMapping && errorMessage in errorMapping[statusCode]) {
|
||||||
|
throw new ApplicationError(errorMapping[statusCode][errorMessage], {
|
||||||
|
level: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.cause?.error) {
|
||||||
|
try {
|
||||||
|
errorMessage = error.cause?.error?.message as string;
|
||||||
|
} catch (ex) {
|
||||||
|
throw new ApplicationError(
|
||||||
|
`Failed to extract error details: ${ex.message || 'Unknown error'}`,
|
||||||
|
{ level: 'error' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApplicationError(`Cosmos DB error response [${statusCode}]: ${errorMessage}`, {
|
||||||
|
level: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPartitionKeyField(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
): Promise<INodeListSearchResult> {
|
||||||
|
const collection = this.getNodeParameter('collId', '') as { mode: string; value: string };
|
||||||
|
|
||||||
|
if (!collection?.value) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Container is required to determine the partition key.',
|
||||||
|
description: 'Please provide a value for "Container" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts: IHttpRequestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/colls/${collection.value}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData: IDataObject = await microsoftCosmosDbRequest.call(this, opts);
|
||||||
|
|
||||||
|
const partitionKey = responseData.partitionKey as
|
||||||
|
| {
|
||||||
|
paths: string[];
|
||||||
|
kind: string;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const partitionKeyPaths = partitionKey?.paths ?? [];
|
||||||
|
|
||||||
|
if (partitionKeyPaths.length === 0) {
|
||||||
|
return { results: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const partitionKeyField = partitionKeyPaths[0].replace('/', '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
name: partitionKeyField,
|
||||||
|
value: partitionKeyField,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateQueryParameters(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const params = this.getNodeParameter('parameters', {}) as {
|
||||||
|
parameters: Array<{ name: string; value: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!params || !Array.isArray(params.parameters)) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'The "parameters" field cannot be empty',
|
||||||
|
description: 'Please provide at least one parameter',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameters = params.parameters;
|
||||||
|
|
||||||
|
for (const parameter of parameters) {
|
||||||
|
if (!parameter.name || parameter.name.trim() === '') {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Each parameter must have a non-empty "Name".',
|
||||||
|
description: 'Please provide a value for "Name" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parameter.value) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: `Invalid value for parameter "${parameter.name}"`,
|
||||||
|
description: 'Please provide a value for "value" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validatePartitionKey(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const operation = this.getNodeParameter('operation') as string;
|
||||||
|
const customProperties = this.getNodeParameter('customProperties', {}) as IDataObject;
|
||||||
|
|
||||||
|
const partitionKeyResult = await fetchPartitionKeyField.call(
|
||||||
|
this as unknown as ILoadOptionsFunctions,
|
||||||
|
);
|
||||||
|
const partitionKeyField =
|
||||||
|
partitionKeyResult.results.length > 0 ? partitionKeyResult.results[0].value : '';
|
||||||
|
|
||||||
|
if (!partitionKeyField) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Partition key not found',
|
||||||
|
description: 'Failed to determine the partition key for this collection.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(typeof partitionKeyField === 'string' || typeof partitionKeyField === 'number')) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid partition key',
|
||||||
|
description: `Partition key must be a string or number, but got ${typeof partitionKeyField}.`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedProperties: Record<string, unknown>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
parsedProperties =
|
||||||
|
typeof customProperties === 'string' ? JSON.parse(customProperties) : customProperties;
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid custom properties format',
|
||||||
|
description: 'Custom properties must be a valid JSON object.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let id: string | undefined | { mode: string; value: string };
|
||||||
|
let partitionKeyValue: string | undefined;
|
||||||
|
|
||||||
|
if (operation === 'create') {
|
||||||
|
if (partitionKeyField === 'id') {
|
||||||
|
partitionKeyValue = this.getNodeParameter('newId', '') as string;
|
||||||
|
} else {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(parsedProperties, partitionKeyField)) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Partition key not found in custom properties',
|
||||||
|
description: `Partition key "${partitionKeyField}" must be present and have a valid, non-empty value in custom properties.`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
partitionKeyValue = parsedProperties[partitionKeyField] as string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (partitionKeyField === 'id') {
|
||||||
|
id = this.getNodeParameter('id', {}) as { mode: string; value: string };
|
||||||
|
|
||||||
|
if (!id?.value) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Item ID is missing or invalid',
|
||||||
|
description: "The item must have a valid value selected from 'Item'",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
partitionKeyValue = id.value;
|
||||||
|
} else {
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
|
||||||
|
partitionKeyValue = additionalFields.partitionKey as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partitionKeyValue === undefined || partitionKeyValue === null || partitionKeyValue === '') {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Partition key value is missing or empty',
|
||||||
|
description: `Provide a value for partition key "${partitionKeyField}" in "Partition Key" field.`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-documentdb-partitionkey': `["${partitionKeyValue}"]`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateOperations(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const rawOperations = this.getNodeParameter('operations', []) as IDataObject;
|
||||||
|
|
||||||
|
if (!rawOperations || !Array.isArray(rawOperations.operations)) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'No operation provided',
|
||||||
|
description: 'The "Operations" field must contain at least one operation.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = rawOperations.operations as Array<{
|
||||||
|
op: string;
|
||||||
|
path?: { mode: string; value: string };
|
||||||
|
toPath?: { mode: string; value: string };
|
||||||
|
from?: { mode: string; value: string };
|
||||||
|
value?: string | number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const transformedOperations = operations.map((operation) => {
|
||||||
|
if (
|
||||||
|
operation.op !== 'move' &&
|
||||||
|
(!operation.path?.value ||
|
||||||
|
typeof operation.path.value !== 'string' ||
|
||||||
|
operation.path.value.trim() === '')
|
||||||
|
) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Each operation must have a valid "path".',
|
||||||
|
description: 'Please provide a value for path',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['set', 'replace', 'add', 'incr'].includes(operation.op) &&
|
||||||
|
(operation.value === undefined || operation.value === null)
|
||||||
|
) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid value',
|
||||||
|
description: `The "${operation.op}" operation must include a valid "value".`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.op === 'move') {
|
||||||
|
if (
|
||||||
|
!operation.from?.value ||
|
||||||
|
typeof operation.from.value !== 'string' ||
|
||||||
|
operation.from.value.trim() === ''
|
||||||
|
) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'The "move" operation must have a valid path.',
|
||||||
|
description: 'Please provide a valid value for field "From Path"',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!operation.toPath?.value ||
|
||||||
|
typeof operation.toPath.value !== 'string' ||
|
||||||
|
operation.toPath.value.trim() === ''
|
||||||
|
) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'The "move" operation must have a valid path.',
|
||||||
|
description: 'Please provide a valid value for field "To Path"',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.op === 'incr' && isNaN(Number(operation.value))) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid value',
|
||||||
|
description: 'Please provide a numeric value for field "Value"',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: operation.op,
|
||||||
|
path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value,
|
||||||
|
...(operation.from ? { from: operation.from.value } : {}),
|
||||||
|
...(operation.op === 'incr'
|
||||||
|
? { value: Number(operation.value) }
|
||||||
|
: operation.value !== undefined
|
||||||
|
? { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
requestOptions.body = transformedOperations;
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateContainerFields(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
|
||||||
|
const manualThroughput = additionalFields.offerThroughput;
|
||||||
|
const autoscaleThroughput = additionalFields.maxThroughput;
|
||||||
|
|
||||||
|
if (manualThroughput && autoscaleThroughput) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Bad parameter',
|
||||||
|
description: 'Please choose only one of Max RU/s (Autoscale) and Manual Throughput RU/s',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoscaleThroughput) {
|
||||||
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-cosmos-offer-autopilot-setting': { maxThroughput: autoscaleThroughput },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manualThroughput) {
|
||||||
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-offer-throughput': manualThroughput,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePagination(
|
||||||
|
this: IExecutePaginationFunctions,
|
||||||
|
resultOptions: DeclarativeRestApiSettings.ResultOptions,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
const aggregatedResult: IDataObject[] = [];
|
||||||
|
let nextPageToken: string | undefined;
|
||||||
|
const returnAll = this.getNodeParameter('returnAll') as boolean;
|
||||||
|
let limit = 60;
|
||||||
|
|
||||||
|
if (!returnAll) {
|
||||||
|
limit = this.getNodeParameter('limit') as number;
|
||||||
|
resultOptions.maxResults = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultOptions.paginate = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (nextPageToken) {
|
||||||
|
resultOptions.options.headers = resultOptions.options.headers ?? {};
|
||||||
|
resultOptions.options.headers['x-ms-continuation'] = nextPageToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await this.makeRoutingRequest(resultOptions);
|
||||||
|
|
||||||
|
if (Array.isArray(responseData)) {
|
||||||
|
for (const responsePage of responseData) {
|
||||||
|
aggregatedResult.push(responsePage);
|
||||||
|
|
||||||
|
if (!returnAll && aggregatedResult.length >= limit) {
|
||||||
|
return aggregatedResult.slice(0, limit).map((result) => ({ json: result }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseData.length > 0) {
|
||||||
|
const lastItem = responseData[responseData.length - 1];
|
||||||
|
|
||||||
|
if ('headers' in lastItem) {
|
||||||
|
const headers = (lastItem as unknown as { headers: { [key: string]: string } }).headers;
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
nextPageToken = headers['x-ms-continuation'] as string | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextPageToken) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (nextPageToken);
|
||||||
|
|
||||||
|
return aggregatedResult.map((result) => ({ json: result }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleErrorPostReceive(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
data: INodeExecutionData[],
|
||||||
|
response: IN8nHttpFullResponse,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
|
||||||
|
const responseBody = response.body as IDataObject;
|
||||||
|
|
||||||
|
let errorMessage = 'Unknown error occurred';
|
||||||
|
let errorDescription = 'An unexpected error was encountered.';
|
||||||
|
|
||||||
|
if (typeof responseBody === 'object' && responseBody !== null) {
|
||||||
|
if (typeof responseBody.code === 'string') {
|
||||||
|
errorMessage = responseBody.code;
|
||||||
|
}
|
||||||
|
if (typeof responseBody.message === 'string') {
|
||||||
|
errorDescription = responseBody.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: errorMessage,
|
||||||
|
description: errorDescription,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchContainers(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
filter?: string,
|
||||||
|
): Promise<INodeListSearchResult> {
|
||||||
|
const opts: IHttpRequestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls',
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData: IDataObject = await microsoftCosmosDbRequest.call(this, opts);
|
||||||
|
|
||||||
|
const responseBody = responseData as {
|
||||||
|
DocumentCollections: IDataObject[];
|
||||||
|
};
|
||||||
|
const collections = responseBody.DocumentCollections;
|
||||||
|
|
||||||
|
if (!collections) {
|
||||||
|
return { results: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: INodeListSearchItems[] = collections
|
||||||
|
.map((collection) => {
|
||||||
|
return {
|
||||||
|
name: String(collection.id),
|
||||||
|
value: String(collection.id),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((collection) => !filter || collection.name.includes(filter))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchItems(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
filter?: string,
|
||||||
|
): Promise<INodeListSearchResult> {
|
||||||
|
const collection = this.getNodeParameter('collId') as { mode: string; value: string };
|
||||||
|
|
||||||
|
if (!collection?.value) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Container is required',
|
||||||
|
description: 'Please provide a value for container in "Container" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const opts: IHttpRequestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/colls/${collection.value}/docs`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData: IDataObject = await microsoftCosmosDbRequest.call(this, opts);
|
||||||
|
|
||||||
|
const responseBody = responseData as {
|
||||||
|
Documents: IDataObject[];
|
||||||
|
};
|
||||||
|
const items = responseBody.Documents;
|
||||||
|
|
||||||
|
if (!items) {
|
||||||
|
return { results: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: INodeListSearchItems[] = items
|
||||||
|
.map((item) => {
|
||||||
|
const idWithoutSpaces = String(item.id).replace(/ /g, '');
|
||||||
|
return {
|
||||||
|
name: String(idWithoutSpaces),
|
||||||
|
value: String(item.id),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item) => !filter || item.name.includes(filter))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFieldPaths(obj: IDataObject, prefix = ''): string[] {
|
||||||
|
let paths: string[] = [];
|
||||||
|
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith('_') || key === 'id') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newPath = prefix ? `${prefix}/${key}` : `/${key}`;
|
||||||
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
|
value.forEach((item, index) => {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
paths = paths.concat(extractFieldPaths(item, `${newPath}/${index}`));
|
||||||
|
} else {
|
||||||
|
paths.push(`${newPath}/${index}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
paths = paths.concat(extractFieldPaths(value as IDataObject, newPath));
|
||||||
|
} else {
|
||||||
|
paths.push(newPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchItemById(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
itemId: string,
|
||||||
|
): Promise<IDataObject | null> {
|
||||||
|
const collection = this.getNodeParameter('collId') as { mode: string; value: string };
|
||||||
|
|
||||||
|
if (!collection?.value) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Container is required',
|
||||||
|
description: 'Please provide a value for container in "Container" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemId) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Item is required',
|
||||||
|
description: 'Please provide a value for item in "Item" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts: IHttpRequestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/colls/${collection.value}/docs/${itemId}`,
|
||||||
|
headers: {
|
||||||
|
'x-ms-documentdb-partitionkey': `["${itemId}"]`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData: IDataObject = await microsoftCosmosDbRequest.call(this, opts);
|
||||||
|
|
||||||
|
if (!responseData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProperties(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||||
|
const itemId = this.getNodeParameter('id', '') as { mode: string; value: string };
|
||||||
|
|
||||||
|
if (!itemId) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Item is required',
|
||||||
|
description: 'Please provide a value for item in "Item" field',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemData = await searchItemById.call(this, itemId.value);
|
||||||
|
|
||||||
|
if (!itemData) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Item not found',
|
||||||
|
description: `Item with ID "${itemId.value}" not found.`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldPaths = extractFieldPaths(itemData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
results: fieldPaths.map((path) => ({
|
||||||
|
name: path,
|
||||||
|
value: path,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function presendLimitField(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const returnAll = this.getNodeParameter('returnAll');
|
||||||
|
let limit;
|
||||||
|
if (!returnAll) {
|
||||||
|
limit = this.getNodeParameter('limit');
|
||||||
|
if (!limit) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Limit value not found',
|
||||||
|
description:
|
||||||
|
' Please provide a value for "Limit" or set "Return All" to true to return all results',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-max-item-count': limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function formatCustomProperties(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const rawCustomProperties = this.getNodeParameter('customProperties', '{}') as string;
|
||||||
|
const newId = this.getNodeParameter('newId') as string;
|
||||||
|
|
||||||
|
if (/\s/.test(newId)) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid ID format: IDs cannot contain spaces.',
|
||||||
|
description: 'Use an underscore (_) or another separator instead.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedProperties: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
parsedProperties = JSON.parse(rawCustomProperties);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid format in "Custom Properties".',
|
||||||
|
description: ' Please provide a valid JSON object.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!requestOptions.body ||
|
||||||
|
typeof requestOptions.body !== 'object' ||
|
||||||
|
requestOptions.body === null
|
||||||
|
) {
|
||||||
|
requestOptions.body = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(requestOptions.body as Record<string, unknown>, { id: newId }, parsedProperties);
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function formatJSONFields(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
requestOptions: IHttpRequestOptions,
|
||||||
|
): Promise<IHttpRequestOptions> {
|
||||||
|
const rawPartitionKey = this.getNodeParameter('partitionKey', '{}') as string;
|
||||||
|
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
|
||||||
|
const indexingPolicy = additionalFields.indexingPolicy as string;
|
||||||
|
|
||||||
|
let parsedPartitionKey: Record<string, unknown>;
|
||||||
|
let parsedIndexPolicy: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedPartitionKey = JSON.parse(rawPartitionKey);
|
||||||
|
|
||||||
|
if (indexingPolicy) {
|
||||||
|
parsedIndexPolicy = JSON.parse(indexingPolicy);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: 'Invalid JSON format in either "Partition Key" or "Indexing Policy".',
|
||||||
|
description: 'Please provide valid JSON objects.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!requestOptions.body ||
|
||||||
|
typeof requestOptions.body !== 'object' ||
|
||||||
|
requestOptions.body === null
|
||||||
|
) {
|
||||||
|
requestOptions.body = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
(requestOptions.body as Record<string, unknown>).partitionKey = parsedPartitionKey;
|
||||||
|
|
||||||
|
if (parsedIndexPolicy) {
|
||||||
|
(requestOptions.body as Record<string, unknown>).indexingPolicy = parsedIndexPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processResponseItems(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
items: INodeExecutionData[],
|
||||||
|
response: IN8nHttpFullResponse,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
||||||
|
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedDocuments: INodeExecutionData[] = items.flatMap((item) => {
|
||||||
|
if (
|
||||||
|
item.json &&
|
||||||
|
typeof item.json === 'object' &&
|
||||||
|
'Documents' in item.json &&
|
||||||
|
Array.isArray(item.json.Documents)
|
||||||
|
) {
|
||||||
|
return item.json.Documents.map((doc) => ({ json: doc }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return extractedDocuments.length ? extractedDocuments : [{ json: {} }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processResponseContainers(
|
||||||
|
this: IExecuteSingleFunctions,
|
||||||
|
items: INodeExecutionData[],
|
||||||
|
response: IN8nHttpFullResponse,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
||||||
|
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.body as { DocumentCollections: IDataObject[] };
|
||||||
|
|
||||||
|
if (data.DocumentCollections.length > 0) {
|
||||||
|
return data.DocumentCollections.map((doc) => ({ json: doc }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
|
@ -0,0 +1,288 @@
|
||||||
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatJSONFields,
|
||||||
|
handleErrorPostReceive,
|
||||||
|
processResponseContainers,
|
||||||
|
validateContainerFields,
|
||||||
|
} from '../GenericFunctions';
|
||||||
|
|
||||||
|
export const containerOperations: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a container',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
preSend: [formatJSONFields, validateContainerFields],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
ignoreHttpStatusErrors: true,
|
||||||
|
method: 'POST',
|
||||||
|
url: '/colls',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Create container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete a container',
|
||||||
|
routing: {
|
||||||
|
request: {
|
||||||
|
ignoreHttpStatusErrors: true,
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [
|
||||||
|
handleErrorPostReceive,
|
||||||
|
{
|
||||||
|
type: 'set',
|
||||||
|
properties: {
|
||||||
|
value: '={{ { "success": true } }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Delete container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Retrieve a container',
|
||||||
|
routing: {
|
||||||
|
request: {
|
||||||
|
ignoreHttpStatusErrors: true,
|
||||||
|
method: 'GET',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Get container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get Many',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve a list of containers',
|
||||||
|
routing: {
|
||||||
|
request: {
|
||||||
|
ignoreHttpStatusErrors: true,
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive, processResponseContainers],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Get many containers',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'ID',
|
||||||
|
name: 'newid',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
description: "Container's ID",
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
type: 'body',
|
||||||
|
property: 'id',
|
||||||
|
value: '={{$value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Partition Key',
|
||||||
|
name: 'partitionKey',
|
||||||
|
type: 'json',
|
||||||
|
default: '{}',
|
||||||
|
placeholder: '"paths": ["/AccountNumber"],"kind": "Hash", "Version": 2',
|
||||||
|
description: 'User-defined JSON object representing the partition key',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Indexing Policy',
|
||||||
|
name: 'indexingPolicy',
|
||||||
|
type: 'json',
|
||||||
|
default: '{}',
|
||||||
|
placeholder:
|
||||||
|
'"automatic": true, "indexingMode": "Consistent", "includedPaths": [{ "path": "/*", "indexes": [{ "dataType": "String", "precision": -1, "kind": "Range" }]}]',
|
||||||
|
description: 'This value is used to configure indexing policy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Max RU/s (for Autoscale)',
|
||||||
|
name: 'maxThroughput',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1000,
|
||||||
|
},
|
||||||
|
default: 1000,
|
||||||
|
description: 'The user specified autoscale max RU/s',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Manual Throughput RU/s',
|
||||||
|
name: 'offerThroughput',
|
||||||
|
type: 'number',
|
||||||
|
default: 400,
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 400,
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
type: 'collection',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to retrieve',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
operation: ['get'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container ID',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container ID must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getAllFields: INodeProperties[] = [];
|
||||||
|
|
||||||
|
export const deleteFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to delete',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['container'],
|
||||||
|
operation: ['delete'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container ID',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container ID must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const containerFields: INodeProperties[] = [
|
||||||
|
...createFields,
|
||||||
|
...deleteFields,
|
||||||
|
...getFields,
|
||||||
|
...getAllFields,
|
||||||
|
];
|
|
@ -0,0 +1,937 @@
|
||||||
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatCustomProperties,
|
||||||
|
handleErrorPostReceive,
|
||||||
|
handlePagination,
|
||||||
|
presendLimitField,
|
||||||
|
processResponseItems,
|
||||||
|
validateOperations,
|
||||||
|
validatePartitionKey,
|
||||||
|
validateQueryParameters,
|
||||||
|
} from '../GenericFunctions';
|
||||||
|
|
||||||
|
export const itemOperations: 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: [formatCustomProperties, validatePartitionKey],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}/docs',
|
||||||
|
headers: {
|
||||||
|
'x-ms-documentdb-is-upsert': 'True',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Create item',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete an existing item',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
preSend: [validatePartitionKey],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [
|
||||||
|
handleErrorPostReceive,
|
||||||
|
{
|
||||||
|
type: 'set',
|
||||||
|
properties: {
|
||||||
|
value: '={{ { "success": true } }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Delete item',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'get',
|
||||||
|
description: 'Retrieve an item',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
preSend: [validatePartitionKey],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
url: '=/colls/{{ $parameter["collId"]}}/docs/{{$parameter["id"]}}',
|
||||||
|
headers: {
|
||||||
|
'x-ms-documentdb-is-upsert': 'True',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Get item',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Get Many',
|
||||||
|
value: 'getAll',
|
||||||
|
description: 'Retrieve a list of items',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
paginate: true,
|
||||||
|
preSend: [presendLimitField],
|
||||||
|
},
|
||||||
|
operations: {
|
||||||
|
pagination: handlePagination,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}/docs',
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [processResponseItems, handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Get many items',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Query',
|
||||||
|
value: 'query',
|
||||||
|
description: 'Query items',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
preSend: [validateQueryParameters],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'POST',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}/docs',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/query+json',
|
||||||
|
'x-ms-documentdb-isquery': 'True',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [processResponseItems, handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Query items',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
description: 'Update an existing item',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
preSend: [validateOperations, validatePartitionKey],
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
method: 'PATCH',
|
||||||
|
url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action: 'Update item',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'getAll',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container Id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'ID',
|
||||||
|
name: 'newId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
description: "Item's ID",
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
type: 'body',
|
||||||
|
property: 'id',
|
||||||
|
value: '={{$value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Custom Properties',
|
||||||
|
name: 'customProperties',
|
||||||
|
type: 'json',
|
||||||
|
default: '{}',
|
||||||
|
placeholder: '{ "LastName": "Andersen", "Address": { "State": "WA", "City": "Seattle" } }',
|
||||||
|
description: 'User-defined JSON object representing the document properties',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['create'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const deleteFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['delete'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Item',
|
||||||
|
name: 'id',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: "Select the item's ID",
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['delete'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchItems',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'itemId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the item id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The item id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Partition Key',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['delete'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Partition Key',
|
||||||
|
name: 'partitionKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Specify the partition key for this item',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['get'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Item',
|
||||||
|
name: 'id',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: "Select the item's ID",
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['get'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchItems',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'itemId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the item id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The item id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Partition Key',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['get'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Partition Key',
|
||||||
|
name: 'partitionKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Specify the partition key for this item',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getAllFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['getAll'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['getAll'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
default: 50,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['getAll'],
|
||||||
|
returnAll: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 1,
|
||||||
|
},
|
||||||
|
validateType: 'number',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const queryFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Query',
|
||||||
|
name: 'query',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
placeholder: 'SELECT * FROM c WHERE c.name = @Name',
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
type: 'body',
|
||||||
|
property: 'query',
|
||||||
|
value: '={{$value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Parameters',
|
||||||
|
name: 'parameters',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
required: true,
|
||||||
|
default: [],
|
||||||
|
placeholder: 'Add Parameter',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'parameters',
|
||||||
|
displayName: 'Parameter',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g., @name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g., John',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
routing: {
|
||||||
|
send: {
|
||||||
|
type: 'body',
|
||||||
|
property: 'parameters',
|
||||||
|
value:
|
||||||
|
'={{$parameter["parameters"] && $parameter["parameters"].parameters ? $parameter["parameters"].parameters : []}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const updateFields: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Container',
|
||||||
|
name: 'collId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: 'Select the container you want to use',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['update'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchContainers',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'containerId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the container id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The container id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. UsersContainer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Item',
|
||||||
|
name: 'id',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
required: true,
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
description: "Select the item's ID",
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['update'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From list',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'searchItems',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'itemId',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the item id',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '^[\\w+=,.@-]+$',
|
||||||
|
errorMessage: 'The item id must follow the allowed pattern.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'e.g. AndersenFamily',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operations',
|
||||||
|
name: 'operations',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Operation',
|
||||||
|
description: 'Patch operations to apply to the document',
|
||||||
|
required: true,
|
||||||
|
default: [],
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['update'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'operations',
|
||||||
|
displayName: 'Operation',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'op',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Add', value: 'add' },
|
||||||
|
{ name: 'Increment', value: 'incr' },
|
||||||
|
{ name: 'Move', value: 'move' },
|
||||||
|
{ name: 'Remove', value: 'remove' },
|
||||||
|
{ name: 'Replace', value: 'replace' },
|
||||||
|
{ name: 'Set', value: 'set' },
|
||||||
|
],
|
||||||
|
default: 'set',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'From Path',
|
||||||
|
name: 'from',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
description: 'Select a field from the list or enter it manually',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
op: ['move'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getProperties',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By Name',
|
||||||
|
name: 'manual',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the field name manually',
|
||||||
|
placeholder: 'e.g. /Parents/0/FamilyName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'To Path',
|
||||||
|
name: 'toPath',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
description: 'Select a field from the list or enter it manually',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
op: ['move'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getProperties',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By Name',
|
||||||
|
name: 'manual',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the field name manually',
|
||||||
|
placeholder: 'e.g. /Parents/0/FamilyName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Path',
|
||||||
|
name: 'path',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
description: 'Select a field from the list or enter it manually',
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
op: ['add', 'remove', 'set', 'incr', 'replace'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getProperties',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By Name',
|
||||||
|
name: 'manual',
|
||||||
|
type: 'string',
|
||||||
|
hint: 'Enter the field name manually',
|
||||||
|
placeholder: 'e.g. /Parents/0/FamilyName',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
op: ['add', 'set', 'replace', 'incr'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Partition Key',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['item'],
|
||||||
|
operation: ['update'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Partition Key',
|
||||||
|
name: 'partitionKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Specify the partition key for this item',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const itemFields: INodeProperties[] = [
|
||||||
|
...createFields,
|
||||||
|
...deleteFields,
|
||||||
|
...getFields,
|
||||||
|
...getAllFields,
|
||||||
|
...queryFields,
|
||||||
|
...updateFields,
|
||||||
|
];
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { fetchPartitionKeyField } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('GenericFunctions - fetchPartitionKeyField', () => {
|
||||||
|
const mockMicrosoftCosmosDbRequest = jest.fn();
|
||||||
|
|
||||||
|
const mockContext = {
|
||||||
|
helpers: {
|
||||||
|
requestWithAuthentication: mockMicrosoftCosmosDbRequest,
|
||||||
|
},
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getCredentials: jest.fn(),
|
||||||
|
} as unknown as ILoadOptionsFunctions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockContext.getNode = jest.fn().mockReturnValue({});
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'test_database',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch the partition key successfully', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({
|
||||||
|
partitionKey: {
|
||||||
|
paths: ['/PartitionKey'],
|
||||||
|
kind: 'Hash',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetchPartitionKeyField.call(mockContext);
|
||||||
|
|
||||||
|
expect(mockMicrosoftCosmosDbRequest).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls/coll-1',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
name: 'PartitionKey',
|
||||||
|
value: 'PartitionKey',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when container ID is missing', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' });
|
||||||
|
|
||||||
|
await expect(fetchPartitionKeyField.call(mockContext)).rejects.toThrowError(
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Container is required to determine the partition key.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if no partition key is found', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({
|
||||||
|
partitionKey: {
|
||||||
|
paths: [],
|
||||||
|
kind: 'Hash',
|
||||||
|
version: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetchPartitionKeyField.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unexpected response format gracefully', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockMicrosoftCosmosDbRequest.mockResolvedValueOnce({ unexpectedKey: 'value' });
|
||||||
|
|
||||||
|
const response = await fetchPartitionKeyField.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,193 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { microsoftCosmosDbRequest } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('GenericFunctions - microsoftCosmosDbRequest', () => {
|
||||||
|
let mockContext: any;
|
||||||
|
let mockRequestWithAuthentication: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRequestWithAuthentication = jest.fn();
|
||||||
|
mockContext = {
|
||||||
|
helpers: {
|
||||||
|
requestWithAuthentication: mockRequestWithAuthentication,
|
||||||
|
},
|
||||||
|
getCredentials: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should make a successful request with correct options', async () => {
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({ success: true });
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com/dbs/first_database_1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await microsoftCosmosDbRequest.call(mockContext, requestOptions);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
baseURL: 'https://us-east-1.documents.azure.com/dbs/first_database_1',
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
|
json: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an error if account is missing in credentials', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'Database account not found in credentials!',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a descriptive error for invalid credentials (403)', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
mockRequestWithAuthentication.mockRejectedValueOnce({
|
||||||
|
statusCode: 403,
|
||||||
|
response: {
|
||||||
|
body: {
|
||||||
|
message: 'The security token included in the request is invalid.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a descriptive error for invalid request signature (403)', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
mockRequestWithAuthentication.mockRejectedValueOnce({
|
||||||
|
statusCode: 403,
|
||||||
|
response: {
|
||||||
|
body: {
|
||||||
|
message: 'The request signature we calculated does not match the signature you provided',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'The Cosmos DB credentials are not valid!',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an error for resource not found (404)', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
mockRequestWithAuthentication.mockRejectedValueOnce({
|
||||||
|
statusCode: 404,
|
||||||
|
response: {
|
||||||
|
body: {
|
||||||
|
message: 'The specified resource does not exist.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'The requested resource was not found!',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a generic error for unexpected response', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
mockRequestWithAuthentication.mockRejectedValueOnce({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Internal Server Error',
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'Cosmos DB error response [500]: Internal Server Error',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unexpected error structures', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
mockRequestWithAuthentication.mockRejectedValueOnce({
|
||||||
|
cause: { error: { message: 'Unexpected failure' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'GET' as const,
|
||||||
|
url: '/example-endpoint',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(microsoftCosmosDbRequest.call(mockContext, requestOptions)).rejects.toThrow(
|
||||||
|
'Cosmos DB error response [undefined]: Unexpected failure',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,130 @@
|
||||||
|
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { searchContainers } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('GenericFunctions - searchContainers', () => {
|
||||||
|
const mockRequestWithAuthentication = jest.fn();
|
||||||
|
|
||||||
|
const mockContext = {
|
||||||
|
helpers: {
|
||||||
|
requestWithAuthentication: mockRequestWithAuthentication,
|
||||||
|
},
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getCredentials: jest.fn(),
|
||||||
|
} as unknown as ILoadOptionsFunctions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make a GET request to fetch containers and return results', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
DocumentCollections: [{ id: 'Collection1' }, { id: 'Collection2' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchContainers.call(mockContext);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
baseURL: 'https://us-east-1.documents.azure.com',
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [
|
||||||
|
{ name: 'Collection1', value: 'Collection1' },
|
||||||
|
{ name: 'Collection2', value: 'Collection2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter containers by the provided filter string', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
DocumentCollections: [{ id: 'Test-Col-1' }, { id: 'Prod-Col-1' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchContainers.call(mockContext, 'Test');
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
baseURL: 'https://us-east-1.documents.azure.com',
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [{ name: 'Test-Col-1', value: 'Test-Col-1' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should sort containers alphabetically by name', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce('db-id-1');
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
DocumentCollections: [{ id: 'z-col' }, { id: 'a-col' }, { id: 'm-col' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchContainers.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [
|
||||||
|
{ name: 'a-col', value: 'a-col' },
|
||||||
|
{ name: 'm-col', value: 'm-col' },
|
||||||
|
{ name: 'z-col', value: 'z-col' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty results when no containers are returned', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce('db-id-1');
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
DocumentCollections: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchContainers.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing DocumentCollections property', async () => {
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({ account: 'us-east-1' });
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce('db-id-1');
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
unexpectedkey: 'value',
|
||||||
|
});
|
||||||
|
const response = await searchContainers.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,119 @@
|
||||||
|
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { searchItemById } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('GenericFunctions - searchItemById', () => {
|
||||||
|
const mockRequestWithAuthentication = jest.fn();
|
||||||
|
|
||||||
|
const mockContext = {
|
||||||
|
helpers: {
|
||||||
|
requestWithAuthentication: mockRequestWithAuthentication,
|
||||||
|
},
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getCredentials: jest.fn(),
|
||||||
|
} as unknown as ILoadOptionsFunctions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockContext.getNode = jest.fn().mockReturnValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch the item successfully', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
const itemId = 'item-123';
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
id: itemId,
|
||||||
|
name: 'Test Item',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchItemById.call(mockContext, itemId);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls/coll-1/docs/item-123',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
id: itemId,
|
||||||
|
name: 'Test Item',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when container ID is missing', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' });
|
||||||
|
|
||||||
|
await expect(searchItemById.call(mockContext, 'item-123')).rejects.toThrowError(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Container is required',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when item ID is missing', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(searchItemById.call(mockContext, '')).rejects.toThrowError(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Item is required',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null if the response is empty', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
const itemId = 'item-123';
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const response = await searchItemById.call(mockContext, itemId);
|
||||||
|
|
||||||
|
expect(response).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unexpected response format gracefully', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
const itemId = 'item-123';
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({ unexpectedKey: 'value' });
|
||||||
|
|
||||||
|
const response = await searchItemById.call(mockContext, itemId);
|
||||||
|
|
||||||
|
expect(response).toEqual({ unexpectedKey: 'value' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,130 @@
|
||||||
|
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { searchItems } from '../GenericFunctions';
|
||||||
|
|
||||||
|
describe('GenericFunctions - searchItems', () => {
|
||||||
|
const mockRequestWithAuthentication = jest.fn();
|
||||||
|
|
||||||
|
const mockContext = {
|
||||||
|
helpers: {
|
||||||
|
requestWithAuthentication: mockRequestWithAuthentication,
|
||||||
|
},
|
||||||
|
getNodeParameter: jest.fn(),
|
||||||
|
getCredentials: jest.fn(),
|
||||||
|
} as unknown as ILoadOptionsFunctions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockContext.getNode = jest.fn().mockReturnValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch documents and return formatted results', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
Documents: [{ id: 'Item 1' }, { id: 'Item 2' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchItems.call(mockContext);
|
||||||
|
|
||||||
|
expect(mockRequestWithAuthentication).toHaveBeenCalledWith(
|
||||||
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/colls/coll-1/docs',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [
|
||||||
|
{ name: 'Item1', value: 'Item 1' },
|
||||||
|
{ name: 'Item2', value: 'Item 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter results based on the provided filter string', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
Documents: [{ id: 'TestItem' }, { id: 'ProdItem' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchItems.call(mockContext, 'Test');
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
results: [{ name: 'TestItem', value: 'TestItem' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if no documents are found', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
Documents: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchItems.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing Documents property gracefully', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({
|
||||||
|
mode: 'list',
|
||||||
|
value: 'coll-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
(mockContext.getCredentials as jest.Mock).mockResolvedValueOnce({
|
||||||
|
account: 'us-east-1',
|
||||||
|
database: 'first_database_1',
|
||||||
|
baseUrl: 'https://us-east-1.documents.azure.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequestWithAuthentication.mockResolvedValueOnce({
|
||||||
|
unexpectedKey: 'value',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await searchItems.call(mockContext);
|
||||||
|
|
||||||
|
expect(response).toEqual({ results: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when container ID is missing', async () => {
|
||||||
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' });
|
||||||
|
|
||||||
|
await expect(searchItems.call(mockContext)).rejects.toThrowError(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Container is required',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -633,6 +633,7 @@
|
||||||
"dist/nodes/Merge/Merge.node.js",
|
"dist/nodes/Merge/Merge.node.js",
|
||||||
"dist/nodes/MessageBird/MessageBird.node.js",
|
"dist/nodes/MessageBird/MessageBird.node.js",
|
||||||
"dist/nodes/Metabase/Metabase.node.js",
|
"dist/nodes/Metabase/Metabase.node.js",
|
||||||
|
"dist/nodes/Microsoft/CosmosDB/CosmosDb.node.js",
|
||||||
"dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js",
|
"dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js",
|
||||||
"dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js",
|
"dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js",
|
||||||
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
|
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
|
||||||
|
|
Loading…
Reference in a new issue