n8n/packages/nodes-base/nodes/Notion/GenericFunctions.ts
Omar Ajoue 7ce7285f7a
Load credentials from the database (#1741)
* Changes to types so that credentials can be always loaded from DB

This first commit changes all return types from the execute functions
and calls to get credentials to be async so we can use await.

This is a first step as previously credentials were loaded in memory and
always available. We will now be loading them from the DB which requires
turning the whole call chain async.

* Fix updated files

* Removed unnecessary credential loading to improve performance

* Fix typo

*  Fix issue

* Updated new nodes to load credentials async

*  Remove not needed comment

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-08-20 18:57:30 +02:00

578 lines
14 KiB
TypeScript

import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IDisplayOptions,
INodeProperties,
IPollFunctions,
NodeApiError,
} from 'n8n-workflow';
import {
camelCase,
capitalCase,
} from 'change-case';
import * as moment from 'moment-timezone';
import { validate as uuidValidate } from 'uuid';
export async function notionApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
try {
let options: OptionsWithUri = {
headers: {
'Notion-Version': '2021-05-13',
},
method,
qs,
body,
uri: uri || `https://api.notion.com/v1${resource}`,
json: true,
};
options = Object.assign({}, options, option);
const credentials = await this.getCredentials('notionApi') as IDataObject;
options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
return this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function notionApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const resource = this.getNodeParameter('resource', 0) as string;
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await notionApiRequest.call(this, method, endpoint, body, query);
const { next_cursor } = responseData;
if (resource === 'block' || resource === 'user') {
query['start_cursor'] = next_cursor;
} else {
body['start_cursor'] = next_cursor;
}
returnData.push.apply(returnData, responseData[propertyName]);
if (query.limit && query.limit <= returnData.length) {
return returnData;
}
} while (
responseData.has_more !== false
);
return returnData;
}
export function getBlockTypes() {
return [
{
name: 'Paragraph',
value: 'paragraph',
},
{
name: 'Heading 1',
value: 'heading_1',
},
{
name: 'Heading 2',
value: 'heading_2',
},
{
name: 'Heading 3',
value: 'heading_3',
},
{
name: 'Toggle',
value: 'toggle',
},
{
name: 'To-Do',
value: 'to_do',
},
// {
// name: 'Child Page',
// value: 'child_page',
// },
{
name: 'Bulleted List Item',
value: 'bulleted_list_item',
},
{
name: 'Numbered List Item',
value: 'numbered_list_item',
},
];
}
function textContent(content: string) {
return {
text: {
content,
},
};
}
export function formatTitle(content: string) {
return {
title: [
textContent(content),
],
};
}
export function formatText(content: string) {
return {
text: [
textContent(content),
],
};
}
function getLink(text: { textLink: string, isLink: boolean }) {
if (text.isLink === true && text.textLink !== '') {
return {
link: {
url: text.textLink,
},
};
}
return {};
}
function getTexts(texts: [{ textType: string, text: string, isLink: boolean, range: boolean, textLink: string, mentionType: string, dateStart: string, dateEnd: string, date: string, annotationUi: IDataObject, expression: string }]) {
const results = [];
for (const text of texts) {
if (text.textType === 'text') {
results.push({
type: 'text',
text: {
content: text.text,
...getLink(text),
},
annotations: text.annotationUi,
});
} else if (text.textType === 'mention') {
if (text.mentionType === 'date') {
results.push({
type: 'mention',
mention: {
type: text.mentionType,
[text.mentionType]: (text.range === true)
? { start: text.dateStart, end: text.dateEnd }
: { start: text.date, end: null },
},
annotations: text.annotationUi,
});
} else {
//@ts-ignore
results.push({
type: 'mention',
mention: {
type: text.mentionType,
//@ts-ignore
[text.mentionType]: { id: text[text.mentionType] as string },
},
annotations: text.annotationUi,
});
}
} else if (text.textType === 'equation') {
results.push({
type: 'equation',
equation: {
expression: text.expression,
},
annotations: text.annotationUi,
});
}
}
return results;
}
export function formatBlocks(blocks: IDataObject[]) {
const results = [];
for (const block of blocks) {
results.push({
object: 'block',
type: block.type,
[block.type as string]: {
...(block.type === 'to_do') ? { checked: block.checked } : { checked: false },
//@ts-expect-error
// tslint:disable-next-line: no-any
text: (block.richText === false) ? formatText(block.textContent).text : getTexts(block.text.text as any || []),
},
});
}
return results;
}
// tslint:disable-next-line: no-any
function getPropertyKeyValue(value: any, type: string, timezone: string) {
let result = {};
switch (type) {
case 'rich_text':
if (value.richText === false) {
result = { rich_text: [{ text: { content: value.textContent } }] };
} else {
result = { rich_text: getTexts(value.text.text) };
}
break;
case 'title':
result = { title: [{ text: { content: value.title } }] };
break;
case 'number':
result = { type: 'number', number: value.numberValue };
break;
case 'url':
result = { type: 'url', url: value.urlValue };
break;
case 'checkbox':
result = { type: 'checkbox', checkbox: value.checkboxValue };
break;
case 'relation':
result = {
// tslint:disable-next-line: no-any
type: 'relation', relation: (value.relationValue).reduce((acc: [], cur: any) => {
return acc.concat(cur.split(',').map((relation: string) => ({ id: relation })));
}, []),
};
break;
case 'multi_select':
const multiSelectValue = value.multiSelectValue;
result = {
type: 'multi_select',
// tslint:disable-next-line: no-any
multi_select: (Array.isArray(multiSelectValue) ? multiSelectValue : multiSelectValue.split(',').map((v: string) => v.trim()))
// tslint:disable-next-line: no-any
.filter((value: any) => value !== null)
.map((option: string) =>
((!uuidValidate(option)) ? { name: option } : { id: option })),
};
break;
case 'email':
result = {
type: 'email', email: value.emailValue,
};
break;
case 'people':
result = {
type: 'people', people: value.peopleValue.map((option: string) => ({ id: option })),
};
break;
case 'phone_number':
result = {
type: 'phone_number', phone_number: value.phoneValue,
};
break;
case 'select':
result = {
type: 'select', select: { id: value.selectValue },
};
break;
case 'date':
const format = getDateFormat(value.includeTime);
const timezoneValue = (value.timezone === 'default') ? timezone : value.timezone;
if (value.range === true) {
result = {
type: 'date',
date: {
start: moment.tz(value.dateStart, timezoneValue).format(format),
end: moment.tz(value.dateEnd, timezoneValue).format(format),
},
};
} else {
result = {
type: 'date',
date: {
start: moment.tz(value.date, timezoneValue).format(format),
end: null,
},
};
}
break;
default:
}
return result;
}
function getDateFormat(includeTime: boolean) {
if (includeTime === false) {
return 'yyyy-MM-DD';
}
return '';
}
function getNameAndType(key: string) {
const [name, type] = key.split('|');
return {
name,
type,
};
}
export function mapProperties(properties: IDataObject[], timezone: string) {
return properties.reduce((obj, value) => Object.assign(obj, {
[`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone),
}), {});
}
export function mapSorting(data: [{ key: string, type: string, direction: string, timestamp: boolean }]) {
return data.map((sort) => {
return {
direction: sort.direction,
[(sort.timestamp) ? 'timestamp' : 'property']: sort.key.split('|')[0],
};
});
}
export function mapFilters(filters: IDataObject[], timezone: string) {
// tslint:disable-next-line: no-any
return filters.reduce((obj, value: { [key: string]: any }) => {
let key = getNameAndType(value.key).type;
let valuePropertyName = key === 'last_edited_time'
? value[camelCase(key)]
: value[`${camelCase(key)}Value`];
if (['is_empty', 'is_not_empty'].includes(value.condition as string)) {
valuePropertyName = true;
} else if (['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(value.condition as string)) {
valuePropertyName = {};
}
if (key === 'rich_text') {
key = 'text';
} else if (key === 'phone_number') {
key = 'phone';
} else if (key === 'date' && !['is_empty', 'is_not_empty'].includes(value.condition as string)) {
valuePropertyName = (valuePropertyName !== undefined && !Object.keys(valuePropertyName).length) ? {} : moment.tz(value.date, timezone).utc().format();
}
return Object.assign(obj, {
['property']: getNameAndType(value.key).name,
[key]: { [`${value.condition}`]: valuePropertyName },
});
}, {});
}
// tslint:disable-next-line: no-any
export function simplifyProperties(properties: any) {
// tslint:disable-next-line: no-any
const results: any = {};
for (const key of Object.keys(properties)) {
const type = (properties[key] as IDataObject).type as string;
if (['text'].includes(properties[key].type)) {
const texts = properties[key].text.map((e: { plain_text: string }) => e.plain_text || {}).join('');
results[`${key}`] = texts;
} else if (['url', 'created_time', 'checkbox', 'number', 'last_edited_time', 'email', 'phone_number', 'date'].includes(properties[key].type)) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type] as any;
} else if (['title'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type]) && properties[key][type].length !== 0) {
results[`${key}`] = properties[key][type][0].plain_text;
} else {
results[`${key}`] = '';
}
} else if (['created_by', 'last_edited_by', 'select'].includes(properties[key].type)) {
results[`${key}`] = properties[key][type].name;
} else if (['people'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type].map((person: any) => person.person.email || {});
} else {
results[`${key}`] = properties[key][type];
}
} else if (['multi_select'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.name || {});
} else {
results[`${key}`] = properties[key][type].options.map((e: IDataObject) => e.name || {});
}
} else if (['relation'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.id || {});
} else {
results[`${key}`] = properties[key][type].database_id;
}
} else if (['formula'].includes(properties[key].type)) {
results[`${key}`] = properties[key][type][properties[key][type].type];
} else if (['rollup'].includes(properties[key].type)) {
//TODO figure how to resolve rollup field type
// results[`${key}`] = properties[key][type][properties[key][type].type];
}
}
return results;
}
// tslint:disable-next-line: no-any
export function simplifyObjects(objects: any) {
if (!Array.isArray(objects)) {
objects = [objects];
}
const results: IDataObject[] = [];
for (const { object, id, properties, parent, title } of objects) {
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
results.push({
id,
title: properties.title.title[0].plain_text,
});
} else if (object === 'page' && parent.type === 'database_id') {
results.push({
id,
...simplifyProperties(properties),
});
} else if (object === 'database') {
results.push({
id,
title: title[0].plain_text,
});
}
}
return results;
}
export function getFormattedChildren(children: IDataObject[]) {
const results: IDataObject[] = [];
for (const child of children) {
const type = child.type;
results.push({ [`${type}`]: child, object: 'block', type });
}
return results;
}
export function getConditions() {
const elements: INodeProperties[] = [];
const types: { [key: string]: string } = {
title: 'rich_text',
rich_text: 'rich_text',
number: 'number',
checkbox: 'checkbox',
select: 'select',
multi_select: 'multi_select',
date: 'date',
people: 'people',
files: 'files',
url: 'rich_text',
email: 'rich_text',
phone_number: 'rich_text',
relation: 'relation',
//formula: 'formula',
created_by: 'people',
created_time: 'date',
last_edited_by: 'people',
last_edited_time: 'date',
};
const typeConditions: { [key: string]: string[] } = {
rich_text: [
'equals',
'does_not_equal',
'contains',
'does_not_contain',
'starts_with',
'ends_with',
'is_empty',
'is_not_empty',
],
number: [
'equals',
'does_not_equal',
'grater_than',
'less_than',
'greater_than_or_equal_to',
'less_than_or_equal_to',
'is_empty',
'is_not_empty',
],
checkbox: [
'equals',
'does_not_equal',
],
select: [
'equals',
'does_not_equal',
'is_empty',
'is_not_empty',
],
multi_select: [
'contains',
'does_not_equal',
'is_empty',
'is_not_empty',
],
date: [
'equals',
'before',
'after',
'on_or_before',
'is_empty',
'is_not_empty',
'on_or_after',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
people: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
files: [
'is_empty',
'is_not_empty',
],
relation: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
formula: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
};
for (const type of Object.keys(types)) {
elements.push(
{
displayName: 'Condition',
name: 'condition',
type: 'options',
displayOptions: {
show: {
type: [
type,
],
},
} as IDisplayOptions,
options: (typeConditions[types[type]] as string[]).map((type: string) => ({ name: capitalCase(type), value: type })),
default: '',
description: 'The value of the property to filter by.',
} as INodeProperties,
);
}
return elements;
}