diff --git a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts index 2fd594756f..68270ce05a 100644 --- a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts @@ -8,6 +8,7 @@ import type { ILoadOptionsFunctions, INode, INodeExecutionData, + INodeParameterResourceLocator, INodeProperties, IPairedItemData, IPollFunctions, @@ -23,7 +24,7 @@ import moment from 'moment-timezone'; import { validate as uuidValidate } from 'uuid'; import set from 'lodash/set'; import { filters } from './descriptions/Filters'; -import { blockUrlExtractionRegexp } from './constants'; +import { blockUrlExtractionRegexp, databasePageUrlValidationRegexp } from './constants'; function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) { if (uuidValidate(value)) return true; @@ -916,6 +917,32 @@ export function extractPageId(page = '') { return page; } +export function getPageId(this: IExecuteFunctions, i: number) { + const page = this.getNodeParameter('pageId', i, {}) as INodeParameterResourceLocator; + let pageId = ''; + + if (page.value && typeof page.value === 'string') { + if (page.mode === 'id') { + pageId = page.value; + } else if (page.value.includes('p=')) { + // e.g https://www.notion.so/xxxxx?v=xxxxx&p=xxxxx&pm=s + pageId = new URLSearchParams(page.value).get('p') || ''; + } else { + // e.g https://www.notion.so/page_name-xxxxx + pageId = page.value.match(databasePageUrlValidationRegexp)?.[1] || ''; + } + } + + if (!pageId) { + throw new NodeOperationError( + this.getNode(), + 'Could not extract page ID from URL: ' + page.value, + ); + } + + return pageId; +} + export function extractDatabaseId(database: string) { if (database.includes('?v=')) { const data = database.split('?v=')[0].split('/'); diff --git a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts index 40b01ea18b..1250a0231a 100644 --- a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts @@ -1,5 +1,9 @@ +import type { IExecuteFunctions, INode, INodeParameterResourceLocator } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; import { databasePageUrlExtractionRegexp } from '../shared/constants'; -import { extractPageId, formatBlocks } from '../shared/GenericFunctions'; +import { extractPageId, formatBlocks, getPageId } from '../shared/GenericFunctions'; +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; describe('Test NotionV2, formatBlocks', () => { it('should format to_do block', () => { @@ -89,3 +93,113 @@ describe('Test Notion', () => { }); }); }); + +describe('Test Notion, getPageId', () => { + let mockExecuteFunctions: MockProxy; + const id = '3ab5bc794647496dac48feca926813fd'; + + beforeEach(() => { + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return page ID directly when mode is id', () => { + const page = { + mode: 'id', + value: id, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('pageId', 0, {}); + }); + + it('should extract page ID from URL with p parameter', () => { + const page = { + mode: 'url', + value: `https://www.notion.so/xxxxx?v=xxxxx&p=${id}&pm=s`, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + }); + + it('should extract page ID from URL using regex', () => { + const page = { + mode: 'url', + value: `https://www.notion.so/page-name-${id}`, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + }); + + it('should throw error when page ID cannot be extracted', () => { + const page = { + mode: 'url', + value: 'https://www.notion.so/invalid-url', + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: https://www.notion.so/invalid-url', + ); + }); + + it('should throw error when page value is empty', () => { + const page = { + mode: 'url', + value: '', + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: ', + ); + }); + + it('should throw error when page value is undefined', () => { + const page = { + mode: 'url', + value: undefined, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: undefined', + ); + }); + + it('should throw error when page value is not a string', () => { + const page = { + mode: 'url', + value: 123 as any, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: 123', + ); + }); +}); diff --git a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts index 6c5205f9d0..198dd325cb 100644 --- a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts +++ b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts @@ -14,7 +14,7 @@ import { extractBlockId, extractDatabaseId, extractDatabaseMentionRLC, - extractPageId, + getPageId, formatBlocks, formatTitle, mapFilters, @@ -401,9 +401,8 @@ export class NotionV2 implements INodeType { if (operation === 'get') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); + const simple = this.getNodeParameter('simple', i) as boolean; responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`); if (simple) { @@ -526,9 +525,7 @@ export class NotionV2 implements INodeType { if (operation === 'update') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); const simple = this.getNodeParameter('simple', i) as boolean; const properties = this.getNodeParameter( 'propertiesUi.propertyValues', @@ -635,9 +632,7 @@ export class NotionV2 implements INodeType { if (operation === 'archive') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); const simple = this.getNodeParameter('simple', i) as boolean; responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, { archived: true, @@ -672,9 +667,7 @@ export class NotionV2 implements INodeType { parent: {}, properties: {}, }; - body.parent.page_id = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + body.parent.page_id = getPageId.call(this, i); body.properties = formatTitle(this.getNodeParameter('title', i) as string); const blockValues = this.getNodeParameter( 'blockUi.blockValues',