import { IExecuteFunctions, } from 'n8n-core'; import { OptionsWithUri, } from 'request'; import { IDataObject, ILoadOptionsFunctions, IPollFunctions, NodeApiError, } from 'n8n-workflow'; import { TDtableMetadataColumns, TDtableViewColumns, TEndpointResolvedExpr, TEndpointVariableName, } from './types'; import { schema, } from './Schema'; import { ICredential, ICtx, IDtableMetadataColumn, IEndpointVariables, IName, IRow, IRowObject, } from './Interfaces'; import * as _ from 'lodash'; export async function seaTableApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, ctx: ICtx, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, url: string | undefined = undefined, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = await this.getCredentials('seaTableApi'); ctx.credentials = credentials as unknown as ICredential; await getBaseAccessToken.call(this, ctx); const options: OptionsWithUri = { headers: { Authorization: `Token ${ctx?.base?.access_token}`, }, method, qs, body, uri: url || `${resolveBaseUri(ctx)}${endpointCtxExpr(ctx, endpoint)}`, json: true, }; if (Object.keys(body).length === 0) { delete options.body; } if (Object.keys(option).length !== 0) { Object.assign(options, option); } try { //@ts-ignore return await this.helpers.request!(options); } catch (error) { throw new NodeApiError(this.getNode(), error); } } export async function setableApiRequestAllItems(this: IExecuteFunctions | IPollFunctions, ctx: ICtx, propertyName: string, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise { // tslint:disable-line:no-any if (query === undefined) { query = {}; } const segment = schema.rowFetchSegmentLimit; query.start = 0; query.limit = segment; const returnData: IDataObject[] = []; let responseData; do { responseData = await seaTableApiRequest.call(this, ctx, method, endpoint, body, query) as unknown as IRow[]; //@ts-ignore returnData.push.apply(returnData, responseData[propertyName]); query.start = +query.start + segment; } while (responseData && responseData.length > segment - 1); return returnData; } export async function getTableColumns(this: ILoadOptionsFunctions | IExecuteFunctions | IPollFunctions, tableName: string, ctx: ICtx = {}): Promise { 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 async function getTableViews(this: ILoadOptionsFunctions | IExecuteFunctions, tableName: string, ctx: ICtx = {}): Promise { const { views } = await seaTableApiRequest.call(this, ctx, 'GET', `/dtable-server/api/v1/dtables/{{dtable_uuid}}/views`, {}, { table_name: tableName }); return views; } 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); } export function resolveBaseUri(ctx: ICtx) { return (ctx?.credentials?.environment === 'cloudHosted') ? 'https://cloud.seatable.io' : userBaseUri(ctx?.credentials?.domain); } export function simplify(data: { results: IRow[] }, metadata: IDataObject) { return data.results.map((row: IDataObject) => { for (const key of Object.keys(row)) { if (!key.startsWith('_')) { row[metadata[key] as string] = row[key]; delete row[key]; } } return row; }); } export function getColumns(data: { metadata: [{ key: string, name: string }] }) { return data.metadata.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.name }), {}); } export function getDownloadableColumns(data: { metadata: [{ key: string, name: string, type: string }] }) { return data.metadata.filter(row => (['image', 'file'].includes(row.type))).map(row => row.name); } 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: ReadonlyArray) => (name: string) => names.find(namePredicate(name)); export function columnNamesToArray(columnNames: string): string[] { return columnNames ? split(columnNames) .filter(nonInternalPredicate) .filter(uniquePredicate) : [] ; } export function columnNamesGlob(columnNames: string[], dtableColumns: TDtableMetadataColumns): string[] { const buffer: string[] = []; const names: string[] = dtableColumns.map(c => c.name).filter(nonInternalPredicate); columnNames.forEach(columnName => { if (columnName !== '*') { buffer.push(columnName); return; } buffer.push(...names); }); return buffer.filter(uniquePredicate); } /** * sequence rows on _seq */ export function rowsSequence(rows: IRow[]) { const l = rows.length; if (l) { const [first] = rows; if (first && first._seq !== undefined) { return; } } for (let i = 0; i < l;) { rows[i]._seq = ++i; } } export function rowDeleteInternalColumns(row: IRow): IRow { Object.keys(schema.internalNames).forEach(columnName => delete row[columnName]); return row; } export function rowsDeleteInternalColumns(rows: IRow[]) { rows = rows.map(rowDeleteInternalColumns); } function rowFormatColumn(input: unknown): boolean | number | string | string[] | null { if (null === input || undefined === input) { return null; } if (typeof input === 'boolean' || typeof input === 'number' || typeof input === 'string') { return input; } if (Array.isArray(input) && input.every(i => (typeof i === 'string'))) { return input; } return null; } export function rowFormatColumns(row: IRow, columnNames: string[]): IRow { const outRow = {} as IRow; columnNames.forEach((c) => (outRow[c] = rowFormatColumn(row[c]))); return outRow; } export function rowsFormatColumns(rows: IRow[], columnNames: string[]) { rows = rows.map((row) => rowFormatColumns(row, columnNames)); } export function rowMapKeyToName(row: IRow, columns: TDtableMetadataColumns): IRow { const mappedRow = {} as IRow; // move internal columns first Object.keys(schema.internalNames).forEach((key) => { if (row[key]) { mappedRow[key] = row[key]; delete row[key]; } }); // pick each by its key for name Object.keys(row).forEach(key => { const column = columns.find(c => c.key === key); if (column) { mappedRow[column.name] = row[key]; } }); return mappedRow; } export function rowExport(row: IRowObject, columns: TDtableMetadataColumns): IRowObject { for (const columnName of Object.keys(columns)) { if (!columns.find(namePredicate(columnName))) { delete row[columnName]; } } return row; } 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); function endpointCtxExpr(this: void, 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) => { return endpointVariables[name] || match; }) as TEndpointResolvedExpr; } const normalize = (subject: string): string => subject ? subject.normalize() : ''; 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)) ; const userBaseUri = (uri?: string) => { if (uri === undefined) { return uri; } if (uri.endsWith('/')) { return uri.slice(0, -1); } return uri; };