Skip to content

Commit

Permalink
fix(Notion Node): Extract page url (#11643)
Browse files Browse the repository at this point in the history
Co-authored-by: Elias Meire <[email protected]>
  • Loading branch information
michael-radency and elsmr authored Nov 11, 2024
1 parent b0ba24c commit cbdd535
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 15 deletions.
29 changes: 28 additions & 1 deletion packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ILoadOptionsFunctions,
INode,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
IPairedItemData,
IPollFunctions,
Expand All @@ -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;
Expand Down Expand Up @@ -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('/');
Expand Down
116 changes: 115 additions & 1 deletion packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -89,3 +93,113 @@ describe('Test Notion', () => {
});
});
});

describe('Test Notion, getPageId', () => {
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
const id = '3ab5bc794647496dac48feca926813fd';

beforeEach(() => {
mockExecuteFunctions = mock<IExecuteFunctions>();
});

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<INode>({ 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<INode>({ 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<INode>({ 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<INode>({ 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',
);
});
});
19 changes: 6 additions & 13 deletions packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
extractBlockId,
extractDatabaseId,
extractDatabaseMentionRLC,
extractPageId,
getPageId,
formatBlocks,
formatTitle,
mapFilters,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit cbdd535

Please sign in to comment.