Worked on requests

This commit is contained in:
Adina Totorean 2025-02-04 16:04:27 +02:00
parent e488e6c0bb
commit 704f9aa81a
5 changed files with 431 additions and 80 deletions

View file

@ -71,7 +71,6 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
...requestOptions.headers,
'x-ms-date': date,
'x-ms-version': '2018-12-31',
'x-ms-partitionkey': '[]',
};
if (credentials.sessionToken) {
@ -80,6 +79,7 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
// This shouldn't be the full url
// Refer to https://stackoverflow.com/questions/45645389/documentdb-rest-api-authorization-token-error
// const url = new URL (requestOptions.uri);
const url = new URL(requestOptions.baseURL + requestOptions.url);
const pathSegments = url.pathname.split('/').filter((segment) => segment);

View file

@ -3,7 +3,7 @@ import { NodeConnectionType } from 'n8n-workflow';
import { containerFields, containerOperations } from './descriptions/ContainerDescription';
import { itemFields, itemOperations } from './descriptions/ItemDescription';
import { presendStringifyBody, searchCollections } from './GenericFunctions';
import { searchCollections, searchItems } from './GenericFunctions';
export class CosmosDb implements INodeType {
description: INodeTypeDescription = {
@ -59,9 +59,9 @@ export class CosmosDb implements INodeType {
type: 'options',
noDataExpression: true,
routing: {
send: {
preSend: [presendStringifyBody],
},
// send: {
// preSend: [presendStringifyBody],
// },
},
options: [
{
@ -85,6 +85,7 @@ export class CosmosDb implements INodeType {
methods = {
listSearch: {
searchCollections,
searchItems,
},
};
}

View file

@ -231,3 +231,235 @@ export async function searchCollections(
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 ApplicationError('Collection ID is required.');
}
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) => {
return {
name: String(item.id),
value: String(item.id),
};
})
.filter((item) => !filter || item.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name));
return {
results,
};
}
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 ApplicationError(
'The "parameters" field cannot be empty. Please add at least one parameter.',
);
}
const parameters = params.parameters;
for (const parameter of parameters) {
if (!parameter.name || parameter.name.trim() === '') {
throw new ApplicationError('Each parameter must have a non-empty "name".');
}
if (!parameter.value) {
throw new ApplicationError(`The parameter "${parameter.name}" must have a valid "value".`);
}
}
return requestOptions;
}
export async function validateOperations(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const rawOperations = this.getNodeParameter('operations', []) as IDataObject;
console.log('Operations', rawOperations);
if (!rawOperations || !Array.isArray(rawOperations.operations)) {
throw new ApplicationError('The "operations" field must contain at least one operation.');
}
const operations = rawOperations.operations as Array<{
op: string;
path: string;
value?: string;
}>;
for (const operation of operations) {
if (!['add', 'increment', 'move', 'remove', 'replace', 'set'].includes(operation.op)) {
throw new ApplicationError(
`Invalid operation type "${operation.op}". Allowed values are "add", "increment", "move", "remove", "replace", and "set".`,
);
}
if (!operation.path || operation.path.trim() === '') {
throw new ApplicationError('Each operation must have a valid "path".');
}
if (
['set', 'replace', 'add', 'increment'].includes(operation.op) &&
(operation.value === undefined || operation.value === null)
) {
throw new ApplicationError(`The operation "${operation.op}" must include a valid "value".`);
}
}
return requestOptions;
}
export async function formatCustomProperties(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const rawCustomProperties = this.getNodeParameter('customProperties', '{}') as string;
let parsedProperties: Record<string, unknown>;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedProperties = JSON.parse(rawCustomProperties);
} catch (error) {
throw new ApplicationError(
'Invalid JSON format in "Custom Properties". Please provide a valid JSON object.',
);
}
if (
typeof parsedProperties !== 'object' ||
parsedProperties === null ||
Array.isArray(parsedProperties)
) {
throw new ApplicationError('The "Custom Properties" field must be a valid JSON object.');
}
if (
!requestOptions.body ||
typeof requestOptions.body !== 'object' ||
requestOptions.body === null
) {
requestOptions.body = {};
}
Object.assign(requestOptions.body as Record<string, unknown>, 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 {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedPartitionKey = JSON.parse(rawPartitionKey);
if (indexingPolicy) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
parsedIndexPolicy = JSON.parse(indexingPolicy);
}
} catch (error) {
throw new ApplicationError(
'Invalid JSON format in either "Partition Key" or "Indexing Policy". 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 mapOperationsToRequest(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const rawOperations = this.getNodeParameter('operations', []) as {
operations: Array<{
op: string;
path: string;
from?: string;
value?: string | number;
}>;
};
if (!rawOperations || !Array.isArray(rawOperations.operations)) {
throw new ApplicationError('Invalid operations format. Expected an array.');
}
// Map and validate operations
const formattedOperations = rawOperations.operations.map((operation) => {
const { op, path, from, value } = operation;
// Validate required fields
if (!op || !path) {
throw new ApplicationError('Each operation must include "op" and "path".');
}
// Construct operation object
const formattedOperation: Record<string, unknown> = { op, path };
// Add optional fields if they exist
if (from && op === 'move') {
formattedOperation.from = from;
}
if (value !== undefined && op !== 'remove') {
formattedOperation.value = value;
}
return formattedOperation;
});
// Assign the formatted operations to the request body
requestOptions.body = { operations: formattedOperations };
return requestOptions;
}

View file

@ -1,5 +1,7 @@
import type { INodeProperties } from 'n8n-workflow';
import { formatJSONFields } from '../GenericFunctions';
export const containerOperations: INodeProperties[] = [
{
displayName: 'Operation',
@ -17,13 +19,13 @@ export const containerOperations: INodeProperties[] = [
value: 'create',
description: 'Create a container',
routing: {
send: {
preSend: [formatJSONFields],
},
request: {
ignoreHttpStatusErrors: true,
method: 'POST',
url: '/colls',
headers: {
headers: {},
},
},
},
action: 'Create container',
@ -75,7 +77,7 @@ export const containerOperations: INodeProperties[] = [
export const createFields: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
name: 'newid',
type: 'string',
default: '',
placeholder: 'e.g. AndersenFamily',
@ -109,13 +111,6 @@ export const createFields: INodeProperties[] = [
operation: ['create'],
},
},
routing: {
send: {
type: 'body',
property: 'partitionKey',
value: '={{$value}}',
},
},
},
{
displayName: 'Additional Fields',
@ -136,13 +131,6 @@ export const createFields: INodeProperties[] = [
placeholder:
'"automatic": true, "indexingMode": "Consistent", "includedPaths": [{ "path": "/*", "indexes": [{ "dataType": "String", "precision": -1, "kind": "Range" }]}]',
description: 'This value is used to configure indexing policy',
routing: {
send: {
type: 'body',
property: 'indexingPolicy',
value: '={{$value}}',
},
},
},
{
displayName: 'Max RU/s (for Autoscale)',
@ -191,7 +179,7 @@ export const createFields: INodeProperties[] = [
export const getFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,
@ -240,7 +228,7 @@ export const getAllFields: INodeProperties[] = [];
export const deleteFields: INodeProperties[] = [
{
displayName: 'Container ID',
displayName: 'Container',
name: 'collId',
type: 'resourceLocator',
required: true,

View file

@ -1,6 +1,11 @@
import type { IExecuteSingleFunctions, IHttpRequestOptions, INodeProperties } from 'n8n-workflow';
import type { INodeProperties } from 'n8n-workflow';
import { handlePagination } from '../GenericFunctions';
import {
formatCustomProperties,
handlePagination,
validateOperations,
validateQueryParameters,
} from '../GenericFunctions';
export const itemOperations: INodeProperties[] = [
{
@ -19,12 +24,17 @@ export const itemOperations: INodeProperties[] = [
value: 'create',
description: 'Create a new item',
routing: {
send: {
preSend: [formatCustomProperties],
},
request: {
ignoreHttpStatusErrors: true,
method: 'POST',
url: '=/colls/{{ $parameter["collId"] }}/docs',
headers: {
// 'x-ms-documentdb-partitionkey': '={{$parameter["partitionKey"]}}',
// 'x-ms-partitionkey': '=["{{$parameter["newId"]}}"]',
// 'x-ms-documentdb-partitionkey': '=["{{$parameter["newId"]}}"]',
'x-ms-documentdb-is-upsert': 'True',
},
},
},
@ -48,16 +58,6 @@ export const itemOperations: INodeProperties[] = [
value: 'get',
description: 'Retrieve an item',
routing: {
send: {
preSend: [
async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
return requestOptions;
},
],
},
request: {
ignoreHttpStatusErrors: true,
method: 'GET',
@ -82,6 +82,16 @@ export const itemOperations: INodeProperties[] = [
method: 'GET',
url: '=/colls/{{ $parameter["collId"] }}/docs',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'json',
},
},
],
},
},
action: 'Get many items',
},
@ -90,6 +100,9 @@ export const itemOperations: INodeProperties[] = [
value: 'query',
description: 'Query items',
routing: {
send: {
preSend: [validateQueryParameters],
},
request: {
ignoreHttpStatusErrors: true,
method: 'POST',
@ -107,12 +120,17 @@ export const itemOperations: INodeProperties[] = [
value: 'update',
description: 'Update an existing item',
routing: {
send: {
preSend: [validateOperations],
},
request: {
ignoreHttpStatusErrors: true,
method: 'PATCH',
url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}',
headers: {
'Content-Type': 'application/json-patch+json',
'x-ms-partitionkey': '=["{{$parameter["id"]}}"]',
'x-ms-documentdb-partitionkey': '=["{{$parameter["id"]}}"]',
},
},
},
@ -170,7 +188,7 @@ export const createFields: INodeProperties[] = [
},
// {
// displayName: 'ID',
// name: 'id',
// name: 'newId',
// type: 'string',
// default: '',
// placeholder: 'e.g. AndersenFamily',
@ -260,19 +278,48 @@ export const deleteFields: INodeProperties[] = [
],
},
{
displayName: 'ID',
displayName: 'Item',
name: 'id',
type: 'string',
default: '',
placeholder: 'e.g. AndersenFamily',
description: 'Unique ID for the item',
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 Name',
name: 'itemName',
type: 'string',
hint: 'Enter the item name',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The item name must follow the allowed pattern.',
},
},
],
placeholder: 'e.g. AndersenFamily',
},
],
},
];
@ -322,19 +369,48 @@ export const getFields: INodeProperties[] = [
],
},
{
displayName: 'ID',
displayName: 'Item',
name: 'id',
type: 'string',
default: '',
placeholder: 'e.g. AndersenFamily',
description: "Item's 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 Name',
name: 'itemName',
type: 'string',
hint: 'Enter the item name',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The item name must follow the allowed pattern.',
},
},
],
placeholder: 'e.g. AndersenFamily',
},
],
},
];
@ -474,7 +550,6 @@ export const queryFields: INodeProperties[] = [
type: 'string',
default: '',
required: true,
description: 'The SQL query text to execute',
displayOptions: {
show: {
resource: ['item'],
@ -532,7 +607,8 @@ export const queryFields: INodeProperties[] = [
send: {
type: 'body',
property: 'parameters',
value: '={{$value}}',
value:
'={{$parameter["parameters"] && $parameter["parameters"].parameters ? $parameter["parameters"].parameters : []}}',
},
},
},
@ -584,56 +660,110 @@ export const updateFields: INodeProperties[] = [
],
},
{
displayName: 'ID',
displayName: 'Item',
name: 'id',
type: 'string',
default: '',
placeholder: 'e.g. AndersenFamily',
description: 'Unique ID for the document',
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 Name',
name: 'itemName',
type: 'string',
hint: 'Enter the item name',
validation: [
{
type: 'regex',
properties: {
regex: '^[\\w+=,.@-]+$',
errorMessage: 'The item name must follow the allowed pattern.',
},
},
],
placeholder: 'e.g. AndersenFamily',
},
],
},
//TO-DO-check-this
{
displayName: 'Operations',
name: 'operations',
type: 'resourceMapper',
default: {
mappingMode: 'defineBelow',
value: null,
},
type: 'fixedCollection',
placeholder: 'Add Operation',
description: 'Patch operations to apply to the document',
required: true,
default: [],
typeOptions: {
resourceMapper: {
resourceMapperMethod: 'getMappingColumns',
mode: 'update',
fieldWords: {
singular: 'operation',
plural: 'operations',
multipleValues: true,
},
addAllFields: true,
multiKeyMatch: false,
supportAutoMap: true,
matchingFieldsLabels: {
title: 'Custom Matching Operations',
description: 'Define the operations to perform, such as "set", "delete", or "add".',
hint: 'Map input data to the expected structure of the operations array.',
},
},
},
description: 'Define the operations to perform, such as setting or updating document fields',
displayOptions: {
show: {
resource: ['item'],
operation: ['update'],
},
},
//TO-DO-presend-function
options: [
{
name: 'operations',
displayName: 'Operation',
values: [
{
displayName: 'Operation',
name: 'op',
type: 'options',
options: [
{ name: 'Add', value: 'add' },
{ name: 'Increment', value: 'increment' },
{ name: 'Move', value: 'move' },
{ name: 'Remove', value: 'remove' },
{ name: 'Replace', value: 'replace' },
{ name: 'Set', value: 'set' },
],
default: 'set',
},
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '',
placeholder: '/Parents/0/FamilyName',
description: 'The path to the document field to be updated',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to set (if applicable)',
},
],
},
],
routing: {
send: {
type: 'body',
property: 'operations',
value: '={{ $parameter["operations"].operations }}',
},
},
},
];