mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 09:04:07 -08:00
rework of seatable-n8n-node
This commit is contained in:
parent
742c8a8534
commit
e791055fcd
|
@ -1,24 +1,10 @@
|
|||
import type { ICredentialType, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
// Get options for timezones
|
||||
const timezones: INodePropertyOptions[] = moment.tz
|
||||
.countries()
|
||||
.reduce((tz: INodePropertyOptions[], country: string) => {
|
||||
const zonesForCountry = moment.tz
|
||||
.zonesForCountry(country)
|
||||
.map((zone) => ({ value: zone, name: zone }));
|
||||
return tz.concat(zonesForCountry);
|
||||
}, []);
|
||||
import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export class SeaTableApi implements ICredentialType {
|
||||
name = 'seaTableApi';
|
||||
|
||||
displayName = 'SeaTable API';
|
||||
|
||||
documentationUrl = 'seaTable';
|
||||
|
||||
documentationUrl =
|
||||
'https://seatable.io/docs/n8n-integration/erstellen-eines-api-tokens-fuer-n8n/?lang=auto';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Environment',
|
||||
|
@ -41,7 +27,7 @@ export class SeaTableApi implements ICredentialType {
|
|||
name: 'domain',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'https://www.mydomain.com',
|
||||
placeholder: 'https://seatable.example.com',
|
||||
displayOptions: {
|
||||
show: {
|
||||
environment: ['selfHosted'],
|
||||
|
@ -52,16 +38,20 @@ export class SeaTableApi implements ICredentialType {
|
|||
displayName: 'API Token (of a Base)',
|
||||
name: 'token',
|
||||
type: 'string',
|
||||
description:
|
||||
'The API-Token of the SeaTable base you would like to use with n8n. n8n can only connect to one base a at a time.',
|
||||
typeOptions: { password: true },
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description: "Seatable server's timezone",
|
||||
options: [...timezones],
|
||||
},
|
||||
];
|
||||
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL: '={{$credentials?.domain || "https://cloud.seatable.io" }}',
|
||||
url: '/api/v2.1/dtable/app-access-token/',
|
||||
headers: {
|
||||
Authorization: '={{"Token " + $credentials.token}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,451 +1,26 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getTableColumns,
|
||||
getTableViews,
|
||||
rowExport,
|
||||
rowFormatColumns,
|
||||
rowMapKeyToName,
|
||||
seaTableApiRequest,
|
||||
setableApiRequestAllItems,
|
||||
split,
|
||||
updateAble,
|
||||
} from './GenericFunctions';
|
||||
import { SeaTableV1 } from './v1/SeaTableV1.node';
|
||||
import { SeaTableV2 } from './v2/SeaTableV2.node';
|
||||
|
||||
import { rowFields, rowOperations } from './RowDescription';
|
||||
|
||||
import type { TColumnsUiValues, TColumnValue } from './types';
|
||||
|
||||
import type { ICtx, IRow, IRowObject } from './Interfaces';
|
||||
|
||||
export class SeaTable implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
export class SeaTable extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'SeaTable',
|
||||
name: 'seaTable',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
name: 'seatable',
|
||||
icon: 'file:seatable.svg',
|
||||
group: ['output'],
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume the SeaTable API',
|
||||
defaults: {
|
||||
name: 'SeaTable',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...rowOperations,
|
||||
...rowFields,
|
||||
],
|
||||
description: 'Read, update, write and delete data from SeaTable',
|
||||
defaultVersion: 2,
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTableNames(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getTableIds(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table._id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter((column) => column.editable)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getAllSortableColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type),
|
||||
)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getViews(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const views = await getTableViews.call(this, tableName);
|
||||
return views.map((view) => ({ name: view.name, value: view.name }));
|
||||
},
|
||||
},
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new SeaTableV1(),
|
||||
2: new SeaTableV2(baseDescription),
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const body: IDataObject = {};
|
||||
const qs: IDataObject = {};
|
||||
const ctx: ICtx = {};
|
||||
|
||||
if (resource === 'row') {
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// row:create
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const { _id: insertId } = responseData;
|
||||
if (insertId === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity after appending row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const newRowInsertData = rowMapKeyToName(responseData as IRow, tableColumns);
|
||||
|
||||
qs.table_name = tableName;
|
||||
qs.convert = true;
|
||||
const newRow = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${encodeURIComponent(
|
||||
insertId as string,
|
||||
)}/`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
if (newRow._id === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity for appended row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const row = rowFormatColumns(
|
||||
{ ...newRowInsertData, ...(newRow as IRow) },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(row),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'get') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableId = this.getNodeParameter('tableId', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${rowId}`,
|
||||
{},
|
||||
{ table_id: tableId, convert: true },
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// row:getAll
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const endpoint = '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
|
||||
qs.table_name = tableName;
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||
|
||||
Object.assign(qs, filters, options);
|
||||
|
||||
if (qs.convert_link_id === false) {
|
||||
delete qs.convert_link_id;
|
||||
}
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await setableApiRequestAllItems.call(
|
||||
this,
|
||||
ctx,
|
||||
'rows',
|
||||
'GET',
|
||||
endpoint,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await seaTableApiRequest.call(this, ctx, 'GET', endpoint, body, qs);
|
||||
responseData = responseData.rows;
|
||||
}
|
||||
|
||||
const rows = responseData.map((row: IRow) =>
|
||||
rowFormatColumns(
|
||||
{ ...row },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(rows as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'delete') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const requestBody: IDataObject = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
};
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'DELETE',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
requestBody,
|
||||
qs,
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// row:update
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
body.table_name = tableName;
|
||||
body.row_id = rowId;
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ _id: rowId, ...(responseData as IDataObject) }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
import type {
|
||||
IPollFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getColumns, rowFormatColumns, seaTableApiRequest, simplify } from './GenericFunctions';
|
||||
import { seaTableApiRequest, simplify_new, enrichColumns } from './v2/GenericFunctions';
|
||||
|
||||
import type { ICtx, IRow, IRowResponse } from './Interfaces';
|
||||
import type {
|
||||
ICtx,
|
||||
IRow,
|
||||
IRowResponse,
|
||||
IGetMetadataResult,
|
||||
IGetRowsResult,
|
||||
IDtableMetadataColumn,
|
||||
ICollaborator,
|
||||
ICollaboratorsResult,
|
||||
IColumnDigitalSignature,
|
||||
} from './v2/actions/Interfaces';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { loadOptions } from './v2/methods';
|
||||
|
||||
export class SeaTableTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SeaTable Trigger',
|
||||
name: 'seaTableTrigger',
|
||||
icon: 'file:seaTable.svg',
|
||||
icon: 'file:seatable.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when SeaTable events occur',
|
||||
|
@ -35,6 +45,29 @@ export class SeaTableTrigger implements INodeType {
|
|||
inputs: [],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'New Row',
|
||||
value: 'newRow',
|
||||
description: 'Trigger on newly created rows',
|
||||
},
|
||||
{
|
||||
name: 'New or Updated Row',
|
||||
value: 'updatedRow',
|
||||
description: 'Trigger has recently created or modified rows',
|
||||
},
|
||||
{
|
||||
name: 'New Signature',
|
||||
value: 'newAsset',
|
||||
description: 'Trigger on new signatures',
|
||||
},
|
||||
],
|
||||
default: 'newRow',
|
||||
},
|
||||
{
|
||||
displayName: 'Table Name or ID',
|
||||
name: 'tableName',
|
||||
|
@ -48,109 +81,206 @@ export class SeaTableTrigger implements INodeType {
|
|||
'The name of SeaTable table to access. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
displayName: 'View Name or ID (optional)',
|
||||
name: 'viewName',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Row Created',
|
||||
value: 'rowCreated',
|
||||
description: 'Trigger on newly created rows',
|
||||
required: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: ['newRow', 'updatedRow'],
|
||||
},
|
||||
// {
|
||||
// name: 'Row Modified',
|
||||
// value: 'rowModified',
|
||||
// description: 'Trigger has recently modified rows',
|
||||
// },
|
||||
],
|
||||
default: 'rowCreated',
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableViews',
|
||||
},
|
||||
default: '',
|
||||
description: 'The name of SeaTable view to access. Choose from the list, or specify ...',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify',
|
||||
displayName: 'Signature column',
|
||||
name: 'assetColumn',
|
||||
type: 'options',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: ['newAsset'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getSignatureColumns',
|
||||
},
|
||||
default: '',
|
||||
description: 'Select the digital-signature column that should be tracked.',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify output',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
|
||||
},
|
||||
{
|
||||
displayName: '"Fetch Test Event" returns max. three items of the last hour.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTableNames(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
methods = { loadOptions };
|
||||
|
||||
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const viewName = (event != 'newAsset' ? this.getNodeParameter('viewName') : '') as string;
|
||||
const assetColumn = (event == 'newAsset' ? this.getNodeParameter('assetColumn') : '') as string;
|
||||
const simple = this.getNodeParameter('simple') as boolean;
|
||||
|
||||
const ctx: ICtx = {};
|
||||
const credentials = await this.getCredentials('seaTableApi');
|
||||
|
||||
const timezone = (credentials.timezone as string) || 'Europe/Berlin';
|
||||
const now = moment().utc().format();
|
||||
const startDate = (webhookData.lastTimeChecked as string) || now;
|
||||
const endDate = now;
|
||||
webhookData.lastTimeChecked = endDate;
|
||||
const startDate =
|
||||
this.getMode() === 'manual'
|
||||
? moment().utc().subtract(1, 'h').format()
|
||||
: (webhookData.lastTimeChecked as string);
|
||||
const endDate = (webhookData.lastTimeChecked = moment().utc().format());
|
||||
|
||||
let rows;
|
||||
// this is working, even if the columns _mtime and _ctime have other names. Only relevant for newRow / updatedRow.
|
||||
const filterField = event === 'newRow' ? '_ctime' : '_mtime';
|
||||
|
||||
const filterField = event === 'rowCreated' ? '_ctime' : '_mtime';
|
||||
// Difference between getRows and SqlQuery:
|
||||
// ====================
|
||||
|
||||
// getRows (if view is selected)
|
||||
// getRows always gets up to 1.000 rows of the selected view.
|
||||
// getRows delivers only the rows, not the metadata
|
||||
// no possibility to filter for _ctime or _mtime with the API call.
|
||||
// Problems, not yet solved:
|
||||
// if a column is empty, the column is not returned!
|
||||
// view with more than 1.000 rows will not work!
|
||||
|
||||
// SqlQuery (if no view is selected)
|
||||
// SqlQuery returns up to 1.000. WHERE by time and ORDER BY _ctime or _mtime is possible.
|
||||
// SqlQuery returns rows and metadata
|
||||
|
||||
let requestMeta: IGetMetadataResult;
|
||||
let requestRows: IGetRowsResult;
|
||||
let metadata: IDtableMetadataColumn[] = [];
|
||||
let rows: IRow[];
|
||||
let sqlResult: IRowResponse;
|
||||
|
||||
const limit = this.getMode() === 'manual' ? 3 : 1000;
|
||||
|
||||
// New Signature
|
||||
if (event == 'newAsset') {
|
||||
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
|
||||
sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT _id, _ctime, _mtime, \`${assetColumn}\` FROM ${tableName} WHERE \`${assetColumn}\` IS NOT NULL ORDER BY _mtime DESC LIMIT ${limit}`,
|
||||
convert_keys: true,
|
||||
});
|
||||
|
||||
metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
const columnType = metadata.find((obj) => obj.name == assetColumn);
|
||||
const assetColumnType = columnType?.type || null;
|
||||
|
||||
// remove unwanted entries
|
||||
rows = sqlResult.results.filter(
|
||||
(obj) => new Date(obj['_mtime']) > new Date(startDate),
|
||||
) as IRow[];
|
||||
|
||||
// split the objects into new lines (not necessary for digital-sign)
|
||||
const newRows: any = [];
|
||||
for (const row of rows) {
|
||||
if (assetColumnType === 'digital-sign') {
|
||||
let signature = (row[assetColumn] as IColumnDigitalSignature) || [];
|
||||
if (signature.sign_time) {
|
||||
if (new Date(signature.sign_time) > new Date(startDate)) {
|
||||
newRows.push(signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// View => use getRows.
|
||||
else if (viewName) {
|
||||
requestMeta = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
requestRows = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
{},
|
||||
{
|
||||
table_name: tableName,
|
||||
view_name: viewName,
|
||||
limit: limit,
|
||||
},
|
||||
);
|
||||
|
||||
// I need only metadata of the selected table.
|
||||
metadata =
|
||||
requestMeta.metadata.tables.find((table) => table.name === tableName)?.columns ?? [];
|
||||
|
||||
// remove unwanted rows that are too old (compare startDate with _ctime or _mtime)
|
||||
if (this.getMode() === 'manual') {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName} LIMIT 1`,
|
||||
})) as IRowResponse;
|
||||
rows = requestRows.rows as IRow[];
|
||||
} else {
|
||||
rows = (await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: `SELECT * FROM ${tableName}
|
||||
WHERE ${filterField} BETWEEN "${moment(startDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"
|
||||
AND "${moment(endDate).tz(timezone).format('YYYY-MM-D HH:mm:ss')}"`,
|
||||
})) as IRowResponse;
|
||||
rows = requestRows.rows.filter(
|
||||
(obj) => new Date(obj[filterField]) > new Date(startDate),
|
||||
) as IRow[];
|
||||
}
|
||||
}
|
||||
|
||||
let response;
|
||||
// No view => use SQL-Query
|
||||
else {
|
||||
const endpoint = '/dtable-db/api/v1/query/{{dtable_uuid}}/';
|
||||
const sqlQuery = `SELECT * FROM \`${tableName}\` WHERE ${filterField} BETWEEN "${moment(
|
||||
startDate,
|
||||
).format('YYYY-MM-D HH:mm:ss')}" AND "${moment(endDate).format(
|
||||
'YYYY-MM-D HH:mm:ss',
|
||||
)}" ORDER BY ${filterField} DESC LIMIT ${limit}`;
|
||||
sqlResult = await seaTableApiRequest.call(this, ctx, 'POST', endpoint, {
|
||||
sql: sqlQuery,
|
||||
convert_keys: true,
|
||||
});
|
||||
metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
rows = sqlResult.results as IRow[];
|
||||
}
|
||||
|
||||
if (rows.metadata && rows.results) {
|
||||
const columns = getColumns(rows);
|
||||
// =========================================
|
||||
// => now I have rows and metadata.
|
||||
|
||||
// lets get the collaborators
|
||||
let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
let collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
|
||||
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
// remove columns starting with _ if simple;
|
||||
if (simple) {
|
||||
response = simplify(rows, columns);
|
||||
} else {
|
||||
response = rows.results;
|
||||
rows = rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
const allColumns = rows.metadata.map((meta) => meta.name);
|
||||
// enrich column types like {collaborator, creator, last_modifier}, {image, file}
|
||||
// remove button column from rows
|
||||
rows = rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
|
||||
response = response
|
||||
//@ts-ignore
|
||||
.map((row: IRow) => rowFormatColumns(row, allColumns))
|
||||
.map((row: IRow) => ({ json: row }));
|
||||
}
|
||||
|
||||
if (Array.isArray(response) && response.length) {
|
||||
return [response];
|
||||
// prepare for final output
|
||||
return [this.helpers.returnJsonArray(rows)];
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
451
packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts
Normal file
451
packages/nodes-base/nodes/SeaTable/v1/SeaTableV1.node.ts
Normal file
|
@ -0,0 +1,451 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
getTableColumns,
|
||||
getTableViews,
|
||||
rowExport,
|
||||
rowFormatColumns,
|
||||
rowMapKeyToName,
|
||||
seaTableApiRequest,
|
||||
setableApiRequestAllItems,
|
||||
split,
|
||||
updateAble,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import { rowFields, rowOperations } from './RowDescription';
|
||||
|
||||
import type { TColumnsUiValues, TColumnValue } from './types';
|
||||
|
||||
import type { ICtx, IRow, IRowObject } from './Interfaces';
|
||||
|
||||
export class SeaTableV1 implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SeaTable',
|
||||
name: 'seaTable',
|
||||
icon: 'file:seaTable.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume the SeaTable API',
|
||||
defaults: {
|
||||
name: 'SeaTable',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...rowOperations,
|
||||
...rowFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
async getTableNames(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
async getTableIds(this: ILoadOptionsFunctions) {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table._id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
async getTableUpdateAbleColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter((column) => column.editable)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getAllSortableColumns(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const columns = await getTableColumns.call(this, tableName);
|
||||
return columns
|
||||
.filter(
|
||||
(column) =>
|
||||
!['file', 'image', 'url', 'collaborator', 'long-text'].includes(column.type),
|
||||
)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
},
|
||||
async getViews(this: ILoadOptionsFunctions) {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const views = await getTableViews.call(this, tableName);
|
||||
return views.map((view) => ({ name: view.name, value: view.name }));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
|
||||
const body: IDataObject = {};
|
||||
const qs: IDataObject = {};
|
||||
const ctx: ICtx = {};
|
||||
|
||||
if (resource === 'row') {
|
||||
if (operation === 'create') {
|
||||
// ----------------------------------
|
||||
// row:create
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const { _id: insertId } = responseData;
|
||||
if (insertId === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity after appending row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const newRowInsertData = rowMapKeyToName(responseData as IRow, tableColumns);
|
||||
|
||||
qs.table_name = tableName;
|
||||
qs.convert = true;
|
||||
const newRow = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${encodeURIComponent(
|
||||
insertId as string,
|
||||
)}/`,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
|
||||
if (newRow._id === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'SeaTable: No identity for appended row.',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
|
||||
const row = rowFormatColumns(
|
||||
{ ...newRowInsertData, ...(newRow as IRow) },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(row),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'get') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableId = this.getNodeParameter('tableId', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'GET',
|
||||
`/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/${rowId}`,
|
||||
{},
|
||||
{ table_id: tableId, convert: true },
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'getAll') {
|
||||
// ----------------------------------
|
||||
// row:getAll
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const endpoint = '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
|
||||
qs.table_name = tableName;
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||
|
||||
Object.assign(qs, filters, options);
|
||||
|
||||
if (qs.convert_link_id === false) {
|
||||
delete qs.convert_link_id;
|
||||
}
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await setableApiRequestAllItems.call(
|
||||
this,
|
||||
ctx,
|
||||
'rows',
|
||||
'GET',
|
||||
endpoint,
|
||||
body,
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await seaTableApiRequest.call(this, ctx, 'GET', endpoint, body, qs);
|
||||
responseData = responseData.rows;
|
||||
}
|
||||
|
||||
const rows = responseData.map((row: IRow) =>
|
||||
rowFormatColumns(
|
||||
{ ...row },
|
||||
tableColumns.map(({ name }) => name).concat(['_id', '_ctime', '_mtime']),
|
||||
),
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(rows as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'delete') {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
const requestBody: IDataObject = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
};
|
||||
const response = (await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'DELETE',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
requestBody,
|
||||
qs,
|
||||
)) as IDataObject;
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// row:update
|
||||
// ----------------------------------
|
||||
|
||||
const tableName = this.getNodeParameter('tableName', 0) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
|
||||
body.table_name = tableName;
|
||||
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', 0) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
let rowInput: IRowObject = {};
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const rowId = this.getNodeParameter('rowId', i) as string;
|
||||
rowInput = {} as IRowObject;
|
||||
try {
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const incomingKeys = Object.keys(items[i].json);
|
||||
const inputDataToIgnore = split(
|
||||
this.getNodeParameter('inputsToIgnore', i, '') as string,
|
||||
);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[i].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter(
|
||||
'columnsUi.columnValues',
|
||||
i,
|
||||
[],
|
||||
) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
body.row = rowExport(rowInput, updateAble(tableColumns));
|
||||
body.table_name = tableName;
|
||||
body.row_id = rowId;
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
ctx,
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ _id: rowId, ...(responseData as IDataObject) }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`);
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
338
packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts
Normal file
338
packages/nodes-base/nodes/SeaTable/v2/GenericFunctions.ts
Normal file
|
@ -0,0 +1,338 @@
|
|||
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';
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
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);
|
61
packages/nodes-base/nodes/SeaTable/v2/Schema.ts
Normal file
61
packages/nodes-base/nodes/SeaTable/v2/Schema.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import type { TColumnType, TDateTimeFormat, TInheritColumnKey } from './types';
|
||||
|
||||
export type ColumnType = keyof typeof schema.columnTypes;
|
||||
|
||||
export const schema = {
|
||||
rowFetchSegmentLimit: 1000,
|
||||
dateTimeFormat: 'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
internalNames: {
|
||||
_id: 'text',
|
||||
_creator: 'creator',
|
||||
_ctime: 'ctime',
|
||||
_last_modifier: 'last-modifier',
|
||||
_mtime: 'mtime',
|
||||
_seq: 'auto-number',
|
||||
},
|
||||
columnTypes: {
|
||||
text: 'Text',
|
||||
'long-text': 'Long Text',
|
||||
number: 'Number',
|
||||
collaborator: 'Collaborator',
|
||||
date: 'Date',
|
||||
duration: 'Duration',
|
||||
'single-select': 'Single Select',
|
||||
'multiple-select': 'Multiple Select',
|
||||
image: 'Image',
|
||||
file: 'File',
|
||||
email: 'Email',
|
||||
url: 'URL',
|
||||
checkbox: 'Checkbox',
|
||||
rate: 'Rating',
|
||||
formula: 'Formula',
|
||||
'link-formula': 'Link-Formula',
|
||||
geolocation: 'Geolocation',
|
||||
link: 'Link',
|
||||
creator: 'Creator',
|
||||
ctime: 'Created time',
|
||||
'last-modifier': 'Last Modifier',
|
||||
mtime: 'Last modified time',
|
||||
'auto-number': 'Auto number',
|
||||
button: 'Button',
|
||||
'digital-sign': 'Digital Signature',
|
||||
},
|
||||
nonUpdateAbleColumnTypes: {
|
||||
creator: 'creator',
|
||||
ctime: 'ctime',
|
||||
'last-modifier': 'last-modifier',
|
||||
mtime: 'mtime',
|
||||
'auto-number': 'auto-number',
|
||||
button: 'button',
|
||||
formula: 'formula',
|
||||
'link-formula': 'link-formula',
|
||||
link: 'link',
|
||||
'digital-sign': 'digital-sign',
|
||||
},
|
||||
} as {
|
||||
rowFetchSegmentLimit: number;
|
||||
dateTimeFormat: TDateTimeFormat;
|
||||
internalNames: { [key in TInheritColumnKey]: ColumnType };
|
||||
columnTypes: { [key in TColumnType]: string };
|
||||
nonUpdateAbleColumnTypes: { [key in ColumnType]: ColumnType };
|
||||
};
|
27
packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts
Normal file
27
packages/nodes-base/nodes/SeaTable/v2/SeaTableV2.node.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypeBaseDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { loadOptions } from './methods';
|
||||
import { router } from './actions/router';
|
||||
|
||||
export class SeaTableV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = { loadOptions };
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return router.call(this);
|
||||
}
|
||||
}
|
195
packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts
Normal file
195
packages/nodes-base/nodes/SeaTable/v2/actions/Interfaces.ts
Normal file
|
@ -0,0 +1,195 @@
|
|||
import type { AllEntities, Entity, PropertiesOf } from 'n8n-workflow';
|
||||
|
||||
type SeaTableMap = {
|
||||
row: 'create' | 'get' | 'search' | 'update' | 'remove' | 'lock' | 'unlock';
|
||||
base: 'snapshot' | 'metadata' | 'apiCall' | 'collaborator';
|
||||
link: 'add' | 'remove';
|
||||
asset: 'upload' | 'getPublicURL';
|
||||
};
|
||||
|
||||
export type SeaTable = AllEntities<SeaTableMap>;
|
||||
|
||||
export type SeaTableRow = Entity<SeaTableMap, 'row'>;
|
||||
export type SeaTableBase = Entity<SeaTableMap, 'base'>;
|
||||
export type SeaTableLink = Entity<SeaTableMap, 'link'>;
|
||||
export type SeaTableAsset = Entity<SeaTableMap, 'asset'>;
|
||||
|
||||
export type RowProperties = PropertiesOf<SeaTableRow>;
|
||||
export type BaseProperties = PropertiesOf<SeaTableBase>;
|
||||
export type LinkProperties = PropertiesOf<SeaTableLink>;
|
||||
export type AssetProperties = PropertiesOf<SeaTableAsset>;
|
||||
|
||||
import type {
|
||||
TColumnType,
|
||||
TColumnValue,
|
||||
TDtableMetadataColumns,
|
||||
TDtableMetadataTables,
|
||||
TSeaTableServerEdition,
|
||||
TSeaTableServerVersion,
|
||||
} from '../types';
|
||||
|
||||
export interface IApi {
|
||||
server: string;
|
||||
token: string;
|
||||
appAccessToken?: IAppAccessToken;
|
||||
info?: IServerInfo;
|
||||
}
|
||||
|
||||
export interface IServerInfo {
|
||||
version: TSeaTableServerVersion;
|
||||
edition: TSeaTableServerEdition;
|
||||
}
|
||||
|
||||
export interface IAppAccessToken {
|
||||
app_name: string;
|
||||
access_token: string;
|
||||
dtable_uuid: string;
|
||||
dtable_server: string;
|
||||
dtable_socket: string;
|
||||
workspace_id: number;
|
||||
dtable_name: string;
|
||||
}
|
||||
|
||||
export interface IDtableMetadataColumn {
|
||||
key: string;
|
||||
name: string;
|
||||
type: TColumnType;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export interface TDtableViewColumn {
|
||||
_id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IDtableMetadataTable {
|
||||
_id: string;
|
||||
name: string;
|
||||
columns: TDtableMetadataColumns;
|
||||
}
|
||||
|
||||
export interface IDtableMetadata {
|
||||
tables: TDtableMetadataTables;
|
||||
version: string;
|
||||
format_version: string;
|
||||
}
|
||||
|
||||
export interface IEndpointVariables {
|
||||
[name: string]: string | number | undefined;
|
||||
}
|
||||
|
||||
export interface IRowObject {
|
||||
[name: string]: TColumnValue | object;
|
||||
}
|
||||
|
||||
export interface IRow extends IRowObject {
|
||||
_id: string;
|
||||
_ctime: string;
|
||||
_mtime: string;
|
||||
_seq?: number;
|
||||
}
|
||||
|
||||
export interface IName {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TOperation = 'cloudHosted' | 'selfHosted';
|
||||
|
||||
export interface ICredential {
|
||||
token: string;
|
||||
domain: string;
|
||||
environment: TOperation;
|
||||
}
|
||||
|
||||
interface IBase {
|
||||
dtable_uuid: string;
|
||||
access_token: string;
|
||||
workspace_id: number;
|
||||
dtable_name: string;
|
||||
}
|
||||
|
||||
export interface ICtx {
|
||||
base?: IBase;
|
||||
credentials?: ICredential;
|
||||
}
|
||||
|
||||
// response object of SQL-Query!
|
||||
export interface IRowResponse {
|
||||
metadata: [
|
||||
{
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
},
|
||||
];
|
||||
results: IRow[];
|
||||
}
|
||||
|
||||
// das ist bad
|
||||
export interface IRowResponse2 {
|
||||
rows: IRow[];
|
||||
}
|
||||
|
||||
/** neu von mir **/
|
||||
|
||||
// response object of SQL-Query!
|
||||
export interface ISqlQueryResult {
|
||||
metadata: [
|
||||
{
|
||||
key: string;
|
||||
name: string;
|
||||
},
|
||||
];
|
||||
results: IRow[];
|
||||
}
|
||||
|
||||
// response object of GetMetadata
|
||||
export interface IGetMetadataResult {
|
||||
metadata: IDtableMetadata;
|
||||
}
|
||||
|
||||
// response object of GetRows
|
||||
export interface IGetRowsResult {
|
||||
rows: IRow[];
|
||||
}
|
||||
|
||||
export interface ICollaboratorsResult {
|
||||
user_list: ICollaborator[];
|
||||
}
|
||||
|
||||
export interface ICollaborator {
|
||||
email: string;
|
||||
name: string;
|
||||
contact_email: string;
|
||||
avatar_url?: string;
|
||||
id_in_org?: string;
|
||||
}
|
||||
|
||||
export interface IColumnDigitalSignature {
|
||||
username: string;
|
||||
sign_image_url: string;
|
||||
sign_time: string;
|
||||
contact_email?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
name: string;
|
||||
size: number;
|
||||
type: 'file';
|
||||
url: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface ILinkData {
|
||||
table_id: string;
|
||||
other_table_id: string;
|
||||
link_id: string;
|
||||
}
|
||||
|
||||
export interface IUploadLink {
|
||||
upload_link: string;
|
||||
parent_path: string;
|
||||
img_relative_path: string;
|
||||
file_relative_path: string;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import type { AssetProperties } from '../../Interfaces';
|
||||
|
||||
export const assetGetPublicURLDescription: AssetProperties = [
|
||||
{
|
||||
displayName: 'Asset path',
|
||||
name: 'assetPath',
|
||||
type: 'string',
|
||||
placeholder: '/images/2023-09/logo.png',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['getPublicURL'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: '',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,21 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function getPublicURL(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const assetPath = this.getNodeParameter('assetPath', index) as string;
|
||||
|
||||
let responseData = [] as IDataObject[];
|
||||
if (assetPath) {
|
||||
responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
`/api/v2.1/dtable/app-download-link/?path=${assetPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { getPublicURL as execute } from './execute';
|
||||
import { assetGetPublicURLDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
36
packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts
Normal file
36
packages/nodes-base/nodes/SeaTable/v2/actions/asset/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as upload from './upload';
|
||||
import * as getPublicURL from './getPublicURL';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export { upload, getPublicURL };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Public URL',
|
||||
value: 'getPublicURL',
|
||||
description: 'Get the public URL from asset path',
|
||||
action: 'Get the public URL from asset path',
|
||||
},
|
||||
{
|
||||
name: 'Upload',
|
||||
value: 'upload',
|
||||
description: 'Add a file/image to an existing row',
|
||||
action: 'Upload a file/image',
|
||||
},
|
||||
],
|
||||
default: 'upload',
|
||||
},
|
||||
...upload.description,
|
||||
...getPublicURL.description,
|
||||
];
|
|
@ -0,0 +1,104 @@
|
|||
import type { AssetProperties } from '../../Interfaces';
|
||||
|
||||
export const assetUploadDescription: AssetProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Column',
|
||||
name: 'uploadColumn',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getAssetColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Select the column for the upload.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Workspace ID',
|
||||
name: 'workspaceId',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
numberStepSize: 1,
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'How to get the workspace ID: https://seatable.io/docs/arbeiten-mit-gruppen/workspace-id-einer-gruppe-ermitteln/?lang=auto',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
description: 'Name of the binary property which contains the data for the file to be written',
|
||||
},
|
||||
/*{
|
||||
displayName: 'Replace',
|
||||
name: 'replace',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['asset'],
|
||||
operation: ['upload'],
|
||||
},
|
||||
},
|
||||
description: 'Replace existing file if the file/image already exists with same name.',
|
||||
},*/
|
||||
];
|
|
@ -0,0 +1,89 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
import type { IUploadLink, IRowObject } from '../../Interfaces';
|
||||
|
||||
export async function upload(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
// step 1: upload file to base
|
||||
const uploadColumn = this.getNodeParameter('uploadColumn', index) as any;
|
||||
const uploadColumnType = uploadColumn.split(':::')[1];
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName', index) as string;
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
const workspaceId = this.getNodeParameter('workspaceId', index) as string;
|
||||
const uploadLink = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/api/v2.1/dtable/app-upload-link/',
|
||||
)) as IUploadLink;
|
||||
|
||||
// Get the binary data
|
||||
const fileBufferData = await this.helpers.getBinaryDataBuffer(index, dataPropertyName);
|
||||
const binaryData = this.helpers.assertBinaryData(index, dataPropertyName);
|
||||
// Create our request option
|
||||
const options = {
|
||||
formData: {
|
||||
file: {
|
||||
value: fileBufferData,
|
||||
options: {
|
||||
filename: binaryData.fileName,
|
||||
contentType: binaryData.mimeType,
|
||||
},
|
||||
},
|
||||
parent_dir: uploadLink.parent_path,
|
||||
replace: '0',
|
||||
relative_path:
|
||||
uploadColumnType === 'image' ? uploadLink.img_relative_path : uploadLink.file_relative_path,
|
||||
},
|
||||
};
|
||||
|
||||
// Send the request
|
||||
let uploadAsset = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
`/seafhttp/upload-api/${uploadLink.upload_link.split('seafhttp/upload-api/')[1]}?ret-json=true`,
|
||||
{},
|
||||
{},
|
||||
'',
|
||||
options,
|
||||
);
|
||||
|
||||
// now step 2 (attaching the file to a column in a base)
|
||||
for (let c = 0; c < uploadAsset.length; c++) {
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
row: {},
|
||||
} as IDataObject;
|
||||
let rowInput = {} as IRowObject;
|
||||
|
||||
const filePath = [
|
||||
`/workspace/${workspaceId}${uploadLink.parent_path}/${uploadLink.img_relative_path}/${uploadAsset[c].name}`,
|
||||
];
|
||||
|
||||
if (uploadColumnType === 'image') {
|
||||
rowInput[uploadColumn.split(':::')[0]] = filePath;
|
||||
} else if (uploadColumnType === 'file') {
|
||||
rowInput[uploadColumn.split(':::')[0]] = uploadAsset;
|
||||
uploadAsset[c].type = 'file';
|
||||
uploadAsset[c].url = filePath;
|
||||
}
|
||||
body.row = rowInput;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
uploadAsset[c]['upload_successful'] = responseData.success;
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(uploadAsset as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { upload as execute } from './execute';
|
||||
import { assetUploadDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,136 @@
|
|||
import type { BaseProperties } from '../../Interfaces';
|
||||
|
||||
export const baseApiCallDescription: BaseProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'HTTP Method',
|
||||
name: 'apiMethod',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'GET',
|
||||
value: 'GET',
|
||||
},
|
||||
{
|
||||
name: 'POST',
|
||||
value: 'POST',
|
||||
},
|
||||
{
|
||||
name: 'DELETE',
|
||||
value: 'DELETE',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Hint: The Authentication header is included automatically.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'apiEndpoint',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
placeholder: '/dtable-server/...',
|
||||
description:
|
||||
'The URL has to start with /dtable-server/ or /dtable-db/. All possible requests can be found at the SeaTable API Reference at https://api.seatable.io \
|
||||
Please be aware that only request from the section Base Operations that use an Base-Token for the authentication are allowed to use.',
|
||||
},
|
||||
{
|
||||
displayName: 'Query String Parameters',
|
||||
name: 'apiParams',
|
||||
type: 'fixedCollection',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
description:
|
||||
'These params will be URL-encoded and appended to the URL when making the request.',
|
||||
options: [
|
||||
{
|
||||
name: 'apiParamsValues',
|
||||
displayName: 'Parameters',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Body',
|
||||
name: 'apiBody',
|
||||
type: 'json',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['apiCall'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
rows: 4,
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Only valid JSON is accepted. n8n will pass anything you enter as raw input. For example, {"foo", "bar"} is perfectly valid. Of cause you can use variables from n8n inside your JSON.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,15 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function apiCall(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { apiCall as execute } from './execute';
|
||||
import { baseApiCallDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,20 @@
|
|||
import type { BaseProperties } from '../../Interfaces';
|
||||
|
||||
export const baseCollaboratorDescription: BaseProperties = [
|
||||
{
|
||||
displayName: 'Name or email of the collaborator',
|
||||
name: 'searchString',
|
||||
type: 'string',
|
||||
placeholder: 'Enter the name or the email or the collaborator',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
operation: ['collaborator'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'SeaTable identifies users with a unique username like 244b43hr6fy54bb4afa2c2cb7369d244@auth.local. Get this username from an email or the name of a collaborator.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,25 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
import { ICollaborator } from '../../Interfaces';
|
||||
|
||||
export async function collaborator(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const searchString = this.getNodeParameter('searchString', index) as string;
|
||||
|
||||
const collaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
const collaborators = collaboratorsResult.user_list || [];
|
||||
|
||||
const collaborator = collaborators.filter(
|
||||
(col: ICollaborator) =>
|
||||
col.contact_email.includes(searchString) || col.name.includes(searchString),
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(collaborator as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { collaborator as execute } from './execute';
|
||||
import { baseCollaboratorDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
53
packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts
Normal file
53
packages/nodes-base/nodes/SeaTable/v2/actions/base/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import * as snapshot from './snapshot';
|
||||
import * as metadata from './metadata';
|
||||
import * as apiCall from './apiCall';
|
||||
import * as collaborator from './collaborator';
|
||||
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export { snapshot, metadata, apiCall, collaborator };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['base'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Snapshot',
|
||||
value: 'snapshot',
|
||||
description: 'Create a snapshot of the base',
|
||||
action: 'Create a Snapshot',
|
||||
},
|
||||
{
|
||||
name: 'Metadata',
|
||||
value: 'metadata',
|
||||
description: 'Get the complete metadata of the base',
|
||||
action: 'Get metadata of a base',
|
||||
},
|
||||
{
|
||||
name: 'API Call',
|
||||
value: 'apiCall',
|
||||
description: 'Perform an authorized API call (Base Operation)',
|
||||
action: 'Make an API Call',
|
||||
},
|
||||
{
|
||||
name: 'Collaborator',
|
||||
value: 'collaborator',
|
||||
description: 'Get this username from the email or name of a collaborator.',
|
||||
action: 'Get username from email or name',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
...snapshot.description,
|
||||
...metadata.description,
|
||||
...apiCall.description,
|
||||
...collaborator.description,
|
||||
];
|
|
@ -0,0 +1,3 @@
|
|||
import type { BaseProperties } from '../../Interfaces';
|
||||
|
||||
export const baseMetadataDescription: BaseProperties = [];
|
|
@ -0,0 +1,15 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function metadata(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata/',
|
||||
);
|
||||
return this.helpers.returnJsonArray(responseData.metadata as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { metadata as execute } from './execute';
|
||||
import { baseMetadataDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,3 @@
|
|||
import type { BaseProperties } from '../../Interfaces';
|
||||
|
||||
export const baseSnapshotDescription: BaseProperties = [];
|
|
@ -0,0 +1,17 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function snapshot(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/snapshot/',
|
||||
{ dtable_name: 'snapshot' },
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { snapshot as execute } from './execute';
|
||||
import { baseSnapshotDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,69 @@
|
|||
import type { LinkProperties } from '../../Interfaces';
|
||||
|
||||
export const linkAddDescription: LinkProperties = [
|
||||
{
|
||||
displayName: 'Table Name (Source)',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNameAndId',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Link column',
|
||||
name: 'linkColumn',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getLinkColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Select the column to create a link.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID from the source table',
|
||||
name: 'linkColumnSourceId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you selected.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID from the target',
|
||||
name: 'linkColumnTargetId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['add'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you want to link.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,25 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function add(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
|
||||
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
|
||||
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/',
|
||||
{
|
||||
link_id: linkColumn.split(':::')[1],
|
||||
table_id: tableName.split(':::')[1],
|
||||
table_row_id: linkColumnSourceId,
|
||||
other_table_id: linkColumn.split(':::')[2],
|
||||
other_table_row_id: linkColumnTargetId,
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { add as execute } from './execute';
|
||||
import { linkAddDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
36
packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts
Normal file
36
packages/nodes-base/nodes/SeaTable/v2/actions/link/index.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as add from './add';
|
||||
import * as remove from './remove';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export { add, remove };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Create a link between two rows in a link column',
|
||||
action: 'Add a row link',
|
||||
},
|
||||
{
|
||||
name: 'Remove',
|
||||
value: 'remove',
|
||||
description: 'Remove a link between two rows from a link column',
|
||||
action: 'Remove a row link',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
},
|
||||
...add.description,
|
||||
...remove.description,
|
||||
];
|
|
@ -0,0 +1,69 @@
|
|||
import type { LinkProperties } from '../../Interfaces';
|
||||
|
||||
export const linkRemoveDescription: LinkProperties = [
|
||||
{
|
||||
displayName: 'Table Name (Source)',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Name of table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNameAndId',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Link column',
|
||||
name: 'linkColumn',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getLinkColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Select the column to create a link.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID from the source table',
|
||||
name: 'linkColumnSourceId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you selected.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID from the target',
|
||||
name: 'linkColumnTargetId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['link'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Provide the row ID of table you want to link.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,28 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function remove(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const linkColumn = this.getNodeParameter('linkColumn', index) as any;
|
||||
const linkColumnSourceId = this.getNodeParameter('linkColumnSourceId', index) as string;
|
||||
const linkColumnTargetId = this.getNodeParameter('linkColumnTargetId', index) as string;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'DELETE',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/links/',
|
||||
{
|
||||
link_id: linkColumn.split(':::')[1],
|
||||
table_id: tableName.split(':::')[1],
|
||||
table_row_id: linkColumnSourceId,
|
||||
other_table_id: linkColumn.split(':::')[2],
|
||||
other_table_row_id: linkColumnTargetId,
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { remove as execute } from './execute';
|
||||
import { linkRemoveDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
56
packages/nodes-base/nodes/SeaTable/v2/actions/router.ts
Normal file
56
packages/nodes-base/nodes/SeaTable/v2/actions/router.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import * as row from './row';
|
||||
import * as base from './base';
|
||||
import * as link from './link';
|
||||
import * as asset from './asset';
|
||||
|
||||
import type { SeaTable } from './Interfaces';
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const operationResult: INodeExecutionData[] = [];
|
||||
let responseData: IDataObject | IDataObject[] = [];
|
||||
|
||||
//console.log('ITEM LENGTH ' + items.length);
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const resource = this.getNodeParameter<SeaTable>('resource', i);
|
||||
const operation = this.getNodeParameter('operation', i);
|
||||
|
||||
//console.log(operation);
|
||||
//console.log(resource);
|
||||
|
||||
const seatable = {
|
||||
resource,
|
||||
operation,
|
||||
} as SeaTable;
|
||||
|
||||
try {
|
||||
if (seatable.resource === 'row') {
|
||||
responseData = await row[seatable.operation].execute.call(this, i);
|
||||
} else if (seatable.resource === 'base') {
|
||||
responseData = await base[seatable.operation].execute.call(this, i);
|
||||
} else if (seatable.resource === 'link') {
|
||||
responseData = await link[seatable.operation].execute.call(this, i);
|
||||
} else if (seatable.resource === 'asset') {
|
||||
responseData = await asset[seatable.operation].execute.call(this, i);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
operationResult.push(...executionData);
|
||||
} catch (err) {
|
||||
if (this.continueOnFail()) {
|
||||
operationResult.push({ json: this.getInputData(i)[0].json, error: err });
|
||||
} else {
|
||||
if (err.context) err.context.itemIndex = i;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [operationResult];
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowCreateDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Data to Send',
|
||||
name: 'fieldsToSend',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto-Map Input Data to Columns',
|
||||
value: 'autoMapInputData',
|
||||
description: 'Use when node input properties match destination column names',
|
||||
},
|
||||
{
|
||||
name: 'Define Below for Each Column',
|
||||
value: 'defineBelow',
|
||||
description: 'Set the value for each destination column',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
displayName: 'Inputs to Ignore',
|
||||
name: 'inputsToIgnore',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
fieldsToSend: ['autoMapInputData'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
|
||||
placeholder: 'Enter properties...',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns to Send',
|
||||
name: 'columnsUi',
|
||||
placeholder: 'Add Column',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Column to Send',
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Column',
|
||||
name: 'columnValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Column Name',
|
||||
name: 'columnName',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableUpdateAbleColumns',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Value',
|
||||
name: 'columnValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
fieldsToSend: ['defineBelow'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Add destination column with its value',
|
||||
},
|
||||
{
|
||||
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,62 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
getTableColumns,
|
||||
split,
|
||||
rowExport,
|
||||
updateAble,
|
||||
splitStringColumnsToArrays,
|
||||
} from '../../../GenericFunctions';
|
||||
import type { IRowObject } from '../../Interfaces';
|
||||
import type { TColumnValue, TColumnsUiValues } from '../../../types';
|
||||
|
||||
export async function create(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
row: {},
|
||||
} as IDataObject;
|
||||
let rowInput = {} as IRowObject;
|
||||
|
||||
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const items = this.getInputData();
|
||||
const incomingKeys = Object.keys(items[index].json);
|
||||
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[index].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// only keep key:value pairs for columns that are allowed to update.
|
||||
rowInput = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
// string to array: multi-select and collaborators
|
||||
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
|
||||
|
||||
body.row = rowInput;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { create as execute } from './execute';
|
||||
import { rowCreateDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,54 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowGetDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify output',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
description:
|
||||
'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,42 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import type { IRow, IRowResponse, IDtableMetadataColumn } from './../../Interfaces';
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
enrichColumns,
|
||||
simplify_new,
|
||||
getBaseCollaborators,
|
||||
} from '../../../GenericFunctions';
|
||||
|
||||
export async function get(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
|
||||
// get parameters
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
const simple = this.getNodeParameter('simple', index) as boolean;
|
||||
|
||||
// get collaborators
|
||||
const collaborators = await getBaseCollaborators.call(this);
|
||||
|
||||
// get rows
|
||||
let sqlResult = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
|
||||
{
|
||||
sql: `SELECT * FROM \`${tableName}\` WHERE _id = '${rowId}'`,
|
||||
convert_keys: true,
|
||||
},
|
||||
)) as IRowResponse;
|
||||
let metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
let rows = sqlResult.results as IRow[];
|
||||
|
||||
// hide columns like button
|
||||
rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
|
||||
// remove columns starting with _ if simple;
|
||||
if (simple) {
|
||||
rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(rows as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { get as execute } from './execute';
|
||||
import { rowGetDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
76
packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts
Normal file
76
packages/nodes-base/nodes/SeaTable/v2/actions/row/index.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as create from './create';
|
||||
import * as get from './get';
|
||||
import * as search from './search';
|
||||
import * as update from './update';
|
||||
import * as remove from './remove';
|
||||
import * as lock from './lock';
|
||||
import * as unlock from './unlock';
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export { create, get, search, update, remove, lock, unlock };
|
||||
|
||||
export const descriptions: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new row',
|
||||
action: 'Create a row',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get the content of a row',
|
||||
action: 'Get a row',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search one or multiple rows',
|
||||
action: 'Search a row by keyword',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update the content of a row',
|
||||
action: 'Update a row',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'remove',
|
||||
description: 'Delete a row',
|
||||
action: 'Delete a row',
|
||||
},
|
||||
{
|
||||
name: 'Lock',
|
||||
value: 'lock',
|
||||
description: 'Lock a row to prevent further changes.',
|
||||
action: 'Add a row lock',
|
||||
},
|
||||
{
|
||||
name: 'Unlock',
|
||||
value: 'unlock',
|
||||
description: 'Remove the lock from a row',
|
||||
action: 'Remove a row lock',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
...create.description,
|
||||
...get.description,
|
||||
...search.description,
|
||||
...update.description,
|
||||
...remove.description,
|
||||
...lock.description,
|
||||
...unlock.description,
|
||||
];
|
|
@ -0,0 +1,40 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowLockDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['lock'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['lock'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,20 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function lock(this: IExecuteFunctions, index: number): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/lock-rows/',
|
||||
{
|
||||
table_name: tableName,
|
||||
row_ids: [rowId],
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { lock as execute } from './execute';
|
||||
import { rowLockDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,40 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowRemoveDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['remove'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,25 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function remove(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
const requestBody: IDataObject = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
};
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'DELETE',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
requestBody,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { remove as execute } from './execute';
|
||||
import { rowRemoveDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,83 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowSearchDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Column',
|
||||
name: 'searchColumn',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getSearchableColumns',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'Select the column to be searched. Not all column types are supported for search.',
|
||||
},
|
||||
{
|
||||
displayName: 'Search term',
|
||||
name: 'searchTerm',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'What to look for?',
|
||||
},
|
||||
{
|
||||
displayName: 'Activate wildcard search',
|
||||
name: 'wildcard',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description:
|
||||
'FALSE: The search only results perfect matches. TRUE: Finds a row even if the search value is part of a string.',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify output',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['search'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Simplified returns only the columns of your base. Non-simplified will return additional columns like _ctime (=creation time), _mtime (=modification time) etc.',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,67 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
enrichColumns,
|
||||
simplify_new,
|
||||
getBaseCollaborators,
|
||||
} from '../../../GenericFunctions';
|
||||
import { IDtableMetadataColumn, IRow, IRowResponse } from '../../Interfaces';
|
||||
|
||||
export async function search(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const searchColumn = this.getNodeParameter('searchColumn', index) as string;
|
||||
const searchTerm = this.getNodeParameter('searchTerm', index) as any; // string or integer
|
||||
const wildcard = this.getNodeParameter('wildcard', index) as boolean;
|
||||
const simple = this.getNodeParameter('simple', index) as boolean;
|
||||
|
||||
// get collaborators
|
||||
const collaborators = await getBaseCollaborators.call(this);
|
||||
|
||||
//let metadata: IDtableMetadataColumn[] = [];
|
||||
//let rows: IRow[];
|
||||
//let sqlResult: IRowResponse;
|
||||
|
||||
// get the collaborators (avoid executing this multiple times !!!!)
|
||||
/*let collaboratorsResult: ICollaboratorsResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/related-users/',
|
||||
);
|
||||
let collaborators: ICollaborator[] = collaboratorsResult.user_list || [];
|
||||
*/
|
||||
|
||||
// this is the base query. The WHERE has to be finalized...
|
||||
let sqlQuery = `SELECT * FROM \`${tableName}\` WHERE \`${searchColumn}\``;
|
||||
|
||||
if (wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE "%' + searchTerm + '%"';
|
||||
else if (!wildcard && isNaN(searchTerm)) sqlQuery = sqlQuery + ' = "' + searchTerm + '"';
|
||||
else if (wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' LIKE %' + searchTerm + '%';
|
||||
else if (!wildcard && !isNaN(searchTerm)) sqlQuery = sqlQuery + ' = ' + searchTerm;
|
||||
|
||||
const sqlResult = (await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
|
||||
{
|
||||
sql: sqlQuery,
|
||||
convert_keys: true,
|
||||
},
|
||||
)) as IRowResponse;
|
||||
const metadata = sqlResult.metadata as IDtableMetadataColumn[];
|
||||
let rows = sqlResult.results as IRow[];
|
||||
|
||||
// hide columns like button
|
||||
rows.map((row) => enrichColumns(row, metadata, collaborators));
|
||||
|
||||
// remove columns starting with _;
|
||||
if (simple) {
|
||||
rows.map((row) => simplify_new(row));
|
||||
}
|
||||
|
||||
return this.helpers.returnJsonArray(rows as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { search as execute } from './execute';
|
||||
import { rowSearchDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,40 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowUnlockDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['unlock'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['unlock'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,23 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { seaTableApiRequest } from '../../../GenericFunctions';
|
||||
|
||||
export async function unlock(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/unlock-rows/',
|
||||
{
|
||||
table_name: tableName,
|
||||
row_ids: [rowId],
|
||||
},
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { unlock as execute } from './execute';
|
||||
import { rowUnlockDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,137 @@
|
|||
import type { RowProperties } from '../../Interfaces';
|
||||
|
||||
export const rowUpdateDescription: RowProperties = [
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'options',
|
||||
placeholder: 'Select a table',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTableNames',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The name of SeaTable table to access. Choose from the list, or specify a name using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Row ID',
|
||||
name: 'rowId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getRowIds',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Data to Send',
|
||||
name: 'fieldsToSend',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto-Map Input Data to Columns',
|
||||
value: 'autoMapInputData',
|
||||
description: 'Use when node input properties match destination column names',
|
||||
},
|
||||
{
|
||||
name: 'Define Below for Each Column',
|
||||
value: 'defineBelow',
|
||||
description: 'Set the value for each destination column',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: 'defineBelow',
|
||||
description: 'Whether to insert the input data this node receives in the new row',
|
||||
},
|
||||
{
|
||||
displayName: 'Inputs to Ignore',
|
||||
name: 'inputsToIgnore',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
fieldsToSend: ['autoMapInputData'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'List of input properties to avoid sending, separated by commas. Leave empty to send all properties.',
|
||||
placeholder: 'Enter properties...',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns to Send',
|
||||
name: 'columnsUi',
|
||||
placeholder: 'Add Column',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Column to Send',
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Column',
|
||||
name: 'columnValues',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Column Name',
|
||||
name: 'columnName',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tableName'],
|
||||
loadOptionsMethod: 'getTableUpdateAbleColumns',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Column Value',
|
||||
name: 'columnValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
fieldsToSend: ['defineBelow'],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
description: 'Add destination column with its value',
|
||||
},
|
||||
{
|
||||
displayName: 'Hint: Link, files, images or digital signatures have to be added separately.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,63 @@
|
|||
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import {
|
||||
seaTableApiRequest,
|
||||
getTableColumns,
|
||||
split,
|
||||
rowExport,
|
||||
updateAble,
|
||||
splitStringColumnsToArrays,
|
||||
} from '../../../GenericFunctions';
|
||||
import type { IRowObject } from '../../Interfaces';
|
||||
import type { TColumnsUiValues, TColumnValue } from '../../../types';
|
||||
|
||||
export async function update(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const tableName = this.getNodeParameter('tableName', index) as string;
|
||||
const tableColumns = await getTableColumns.call(this, tableName);
|
||||
const fieldsToSend = this.getNodeParameter('fieldsToSend', index) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData';
|
||||
const rowId = this.getNodeParameter('rowId', index) as string;
|
||||
|
||||
const body = {
|
||||
table_name: tableName,
|
||||
row_id: rowId,
|
||||
} as IDataObject;
|
||||
let rowInput = {} as IRowObject;
|
||||
|
||||
// get rowInput, an object of key:value pairs like { Name: 'Promo Action 1', Status: "Draft" }.
|
||||
if (fieldsToSend === 'autoMapInputData') {
|
||||
const items = this.getInputData();
|
||||
const incomingKeys = Object.keys(items[index].json);
|
||||
const inputDataToIgnore = split(this.getNodeParameter('inputsToIgnore', index, '') as string);
|
||||
for (const key of incomingKeys) {
|
||||
if (inputDataToIgnore.includes(key)) continue;
|
||||
rowInput[key] = items[index].json[key] as TColumnValue;
|
||||
}
|
||||
} else {
|
||||
const columns = this.getNodeParameter('columnsUi.columnValues', index, []) as TColumnsUiValues;
|
||||
for (const column of columns) {
|
||||
rowInput[column.columnName] = column.columnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// only keep key:value pairs for columns that are allowed to update.
|
||||
rowInput = rowExport(rowInput, updateAble(tableColumns));
|
||||
|
||||
// string to array: multi-select and collaborators
|
||||
rowInput = splitStringColumnsToArrays(rowInput, tableColumns);
|
||||
|
||||
body.row = rowInput;
|
||||
|
||||
const responseData = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'PUT',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/',
|
||||
body,
|
||||
);
|
||||
|
||||
return this.helpers.returnJsonArray(responseData as IDataObject[]);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { update as execute } from './execute';
|
||||
import { rowUpdateDescription as description } from './description';
|
||||
|
||||
export { description, execute };
|
|
@ -0,0 +1,57 @@
|
|||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import * as row from './row';
|
||||
import * as base from './base';
|
||||
import * as link from './link';
|
||||
import * as asset from './asset';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'SeaTable',
|
||||
name: 'seaTable',
|
||||
icon: 'file:seatable.svg',
|
||||
group: ['output'],
|
||||
version: 2,
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume the SeaTable API',
|
||||
defaults: {
|
||||
name: 'SeaTable',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'seaTableApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Row',
|
||||
value: 'row',
|
||||
},
|
||||
{
|
||||
name: 'Base',
|
||||
value: 'base',
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
value: 'link',
|
||||
},
|
||||
{
|
||||
name: 'Asset',
|
||||
value: 'asset',
|
||||
},
|
||||
],
|
||||
default: 'row',
|
||||
},
|
||||
...row.descriptions,
|
||||
...base.descriptions,
|
||||
...link.descriptions,
|
||||
...asset.descriptions,
|
||||
],
|
||||
};
|
1
packages/nodes-base/nodes/SeaTable/v2/methods/index.ts
Normal file
1
packages/nodes-base/nodes/SeaTable/v2/methods/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * as loadOptions from './loadOptions';
|
227
packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts
Normal file
227
packages/nodes-base/nodes/SeaTable/v2/methods/loadOptions.ts
Normal file
|
@ -0,0 +1,227 @@
|
|||
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
||||
import { getTableColumns, seaTableApiRequest, updateAble } from '../GenericFunctions';
|
||||
import type { IRow } from '../actions/Interfaces';
|
||||
|
||||
export async function getTableNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
// this.getCurrentNodeParameter('viewName'); // das kommt vom trigger. Brauche ich das???
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getTableNameAndId(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const {
|
||||
metadata: { tables },
|
||||
} = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/metadata',
|
||||
);
|
||||
for (const table of tables) {
|
||||
returnData.push({
|
||||
name: table.name,
|
||||
value: table.name + ':::' + table._id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getSearchableColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (
|
||||
col.type === 'text' ||
|
||||
col.type === 'long-text' ||
|
||||
col.type === 'number' ||
|
||||
col.type === 'single-select' ||
|
||||
col.type === 'email' ||
|
||||
col.type === 'url' ||
|
||||
col.type === 'rate' ||
|
||||
col.type === 'formula'
|
||||
) {
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getLinkColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
let tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
tableName = tableName.split(':::')[0];
|
||||
//const tableId = (tableName.split(':::')[1] ? tableName.split(':::')[1] : "");
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'link') {
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name + ':::' + col.data.link_id + ':::' + col.data.other_table_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getAssetColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
if (tableName) {
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'image' || col.type === 'file') {
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name + ':::' + col.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getSignatureColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
if (tableName) {
|
||||
// only execute if table is selected
|
||||
const columns = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/columns',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
for (const col of columns.columns) {
|
||||
if (col.type === 'digital-sign') {
|
||||
// file+image are difficult: every time the row changes, all files trigger.
|
||||
returnData.push({
|
||||
name: col.name,
|
||||
value: col.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getTableUpdateAbleColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
let columns = await getTableColumns.call(this, tableName);
|
||||
|
||||
// remove columns that could not be filled
|
||||
columns = updateAble(columns);
|
||||
|
||||
return columns
|
||||
.filter((column) => column.editable)
|
||||
.map((column) => ({ name: column.name, value: column.name }));
|
||||
}
|
||||
|
||||
export async function getRowIds(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const tableName = this.getNodeParameter('tableName') as string;
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
if (tableName) {
|
||||
const sqlResult = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'POST',
|
||||
'/dtable-db/api/v1/query/{{dtable_uuid}}/',
|
||||
{
|
||||
sql: `SELECT * FROM \`${tableName}\` LIMIT 1000`,
|
||||
convert_keys: false,
|
||||
},
|
||||
);
|
||||
let rows = sqlResult.results as IRow[];
|
||||
|
||||
for (const row of rows) {
|
||||
returnData.push({
|
||||
name: row['0000'] + ' (' + row._id + ')',
|
||||
value: row._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export async function getTableViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tableName = this.getCurrentNodeParameter('tableName') as string;
|
||||
if (tableName) {
|
||||
// only execute if table is selected, to avoid unnecessary API requests
|
||||
const { views } = await seaTableApiRequest.call(
|
||||
this,
|
||||
{},
|
||||
'GET',
|
||||
'/dtable-server/api/v1/dtables/{{dtable_uuid}}/views',
|
||||
{},
|
||||
{ table_name: tableName },
|
||||
);
|
||||
returnData.push({
|
||||
name: '<Do not limit to a view>',
|
||||
value: '',
|
||||
});
|
||||
for (const view of views) {
|
||||
returnData.push({
|
||||
name: view.name,
|
||||
value: view.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
97
packages/nodes-base/nodes/SeaTable/v2/types.ts
Normal file
97
packages/nodes-base/nodes/SeaTable/v2/types.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
// ----------------------------------
|
||||
// SeaTable
|
||||
// ----------------------------------
|
||||
|
||||
export type TSeaTableServerVersion = '2.0.6';
|
||||
export type TSeaTableServerEdition = 'enterprise edition';
|
||||
|
||||
// ----------------------------------
|
||||
// dtable
|
||||
// ----------------------------------
|
||||
|
||||
import type {
|
||||
IDtableMetadataColumn,
|
||||
IDtableMetadataTable,
|
||||
TDtableViewColumn,
|
||||
} from './actions/Interfaces';
|
||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||
|
||||
export type TColumnType =
|
||||
| 'text'
|
||||
| 'long-text'
|
||||
| 'number'
|
||||
| 'collaborator'
|
||||
| 'date'
|
||||
| 'duration'
|
||||
| 'single-select'
|
||||
| 'multiple-select'
|
||||
| 'image'
|
||||
| 'file'
|
||||
| 'email'
|
||||
| 'url'
|
||||
| 'checkbox'
|
||||
| 'rate'
|
||||
| 'formula'
|
||||
| 'link-formula'
|
||||
| 'geolocation'
|
||||
| 'link'
|
||||
| 'creator'
|
||||
| 'ctime'
|
||||
| 'last-modifier'
|
||||
| 'mtime'
|
||||
| 'auto-number'
|
||||
| 'button'
|
||||
| 'digital-sign';
|
||||
|
||||
export type TInheritColumnKey =
|
||||
| '_id'
|
||||
| '_creator'
|
||||
| '_ctime'
|
||||
| '_last_modifier'
|
||||
| '_mtime'
|
||||
| '_seq'
|
||||
| '_archived'
|
||||
| '_locked'
|
||||
| '_locked_by';
|
||||
|
||||
export type TColumnValue = undefined | boolean | number | string | string[] | null;
|
||||
export type TColumnKey = TInheritColumnKey | string;
|
||||
|
||||
export type TDtableMetadataTables = readonly IDtableMetadataTable[];
|
||||
export type TDtableMetadataColumns = IDtableMetadataColumn[];
|
||||
export type TDtableViewColumns = readonly TDtableViewColumn[];
|
||||
|
||||
// ----------------------------------
|
||||
// api
|
||||
// ----------------------------------
|
||||
|
||||
export type TEndpointVariableName = 'access_token' | 'dtable_uuid' | 'server';
|
||||
|
||||
// Template Literal Types requires-ts-4.1.5 -- deferred
|
||||
export type TMethod = 'GET' | 'POST';
|
||||
type TEndpoint =
|
||||
| '/api/v2.1/dtable/app-access-token/'
|
||||
| '/dtable-server/api/v1/dtables/{{dtable_uuid}}/rows/';
|
||||
export type TEndpointExpr = TEndpoint;
|
||||
export type TEndpointResolvedExpr =
|
||||
TEndpoint; /* deferred: but already in use for header values, e.g. authentication */
|
||||
|
||||
export type TDateTimeFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZ' /* moment.js */;
|
||||
|
||||
// ----------------------------------
|
||||
// node
|
||||
// ----------------------------------
|
||||
|
||||
export type TCredentials = ICredentialDataDecryptedObject | undefined;
|
||||
|
||||
export type TTriggerOperation = 'create' | 'update';
|
||||
|
||||
export type TOperation = 'append' | 'list' | 'metadata';
|
||||
|
||||
export type TLoadedResource = {
|
||||
name: string;
|
||||
};
|
||||
export type TColumnsUiValues = Array<{
|
||||
columnName: string;
|
||||
columnValue: string;
|
||||
}>;
|
Loading…
Reference in a new issue