Fixed get and delete requests for both resources

This commit is contained in:
Adina Totorean 2025-02-05 15:58:27 +02:00
parent 1a51a0fdb3
commit 91e5f4cabf
4 changed files with 172 additions and 130 deletions

View file

@ -91,11 +91,21 @@ export class MicrosoftCosmosDbSharedKeyApi implements ICredentialType {
if (pathSegments.includes('docs')) { if (pathSegments.includes('docs')) {
const docsIndex = pathSegments.lastIndexOf('docs'); const docsIndex = pathSegments.lastIndexOf('docs');
resourceType = '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')) { } else if (pathSegments.includes('colls')) {
const collsIndex = pathSegments.lastIndexOf('colls'); const collsIndex = pathSegments.lastIndexOf('colls');
resourceType = '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')) { } else if (pathSegments.includes('dbs')) {
const dbsIndex = pathSegments.lastIndexOf('dbs'); const dbsIndex = pathSegments.lastIndexOf('dbs');
resourceType = 'dbs'; resourceType = 'dbs';

View file

@ -260,8 +260,9 @@ export async function searchItems(
const results: INodeListSearchItems[] = items const results: INodeListSearchItems[] = items
.map((item) => { .map((item) => {
const idWithoutSpaces = String(item.id).replace(/ /g, '');
return { return {
name: String(item.id), name: String(idWithoutSpaces),
value: String(item.id), value: String(item.id),
}; };
}) })
@ -347,6 +348,12 @@ export async function formatCustomProperties(
const rawCustomProperties = this.getNodeParameter('customProperties', '{}') as string; const rawCustomProperties = this.getNodeParameter('customProperties', '{}') as string;
const newId = this.getNodeParameter('newId') 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<string, unknown>; let parsedProperties: Record<string, unknown>;
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@ -420,7 +427,36 @@ export async function formatJSONFields(
return requestOptions; return requestOptions;
} }
export async function processResponse( export async function validateFields(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
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, this: IExecuteSingleFunctions,
items: INodeExecutionData[], items: INodeExecutionData[],
response: IN8nHttpFullResponse, response: IN8nHttpFullResponse,
@ -445,48 +481,20 @@ export async function processResponse(
return extractedDocuments; return extractedDocuments;
} }
//WIP export async function processResponseContainers(
export async function mapOperationsToRequest(
this: IExecuteSingleFunctions, this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions, items: INodeExecutionData[],
): Promise<IHttpRequestOptions> { response: IN8nHttpFullResponse,
const rawOperations = this.getNodeParameter('operations', []) as { ): Promise<any> {
operations: Array<{ if (!response || typeof response !== 'object' || !Array.isArray(items)) {
op: string; throw new ApplicationError('Invalid response format from Cosmos DB.');
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 data = response.body as { DocumentCollections: IDataObject[] };
const formattedOperations = rawOperations.operations.map((operation) => {
const { op, path, from, value } = operation;
// Validate required fields if (data.DocumentCollections.length > 0) {
if (!op || !path) { return data.DocumentCollections.map((doc) => ({ json: doc }));
throw new ApplicationError('Each operation must include "op" and "path".'); }
}
// Construct operation object return [];
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;
});
requestOptions.body = { operations: formattedOperations };
return requestOptions;
} }

View file

@ -1,6 +1,6 @@
import type { INodeProperties } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow';
import { formatJSONFields } from '../GenericFunctions'; import { formatJSONFields, processResponseContainers, validateFields } from '../GenericFunctions';
export const containerOperations: INodeProperties[] = [ export const containerOperations: INodeProperties[] = [
{ {
@ -20,7 +20,7 @@ export const containerOperations: INodeProperties[] = [
description: 'Create a container', description: 'Create a container',
routing: { routing: {
send: { send: {
preSend: [formatJSONFields], preSend: [formatJSONFields, validateFields],
}, },
request: { request: {
ignoreHttpStatusErrors: true, ignoreHttpStatusErrors: true,
@ -40,6 +40,16 @@ export const containerOperations: INodeProperties[] = [
method: 'DELETE', method: 'DELETE',
url: '=/colls/{{ $parameter["collId"] }}', url: '=/colls/{{ $parameter["collId"] }}',
}, },
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}',
},
},
],
},
}, },
action: 'Delete container', action: 'Delete container',
}, },
@ -66,6 +76,9 @@ export const containerOperations: INodeProperties[] = [
method: 'GET', method: 'GET',
url: '/colls', url: '/colls',
}, },
output: {
postReceive: [processResponseContainers],
},
}, },
action: 'Get many containers', action: 'Get many containers',
}, },
@ -136,20 +149,11 @@ export const createFields: INodeProperties[] = [
displayName: 'Max RU/s (for Autoscale)', displayName: 'Max RU/s (for Autoscale)',
name: 'maxThroughput', name: 'maxThroughput',
type: 'number', type: 'number',
typeOptions: {
minValue: 1000,
},
default: 1000, default: 1000,
description: 'The user specified autoscale max RU/s', 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)', displayName: 'Max RU/s (for Manual Throughput)',
@ -158,11 +162,6 @@ export const createFields: INodeProperties[] = [
default: 400, default: 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',
displayOptions: {
show: {
maxThroughput: [undefined],
},
},
routing: { routing: {
send: { send: {
type: 'query', type: 'query',

View file

@ -3,7 +3,7 @@ import type { INodeProperties } from 'n8n-workflow';
import { import {
formatCustomProperties, formatCustomProperties,
handlePagination, handlePagination,
processResponse, processResponseItems,
validateOperations, validateOperations,
validateQueryParameters, validateQueryParameters,
} from '../GenericFunctions'; } from '../GenericFunctions';
@ -32,6 +32,7 @@ export const itemOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true, ignoreHttpStatusErrors: true,
method: 'POST', method: 'POST',
url: '=/colls/{{ $parameter["collId"] }}/docs', url: '=/colls/{{ $parameter["collId"] }}/docs',
//To-Do-do it based on the partition key of collection and only one
headers: { headers: {
'x-ms-documentdb-partitionkey': '=["{{$parameter["newId"]}}"]', 'x-ms-documentdb-partitionkey': '=["{{$parameter["newId"]}}"]',
'x-ms-documentdb-is-upsert': 'True', 'x-ms-documentdb-is-upsert': 'True',
@ -49,6 +50,20 @@ export const itemOperations: INodeProperties[] = [
ignoreHttpStatusErrors: true, ignoreHttpStatusErrors: true,
method: 'DELETE', method: 'DELETE',
url: '=/colls/{{ $parameter["collId"] }}/docs/{{ $parameter["id"] }}', 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', action: 'Delete item',
@ -61,7 +76,12 @@ export const itemOperations: INodeProperties[] = [
request: { request: {
ignoreHttpStatusErrors: true, ignoreHttpStatusErrors: true,
method: 'GET', 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', action: 'Get item',
@ -83,7 +103,7 @@ export const itemOperations: INodeProperties[] = [
url: '=/colls/{{ $parameter["collId"] }}/docs', url: '=/colls/{{ $parameter["collId"] }}/docs',
}, },
output: { output: {
postReceive: [processResponse], postReceive: [processResponseItems],
}, },
}, },
action: 'Get many items', action: 'Get many items',
@ -136,7 +156,7 @@ export const itemOperations: INodeProperties[] = [
export const createFields: INodeProperties[] = [ export const createFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -144,7 +164,7 @@ export const createFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -162,20 +182,20 @@ export const createFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container ID',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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'], operation: ['create'],
}, },
}, },
//To-Do-add preSend function
routing: {
send: {
type: 'body',
value: '={{$value}}',
},
},
}, },
]; ];
export const deleteFields: INodeProperties[] = [ export const deleteFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -235,7 +248,7 @@ export const deleteFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -253,20 +266,20 @@ export const deleteFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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', displayName: 'By ID',
name: 'itemName', name: 'itemId',
type: 'string', type: 'string',
hint: 'Enter the item name', hint: 'Enter the item id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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[] = [ export const getFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -326,7 +339,7 @@ export const getFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -344,20 +357,20 @@ export const getFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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', displayName: 'By ID',
name: 'itemName', name: 'itemId',
type: 'string', type: 'string',
hint: 'Enter the item name', hint: 'Enter the item id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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[] = [ export const getAllFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -417,7 +430,7 @@ export const getAllFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -435,20 +448,20 @@ export const getAllFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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[] = [ export const queryFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -502,7 +515,7 @@ export const queryFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -520,20 +533,20 @@ export const queryFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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[] = [ export const updateFields: INodeProperties[] = [
{ {
displayName: 'Collection ID', displayName: 'Container ID',
name: 'collId', name: 'collId',
type: 'resourceLocator', type: 'resourceLocator',
required: true, required: true,
@ -617,7 +630,7 @@ export const updateFields: INodeProperties[] = [
mode: 'list', mode: 'list',
value: '', value: '',
}, },
description: 'Select the collection you want to use', description: 'Select the container you want to use',
displayOptions: { displayOptions: {
show: { show: {
resource: ['item'], resource: ['item'],
@ -635,20 +648,20 @@ export const updateFields: INodeProperties[] = [
}, },
}, },
{ {
displayName: 'By Name', displayName: 'By ID',
name: 'collectionName', name: 'containerId',
type: 'string', type: 'string',
hint: 'Enter the collection name', hint: 'Enter the container id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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', displayName: 'By ID',
name: 'itemName', name: 'itemId',
type: 'string', type: 'string',
hint: 'Enter the item name', hint: 'Enter the item id',
validation: [ validation: [
{ {
type: 'regex', type: 'regex',
properties: { properties: {
regex: '^[\\w+=,.@-]+$', 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: 'Increment', value: 'increment' },
{ name: 'Move', value: 'move' }, { name: 'Move', value: 'move' },
{ name: 'Remove', value: 'remove' }, { name: 'Remove', value: 'remove' },
{ name: 'Replace', value: 'replace' },
{ name: 'Set', value: 'set' }, { name: 'Set', value: 'set' },
], ],
default: 'set', default: 'set',
}, },
{
displayName: 'From',
name: 'from',
type: 'string',
default: '',
displayOptions: {
show: {
op: ['move'],
},
},
},
{ {
displayName: 'Path', displayName: 'Path',
name: 'path', name: 'path',
type: 'string', type: 'string',
default: '', default: '',
placeholder: '/Parents/0/FamilyName',
description: 'The path to the document field to be updated',
}, },
{ {
displayName: 'Value', displayName: 'Value',
name: 'value', name: 'value',
type: 'string', type: 'string',
default: '', default: '',
description: 'The value to set (if applicable)', displayOptions: {
show: {
op: ['add', 'set', 'increment'],
},
},
}, },
], ],
}, },