n8n/packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts
2024-01-29 10:08:13 +01:00

369 lines
10 KiB
TypeScript

import type { OptionsWithUri } from 'request';
import FormData from 'form-data';
import type {
IDataObject,
IExecuteFunctions,
ILoadOptionsFunctions,
IPollFunctions,
JsonObject,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type { TDtableMetadataColumns, TEndpointVariableName } from './types';
import { schema } from './Schema';
import type {
ICollaborator,
ICollaboratorsResult,
ICredential,
ICtx,
IDtableMetadataColumn,
IEndpointVariables,
IName,
IRow,
IRowObject,
IColumnDigitalSignature,
IFile,
} from './actions/Interfaces';
// for date transformations
import moment from 'moment';
// remove last backslash
const userBaseUri = (uri?: string) => {
if (uri === undefined) return uri;
if (uri.endsWith('/')) return uri.slice(0, -1);
return uri;
};
export function resolveBaseUri(ctx: ICtx) {
return ctx?.credentials?.environment === 'cloudHosted'
? 'https://cloud.seatable.io'
: userBaseUri(ctx?.credentials?.domain);
}
export async function getBaseAccessToken(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
ctx: ICtx,
) {
if (ctx?.base?.access_token !== undefined) return;
const options: OptionsWithUri = {
headers: {
Authorization: `Token ${ctx?.credentials?.token}`,
},
uri: `${resolveBaseUri(ctx)}/api/v2.1/dtable/app-access-token/`,
json: true,
};
ctx.base = await this.helpers.request(options);
}
function endpointCtxExpr(ctx: ICtx, endpoint: string): string {
const endpointVariables: IEndpointVariables = {};
endpointVariables.access_token = ctx?.base?.access_token;
endpointVariables.dtable_uuid = ctx?.base?.dtable_uuid;
return endpoint.replace(
/({{ *(access_token|dtable_uuid|server) *}})/g,
(match: string, expr: string, name: TEndpointVariableName) => {
// I need expr. Why?
return (endpointVariables[name] as string) || match;
},
);
}
export async function seaTableApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
ctx: ICtx,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject | FormData | string | Buffer = {},
qs: IDataObject = {},
url: string | undefined = undefined,
option: IDataObject = {},
): Promise<any> {
const credentials = await this.getCredentials('seaTableApi');
ctx.credentials = credentials as unknown as ICredential;
await getBaseAccessToken.call(this, ctx);
// some API endpoints require the api_token instead of base_access_token.
const token =
endpoint.indexOf('/api/v2.1/dtable/app-download-link/') === 0 ||
endpoint == '/api/v2.1/dtable/app-upload-link/' ||
endpoint.indexOf('/seafhttp/upload-api') === 0
? `${ctx?.credentials?.token}`
: `${ctx?.base?.access_token}`;
let options: OptionsWithUri = {
uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`,
headers: {
Authorization: `Token ${token}`,
},
method,
qs,
body,
json: true,
};
if (Object.keys(option).length !== 0) {
options = Object.assign({}, options, option);
}
// remove header from download request.
if (endpoint.indexOf('/seafhttp/files/') === 0) {
delete options.headers;
}
// enhance header for upload request
if (endpoint.indexOf('/seafhttp/upload-api') === 0) {
options.json = true;
options.headers = {
...options.headers,
'Content-Type': 'multipart/form-data',
};
}
// DEBUG-MODE OR API-REQUESTS
// console.log(options);
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return this.helpers.requestWithAuthentication.call(this, 'seaTableApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function getBaseCollaborators(
this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions,
): Promise<any> {
let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
this,
{},
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
);
let collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
return collaborators;
}
export async function getTableColumns(
this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions,
tableName: string,
ctx: ICtx = {},
): Promise<TDtableMetadataColumns> {
const {
metadata: { tables },
} = await seaTableApiRequest.call(
this,
ctx,
'GET',
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
);
for (const table of tables) {
if (table.name === tableName) {
return table.columns;
}
}
return [];
}
export function simplify_new(row: IRow) {
for (const key of Object.keys(row)) {
if (key.startsWith('_')) delete row[key];
}
return row;
}
/*const uniquePredicate = (current: string, index: number, all: string[]) =>
all.indexOf(current) === index;
const nonInternalPredicate = (name: string) => !Object.keys(schema.internalNames).includes(name);*/
const namePredicate = (name: string) => (named: IName) => named.name === name;
export const nameOfPredicate = (names: readonly IName[]) => (name: string) =>
names.find(namePredicate(name));
const normalize = (subject: string): string => (subject ? subject.normalize() : '');
/* will ich diesen call ? */
export const split = (subject: string): string[] =>
normalize(subject)
.split(/\s*((?:[^\\,]*?(?:\\[\s\S])*)*?)\s*(?:,|$)/)
.filter((s) => s.length)
.map((s) => s.replace(/\\([\s\S])/gm, ($0, $1) => $1));
// INTERNAL: get collaborator info from @auth.local address
function getCollaboratorInfo(
authLocal: string | null | undefined,
collaboratorList: ICollaborator[],
) {
let collaboratorDetails: ICollaborator;
collaboratorDetails = collaboratorList.find(
(singleCollaborator) => singleCollaborator['email'] === authLocal,
) || { contact_email: 'unknown', name: 'unkown', email: 'unknown' };
return collaboratorDetails;
}
// INTERNAL: split asset path.
function getAssetPath(type: string, url: string) {
const parts = url.split(`/${type}/`);
if (parts[1]) {
return '/' + type + '/' + parts[1];
}
return url;
}
// CDB: neu von mir
export function enrichColumns(
row: IRow,
metadata: IDtableMetadataColumn[],
collaboratorList: ICollaborator[],
): IRow {
Object.keys(row).forEach((key) => {
let columnDef = metadata.find((obj) => obj.name === key || obj.key === key);
//console.log(key + " is from type " + columnDef?.type);
if (columnDef?.type === 'collaborator') {
// collaborator is an array of strings.
let collaborators = (row[key] as string[]) || [];
if (collaborators.length > 0) {
let newArray = collaborators.map((email) => {
let collaboratorDetails = getCollaboratorInfo(email, collaboratorList);
let newColl = {
email: email,
contact_email: collaboratorDetails['contact_email'],
name: collaboratorDetails['name'],
};
return newColl;
});
row[key] = newArray;
}
}
if (
columnDef?.type === 'last-modifier' ||
columnDef?.type === 'creator' ||
columnDef?.key === '_creator' ||
columnDef?.key === '_last_modifier'
) {
// creator or last-modifier are always a single string.
let collaboratorDetails = getCollaboratorInfo(row[key] as string, collaboratorList);
row[key] = {
email: row[key],
contact_email: collaboratorDetails['contact_email'],
name: collaboratorDetails['name'],
};
}
if (columnDef?.type === 'image') {
let pictures = (row[key] as string[]) || [];
if (pictures.length > 0) {
let newArray = pictures.map((url) => ({
name: url.split('/').pop(),
size: 0,
type: 'image',
url: url,
path: getAssetPath('images', url),
}));
row[key] = newArray;
}
}
if (columnDef?.type === 'file') {
let files = (row[key] as IFile[]) || [];
files.forEach((file) => {
file.path = getAssetPath('files', file.url);
});
}
if (columnDef?.type === 'digital-sign') {
let digitalSignature: IColumnDigitalSignature | any = row[key];
let collaboratorDetails = getCollaboratorInfo(digitalSignature?.username, collaboratorList);
if (digitalSignature?.username) {
digitalSignature.contact_email = collaboratorDetails['contact_email'];
digitalSignature.name = collaboratorDetails['name'];
}
}
if (columnDef?.type === 'button') {
delete row[key];
}
});
return row;
}
// using create, I input a string like a5adebe279e04415a28b2c7e256e9e8d@auth.local and it should be transformed to an array.
// same with multi-select.
export function splitStringColumnsToArrays(
row: IRowObject,
columns: TDtableMetadataColumns,
): IRowObject {
columns.map((column) => {
if (column.type == 'collaborator' || column.type == 'multiple-select') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = input.split(',').map((item) => item.trim());
}
}
if (column.type == 'number') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = parseFloat(input);
}
}
if (column.type == 'rate' || column.type == 'duration') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = parseInt(input);
}
}
if (column.type == 'checkbox') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = false;
if (input === 'true' || input === 'on' || input === '1') {
row[column.name] = true;
}
}
}
if (column.type == 'date') {
if (typeof row[column.name] === 'string') {
const input = row[column.name] as string;
row[column.name] = moment(input, 'YYYY-mm-dd', true);
}
}
});
return row;
}
// sollte eher heißen: remove nonUpdateColumnTypes and only use allowed columns!
export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject {
let rowAllowed = {} as IRowObject;
columns.map((column) => {
if (row[column.name]) {
rowAllowed[column.name] = row[column.name];
}
});
return rowAllowed;
}
export const dtableSchemaIsColumn = (column: IDtableMetadataColumn): boolean =>
!!schema.columnTypes[column.type];
const dtableSchemaIsUpdateAbleColumn = (column: IDtableMetadataColumn): boolean =>
!!schema.columnTypes[column.type] && !schema.nonUpdateAbleColumnTypes[column.type];
export const dtableSchemaColumns = (columns: TDtableMetadataColumns): TDtableMetadataColumns =>
columns.filter(dtableSchemaIsColumn);
export const updateAble = (columns: TDtableMetadataColumns): TDtableMetadataColumns =>
columns.filter(dtableSchemaIsUpdateAbleColumn);