Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,31 @@
import { nocodbAuth } from '../../';
import { createAction, DynamicPropsValue } from '@activepieces/pieces-framework';
import { makeClient, nocodbCommon } from '../common';
export const createRecordAction = createAction({
auth: nocodbAuth,
name: 'nocodb-create-record',
displayName: 'Create a Record',
description: 'Creates a new record in the given table.',
props: {
workspaceId: nocodbCommon.workspaceId,
baseId: nocodbCommon.baseId,
tableId: nocodbCommon.tableId,
tableColumns: nocodbCommon.tableColumns,
},
async run(context) {
const { baseId, tableId, tableColumns } = context.propsValue;
const recordInput: DynamicPropsValue = {};
Object.entries(tableColumns).forEach(([key, value]) => {
if (Array.isArray(value)) {
recordInput[key] = value.join(',');
} else {
recordInput[key] = value;
}
});
const client = makeClient(context.auth);
return await client.createRecord(baseId, tableId, recordInput, context.auth.props.version || 3);
},
});

View File

@@ -0,0 +1,25 @@
import { nocodbAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { makeClient, nocodbCommon } from '../common';
export const deleteRecordAction = createAction({
auth: nocodbAuth,
name: 'nocodb-delete-record',
displayName: 'Delete a Record',
description: 'Deletes a record with the given Record ID.',
props: {
workspaceId: nocodbCommon.workspaceId,
baseId: nocodbCommon.baseId,
tableId: nocodbCommon.tableId,
recordId: Property.Number({
displayName: 'Record ID',
required: true,
}),
},
async run(context) {
const { baseId, tableId, recordId } = context.propsValue;
const client = makeClient(context.auth);
return await client.deleteRecord(baseId, tableId, recordId, context.auth.props.version || 3);
},
});

View File

@@ -0,0 +1,25 @@
import { nocodbAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { makeClient, nocodbCommon } from '../common';
export const getRecordAction = createAction({
auth: nocodbAuth,
name: 'nocodb-get-record',
displayName: 'Get a Record',
description: 'Gets a record by the Record ID.',
props: {
workspaceId: nocodbCommon.workspaceId,
baseId: nocodbCommon.baseId,
tableId: nocodbCommon.tableId,
recordId: Property.Number({
displayName: 'Record ID',
required: true,
}),
},
async run(context) {
const { baseId, tableId, recordId } = context.propsValue;
const client = makeClient(context.auth);
return await client.getRecord(baseId, tableId, recordId, context.auth.props.version || 3);
},
});

View File

@@ -0,0 +1,56 @@
import { nocodbAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { makeClient, nocodbCommon } from '../common';
import { ListAPIResponse, ListAPIV3Response } from '../common/types';
export const searchRecordsAction = createAction({
auth: nocodbAuth,
name: 'nocodb-search-records',
displayName: 'Search Records',
description: 'Returns a list of records matching the where condition.',
props: {
workspaceId: nocodbCommon.workspaceId,
baseId: nocodbCommon.baseId,
tableId: nocodbCommon.tableId,
columnId: nocodbCommon.columnId,
whereCondition: Property.LongText({
displayName: 'Where',
required: false,
description: `Enables you to define specific conditions for filtering records.See docs [here](https://docs.nocodb.com/0.109.7/developer-resources/rest-apis/#comparison-operators).`,
}),
limit: Property.Number({
displayName: 'Limit',
required: true,
defaultValue: 10,
description:
'Enables you to set a limit on the number of records you want to retrieve.',
}),
sort: Property.LongText({
displayName: 'Sort',
required: false,
description: `Comma separated field names without space.Example: **field1,-field2** will sort the records first by 'field1' in ascending order and then by 'field2' in descending order.`,
}),
},
async run(context) {
const { baseId, tableId, columnId, limit, whereCondition, sort } =
context.propsValue;
const client = makeClient(context.auth);
const authVersion = context.auth.props.version || 3;
const response = await client.listRecords(
baseId,
tableId,
{
fields: columnId ? columnId.join(',') : undefined,
where: whereCondition,
sort,
offset: 0,
limit,
},
authVersion
);
return authVersion === 4
? (response as ListAPIV3Response<Record<string, unknown>>).records
: (response as ListAPIResponse<Record<string, unknown>>).list;
},
});

View File

@@ -0,0 +1,53 @@
import { nocodbAuth } from '../../';
import {
createAction,
DynamicPropsValue,
Property,
} from '@activepieces/pieces-framework';
import { makeClient, nocodbCommon } from '../common';
export const updateRecordAction = createAction({
auth: nocodbAuth,
name: 'nocodb-update-record',
displayName: 'Update a Record',
description: 'Updates an existing record with the given Record ID.',
props: {
workspaceId: nocodbCommon.workspaceId,
baseId: nocodbCommon.baseId,
tableId: nocodbCommon.tableId,
recordId: Property.Number({
displayName: 'Record ID',
required: true,
}),
tableColumns: nocodbCommon.tableColumns,
},
async run(context) {
const { baseId, tableId, recordId, tableColumns } = context.propsValue;
const authVersion = context.auth.props.version || 3;
let recordInput: DynamicPropsValue = {};
if (authVersion === 4) {
recordInput['id'] = recordId;
recordInput['fields'] = {};
} else {
recordInput = {
Id: recordId,
};
}
Object.entries(tableColumns).forEach(([key, value]) => {
if(authVersion === 4) {
recordInput['fields'][key] = value;
} else {
if (Array.isArray(value)) {
recordInput[key] = value.join(',');
} else {
recordInput[key] = value;
}
}
});
const client = makeClient(context.auth);
return await client.updateRecord(baseId, tableId, recordInput, authVersion);
},
});

View File

@@ -0,0 +1,266 @@
import {
HttpMessageBody,
HttpMethod,
QueryParams,
httpClient,
HttpRequest,
} from '@activepieces/pieces-common';
import {
BaseResponse,
DataOperationResponse,
DataOperationV3Response,
GetTableResponse,
GetTableV3Response,
ListAPIResponse,
ListAPIV3Response,
ListRecordsParams,
TableResponse,
WorkspaceResponse,
} from './types';
export class NocoDBClient {
constructor(private hostUrl: string, private apiToken: string) {}
async makeRequest<T extends HttpMessageBody>(
method: HttpMethod,
resourceUri: string,
query?: Record<string, string | number | string[] | undefined>,
body: Record<string, unknown> | undefined = undefined
): Promise<T> {
const baseUrl = this.hostUrl.replace(/\/$/, '');
const params: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
params[key] = String(value);
}
}
}
const request: HttpRequest = {
method: method,
url: baseUrl + '/api' + resourceUri,
headers: {
'xc-token': this.apiToken,
},
queryParams: params,
body: body,
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}
async listWorkspaces(): Promise<ListAPIResponse<WorkspaceResponse>> {
return await this.makeRequest<ListAPIResponse<WorkspaceResponse>>(
HttpMethod.GET,
'/v1/workspaces/'
);
}
async listBases(
workspaceId?: string,
version = 3
): Promise<ListAPIResponse<BaseResponse>> {
if (workspaceId && workspaceId !== 'none') {
// Cloud version
const endpoint =
version === 4
? `/v3/meta/workspaces/${workspaceId}/bases/`
: `/v1/workspaces/${workspaceId}/bases/`;
return await this.makeRequest<ListAPIResponse<BaseResponse>>(
HttpMethod.GET,
endpoint
);
} else {
// Self-hosted version
const endpoint =
version === 4
? '/v3/meta/workspaces/nc/bases/'
: version === 3
? '/v2/meta/bases/'
: '/v1/db/meta/projects/';
return await this.makeRequest<ListAPIResponse<BaseResponse>>(
HttpMethod.GET,
endpoint
);
}
}
async listTables(
baseId: string,
version = 3
): Promise<ListAPIResponse<TableResponse>> {
const endpoint =
version === 4
? `/v3/meta/bases/${baseId}/tables`
: version === 3
? `/v2/meta/bases/${baseId}/tables`
: `/v1/db/meta/projects/${baseId}/tables`;
return await this.makeRequest<ListAPIResponse<TableResponse>>(
HttpMethod.GET,
endpoint
);
}
async getTable(
_baseId: string,
tableId: string,
version = 3
): Promise<GetTableResponse> {
const endpoint =
version === 3
? `/v2/meta/tables/${tableId}/`
: `/v1/db/meta/tables/${tableId}/`;
return await this.makeRequest<GetTableResponse>(HttpMethod.GET, endpoint);
}
async getTableV3(
baseId: string,
tableId: string,
_version = 4
): Promise<GetTableV3Response> {
const endpoint = `/v3/meta/bases/${baseId}/tables/${tableId}`;
return await this.makeRequest<GetTableV3Response>(HttpMethod.GET, endpoint);
}
async createRecord(
baseId: string,
tableId: string,
recordInput: Record<string, unknown>,
version = 3
) {
const endpoint =
version === 4
? `/v3/data/${baseId}/${tableId}/records`
: version === 3
? `/v2/tables/${tableId}/records`
: `/v1/db/data/noco/${tableId}`;
const response = await this.makeRequest<DataOperationResponse>(
HttpMethod.POST,
endpoint,
undefined,
version === 4 ? { fields: recordInput } : recordInput
);
if (version === 4) {
return (response as DataOperationV3Response).records?.[0] ?? response;
} else {
return response;
}
}
async getRecord(
baseId: string,
tableId: string,
recordId: number,
version = 3
) {
const endpoint =
version === 4
? `/v3/data/${baseId}/${tableId}/records/${recordId}`
: version === 3
? `/v2/tables/${tableId}/records/${recordId}`
: `/v1/db/data/noco/${tableId}/${recordId}`;
return await this.makeRequest(HttpMethod.GET, endpoint);
}
async updateRecord(
baseId: string,
tableId: string,
recordInput: Record<string, unknown>,
version = 3
) {
const endpoint =
version === 4
? `/v3/data/${baseId}/${tableId}/records`
: version === 3
? `/v2/tables/${tableId}/records/`
: `/v1/db/data/noco/${tableId}`;
const response = await this.makeRequest<DataOperationResponse>(
HttpMethod.PATCH,
endpoint,
undefined,
recordInput
);
if (version === 4) {
return (response as DataOperationV3Response).records?.[0] ?? response;
} else {
return response;
}
}
async deleteRecord(
baseId: string,
tableId: string,
recordId: number,
version = 3
) {
const endpoint =
version === 4
? `/v3/data/${baseId}/${tableId}/records/`
: version === 3
? `/v2/tables/${tableId}/records/`
: `/v1/db/data/noco/${tableId}/${recordId}`;
const body =
version === 4
? { id: recordId }
: version === 3
? { Id: recordId }
: undefined;
const response = await this.makeRequest<DataOperationResponse>(HttpMethod.DELETE, endpoint, undefined, body);
if (version === 4) {
return (response as DataOperationV3Response).records?.[0] ?? response;
} else {
return response;
}
}
async listRecords(
baseId: string,
tableId: string,
params: ListRecordsParams,
version = 3
): Promise<
| ListAPIResponse<Record<string, unknown>>
| ListAPIV3Response<Record<string, unknown>>
> {
const endpoint =
version === 4
? `/v3/data/${baseId}/${tableId}/records`
: version === 3
? `/v2/tables/${tableId}/records/`
: `/v1/db/data/noco/${tableId}`;
if (version === 4 && params.sort && typeof params.sort === 'string') {
const sortItems = params.sort.split(',');
// format to v3
params.sort = JSON.stringify(
sortItems.map((item) => {
if (item.startsWith('-')) {
return {
field: item.substring(1),
direction: 'desc',
};
} else {
return {
field: item,
direction: 'asc',
};
}
})
);
}
const makeRequest = async <T>() => {
return await this.makeRequest<T>(HttpMethod.GET, endpoint, params);
};
if (version === 4) {
return makeRequest<ListAPIV3Response<Record<string, unknown>>>();
} else {
return makeRequest<ListAPIResponse<Record<string, unknown>>>();
}
}
}

View File

@@ -0,0 +1,370 @@
import { nocodbAuth } from '../../';
import {
DynamicPropsValue,
AppConnectionValueForAuthProperty,
Property,
} from '@activepieces/pieces-framework';
import { NocoDBClient } from './client';
import {
ColumnResponse,
ColumnV3Response,
GetTableResponse,
GetTableV3Response,
} from './types';
export function makeClient(auth: AppConnectionValueForAuthProperty<typeof nocodbAuth>) {
return new NocoDBClient(auth.props.baseUrl, auth.props.apiToken);
}
export const nocodbCommon = {
workspaceId: Property.Dropdown({
auth:nocodbAuth,
displayName: 'Workspace ID',
refreshers: [],
required: false,
description: 'For self-hosted instances,select "No Workspace".',
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first.',
options: [],
};
}
const client = makeClient(
auth
);
try {
const response = await client.listWorkspaces();
return {
disabled: false,
options: response.list.map((workspace) => {
return {
label: workspace.title,
value: workspace.id,
};
}),
};
} catch (error) {
return {
disabled: false,
options: [
{
label: 'No Workspace',
value: 'none',
},
],
};
}
},
}),
baseId: Property.Dropdown({
auth:nocodbAuth,
displayName: 'Base ID',
refreshers: ['workspaceId'],
required: true,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first.',
options: [],
};
}
try {
const client = makeClient(
auth
);
const response = await client.listBases(
(workspaceId as string) || undefined,
(auth).props.version || 3
);
return {
disabled: false,
options: response.list.map((base) => {
return {
label: base.title,
value: base.id,
};
}),
};
} catch (error) {
console.error('Error fetching bases:', error);
return {
disabled: true,
placeholder:
'Error fetching bases. Please check your connection and version.',
options: [],
};
}
},
}),
tableId: Property.Dropdown({
auth:nocodbAuth,
displayName: 'Table ID',
refreshers: ['workspaceId', 'baseId'],
required: true,
options: async ({ auth, baseId }) => {
if (!auth || !baseId) {
return {
disabled: true,
placeholder: 'Please connect your account first and select base.',
options: [],
};
}
const client = makeClient(
auth
);
const response = await client.listTables(
baseId as string,
(auth ).props.version || 3
);
return {
disabled: false,
options: response.list.map((table) => {
return {
label: table.title,
value: table.id,
};
}),
};
},
}),
columnId: Property.MultiSelectDropdown({
auth:nocodbAuth,
displayName: 'Fields',
description:
'Allows you to specify the fields that you wish to include in your API response. By default, all the fields are included in the response.',
refreshers: ['workspaceId', 'baseId', 'tableId'],
required: false,
options: async ({ auth, baseId, tableId }) => {
if (!auth || !baseId || !tableId) {
return {
disabled: true,
placeholder: 'Please connect your account first and select base.',
options: [],
};
}
const client = makeClient(
auth
);
const authVersion =
(auth ).props.version || 3;
const response =
authVersion === 4
? await client.getTableV3(
baseId as unknown as string,
tableId as unknown as string,
authVersion
)
: await client.getTable(
baseId as unknown as string,
tableId as unknown as string,
authVersion
);
return {
disabled: false,
options:
authVersion === 4
? (response as GetTableV3Response).fields.map((field) => {
return {
label: field.title,
value: field.title,
};
})
: (response as GetTableResponse).columns.map((column) => {
return {
label: column.title,
value: column.title,
};
}),
};
},
}),
tableColumns: Property.DynamicProperties({
auth:nocodbAuth,
displayName: 'Table Columns',
refreshers: ['baseId', 'tableId'],
required: true,
props: async ({ auth, baseId, tableId }) => {
if (!auth) return {};
if (!baseId) return {};
if (!tableId) return {};
const fields: DynamicPropsValue = {};
const client = makeClient(
auth
);
const authVersion =
(auth ).props.version || 3;
const response =
authVersion === 4
? await client.getTableV3(
baseId as unknown as string,
tableId as unknown as string,
authVersion
)
: await client.getTable(
baseId as unknown as string,
tableId as unknown as string,
authVersion
);
const columns =
authVersion === 4
? (response as GetTableV3Response).fields
: (response as GetTableResponse).columns;
for (const column of (columns ?? [])) {
const uidt =
authVersion === 4
? (column as ColumnV3Response).type
: (column as ColumnResponse).uidt;
switch (uidt) {
case 'SingleLineText':
case 'PhoneNumber':
case 'Email':
case 'URL':
fields[column.title] = Property.ShortText({
displayName: column.title,
required: false,
});
break;
case 'LongText':
fields[column.title] = Property.LongText({
displayName: column.title,
required: false,
});
break;
case 'Number':
case 'Decimal':
case 'Percent':
case 'Rating':
case 'Currency':
case 'Year':
fields[column.title] = Property.Number({
displayName: column.title,
required: false,
});
break;
case 'Checkbox':
fields[column.title] = Property.Checkbox({
displayName: column.title,
required: true,
});
break;
case 'MultiSelect': {
const getOptionsForAuthVersion4 = () => {
const colOptions = (column as ColumnV3Response).options;
return (colOptions?.['choices'] as any[])?.map((option) => {
return {
label: option.title,
value: option.title,
};
});
};
const options =
authVersion === 4
? getOptionsForAuthVersion4()
: (column as ColumnResponse).colOptions?.options?.map(
(option) => {
return {
label: option.title,
value: option.title,
};
}
);
fields[column.title] = Property.StaticMultiSelectDropdown({
displayName: column.title,
required: false,
options: {
disabled: false,
options: options ?? [],
},
});
break;
}
case 'SingleSelect': {
const getOptionsForAuthVersion4 = () => {
const colOptions = (column as ColumnV3Response).options;
return (colOptions?.['choices'] as any[])?.map((option) => {
return {
label: option.title,
value: option.title,
};
});
};
const options =
authVersion === 4
? getOptionsForAuthVersion4()
: (column as ColumnResponse).colOptions?.options?.map(
(option) => {
return {
label: option.title,
value: option.title,
};
}
);
fields[column.title] = Property.StaticDropdown({
displayName: column.title,
required: false,
options: {
disabled: false,
options: options ?? [],
},
});
break;
}
case 'Date':{
const columnMeta = authVersion === 4 ?
(column as ColumnV3Response).options:
(column as ColumnResponse).meta;
fields[column.title] = Property.ShortText({
displayName: column.title,
required: false,
description: columnMeta?.['date_format']
? `Please provide date in ${columnMeta['date_format']} format.`
: '',
});
break;}
case 'Time':
fields[column.title] = Property.ShortText({
displayName: column.title,
required: false,
description: 'Please provide time in HH:mm:ss format.',
});
break;
case 'DateTime':
fields[column.title] = Property.DateTime({
displayName: column.title,
required: false,
});
break;
case 'JSON':
fields[column.title] = Property.Json({
displayName: column.title,
required: false,
});
break;
default:
fields[column.title] = Property.ShortText({
displayName: column.title,
required: false,
});
break;
}
}
return fields;
},
}),
};

