mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Fixed requests
This commit is contained in:
parent
9801c907fc
commit
d210fcc172
|
@ -75,15 +75,22 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
|
||||||
requestOptions.headers['x-ms-session-token'] = credentials.sessionToken;
|
requestOptions.headers['x-ms-session-token'] = credentials.sessionToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const url = new URL (requestOptions.uri);
|
let url;
|
||||||
|
|
||||||
const url = new URL(requestOptions.baseURL + requestOptions.url);
|
if (requestOptions.url) {
|
||||||
const pathSegments = url.pathname.split('/').filter((segment) => segment);
|
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 resourceType = '';
|
||||||
let resourceId = '';
|
let resourceId = '';
|
||||||
|
|
||||||
if (pathSegments.includes('docs')) {
|
if (pathSegments?.includes('docs')) {
|
||||||
const docsIndex = pathSegments.lastIndexOf('docs');
|
const docsIndex = pathSegments.lastIndexOf('docs');
|
||||||
resourceType = 'docs';
|
resourceType = 'docs';
|
||||||
if (pathSegments[docsIndex + 1]) {
|
if (pathSegments[docsIndex + 1]) {
|
||||||
|
@ -92,7 +99,7 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
|
||||||
} else {
|
} else {
|
||||||
resourceId = pathSegments.slice(0, docsIndex).join('/');
|
resourceId = pathSegments.slice(0, docsIndex).join('/');
|
||||||
}
|
}
|
||||||
} else if (pathSegments.includes('colls')) {
|
} else if (pathSegments?.includes('colls')) {
|
||||||
const collsIndex = pathSegments.lastIndexOf('colls');
|
const collsIndex = pathSegments.lastIndexOf('colls');
|
||||||
resourceType = 'colls';
|
resourceType = 'colls';
|
||||||
if (pathSegments[collsIndex + 1]) {
|
if (pathSegments[collsIndex + 1]) {
|
||||||
|
@ -101,7 +108,7 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
|
||||||
} else {
|
} else {
|
||||||
resourceId = pathSegments.slice(0, collsIndex).join('/');
|
resourceId = pathSegments.slice(0, collsIndex).join('/');
|
||||||
}
|
}
|
||||||
} else if (pathSegments.includes('dbs')) {
|
} else if (pathSegments?.includes('dbs')) {
|
||||||
const dbsIndex = pathSegments.lastIndexOf('dbs');
|
const dbsIndex = pathSegments.lastIndexOf('dbs');
|
||||||
resourceType = 'dbs';
|
resourceType = 'dbs';
|
||||||
resourceId = pathSegments.slice(0, dbsIndex + 2).join('/');
|
resourceId = pathSegments.slice(0, dbsIndex + 2).join('/');
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
/* 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 * as crypto from 'crypto';
|
||||||
import type {
|
import type {
|
||||||
DeclarativeRestApiSettings,
|
DeclarativeRestApiSettings,
|
||||||
|
@ -71,6 +75,7 @@ export async function microsoftCosmosDbRequest(
|
||||||
|
|
||||||
const requestOptions: IHttpRequestOptions = {
|
const requestOptions: IHttpRequestOptions = {
|
||||||
...opts,
|
...opts,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
||||||
baseURL: `${credentials.baseUrl}`,
|
baseURL: `${credentials.baseUrl}`,
|
||||||
headers: {
|
headers: {
|
||||||
...opts.headers,
|
...opts.headers,
|
||||||
|
@ -99,8 +104,6 @@ export async function microsoftCosmosDbRequest(
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Final Request Options before Request:', requestOptions);
|
|
||||||
|
|
||||||
return (await this.helpers.requestWithAuthentication.call(
|
return (await this.helpers.requestWithAuthentication.call(
|
||||||
this,
|
this,
|
||||||
'microsoftCosmosDbSharedKeyApi',
|
'microsoftCosmosDbSharedKeyApi',
|
||||||
|
@ -442,14 +445,15 @@ export async function validateOperations(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//To-Do-check to not send properties it doesn't need
|
|
||||||
return {
|
return {
|
||||||
op: operation.op,
|
op: operation.op,
|
||||||
path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value,
|
path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value,
|
||||||
...(operation.from ? { from: operation.from.value } : {}),
|
...(operation.from ? { from: operation.from.value } : {}),
|
||||||
...(operation.op === 'incr'
|
...(operation.op === 'incr'
|
||||||
? { value: Number(operation.value) }
|
? { value: Number(operation.value) }
|
||||||
: { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) }),
|
: operation.value !== undefined
|
||||||
|
? { value: isNaN(Number(operation.value)) ? operation.value : Number(operation.value) }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -458,12 +462,11 @@ export async function validateOperations(
|
||||||
return requestOptions;
|
return requestOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateFields(
|
export async function validateContainerFields(
|
||||||
this: IExecuteSingleFunctions,
|
this: IExecuteSingleFunctions,
|
||||||
requestOptions: IHttpRequestOptions,
|
requestOptions: IHttpRequestOptions,
|
||||||
): Promise<IHttpRequestOptions> {
|
): Promise<IHttpRequestOptions> {
|
||||||
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
|
const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject;
|
||||||
const indexingPolicy = additionalFields.indexingPolicy;
|
|
||||||
const manualThroughput = additionalFields.offerThroughput;
|
const manualThroughput = additionalFields.offerThroughput;
|
||||||
const autoscaleThroughput = additionalFields.maxThroughput;
|
const autoscaleThroughput = additionalFields.maxThroughput;
|
||||||
|
|
||||||
|
@ -473,32 +476,28 @@ export async function validateFields(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
message: 'Bad parameter',
|
message: 'Bad parameter',
|
||||||
description:
|
description: 'Please choose only one of Max RU/s (Autoscale) and Manual Throughput RU/s',
|
||||||
'Please choose only one of Max RU/s (Autoscale) and Max RU/s (Manual Throughput)',
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (autoscaleThroughput && requestOptions?.qs) {
|
|
||||||
requestOptions.qs['x-ms-cosmos-offer-autopilot-settings'] = {
|
if (autoscaleThroughput) {
|
||||||
maxThroughput: autoscaleThroughput,
|
requestOptions.headers = {
|
||||||
|
...requestOptions.headers,
|
||||||
|
'x-ms-cosmos-offer-autopilot-setting': { maxThroughput: autoscaleThroughput },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!indexingPolicy || Object.keys(indexingPolicy).length === 0) {
|
if (manualThroughput) {
|
||||||
throw new NodeApiError(
|
requestOptions.headers = {
|
||||||
this.getNode(),
|
...requestOptions.headers,
|
||||||
{},
|
'x-ms-offer-throughput': manualThroughput,
|
||||||
{
|
};
|
||||||
message: 'Invalid Indexing Policy',
|
|
||||||
description: 'Please provide a valid indexingPolicy JSON.',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestOptions;
|
return requestOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
//WIP
|
|
||||||
export async function handlePagination(
|
export async function handlePagination(
|
||||||
this: IExecutePaginationFunctions,
|
this: IExecutePaginationFunctions,
|
||||||
resultOptions: DeclarativeRestApiSettings.ResultOptions,
|
resultOptions: DeclarativeRestApiSettings.ResultOptions,
|
||||||
|
@ -533,7 +532,6 @@ export async function handlePagination(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TO-DO-check-if-works
|
|
||||||
if (responseData.length > 0) {
|
if (responseData.length > 0) {
|
||||||
const lastItem = responseData[responseData.length - 1];
|
const lastItem = responseData[responseData.length - 1];
|
||||||
|
|
||||||
|
@ -554,40 +552,34 @@ export async function handlePagination(
|
||||||
return aggregatedResult.map((result) => ({ json: result }));
|
return aggregatedResult.map((result) => ({ json: result }));
|
||||||
}
|
}
|
||||||
|
|
||||||
//WIP
|
|
||||||
export async function handleErrorPostReceive(
|
export async function handleErrorPostReceive(
|
||||||
this: IExecuteSingleFunctions,
|
this: IExecuteSingleFunctions,
|
||||||
data: INodeExecutionData[],
|
data: INodeExecutionData[],
|
||||||
response: IN8nHttpFullResponse,
|
response: IN8nHttpFullResponse,
|
||||||
): Promise<INodeExecutionData[]> {
|
): Promise<INodeExecutionData[]> {
|
||||||
console.log('Status code❌', response.statusCode);
|
|
||||||
|
|
||||||
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
|
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
|
||||||
const responseBody = response.body as IDataObject;
|
const responseBody = response.body as IDataObject;
|
||||||
console.log('Got here ❌', responseBody);
|
|
||||||
let errorMessage = 'Unknown error occurred';
|
let errorMessage = 'Unknown error occurred';
|
||||||
|
let errorDescription = 'An unexpected error was encountered.';
|
||||||
|
|
||||||
if (typeof responseBody.message === 'string') {
|
if (typeof responseBody === 'object' && responseBody !== null) {
|
||||||
try {
|
if (typeof responseBody.code === 'string') {
|
||||||
const jsonMatch = responseBody.message.match(/Message: (\{.*\})/);
|
errorMessage = responseBody.code;
|
||||||
|
}
|
||||||
if (jsonMatch && jsonMatch[1]) {
|
if (typeof responseBody.message === 'string') {
|
||||||
const parsedMessage = JSON.parse(jsonMatch[1]);
|
errorDescription = responseBody.message;
|
||||||
|
|
||||||
if (
|
|
||||||
parsedMessage.Errors &&
|
|
||||||
Array.isArray(parsedMessage.Errors) &&
|
|
||||||
parsedMessage.Errors.length > 0
|
|
||||||
) {
|
|
||||||
errorMessage = parsedMessage.Errors[0].split(' Learn more:')[0].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage = 'Failed to extract error message';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ApplicationError(errorMessage);
|
throw new NodeApiError(
|
||||||
|
this.getNode(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
message: errorMessage,
|
||||||
|
description: errorDescription,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -675,7 +667,7 @@ export async function searchItems(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFieldPaths(obj: any, prefix = ''): string[] {
|
function extractFieldPaths(obj: IDataObject, prefix = ''): string[] {
|
||||||
let paths: string[] = [];
|
let paths: string[] = [];
|
||||||
|
|
||||||
Object.entries(obj).forEach(([key, value]) => {
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
@ -692,7 +684,7 @@ function extractFieldPaths(obj: any, prefix = ''): string[] {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (typeof value === 'object' && value !== null) {
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
paths = paths.concat(extractFieldPaths(value, newPath));
|
paths = paths.concat(extractFieldPaths(value as IDataObject, newPath));
|
||||||
} else {
|
} else {
|
||||||
paths.push(newPath);
|
paths.push(newPath);
|
||||||
}
|
}
|
||||||
|
@ -783,6 +775,34 @@ export async function getProperties(this: ILoadOptionsFunctions): Promise<INodeL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
export async function formatCustomProperties(
|
||||||
this: IExecuteSingleFunctions,
|
this: IExecuteSingleFunctions,
|
||||||
requestOptions: IHttpRequestOptions,
|
requestOptions: IHttpRequestOptions,
|
||||||
|
@ -803,7 +823,6 @@ export async function formatCustomProperties(
|
||||||
|
|
||||||
let parsedProperties: Record<string, unknown>;
|
let parsedProperties: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
parsedProperties = JSON.parse(rawCustomProperties);
|
parsedProperties = JSON.parse(rawCustomProperties);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeApiError(
|
throw new NodeApiError(
|
||||||
|
@ -841,11 +860,9 @@ export async function formatJSONFields(
|
||||||
let parsedIndexPolicy: Record<string, unknown> | undefined;
|
let parsedIndexPolicy: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
parsedPartitionKey = JSON.parse(rawPartitionKey);
|
parsedPartitionKey = JSON.parse(rawPartitionKey);
|
||||||
|
|
||||||
if (indexingPolicy) {
|
if (indexingPolicy) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
parsedIndexPolicy = JSON.parse(indexingPolicy);
|
parsedIndexPolicy = JSON.parse(indexingPolicy);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -880,32 +897,32 @@ export async function processResponseItems(
|
||||||
this: IExecuteSingleFunctions,
|
this: IExecuteSingleFunctions,
|
||||||
items: INodeExecutionData[],
|
items: INodeExecutionData[],
|
||||||
response: IN8nHttpFullResponse,
|
response: IN8nHttpFullResponse,
|
||||||
): Promise<any> {
|
): Promise<INodeExecutionData[]> {
|
||||||
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
||||||
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedDocuments: IDataObject[] = items.flatMap((item) => {
|
const extractedDocuments: INodeExecutionData[] = items.flatMap((item) => {
|
||||||
if (
|
if (
|
||||||
item.json &&
|
item.json &&
|
||||||
typeof item.json === 'object' &&
|
typeof item.json === 'object' &&
|
||||||
'Documents' in item.json &&
|
'Documents' in item.json &&
|
||||||
Array.isArray(item.json.Documents)
|
Array.isArray(item.json.Documents)
|
||||||
) {
|
) {
|
||||||
return item.json.Documents as IDataObject[];
|
return item.json.Documents.map((doc) => ({ json: doc }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return extractedDocuments;
|
return extractedDocuments.length ? extractedDocuments : [{ json: {} }];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processResponseContainers(
|
export async function processResponseContainers(
|
||||||
this: IExecuteSingleFunctions,
|
this: IExecuteSingleFunctions,
|
||||||
items: INodeExecutionData[],
|
items: INodeExecutionData[],
|
||||||
response: IN8nHttpFullResponse,
|
response: IN8nHttpFullResponse,
|
||||||
): Promise<any> {
|
): Promise<INodeExecutionData[]> {
|
||||||
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
if (!response || typeof response !== 'object' || !Array.isArray(items)) {
|
||||||
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
throw new ApplicationError('Invalid response format from Cosmos DB.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
import { formatJSONFields, processResponseContainers, validateFields } from '../GenericFunctions';
|
import {
|
||||||
|
formatJSONFields,
|
||||||
|
handleErrorPostReceive,
|
||||||
|
processResponseContainers,
|
||||||
|
validateContainerFields,
|
||||||
|
} from '../GenericFunctions';
|
||||||
|
|
||||||
export const containerOperations: INodeProperties[] = [
|
export const containerOperations: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
|
@ -20,13 +25,16 @@ export const containerOperations: INodeProperties[] = [
|
||||||
description: 'Create a container',
|
description: 'Create a container',
|
||||||
routing: {
|
routing: {
|
||||||
send: {
|
send: {
|
||||||
preSend: [formatJSONFields, validateFields],
|
preSend: [formatJSONFields, validateContainerFields],
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
ignoreHttpStatusErrors: true,
|
ignoreHttpStatusErrors: true,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/colls',
|
url: '/colls',
|
||||||
},
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
action: 'Create container',
|
action: 'Create container',
|
||||||
},
|
},
|
||||||
|
@ -42,6 +50,7 @@ export const containerOperations: INodeProperties[] = [
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
postReceive: [
|
postReceive: [
|
||||||
|
handleErrorPostReceive,
|
||||||
{
|
{
|
||||||
type: 'set',
|
type: 'set',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -63,6 +72,9 @@ export const containerOperations: INodeProperties[] = [
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '=/colls/{{ $parameter["collId"] }}',
|
url: '=/colls/{{ $parameter["collId"] }}',
|
||||||
},
|
},
|
||||||
|
output: {
|
||||||
|
postReceive: [handleErrorPostReceive],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
action: 'Get container',
|
action: 'Get container',
|
||||||
},
|
},
|
||||||
|
@ -77,7 +89,7 @@ export const containerOperations: INodeProperties[] = [
|
||||||
url: '/colls',
|
url: '/colls',
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
postReceive: [processResponseContainers],
|
postReceive: [handleErrorPostReceive, processResponseContainers],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
action: 'Get many containers',
|
action: 'Get many containers',
|
||||||
|
@ -156,19 +168,15 @@ export const createFields: INodeProperties[] = [
|
||||||
description: 'The user specified autoscale max RU/s',
|
description: 'The user specified autoscale max RU/s',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Max RU/s (for Manual Throughput)',
|
displayName: 'Manual Throughput RU/s',
|
||||||
name: 'offerThroughput',
|
name: 'offerThroughput',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 400,
|
default: 400,
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 400,
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second',
|
'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second',
|
||||||
routing: {
|
|
||||||
send: {
|
|
||||||
type: 'query',
|
|
||||||
property: 'x-ms-offer-throughput',
|
|
||||||
value: '={{$value}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
placeholder: 'Add Option',
|
placeholder: 'Add Option',
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
formatCustomProperties,
|
formatCustomProperties,
|
||||||
handleErrorPostReceive,
|
handleErrorPostReceive,
|
||||||
handlePagination,
|
handlePagination,
|
||||||
|
presendLimitField,
|
||||||
processResponseItems,
|
processResponseItems,
|
||||||
validateOperations,
|
validateOperations,
|
||||||
validatePartitionKey,
|
validatePartitionKey,
|
||||||
|
@ -97,6 +98,7 @@ export const itemOperations: INodeProperties[] = [
|
||||||
routing: {
|
routing: {
|
||||||
send: {
|
send: {
|
||||||
paginate: true,
|
paginate: true,
|
||||||
|
preSend: [presendLimitField],
|
||||||
},
|
},
|
||||||
operations: {
|
operations: {
|
||||||
pagination: handlePagination,
|
pagination: handlePagination,
|
||||||
|
@ -128,7 +130,7 @@ export const itemOperations: INodeProperties[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
postReceive: [handleErrorPostReceive],
|
postReceive: [processResponseItems, handleErrorPostReceive],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
action: 'Query items',
|
action: 'Query items',
|
||||||
|
@ -539,13 +541,6 @@ export const getAllFields: INodeProperties[] = [
|
||||||
returnAll: [false],
|
returnAll: [false],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routing: {
|
|
||||||
send: {
|
|
||||||
property: 'x-ms-max-item-count',
|
|
||||||
type: 'query',
|
|
||||||
value: '={{ $value }}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: 'number',
|
type: 'number',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
minValue: 1,
|
minValue: 1,
|
||||||
|
@ -611,7 +606,7 @@ export const queryFields: INodeProperties[] = [
|
||||||
operation: ['query'],
|
operation: ['query'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
placeholder: 'SELECT * FROM c WHERE c.name = @name',
|
placeholder: 'SELECT * FROM c WHERE c.name = @Name',
|
||||||
routing: {
|
routing: {
|
||||||
send: {
|
send: {
|
||||||
type: 'body',
|
type: 'body',
|
||||||
|
|
|
@ -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: [] });
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,6 @@
|
||||||
|
/* 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';
|
import { microsoftCosmosDbRequest } from '../GenericFunctions';
|
||||||
|
|
||||||
describe('GenericFunctions - microsoftCosmosDbRequest', () => {
|
describe('GenericFunctions - microsoftCosmosDbRequest', () => {
|
||||||
|
|
|
@ -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' });
|
||||||
|
});
|
||||||
|
});
|
|
@ -15,6 +15,7 @@ describe('GenericFunctions - searchItems', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
mockContext.getNode = jest.fn().mockReturnValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch documents and return formatted results', async () => {
|
it('should fetch documents and return formatted results', async () => {
|
||||||
|
@ -45,7 +46,7 @@ describe('GenericFunctions - searchItems', () => {
|
||||||
|
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
results: [
|
results: [
|
||||||
{ name: 'Item1', value: 'Item 1' }, // Space removed from 'Item 1'
|
{ name: 'Item1', value: 'Item 1' },
|
||||||
{ name: 'Item2', value: 'Item 2' },
|
{ name: 'Item2', value: 'Item 2' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -119,6 +120,11 @@ describe('GenericFunctions - searchItems', () => {
|
||||||
it('should throw an error when container ID is missing', async () => {
|
it('should throw an error when container ID is missing', async () => {
|
||||||
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' });
|
(mockContext.getNodeParameter as jest.Mock).mockReturnValueOnce({ mode: 'list', value: '' });
|
||||||
|
|
||||||
await expect(searchItems.call(mockContext)).rejects.toThrow('Container is required');
|
await expect(searchItems.call(mockContext)).rejects.toThrowError(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Container is required',
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue