From 91e5f4cabf600fde0d478d0baf28786b0d226b82 Mon Sep 17 00:00:00 2001 From: Adina Totorean Date: Wed, 5 Feb 2025 15:58:27 +0200 Subject: [PATCH] Fixed get and delete requests for both resources --- ...crosoftCosmosDbSharedKeyApi.credentials.ts | 14 +- .../Microsoft/CosmosDB/GenericFunctions.ts | 90 +++++----- .../descriptions/ContainerDescription.ts | 37 ++-- .../CosmosDB/descriptions/ItemDescription.ts | 161 ++++++++++-------- 4 files changed, 172 insertions(+), 130 deletions(-) diff --git a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts index 987fd13a9f..37fea92edf 100644 --- a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts @@ -91,11 +91,21 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType { if (pathSegments.includes('docs')) { const docsIndex = pathSegments.lastIndexOf('docs'); resourceType = 'docs'; - resourceId = pathSegments.slice(0, docsIndex).join('/'); + 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'; - resourceId = pathSegments.slice(0, collsIndex).join('/'); + 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'; diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts index d19f58843c..d63f68c7ab 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts @@ -260,8 +260,9 @@ export async function searchItems( const results: INodeListSearchItems[] = items .map((item) => { + const idWithoutSpaces = String(item.id).replace(/ /g, ''); return { - name: String(item.id), + name: String(idWithoutSpaces), value: String(item.id), }; }) @@ -347,6 +348,12 @@ export async function formatCustomProperties( const rawCustomProperties = this.getNodeParameter('customProperties', '{}') as string; const newId = this.getNodeParameter('newId') as string; + if (/\s/.test(newId)) { + throw new ApplicationError( + 'Invalid ID: IDs cannot contain spaces. Use an underscore (_) or another separator instead.', + ); + } + let parsedProperties: Record; try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -420,7 +427,36 @@ export async function formatJSONFields( return requestOptions; } -export async function processResponse( +export async function validateFields( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + const additionalFields = this.getNodeParameter('additionalFields', {}) as IDataObject; + const indexingPolicy = additionalFields.indexingPolicy; + const manualThroughput = additionalFields.offerThroughput; + const autoscaleThroughput = additionalFields.maxThroughput; + + if (manualThroughput && autoscaleThroughput) { + throw new ApplicationError( + 'You cannot set both "Max RU/s (Autoscale)" and "Max RU/s (Manual Throughput)". Please choose only one.', + ); + } + if (autoscaleThroughput && requestOptions?.qs) { + requestOptions.qs['x-ms-cosmos-offer-autopilot-settings'] = { + maxThroughput: autoscaleThroughput, + }; + } + + if (!indexingPolicy || Object.keys(indexingPolicy).length === 0) { + throw new ApplicationError( + 'Invalid Indexing Policy: Please provide a valid indexingPolicy JSON.', + ); + } + + return requestOptions; +} + +export async function processResponseItems( this: IExecuteSingleFunctions, items: INodeExecutionData[], response: IN8nHttpFullResponse, @@ -445,48 +481,20 @@ export async function processResponse( return extractedDocuments; } -//WIP -export async function mapOperationsToRequest( +export async function processResponseContainers( this: IExecuteSingleFunctions, - requestOptions: IHttpRequestOptions, -): Promise { - 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.'); + items: INodeExecutionData[], + response: IN8nHttpFullResponse, +): Promise { + if (!response || typeof response !== 'object' || !Array.isArray(items)) { + throw new ApplicationError('Invalid response format from Cosmos DB.'); } - // Map and validate operations - const formattedOperations = rawOperations.operations.map((operation) => { - const { op, path, from, value } = operation; + const data = response.body as { DocumentCollections: IDataObject[] }; - // Validate required fields - if (!op || !path) { - throw new ApplicationError('Each operation must include "op" and "path".'); - } + if (data.DocumentCollections.length > 0) { + return data.DocumentCollections.map((doc) => ({ json: doc })); + } - // Construct operation object - const formattedOperation: Record = { 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; - }); - - requestOptions.body = { operations: formattedOperations }; - - return requestOptions; + return []; } diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts index d32a8b12e3..330ffb7de4 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ContainerDescription.ts @@ -1,6 +1,6 @@ import type { INodeProperties } from 'n8n-workflow'; -import { formatJSONFields } from '../GenericFunctions'; +import { formatJSONFields, processResponseContainers, validateFields } from '../GenericFunctions'; export const containerOperations: INodeProperties[] = [ { @@ -20,7 +20,7 @@ export const containerOperations: INodeProperties[] = [ description: 'Create a container', routing: { send: { - preSend: [formatJSONFields], + preSend: [formatJSONFields, validateFields], }, request: { ignoreHttpStatusErrors: true, @@ -40,6 +40,16 @@ export const containerOperations: INodeProperties[] = [ method: 'DELETE', url: '=/colls/{{ $parameter["collId"] }}', }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "success": true } }}', + }, + }, + ], + }, }, action: 'Delete container', }, @@ -66,6 +76,9 @@ export const containerOperations: INodeProperties[] = [ method: 'GET', url: '/colls', }, + output: { + postReceive: [processResponseContainers], + }, }, action: 'Get many containers', }, @@ -136,20 +149,11 @@ export const createFields: INodeProperties[] = [ displayName: 'Max RU/s (for Autoscale)', name: 'maxThroughput', type: 'number', + typeOptions: { + minValue: 1000, + }, default: 1000, description: 'The user specified autoscale max RU/s', - displayOptions: { - show: { - offerThroughput: [undefined], - }, - }, - routing: { - send: { - type: 'query', - property: 'x-ms-cosmos-offer-autopilot-settings', - value: '={{"{"maxThroughput": " + $value + "}"}', - }, - }, }, { displayName: 'Max RU/s (for Manual Throughput)', @@ -158,11 +162,6 @@ export const createFields: INodeProperties[] = [ default: 400, description: 'The user specified manual throughput (RU/s) for the collection expressed in units of 100 request units per second', - displayOptions: { - show: { - maxThroughput: [undefined], - }, - }, routing: { send: { type: 'query', diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts index 868e8e0ce8..89e3639f10 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts @@ -3,7 +3,7 @@ import type { INodeProperties } from 'n8n-workflow'; import { formatCustomProperties, handlePagination, - processResponse, + processResponseItems, validateOperations, validateQueryParameters, } from '../GenericFunctions'; @@ -32,6 +32,7 @@ export const itemOperations: INodeProperties[] = [ ignoreHttpStatusErrors: true, method: 'POST', url: '=/colls/{{ $parameter["collId"] }}/docs', + //To-Do-do it based on the partition key of collection and only one headers: { 'x-ms-documentdb-partitionkey': '=["{{$parameter["newId"]}}"]', 'x-ms-documentdb-is-upsert': 'True', @@ -49,6 +50,20 @@ export const itemOperations: INodeProperties[] = [ ignoreHttpStatusErrors: true, method: 'DELETE', url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', + //To-Do-do it based on the partition key of collection and only one + headers: { + 'x-ms-documentdb-partitionkey': '=["{{$parameter["id"]}}"]', + }, + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "success": true } }}', + }, + }, + ], }, }, action: 'Delete item', @@ -61,7 +76,12 @@ export const itemOperations: INodeProperties[] = [ request: { ignoreHttpStatusErrors: true, method: 'GET', - url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', + url: '=/colls/{{ $parameter["collId"]}}/docs/{{$parameter["id"]}}', + headers: { + //To-Do-do it based on the partition key of collection and only one + 'x-ms-documentdb-partitionkey': '=["{{$parameter["id"]}}"]', + 'x-ms-documentdb-is-upsert': 'True', + }, }, }, action: 'Get item', @@ -83,7 +103,7 @@ export const itemOperations: INodeProperties[] = [ url: '=/colls/{{ $parameter["collId"] }}/docs', }, output: { - postReceive: [processResponse], + postReceive: [processResponseItems], }, }, action: 'Get many items', @@ -136,7 +156,7 @@ export const itemOperations: INodeProperties[] = [ export const createFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -144,7 +164,7 @@ export const createFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -162,20 +182,20 @@ export const createFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container ID', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container id must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -215,19 +235,12 @@ export const createFields: INodeProperties[] = [ operation: ['create'], }, }, - //To-Do-add preSend function - routing: { - send: { - type: 'body', - value: '={{$value}}', - }, - }, }, ]; export const deleteFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -235,7 +248,7 @@ export const deleteFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -253,20 +266,20 @@ export const deleteFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container name must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -297,16 +310,16 @@ export const deleteFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'itemName', + displayName: 'By ID', + name: 'itemId', type: 'string', - hint: 'Enter the item name', + hint: 'Enter the item id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The item name must follow the allowed pattern.', + errorMessage: 'The item id must follow the allowed pattern.', }, }, ], @@ -318,7 +331,7 @@ export const deleteFields: INodeProperties[] = [ export const getFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -326,7 +339,7 @@ export const getFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -344,20 +357,20 @@ export const getFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container name must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -388,16 +401,16 @@ export const getFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'itemName', + displayName: 'By ID', + name: 'itemId', type: 'string', - hint: 'Enter the item name', + hint: 'Enter the item id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The item name must follow the allowed pattern.', + errorMessage: 'The item id must follow the allowed pattern.', }, }, ], @@ -409,7 +422,7 @@ export const getFields: INodeProperties[] = [ export const getAllFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -417,7 +430,7 @@ export const getAllFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -435,20 +448,20 @@ export const getAllFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container name must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -494,7 +507,7 @@ export const getAllFields: INodeProperties[] = [ export const queryFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -502,7 +515,7 @@ export const queryFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -520,20 +533,20 @@ export const queryFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container id must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -609,7 +622,7 @@ export const queryFields: INodeProperties[] = [ export const updateFields: INodeProperties[] = [ { - displayName: 'Collection ID', + displayName: 'Container ID', name: 'collId', type: 'resourceLocator', required: true, @@ -617,7 +630,7 @@ export const updateFields: INodeProperties[] = [ mode: 'list', value: '', }, - description: 'Select the collection you want to use', + description: 'Select the container you want to use', displayOptions: { show: { resource: ['item'], @@ -635,20 +648,20 @@ export const updateFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'collectionName', + displayName: 'By ID', + name: 'containerId', type: 'string', - hint: 'Enter the collection name', + hint: 'Enter the container id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The collection name must follow the allowed pattern.', + errorMessage: 'The container name must follow the allowed pattern.', }, }, ], - placeholder: 'e.g. UsersCollection', + placeholder: 'e.g. UsersContainer', }, ], }, @@ -679,16 +692,16 @@ export const updateFields: INodeProperties[] = [ }, }, { - displayName: 'By Name', - name: 'itemName', + displayName: 'By ID', + name: 'itemId', type: 'string', - hint: 'Enter the item name', + hint: 'Enter the item id', validation: [ { type: 'regex', properties: { regex: '^[\\w+=,.@-]+$', - errorMessage: 'The item name must follow the allowed pattern.', + errorMessage: 'The item id must follow the allowed pattern.', }, }, ], @@ -727,25 +740,37 @@ export const updateFields: INodeProperties[] = [ { name: 'Increment', value: 'increment' }, { name: 'Move', value: 'move' }, { name: 'Remove', value: 'remove' }, - { name: 'Replace', value: 'replace' }, { name: 'Set', value: 'set' }, ], default: 'set', }, + { + displayName: 'From', + name: 'from', + type: 'string', + default: '', + displayOptions: { + show: { + op: ['move'], + }, + }, + }, { 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)', + displayOptions: { + show: { + op: ['add', 'set', 'increment'], + }, + }, }, ], },