View File

@@ -0,0 +1,150 @@
export interface ListAPIV3Response<T> {
records: T[];
next?: string;
prev?: string;
}
export interface ListAPIResponse<T> {
list: T[];
pageInfo: {
totalRows: number;
page: number;
pageSize: number;
isFirstPage: boolean;
isLastPage: boolean;
};
}
export interface WorkspaceResponse {
id: string;
title: string;
description: string;
deleted: boolean;
deleted_at: string;
status: number;
order: number;
}
export interface BaseResponse {
id: string;
title: string;
description: string;
deleted: boolean;
created_at: string;
updated_at: string;
status: number;
order: number;
type: string;
}
export interface TableResponse {
id: string;
source_id: string;
description: string;
base_id: string;
table_name: string;
title: string;
type: string;
created_at: string;
updated_at: string;
order: number;
enabled: boolean;
}
type ColumnType =
| 'Attachment'
| 'AutoNumber'
| 'Barcode'
| 'Button'
| 'Checkbox'
| 'Collaborator'
| 'Count'
| 'CreatedTime'
| 'Currency'
| 'Date'
| 'DateTime'
| 'Decimal'
| 'Duration'
| 'Email'
| 'Formula'
| 'ForeignKey'
| 'GeoData'
| 'Geometry'
| 'ID'
| 'JSON'
| 'LastModifiedTime'
| 'LongText'
| 'LinkToAnotherRecord'
| 'Lookup'
| 'MultiSelect'
| 'Number'
| 'Percent'
| 'PhoneNumber'
| 'Rating'
| 'Rollup'
| 'SingleLineText'
| 'SingleSelect'
| 'SpecificDBType'
| 'Time'
| 'URL'
| 'Year'
| 'QrCode'
| 'Links'
| 'User'
| 'CreatedBy'
| 'LastModifiedBy';
export interface ColumnResponse {
id: string;
title: string;
column_name: string;
uidt: ColumnType;
colOptions?: {
options: Array<{ title: string; id: string }>;
};
meta: Record<string, unknown> | null;
}
export interface ColumnV3Response {
id: string;
title: string;
type: ColumnType;
options: Record<string, unknown> | null;
}
export interface GetTableV3Response {
id: string;
title: string;
fields: Array<ColumnV3Response>;
}
export interface GetTableResponse {
id: string;
source_id: string;
table_name: string;
title: string;
type: string;
created_at: string;
updated_at: string;
order: number;
enabled: boolean;
columns: Array<ColumnResponse>;
}
export interface ListRecordsParams
extends Record<string, string | number | string[] | undefined> {
fields?: string;
sort?: string;
where?: string;
offset: number;
limit: number;
viewId?: string;
filter?: string;
}
export interface DataOperationV3Response {
records: { id?: string | number; fields: Record<string, unknown> }[];
}
export type DataOperationResponse =
| Record<string, unknown>
| DataOperationV3Response;