diff --git a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts index 6445158366..a4a429b67f 100644 --- a/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftCosmosDbSharedKeyApi.credentials.ts @@ -79,7 +79,6 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType { const url = new URL(requestOptions.baseURL + requestOptions.url); const pathSegments = url.pathname.split('/').filter((segment) => segment); - console.log('Filtered Path Segments:', pathSegments); let resourceType = ''; let resourceId = ''; diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts index f22a1c342a..50dec6fbb8 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/GenericFunctions.ts @@ -11,7 +11,7 @@ import type { INodeListSearchItems, INodeListSearchResult, } from 'n8n-workflow'; -import { ApplicationError } from 'n8n-workflow'; +import { ApplicationError, NodeApiError } from 'n8n-workflow'; export const HeaderConstants = { // Required @@ -123,9 +123,11 @@ export async function handleErrorPostReceive( data: INodeExecutionData[], response: IN8nHttpFullResponse, ): Promise { + console.log('Status code❌', response.statusCode); + if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) { const responseBody = response.body as IDataObject; - + console.log('Got here ❌', responseBody); let errorMessage = 'Unknown error occurred'; if (typeof responseBody.message === 'string') { @@ -386,6 +388,7 @@ export async function validateOperations( throw new ApplicationError('The "increment" operation must have a numeric value.'); } + //To-Do-check to not send properties it doesn't need return { op: operation.op, path: operation.op === 'move' ? operation.toPath?.value : operation.path?.value, @@ -640,3 +643,156 @@ export async function getDynamicFields( })), }; } + +export async function fetchPartitionKeyField( + this: ILoadOptionsFunctions, +): Promise { + const collection = this.getNodeParameter('collId', '') as { mode: string; value: string }; + + if (!collection?.value) { + throw new ApplicationError('Collection ID is required to determine the partition key.'); + } + + 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 validatePartitionKey( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + 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; + + 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; +} diff --git a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts index 140c1d9196..60c2f422df 100644 --- a/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/CosmosDB/descriptions/ItemDescription.ts @@ -6,6 +6,7 @@ import { handlePagination, processResponseItems, validateOperations, + validatePartitionKey, validateQueryParameters, } from '../GenericFunctions'; @@ -27,18 +28,18 @@ export const itemOperations: INodeProperties[] = [ description: 'Create a new item', routing: { send: { - preSend: [formatCustomProperties], + preSend: [formatCustomProperties, validatePartitionKey], }, request: { - 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', }, }, + output: { + postReceive: [handleErrorPostReceive], + }, }, action: 'Create item', }, @@ -47,17 +48,16 @@ export const itemOperations: INodeProperties[] = [ value: 'delete', description: 'Delete an existing item', routing: { + send: { + preSend: [validatePartitionKey], + }, request: { - 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: [ + handleErrorPostReceive, { type: 'set', properties: { @@ -74,16 +74,19 @@ export const itemOperations: INodeProperties[] = [ value: 'get', description: 'Retrieve an item', routing: { + send: { + preSend: [validatePartitionKey], + }, request: { - ignoreHttpStatusErrors: true, method: 'GET', 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', }, }, + output: { + postReceive: [handleErrorPostReceive], + }, }, action: 'Get item', }, @@ -99,12 +102,11 @@ export const itemOperations: INodeProperties[] = [ pagination: handlePagination, }, request: { - ignoreHttpStatusErrors: true, method: 'GET', url: '=/colls/{{ $parameter["collId"] }}/docs', }, output: { - postReceive: [processResponseItems], + postReceive: [processResponseItems, handleErrorPostReceive], }, }, action: 'Get many items', @@ -118,7 +120,6 @@ export const itemOperations: INodeProperties[] = [ preSend: [validateQueryParameters], }, request: { - ignoreHttpStatusErrors: true, method: 'POST', url: '=/colls/{{ $parameter["collId"] }}/docs', headers: { @@ -126,6 +127,9 @@ export const itemOperations: INodeProperties[] = [ 'x-ms-documentdb-isquery': 'True', }, }, + output: { + postReceive: [handleErrorPostReceive], + }, }, action: 'Query items', }, @@ -135,15 +139,13 @@ export const itemOperations: INodeProperties[] = [ description: 'Update an existing item', routing: { send: { - preSend: [validateOperations], + preSend: [validateOperations, validatePartitionKey], }, request: { - ignoreHttpStatusErrors: true, method: 'PATCH', url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', headers: { 'Content-Type': 'application/json-patch+json', - 'x-ms-documentdb-partitionkey': '=["{{$parameter["id"]}}"]', }, }, output: { @@ -330,6 +332,28 @@ export const deleteFields: INodeProperties[] = [ }, ], }, + { + 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[] = [ @@ -421,6 +445,28 @@ export const getFields: INodeProperties[] = [ }, ], }, + { + 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[] = [ @@ -862,6 +908,28 @@ export const updateFields: INodeProperties[] = [ }, ], }, + { + 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[] = [