fix(Notion Node): Extract page url (#11643)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Michael Kret 2024-11-11 15:03:12 +02:00 committed by GitHub
parent b0ba24cbbc
commit cbdd535fe0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 149 additions and 15 deletions

View file

@ -8,6 +8,7 @@ import type {
ILoadOptionsFunctions, ILoadOptionsFunctions,
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties, INodeProperties,
IPairedItemData, IPairedItemData,
IPollFunctions, IPollFunctions,
@ -23,7 +24,7 @@ import moment from 'moment-timezone';
import { validate as uuidValidate } from 'uuid'; import { validate as uuidValidate } from 'uuid';
import set from 'lodash/set'; import set from 'lodash/set';
import { filters } from './descriptions/Filters'; import { filters } from './descriptions/Filters';
import { blockUrlExtractionRegexp } from './constants'; import { blockUrlExtractionRegexp, databasePageUrlValidationRegexp } from './constants';
function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) { function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) {
if (uuidValidate(value)) return true; if (uuidValidate(value)) return true;
@ -916,6 +917,32 @@ export function extractPageId(page = '') {
return 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) { export function extractDatabaseId(database: string) {
if (database.includes('?v=')) { if (database.includes('?v=')) {
const data = database.split('?v=')[0].split('/'); const data = database.split('?v=')[0].split('/');

View file

@ -1,5 +1,9 @@
import type { IExecuteFunctions, INode, INodeParameterResourceLocator } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { databasePageUrlExtractionRegexp } from '../shared/constants'; 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', () => { describe('Test NotionV2, formatBlocks', () => {
it('should format to_do block', () => { it('should format to_do block', () => {
@ -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',
);
});
});

View file

@ -14,7 +14,7 @@ import {
extractBlockId, extractBlockId,
extractDatabaseId, extractDatabaseId,
extractDatabaseMentionRLC, extractDatabaseMentionRLC,
extractPageId, getPageId,
formatBlocks, formatBlocks,
formatTitle, formatTitle,
mapFilters, mapFilters,
@ -401,9 +401,8 @@ export class NotionV2 implements INodeType {
if (operation === 'get') { if (operation === 'get') {
for (let i = 0; i < itemsLength; i++) { for (let i = 0; i < itemsLength; i++) {
try { try {
const pageId = extractPageId( const pageId = getPageId.call(this, i);
this.getNodeParameter('pageId', i, '', { extractValue: true }) as string,
);
const simple = this.getNodeParameter('simple', i) as boolean; const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`); responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
if (simple) { if (simple) {
@ -526,9 +525,7 @@ export class NotionV2 implements INodeType {
if (operation === 'update') { if (operation === 'update') {
for (let i = 0; i < itemsLength; i++) { for (let i = 0; i < itemsLength; i++) {
try { try {
const pageId = extractPageId( const pageId = getPageId.call(this, i);
this.getNodeParameter('pageId', i, '', { extractValue: true }) as string,
);
const simple = this.getNodeParameter('simple', i) as boolean; const simple = this.getNodeParameter('simple', i) as boolean;
const properties = this.getNodeParameter( const properties = this.getNodeParameter(
'propertiesUi.propertyValues', 'propertiesUi.propertyValues',
@ -635,9 +632,7 @@ export class NotionV2 implements INodeType {
if (operation === 'archive') { if (operation === 'archive') {
for (let i = 0; i < itemsLength; i++) { for (let i = 0; i < itemsLength; i++) {
try { try {
const pageId = extractPageId( const pageId = getPageId.call(this, i);
this.getNodeParameter('pageId', i, '', { extractValue: true }) as string,
);
const simple = this.getNodeParameter('simple', i) as boolean; const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, { responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, {
archived: true, archived: true,
@ -672,9 +667,7 @@ export class NotionV2 implements INodeType {
parent: {}, parent: {},
properties: {}, properties: {},
}; };
body.parent.page_id = extractPageId( body.parent.page_id = getPageId.call(this, i);
this.getNodeParameter('pageId', i, '', { extractValue: true }) as string,
);
body.properties = formatTitle(this.getNodeParameter('title', i) as string); body.properties = formatTitle(this.getNodeParameter('title', i) as string);
const blockValues = this.getNodeParameter( const blockValues = this.getNodeParameter(
'blockUi.blockValues', 'blockUi.blockValues',