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:
@@ -0,0 +1,27 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { fireberryAuth } from '../../index';
|
||||
import { objectTypeDropdown, objectFields } from '../common/props';
|
||||
import { FireberryClient } from '../common/client';
|
||||
|
||||
export const createRecordAction = createAction({
|
||||
name: 'create_record',
|
||||
displayName: 'Create Record',
|
||||
description: 'Create a new record in Fireberry.',
|
||||
auth: fireberryAuth,
|
||||
props: {
|
||||
objectType: objectTypeDropdown,
|
||||
fields: objectFields,
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType, fields } = propsValue;
|
||||
|
||||
const fieldsObj = typeof fields === 'string' ? JSON.parse(fields) : fields;
|
||||
|
||||
if (typeof fieldsObj !== 'object' || fieldsObj === null) {
|
||||
throw new Error('Fields must be an object');
|
||||
}
|
||||
|
||||
return await client.batchCreate(objectType as string, [fieldsObj]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fireberryAuth } from '../../index';
|
||||
import { objectTypeDropdown } from '../common/props';
|
||||
import { FireberryClient } from '../common/client';
|
||||
|
||||
const recordsToDeleteDropdown = Property.MultiSelectDropdown({
|
||||
auth: fireberryAuth,
|
||||
displayName: 'Records to Delete',
|
||||
required: true,
|
||||
refreshers: ['objectType'],
|
||||
options: async ({ auth, objectType }) => {
|
||||
if (!auth || !objectType) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Select object type first',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const objectTypeStr = typeof objectType === 'string' ? objectType : (objectType as { value: string })?.value;
|
||||
const client = new FireberryClient(auth);
|
||||
|
||||
const response = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
Records: Array<Record<string, any>>;
|
||||
PrimaryField: string;
|
||||
PrimaryKey: string;
|
||||
Total_Records: number;
|
||||
}
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
resourceUri: `/api/record/${objectTypeStr}?$top=100`,
|
||||
});
|
||||
|
||||
if (!response.data?.Records || !Array.isArray(response.data.Records)) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: [],
|
||||
placeholder: response.data?.Total_Records === 0 ? 'No records found' : 'Error loading records',
|
||||
};
|
||||
}
|
||||
|
||||
const primaryField = response.data.PrimaryField;
|
||||
const primaryKey = response.data.PrimaryKey;
|
||||
|
||||
const options = response.data.Records.map((record: any) => {
|
||||
const displayName = record[primaryField] ||
|
||||
record.name || record.title || record.subject ||
|
||||
record.firstname || record.lastname || record.email ||
|
||||
record.accountname || record.contactname ||
|
||||
`Record ${record[primaryKey]?.substring(0, 8) || 'Unknown'}`;
|
||||
|
||||
return {
|
||||
label: displayName,
|
||||
value: record[primaryKey],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: options.slice(0, 100),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: [],
|
||||
placeholder: 'Error loading records',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteRecordAction = createAction({
|
||||
name: 'delete_record',
|
||||
displayName: 'Delete Records',
|
||||
description: 'Delete records from Fireberry.',
|
||||
auth: fireberryAuth,
|
||||
props: {
|
||||
objectType: objectTypeDropdown,
|
||||
recordIds: recordsToDeleteDropdown,
|
||||
confirmDeletion: Property.Checkbox({
|
||||
displayName: 'Confirm Deletion',
|
||||
required: true,
|
||||
description: 'Check this box to confirm you want to permanently delete the selected records. This action cannot be undone.',
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType, recordIds, confirmDeletion } = propsValue;
|
||||
|
||||
if (!confirmDeletion) {
|
||||
throw new Error('You must confirm deletion by checking the confirmation box');
|
||||
}
|
||||
|
||||
if (!recordIds || !Array.isArray(recordIds) || recordIds.length === 0) {
|
||||
throw new Error('At least one record must be selected for deletion');
|
||||
}
|
||||
|
||||
if (recordIds.length > 20) {
|
||||
throw new Error('Maximum 20 records can be deleted at once');
|
||||
}
|
||||
|
||||
const result = await client.batchDelete(objectType as string, recordIds);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: recordIds.length,
|
||||
recordIds: recordIds,
|
||||
message: `Successfully deleted ${recordIds.length} record(s)`,
|
||||
apiResponse: result,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fireberryAuth } from '../../index';
|
||||
import { objectTypeDropdown } from '../common/props';
|
||||
import { FireberryClient } from '../common/client';
|
||||
|
||||
const fieldsToReturn = Property.DynamicProperties({
|
||||
displayName: 'Fields to Return',
|
||||
refreshers: ['objectType'],
|
||||
required: false,
|
||||
auth: fireberryAuth,
|
||||
props: async ({ auth, objectType }) => {
|
||||
if (!auth || !objectType) return {};
|
||||
|
||||
const objectTypeStr = typeof objectType === 'string' ? objectType : (objectType as { value: string })?.value;
|
||||
const client = new FireberryClient(auth);
|
||||
|
||||
try {
|
||||
const metadata = await client.getObjectFieldsMetadata(objectTypeStr);
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
for (const field of metadata.data) {
|
||||
const systemFields = ['createdby', 'modifiedby', 'deletedby'];
|
||||
if (field.fieldName.endsWith('id') && !field.label) {
|
||||
continue;
|
||||
}
|
||||
if (systemFields.includes(field.fieldName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
props[field.fieldName] = Property.Checkbox({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: false,
|
||||
description: `Include ${field.label || field.fieldName} in search results`,
|
||||
});
|
||||
}
|
||||
|
||||
return props;
|
||||
} catch (error) {
|
||||
console.error('Error fetching fields for selection:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const findRecordAction = createAction({
|
||||
name: 'find_record',
|
||||
displayName: 'Find Records',
|
||||
description: 'Search for records in Fireberry.',
|
||||
auth: fireberryAuth,
|
||||
props: {
|
||||
objectType: objectTypeDropdown,
|
||||
searchQuery: Property.LongText({
|
||||
displayName: 'Search Query',
|
||||
required: false,
|
||||
description: 'Enter search criteria (e.g., "accountname=John" or "email contains @example.com"). Leave empty to get all records.',
|
||||
}),
|
||||
fieldsToReturn: fieldsToReturn,
|
||||
sortBy: Property.ShortText({
|
||||
displayName: 'Sort By Field',
|
||||
required: false,
|
||||
description: 'System name of field to sort by (e.g., "createdon", "accountname")',
|
||||
}),
|
||||
sortOrder: Property.StaticDropdown({
|
||||
displayName: 'Sort Order',
|
||||
required: false,
|
||||
defaultValue: 'desc',
|
||||
options: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{ label: 'Descending (newest first)', value: 'desc' },
|
||||
{ label: 'Ascending (oldest first)', value: 'asc' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
pageSize: Property.Number({
|
||||
displayName: 'Page Size',
|
||||
required: false,
|
||||
defaultValue: 25,
|
||||
description: 'Number of records to return (max 50)',
|
||||
}),
|
||||
pageNumber: Property.Number({
|
||||
displayName: 'Page Number',
|
||||
required: false,
|
||||
defaultValue: 1,
|
||||
description: 'Page number to retrieve (max 10)',
|
||||
}),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType, searchQuery, fieldsToReturn, sortBy, sortOrder, pageSize, pageNumber } = propsValue;
|
||||
|
||||
const selectedFields: string[] = [];
|
||||
if (fieldsToReturn && typeof fieldsToReturn === 'object') {
|
||||
for (const [fieldName, isSelected] of Object.entries(fieldsToReturn)) {
|
||||
if (isSelected === true) {
|
||||
selectedFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const objectsMetadata = await client.getObjectsMetadata();
|
||||
const targetObject = objectsMetadata.data.find(obj => obj.systemName === objectType);
|
||||
|
||||
if (!targetObject) {
|
||||
throw new Error(`Object type '${objectType}' not found`);
|
||||
}
|
||||
|
||||
const queryBody: Record<string, any> = {
|
||||
objecttype: parseInt(targetObject.objectType),
|
||||
};
|
||||
|
||||
if (selectedFields.length > 0) {
|
||||
queryBody['fields'] = selectedFields.join(',');
|
||||
}
|
||||
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
queryBody['query'] = searchQuery.trim();
|
||||
}
|
||||
|
||||
if (sortBy && sortBy.trim()) {
|
||||
queryBody['sort_by'] = sortBy.trim();
|
||||
queryBody['sort_type'] = sortOrder || 'desc';
|
||||
}
|
||||
|
||||
if (pageSize) {
|
||||
queryBody['page_size'] = Math.min(Math.max(1, pageSize), 50);
|
||||
}
|
||||
if (pageNumber) {
|
||||
queryBody['page_number'] = Math.min(Math.max(1, pageNumber), 10);
|
||||
}
|
||||
|
||||
const response = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
ObjectName: string;
|
||||
SystemName: string;
|
||||
ObjectType: number;
|
||||
PrimaryKey: string;
|
||||
PrimaryField: string;
|
||||
PageNum: number;
|
||||
SortBy: string;
|
||||
SortBy_Desc: boolean;
|
||||
IsLastPage: boolean;
|
||||
Columns: Array<Record<string, any>>;
|
||||
Data: Array<Record<string, any>>;
|
||||
};
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: '/api/query',
|
||||
body: queryBody,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Query failed');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
query: queryBody,
|
||||
results: {
|
||||
objectName: response.data.ObjectName,
|
||||
systemName: response.data.SystemName,
|
||||
objectType: response.data.ObjectType,
|
||||
pageNumber: response.data.PageNum,
|
||||
sortBy: response.data.SortBy,
|
||||
sortDescending: response.data.SortBy_Desc,
|
||||
isLastPage: response.data.IsLastPage,
|
||||
primaryKey: response.data.PrimaryKey,
|
||||
primaryField: response.data.PrimaryField,
|
||||
columns: response.data.Columns,
|
||||
records: response.data.Data,
|
||||
recordCount: response.data.Data.length,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fireberryAuth } from '../../index';
|
||||
import { objectTypeDropdown } from '../common/props';
|
||||
import { FireberryClient } from '../common/client';
|
||||
|
||||
const recordDropdown = Property.Dropdown({
|
||||
auth: fireberryAuth,
|
||||
displayName: 'Record',
|
||||
required: true,
|
||||
refreshers: ['objectType'],
|
||||
options: async ({ auth, objectType }) => {
|
||||
if (!auth || !objectType) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Select object type first',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const objectTypeStr = typeof objectType === 'string' ? objectType : (objectType as { value: string })?.value;
|
||||
const client = new FireberryClient(auth);
|
||||
|
||||
const response = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
Records: Array<Record<string, any>>;
|
||||
PrimaryField: string;
|
||||
PrimaryKey: string;
|
||||
Total_Records: number;
|
||||
}
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
resourceUri: `/api/record/${objectTypeStr}?$top=50`,
|
||||
});
|
||||
|
||||
if (!response.data?.Records || !Array.isArray(response.data.Records)) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: [],
|
||||
placeholder: response.data?.Total_Records === 0 ? 'No records found' : 'Error loading records',
|
||||
};
|
||||
}
|
||||
|
||||
const primaryField = response.data.PrimaryField;
|
||||
const primaryKey = response.data.PrimaryKey;
|
||||
|
||||
const options = response.data.Records.map((record: any) => {
|
||||
const displayName = record[primaryField] ||
|
||||
record.name || record.title || record.subject ||
|
||||
record.firstname || record.lastname || record.email ||
|
||||
record.accountname || record.contactname ||
|
||||
`Record ${record[primaryKey]?.substring(0, 8) || 'Unknown'}`;
|
||||
|
||||
return {
|
||||
label: displayName,
|
||||
value: record[primaryKey],
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: options.slice(0, 50),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: [],
|
||||
placeholder: 'Error loading records',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const updateFields = Property.DynamicProperties({
|
||||
displayName: 'Fields to Update',
|
||||
refreshers: ['objectType'],
|
||||
required: true,
|
||||
auth: fireberryAuth,
|
||||
props: async ({ auth, objectType }) => {
|
||||
if (!auth || !objectType) return {};
|
||||
|
||||
const objectTypeStr = typeof objectType === 'string' ? objectType : (objectType as { value: string })?.value;
|
||||
const client = new FireberryClient(auth);
|
||||
|
||||
try {
|
||||
const metadata = await client.getObjectFieldsMetadata(objectTypeStr);
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
const fieldTypeMap: Record<string, string> = {
|
||||
'a1e7ed6f-5083-477b-b44c-9943a6181359': 'text',
|
||||
'ce972d02-5013-46d4-9d1d-f09df1ac346a': 'datetime',
|
||||
'6a34bfe3-fece-4da1-9136-a7b1e5ae3319': 'number',
|
||||
'a8fcdf65-91bc-46fd-82f6-1234758345a1': 'lookup',
|
||||
'b4919f2e-2996-48e4-a03c-ba39fb64386c': 'picklist',
|
||||
'80108f9d-1e75-40fa-9fa9-02be4ddc1da1': 'longtext',
|
||||
};
|
||||
|
||||
const picklistCache: Record<string, any> = {};
|
||||
const picklistFields = metadata.data.filter(field =>
|
||||
fieldTypeMap[field.systemFieldTypeId] === 'picklist'
|
||||
);
|
||||
|
||||
for (const field of picklistFields) {
|
||||
const largeLists = ['objecttypecode', 'resultcode'];
|
||||
if (!largeLists.includes(field.fieldName.toLowerCase())) {
|
||||
try {
|
||||
const picklistData = await client.getPicklistValues(objectTypeStr, field.fieldName);
|
||||
if (picklistData.data?.values && Array.isArray(picklistData.data.values)) {
|
||||
picklistCache[field.fieldName] = picklistData.data.values;
|
||||
}
|
||||
} catch (error) {
|
||||
picklistCache[field.fieldName] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of metadata.data) {
|
||||
const systemFields = ['createdby', 'modifiedby', 'deletedby', 'createdon', 'modifiedon', 'deletedon'];
|
||||
if (field.fieldName.endsWith('id') && !field.label) {
|
||||
continue;
|
||||
}
|
||||
if (systemFields.includes(field.fieldName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldType = fieldTypeMap[field.systemFieldTypeId] || 'text';
|
||||
const isRequired = false;
|
||||
|
||||
switch (fieldType) {
|
||||
case 'text':
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Leave empty to keep current value',
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
props[field.fieldName] = Property.Number({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Leave empty to keep current value',
|
||||
});
|
||||
break;
|
||||
case 'datetime':
|
||||
props[field.fieldName] = Property.DateTime({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Leave empty to keep current value',
|
||||
});
|
||||
break;
|
||||
case 'picklist': {
|
||||
const largeLists = ['objecttypecode', 'resultcode'];
|
||||
if (largeLists.includes(field.fieldName.toLowerCase())) {
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Enter numeric value (leave empty to keep current)',
|
||||
});
|
||||
} else {
|
||||
const values = picklistCache[field.fieldName] || [];
|
||||
if (values.length > 0 && values.length <= 20) {
|
||||
const options = values.map((option: any) => ({
|
||||
label: option.name || option.value,
|
||||
value: option.value,
|
||||
}));
|
||||
|
||||
props[field.fieldName] = Property.StaticDropdown({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
options: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{ label: '-- Keep Current Value --', value: '' },
|
||||
...options
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: values.length > 20
|
||||
? `Enter numeric value (${values.length} options available, leave empty to keep current)`
|
||||
: 'Enter value (leave empty to keep current)',
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'longtext': {
|
||||
props[field.fieldName] = Property.LongText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Leave empty to keep current value',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'lookup': {
|
||||
let description = 'Record ID (leave empty to keep current)';
|
||||
if (field.fieldName.includes('account')) description = 'Account record ID (leave empty to keep current)';
|
||||
else if (field.fieldName.includes('contact')) description = 'Contact record ID (leave empty to keep current)';
|
||||
else if (field.fieldName.includes('owner')) description = 'User record ID for owner (leave empty to keep current)';
|
||||
else if (field.fieldName.includes('product')) description = 'Product record ID (leave empty to keep current)';
|
||||
else if (field.fieldName.includes('user')) description = 'User record ID (leave empty to keep current)';
|
||||
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Leave empty to keep current value',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return props;
|
||||
} catch (error) {
|
||||
console.error('Error fetching update fields:', error);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const updateRecordAction = createAction({
|
||||
name: 'update_record',
|
||||
displayName: 'Update Record',
|
||||
description: 'Update an existing record in Fireberry.',
|
||||
auth: fireberryAuth,
|
||||
props: {
|
||||
objectType: objectTypeDropdown,
|
||||
recordId: recordDropdown,
|
||||
fields: updateFields,
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType, recordId, fields } = propsValue;
|
||||
|
||||
if (!recordId) {
|
||||
throw new Error('Record ID is required');
|
||||
}
|
||||
|
||||
const fieldsObj = typeof fields === 'string' ? JSON.parse(fields) : fields;
|
||||
|
||||
if (typeof fieldsObj !== 'object' || fieldsObj === null) {
|
||||
throw new Error('Fields must be an object');
|
||||
}
|
||||
|
||||
const updateData: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(fieldsObj)) {
|
||||
if (value !== '' && value !== null && value !== undefined) {
|
||||
updateData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const recordToUpdate = { id: recordId, record: updateData };
|
||||
|
||||
return await client.batchUpdate(objectType as string, [recordToUpdate]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { HttpMethod, httpClient, HttpRequest, AuthenticationType } from '@activepieces/pieces-common';
|
||||
import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { fireberryAuth } from '../..';
|
||||
|
||||
const FIREBERRY_API_BASE_URL = 'https://api.fireberry.com';
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY_MS = 1000;
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function normalizeQueryParams(params?: Record<string, string | number | boolean>): Record<string, string> | undefined {
|
||||
if (!params) return undefined;
|
||||
const result: Record<string, string> = {};
|
||||
for (const key in params) {
|
||||
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
||||
result[key] = String(params[key]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class FireberryClient {
|
||||
private apiKey: string;
|
||||
|
||||
constructor(apiKey: AppConnectionValueForAuthProperty<typeof fireberryAuth>) {
|
||||
this.apiKey = apiKey.secret_text;
|
||||
}
|
||||
|
||||
private parseError(error: any): string {
|
||||
if (error?.response?.body) {
|
||||
try {
|
||||
const body = typeof error.response.body === 'string' ? JSON.parse(error.response.body) : error.response.body;
|
||||
if (body?.error) {
|
||||
if (typeof body.error === 'string') return body.error;
|
||||
if (body.error.message) return body.error.message;
|
||||
}
|
||||
if (body?.message) return body.message;
|
||||
} catch {
|
||||
return 'Unknown error';
|
||||
}
|
||||
}
|
||||
if (error?.message) return error.message;
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
private shouldRetry(status: number): boolean {
|
||||
return status === 429 || (status >= 500 && status < 600);
|
||||
}
|
||||
|
||||
async request<T = unknown>({
|
||||
method,
|
||||
resourceUri,
|
||||
body,
|
||||
queryParams,
|
||||
}: {
|
||||
method: HttpMethod;
|
||||
resourceUri: string;
|
||||
body?: unknown;
|
||||
queryParams?: Record<string, string | number | boolean>;
|
||||
}): Promise<T> {
|
||||
const request: HttpRequest = {
|
||||
method,
|
||||
url: `${FIREBERRY_API_BASE_URL}${resourceUri}`,
|
||||
headers: {
|
||||
'tokenid': this.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
queryParams: normalizeQueryParams(queryParams),
|
||||
timeout: 10000, // 10 seconds
|
||||
};
|
||||
let lastError: any = null;
|
||||
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await httpClient.sendRequest<T>(request);
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.body;
|
||||
} else {
|
||||
const errorMsg = this.parseError({ response });
|
||||
if (this.shouldRetry(response.status) && attempt < MAX_RETRIES - 1) {
|
||||
await delay(RETRY_DELAY_MS * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Fireberry API error (${response.status}): ${errorMsg}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const status = error?.response?.status;
|
||||
if (status && this.shouldRetry(status) && attempt < MAX_RETRIES - 1) {
|
||||
await delay(RETRY_DELAY_MS * (attempt + 1));
|
||||
lastError = error;
|
||||
continue;
|
||||
}
|
||||
const errorMsg = this.parseError(error);
|
||||
throw new Error(`Fireberry API error${status ? ` (${status})` : ''}: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Fireberry API error: ${this.parseError(lastError)}`);
|
||||
}
|
||||
|
||||
async getObjectsMetadata(): Promise<{ success: boolean; data: Array<{ name: string; systemName: string; objectType: string }> }> {
|
||||
return this.request<{ success: boolean; data: Array<{ name: string; systemName: string; objectType: string }> }>({
|
||||
method: HttpMethod.GET,
|
||||
resourceUri: '/metadata/records',
|
||||
});
|
||||
}
|
||||
|
||||
async getObjectFieldsMetadata(object: string): Promise<{ success: boolean; data: Array<{ label: string; fieldName: string; systemFieldTypeId: string; systemName: string }> }> {
|
||||
return this.request<{ success: boolean; data: Array<{ label: string; fieldName: string; systemFieldTypeId: string; systemName: string }> }>({
|
||||
method: HttpMethod.GET,
|
||||
resourceUri: `/metadata/records/${object}/fields`,
|
||||
});
|
||||
}
|
||||
|
||||
async getPicklistValues(object: string, fieldName: string): Promise<{ success: boolean; data: { values: Array<{ name: string; value: string }> } }> {
|
||||
return this.request<{ success: boolean; data: { values: Array<{ name: string; value: string }> } }>({
|
||||
method: HttpMethod.GET,
|
||||
resourceUri: `/metadata/records/${object}/fields/${fieldName}/values`,
|
||||
});
|
||||
}
|
||||
|
||||
async batchCreate(object: string, records: any[]): Promise<any> {
|
||||
return this.request({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: `/api/v3/record/${object}/batch/create`,
|
||||
body: { data: records },
|
||||
});
|
||||
}
|
||||
|
||||
async batchUpdate(object: string, records: any[]): Promise<any> {
|
||||
return this.request({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: `/api/v3/record/${object}/batch/update`,
|
||||
body: { data: records },
|
||||
});
|
||||
}
|
||||
|
||||
async batchDelete(object: string, ids: string[]): Promise<any> {
|
||||
return this.request({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: `/api/v3/record/${object}/batch/delete`,
|
||||
body: { data: ids },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { FireberryClient } from './client';
|
||||
import { fireberryAuth } from '../..';
|
||||
|
||||
export const objectTypeDropdown = Property.Dropdown({
|
||||
displayName: 'Object Type',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: fireberryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Connect your Fireberry account',
|
||||
};
|
||||
}
|
||||
const client = new FireberryClient(auth);
|
||||
const metadata = await client.getObjectsMetadata();
|
||||
const options = metadata.data.map(obj => ({
|
||||
label: obj.name,
|
||||
value: obj.systemName,
|
||||
}));
|
||||
return {
|
||||
disabled: false,
|
||||
options,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const objectFields = Property.DynamicProperties({
|
||||
displayName: 'Fields',
|
||||
refreshers: ['objectType'],
|
||||
required: true,
|
||||
auth: fireberryAuth,
|
||||
props: async ({ auth, objectType }) => {
|
||||
if (!auth || !objectType) return {};
|
||||
const objectTypeStr = typeof objectType === 'string' ? objectType : (objectType as { value: string })?.value;
|
||||
const client = new FireberryClient(auth);
|
||||
const metadata = await client.getObjectFieldsMetadata(objectTypeStr);
|
||||
const props: Record<string, any> = {};
|
||||
|
||||
const fieldTypeMap: Record<string, string> = {
|
||||
'a1e7ed6f-5083-477b-b44c-9943a6181359': 'text',
|
||||
'ce972d02-5013-46d4-9d1d-f09df1ac346a': 'datetime',
|
||||
'6a34bfe3-fece-4da1-9136-a7b1e5ae3319': 'number',
|
||||
'a8fcdf65-91bc-46fd-82f6-1234758345a1': 'lookup',
|
||||
'b4919f2e-2996-48e4-a03c-ba39fb64386c': 'picklist',
|
||||
'80108f9d-1e75-40fa-9fa9-02be4ddc1da1': 'longtext',
|
||||
};
|
||||
|
||||
const picklistCache: Record<string, any> = {};
|
||||
const picklistFields = metadata.data.filter(field =>
|
||||
fieldTypeMap[field.systemFieldTypeId] === 'picklist'
|
||||
);
|
||||
|
||||
for (const field of picklistFields) {
|
||||
const largeLists = ['objecttypecode', 'resultcode'];
|
||||
if (!largeLists.includes(field.fieldName.toLowerCase())) {
|
||||
try {
|
||||
const picklistData = await client.getPicklistValues(objectTypeStr, field.fieldName);
|
||||
if (picklistData.data?.values && Array.isArray(picklistData.data.values)) {
|
||||
picklistCache[field.fieldName] = picklistData.data.values;
|
||||
}
|
||||
} catch (error) {
|
||||
picklistCache[field.fieldName] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of metadata.data) {
|
||||
const systemFields = ['createdby', 'modifiedby', 'deletedby', 'createdon', 'modifiedon', 'deletedon'];
|
||||
if (field.fieldName.endsWith('id') && !field.label) {
|
||||
continue;
|
||||
}
|
||||
if (systemFields.includes(field.fieldName.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldType = fieldTypeMap[field.systemFieldTypeId] || 'text';
|
||||
const isRequired = false;
|
||||
|
||||
switch (fieldType) {
|
||||
case 'text':
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
});
|
||||
break;
|
||||
case 'number':
|
||||
props[field.fieldName] = Property.Number({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
});
|
||||
break;
|
||||
case 'datetime':
|
||||
props[field.fieldName] = Property.DateTime({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Date and time in UTC format',
|
||||
});
|
||||
break;
|
||||
case 'picklist':{
|
||||
const largeLists = ['objecttypecode', 'resultcode'];
|
||||
if (largeLists.includes(field.fieldName.toLowerCase())) {
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Enter the numeric value for this field',
|
||||
});
|
||||
} else {
|
||||
const values = picklistCache[field.fieldName] || [];
|
||||
if (values.length > 0 && values.length <= 20) {
|
||||
const options = values.map((option: any) => ({
|
||||
label: option.name || option.value,
|
||||
value: option.value,
|
||||
}));
|
||||
|
||||
props[field.fieldName] = Property.StaticDropdown({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
options: {
|
||||
disabled: false,
|
||||
options,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: values.length > 20
|
||||
? `Enter numeric value (${values.length} options available)`
|
||||
: 'Enter the value for this field',
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'longtext':{
|
||||
props[field.fieldName] = Property.LongText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: 'Long text content',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'lookup':{
|
||||
let description = 'Record ID (GUID)';
|
||||
if (field.fieldName.includes('account')) description = 'Account record ID';
|
||||
else if (field.fieldName.includes('contact')) description = 'Contact record ID';
|
||||
else if (field.fieldName.includes('owner')) description = 'User record ID for owner';
|
||||
else if (field.fieldName.includes('product')) description = 'Product record ID';
|
||||
else if (field.fieldName.includes('user')) description = 'User record ID';
|
||||
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:{
|
||||
props[field.fieldName] = Property.ShortText({
|
||||
displayName: field.label || field.fieldName,
|
||||
required: isRequired,
|
||||
description: `${fieldType} field`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return props;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,216 @@
|
||||
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { fireberryAuth } from '../../index';
|
||||
import { objectTypeDropdown } from '../common/props';
|
||||
import { FireberryClient } from '../common/client';
|
||||
|
||||
export const recordCreatedOrUpdatedTrigger = createTrigger({
|
||||
name: 'record_created_or_updated',
|
||||
displayName: 'Record Created or Updated',
|
||||
description: 'Fires when a record is created or updated in Fireberry.',
|
||||
auth: fireberryAuth,
|
||||
props: {
|
||||
objectType: objectTypeDropdown,
|
||||
triggerType: Property.StaticDropdown({
|
||||
displayName: 'Trigger Type',
|
||||
required: true,
|
||||
defaultValue: 'both',
|
||||
options: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{ label: 'Created or Updated', value: 'both' },
|
||||
{ label: 'Created Only', value: 'created' },
|
||||
{ label: 'Updated Only', value: 'updated' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
lookbackMinutes: Property.Number({
|
||||
displayName: 'Lookback Period (minutes)',
|
||||
required: false,
|
||||
defaultValue: 60,
|
||||
description: 'How far back to look for records on first run (default: 60 minutes)',
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
sampleData: {
|
||||
accountname: "Sample Account",
|
||||
emailaddress1: "sample@example.com",
|
||||
createdon: "2025-07-17T10:39:30.003",
|
||||
modifiedon: "2025-07-17T10:39:30.003",
|
||||
accountid: "12345678-1234-1234-1234-123456789abc",
|
||||
},
|
||||
async test({ auth, propsValue }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType } = propsValue;
|
||||
|
||||
if (!objectType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const objectsMetadata = await client.getObjectsMetadata();
|
||||
const targetObject = objectsMetadata.data.find(obj => obj.systemName === objectType);
|
||||
|
||||
if (!targetObject) {
|
||||
throw new Error(`Object type '${objectType}' not found`);
|
||||
}
|
||||
|
||||
const objectNumber = parseInt(targetObject.objectType);
|
||||
|
||||
const sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
||||
|
||||
const response = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
ObjectName: string;
|
||||
SystemName: string;
|
||||
ObjectType: number;
|
||||
PrimaryKey: string;
|
||||
PrimaryField: string;
|
||||
PageNum: number;
|
||||
SortBy: string;
|
||||
SortBy_Desc: boolean;
|
||||
IsLastPage: boolean;
|
||||
Columns: Array<Record<string, any>>;
|
||||
Data: Array<Record<string, any>>;
|
||||
};
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: '/api/query',
|
||||
body: {
|
||||
objecttype: objectNumber,
|
||||
query: `modifiedon >= '${sevenDaysAgo}'`,
|
||||
sort_by: 'modifiedon',
|
||||
sort_type: 'desc',
|
||||
page_size: 3,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to fetch sample data from Fireberry');
|
||||
}
|
||||
|
||||
const records = response.data.Data || [];
|
||||
|
||||
if (records.length === 0) {
|
||||
const fallbackResponse = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
Data: Array<Record<string, any>>;
|
||||
};
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: '/api/query',
|
||||
body: {
|
||||
objecttype: objectNumber,
|
||||
sort_by: 'modifiedon',
|
||||
sort_type: 'desc',
|
||||
page_size: 3,
|
||||
},
|
||||
});
|
||||
|
||||
if (fallbackResponse.success && fallbackResponse.data.Data) {
|
||||
return fallbackResponse.data.Data;
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to generate sample data:', error);
|
||||
return [{
|
||||
[objectType === 'Contact' ? 'firstname' : 'accountname']: 'Sample Record',
|
||||
createdon: new Date().toISOString(),
|
||||
modifiedon: new Date().toISOString(),
|
||||
}];
|
||||
}
|
||||
},
|
||||
async onEnable({ store }) {
|
||||
await store.put('lastPollTime', new Date().toISOString());
|
||||
},
|
||||
async onDisable({ store }) {
|
||||
await store.delete('lastPollTime');
|
||||
},
|
||||
async run({ auth, propsValue, store }) {
|
||||
const client = new FireberryClient(auth);
|
||||
const { objectType, triggerType, lookbackMinutes } = propsValue;
|
||||
|
||||
let lastPollTime = await store.get<string>('lastPollTime');
|
||||
|
||||
if (!lastPollTime) {
|
||||
const lookback = lookbackMinutes || 60;
|
||||
const cutoffTime = new Date(Date.now() - (lookback * 60 * 1000));
|
||||
lastPollTime = cutoffTime.toISOString();
|
||||
}
|
||||
|
||||
const objectsMetadata = await client.getObjectsMetadata();
|
||||
const targetObject = objectsMetadata.data.find(obj => obj.systemName === objectType);
|
||||
|
||||
if (!targetObject) {
|
||||
throw new Error(`Object type '${objectType}' not found`);
|
||||
}
|
||||
|
||||
const objectNumber = parseInt(targetObject.objectType);
|
||||
|
||||
let query = '';
|
||||
const cutoffDate = new Date(lastPollTime).toISOString().split('T')[0];
|
||||
|
||||
if (triggerType === 'created') {
|
||||
query = `createdon >= '${cutoffDate}'`;
|
||||
} else if (triggerType === 'updated') {
|
||||
query = `modifiedon >= '${cutoffDate}'`;
|
||||
} else {
|
||||
query = `modifiedon >= '${cutoffDate}'`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.request<{
|
||||
success: boolean;
|
||||
data: {
|
||||
ObjectName: string;
|
||||
SystemName: string;
|
||||
ObjectType: number;
|
||||
PrimaryKey: string;
|
||||
PrimaryField: string;
|
||||
PageNum: number;
|
||||
SortBy: string;
|
||||
SortBy_Desc: boolean;
|
||||
IsLastPage: boolean;
|
||||
Columns: Array<Record<string, any>>;
|
||||
Data: Array<Record<string, any>>;
|
||||
};
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
resourceUri: '/api/query',
|
||||
body: {
|
||||
objecttype: objectNumber,
|
||||
query: query,
|
||||
sort_by: 'modifiedon',
|
||||
sort_type: 'desc',
|
||||
page_size: 50,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to fetch records from Fireberry');
|
||||
}
|
||||
|
||||
const records = response.data.Data || [];
|
||||
|
||||
await store.put('lastPollTime', new Date().toISOString());
|
||||
|
||||
return records.reverse();
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('429')) {
|
||||
throw new Error('Rate limit exceeded. Please try again later.');
|
||||
}
|
||||
if (error.message?.includes('401') || error.message?.includes('403')) {
|
||||
throw new Error('Authentication failed. Please check your Fireberry API key.');
|
||||
}
|
||||
|
||||
console.error('Fireberry trigger error:', error);
|
||||
throw new Error(`Failed to fetch records: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user