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,53 @@
|
||||
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import { instanceLogin, recordProperty } from '../common';
|
||||
import { elementTypeProperty } from '../common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const createRecord = createAction({
|
||||
name: 'create_record',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Create Record',
|
||||
description: 'Create a Record',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
record: recordProperty(),
|
||||
},
|
||||
async run({ propsValue: { elementType, record }, auth }) {
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
|
||||
if (instance !== null) {
|
||||
const response = await httpClient.sendRequest<Record<string, unknown>[]>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
operation: 'create',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType,
|
||||
element: JSON.stringify(record),
|
||||
},
|
||||
});
|
||||
|
||||
console.debug({
|
||||
operation: 'create',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType,
|
||||
element: JSON.stringify(record),
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import { instanceLogin, recordIdProperty } from '../common';
|
||||
import { elementTypeProperty } from '../common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const deleteRecord = createAction({
|
||||
name: 'delete_record',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Delete Record',
|
||||
description: 'Delete a Record',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
record: recordIdProperty(),
|
||||
},
|
||||
async run({
|
||||
propsValue: { elementType, record },
|
||||
auth
|
||||
}) {
|
||||
const instance = await instanceLogin(auth.props.instance_url, auth.props.username, auth.props.password);
|
||||
|
||||
if (instance !== null) {
|
||||
const response = await httpClient.sendRequest<Record<string, unknown>[]>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
operation: 'delete',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType,
|
||||
id: record['id'],
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import { instanceLogin, recordIdProperty } from '../common';
|
||||
import { elementTypeProperty } from '../common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const getRecord = createAction({
|
||||
name: 'get_record',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Get Record',
|
||||
description: 'Get a Record by value',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
record: recordIdProperty(),
|
||||
},
|
||||
async run({
|
||||
propsValue: { elementType, record },
|
||||
auth
|
||||
}) {
|
||||
const instance = await instanceLogin(auth.props.instance_url, auth.props.username, auth.props.password);
|
||||
|
||||
if (instance !== null) {
|
||||
const response = await httpClient.sendRequest<Record<string, unknown>[]>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
operation: 'retrieve',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType as unknown as string,
|
||||
...record,
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import { instanceLogin } from '../common';
|
||||
import {
|
||||
AuthenticationType,
|
||||
HttpHeaders,
|
||||
HttpMessageBody,
|
||||
HttpMethod,
|
||||
HttpRequest,
|
||||
httpClient,
|
||||
} from '@activepieces/pieces-common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const makeAPICall = createAction({
|
||||
name: 'make_api_call',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Custom API Call (Deprecated)',
|
||||
description: 'Performs an arbitrary authorized API call. (Deprecated)',
|
||||
props: {
|
||||
method: Property.StaticDropdown<HttpMethod>({
|
||||
displayName: 'Http Method',
|
||||
description: 'Select the HTTP method you want to use',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'GET', value: HttpMethod.GET },
|
||||
{ label: 'POST', value: HttpMethod.POST },
|
||||
{ label: 'PUT', value: HttpMethod.PUT },
|
||||
{ label: 'PATCH', value: HttpMethod.PATCH },
|
||||
{ label: 'DELETE', value: HttpMethod.DELETE },
|
||||
],
|
||||
},
|
||||
}),
|
||||
url: Property.ShortText({
|
||||
displayName: 'URL',
|
||||
description:
|
||||
'Absolute URL or path. If a relative path is provided (e.g., /me, /listtypes, /describe), it will be called against the REST base.',
|
||||
required: false,
|
||||
}),
|
||||
urlPath: Property.ShortText({
|
||||
displayName: 'URL Path (deprecated)',
|
||||
description:
|
||||
"Deprecated. Use 'URL' instead. API endpoint's URL path (example: /me, /listtypes, /describe)",
|
||||
required: false,
|
||||
}),
|
||||
headers: Property.Json({
|
||||
displayName: 'Headers',
|
||||
description: `Enter the desired request headers. Skip the authorization headers`,
|
||||
required: false,
|
||||
defaultValue: {},
|
||||
}),
|
||||
data: Property.Json({
|
||||
displayName: 'Data',
|
||||
description: `Enter the data to pass. if its POST, it will be sent as body data, and if GET, as query string`,
|
||||
required: false,
|
||||
defaultValue: {},
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const method = propsValue.method ?? HttpMethod.GET;
|
||||
const urlPath = propsValue.urlPath;
|
||||
const url = propsValue.url;
|
||||
|
||||
if (urlPath && !urlPath.startsWith('/')) {
|
||||
return {
|
||||
error:
|
||||
'URL path must start with a slash, example: /me, /listtypes, /describe',
|
||||
};
|
||||
}
|
||||
|
||||
let finalUrl = `${auth.props.instance_url}/webservice.php`;
|
||||
let useRestAuth = false;
|
||||
|
||||
if (url) {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
finalUrl = url;
|
||||
} else if (url.startsWith('/')) {
|
||||
finalUrl = `${auth.props.instance_url}/restapi/v1/vtiger/default${url}`;
|
||||
useRestAuth = true;
|
||||
} else {
|
||||
finalUrl = `${auth.props.instance_url}/restapi/v1/vtiger/default/${url}`;
|
||||
useRestAuth = true;
|
||||
}
|
||||
} else if (urlPath) {
|
||||
finalUrl = `${auth.props.instance_url}/restapi/v1/vtiger/default${urlPath}`;
|
||||
useRestAuth = true;
|
||||
}
|
||||
|
||||
const normalizeHeaders = (h: unknown): HttpHeaders => {
|
||||
const out: HttpHeaders = {};
|
||||
if (h && typeof h === 'object' && !Array.isArray(h)) {
|
||||
for (const [k, v] of Object.entries(h as Record<string, unknown>)) {
|
||||
if (v === undefined || v === null) {
|
||||
out[k] = undefined;
|
||||
} else if (Array.isArray(v)) {
|
||||
out[k] = (v as unknown[]).map((x) => String(x));
|
||||
} else if (typeof v === 'string') {
|
||||
out[k] = v;
|
||||
} else {
|
||||
out[k] = String(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const headers: HttpHeaders = normalizeHeaders(propsValue.headers);
|
||||
|
||||
if (useRestAuth) {
|
||||
// Default JSON for REST when not GET and no explicit content-type provided
|
||||
if (
|
||||
method !== HttpMethod.GET &&
|
||||
!Object.keys(headers).some(
|
||||
(k) => k.toLowerCase() === 'content-type'
|
||||
)
|
||||
) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
} else {
|
||||
// webservice.php defaults to urlencoded for POST operations
|
||||
if (
|
||||
method !== HttpMethod.GET &&
|
||||
!Object.keys(headers).some(
|
||||
(k) => k.toLowerCase() === 'content-type'
|
||||
)
|
||||
) {
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
|
||||
const httpRequest: HttpRequest<HttpMessageBody> = {
|
||||
url: finalUrl,
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
let data: Record<string, unknown> = propsValue.data ?? {};
|
||||
|
||||
const toQueryParams = (obj: Record<string, unknown>): Record<string, string> => {
|
||||
const qp: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(obj ?? {})) {
|
||||
if (v === undefined || v === null) continue;
|
||||
qp[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
||||
}
|
||||
return qp;
|
||||
};
|
||||
|
||||
if (useRestAuth) {
|
||||
httpRequest.authentication = {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
};
|
||||
} else {
|
||||
const vtigerInstance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (vtigerInstance === null) return;
|
||||
|
||||
data = {
|
||||
sessionName: vtigerInstance.sessionId ?? vtigerInstance.sessionName,
|
||||
...(propsValue.data ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
if (method === HttpMethod.GET) {
|
||||
httpRequest['queryParams'] = toQueryParams(data);
|
||||
} else {
|
||||
// For REST with JSON default, send raw object; else url-encode
|
||||
const contentType = Object.entries(headers).find(([k]) => k.toLowerCase() === 'content-type')?.[1];
|
||||
const ct = Array.isArray(contentType) ? contentType[0] : contentType;
|
||||
if (useRestAuth && ct === 'application/json') {
|
||||
httpRequest['body'] = data;
|
||||
} else {
|
||||
httpRequest['body'] = toQueryParams(data);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<Record<string, unknown>[]>(
|
||||
httpRequest
|
||||
);
|
||||
|
||||
if ([200, 201].includes(response.status)) {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return {
|
||||
error: 'Unexpected outcome!',
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import {
|
||||
Operation,
|
||||
elementTypeProperty,
|
||||
instanceLogin,
|
||||
prepareHttpRequest,
|
||||
} from '../common';
|
||||
import { httpClient } from '@activepieces/pieces-common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const queryRecords = createAction({
|
||||
name: 'query_records',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Query Records',
|
||||
description: 'Query records by SQL statement.',
|
||||
props: {
|
||||
query: Property.LongText({
|
||||
displayName: 'Query',
|
||||
description:
|
||||
'Enter the query statement, e.g. SELECT count(*) FROM Contacts;',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const vtigerInstance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (vtigerInstance === null) return;
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, unknown>[];
|
||||
}>(
|
||||
prepareHttpRequest(
|
||||
auth.props.instance_url,
|
||||
vtigerInstance.sessionId ?? vtigerInstance.sessionName,
|
||||
'query' as Operation,
|
||||
{ query: propsValue.query }
|
||||
)
|
||||
);
|
||||
|
||||
if (response.body.success) {
|
||||
return response.body.result;
|
||||
} else {
|
||||
console.debug(response);
|
||||
|
||||
return {
|
||||
error: 'Unexpected outcome!',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
createAction,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import {
|
||||
VTigerAuthValue,
|
||||
queryRecords,
|
||||
countRecords,
|
||||
elementTypeProperty,
|
||||
generateElementFields,
|
||||
instanceLogin,
|
||||
} from '../common';
|
||||
|
||||
//Docs: https://code.vtiger.com/vtiger/vtigercrm-manual/-/wikis/Webservice-Docs
|
||||
//Extra: https://help.vtiger.com/article/147111249-Rest-API-Manual
|
||||
|
||||
export const searchRecords = createAction({
|
||||
name: 'search_records',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Search Records',
|
||||
description: 'Search for a record.',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
fields: Property.DynamicProperties({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Search Fields',
|
||||
description: 'Enter your filter criteria',
|
||||
required: true,
|
||||
refreshers: ['elementType'],
|
||||
props: async ({ auth, elementType }) => {
|
||||
if (!auth || !elementType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
|
||||
if (instance === null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return generateElementFields(
|
||||
auth,
|
||||
elementType as unknown as string,
|
||||
{},
|
||||
true
|
||||
);
|
||||
},
|
||||
}),
|
||||
limit: Property.Number({
|
||||
displayName: 'Limit',
|
||||
description: 'Enter the maximum number of records to return.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const vtigerInstance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (vtigerInstance === null) return;
|
||||
|
||||
const count = await countRecords(auth, propsValue.elementType as string);
|
||||
if (count > 0) {
|
||||
const records: Record<string, unknown>[] = await queryRecords(
|
||||
auth,
|
||||
propsValue.elementType as string,
|
||||
0,
|
||||
count
|
||||
);
|
||||
|
||||
const filtered = records.filter((record) => {
|
||||
return Object.entries(propsValue['fields']).every(([key, value]) => {
|
||||
const recordValue = record[key];
|
||||
if (typeof value === 'string') {
|
||||
const rv = typeof recordValue === 'string' ? recordValue : String(recordValue ?? '');
|
||||
return rv.toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
return recordValue === value;
|
||||
});
|
||||
});
|
||||
|
||||
return propsValue.limit ? filtered.slice(0, propsValue.limit) : filtered;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
|
||||
import {
|
||||
DropdownState,
|
||||
DynamicPropsValue,
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
createAction,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import {
|
||||
instanceLogin,
|
||||
VTigerAuthValue,
|
||||
Modules,
|
||||
Field,
|
||||
getRecordReference,
|
||||
} from '../common';
|
||||
import { elementTypeProperty } from '../common';
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export const updateRecord = createAction({
|
||||
name: 'update_record',
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Update Record',
|
||||
description: 'Update a Record',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
id: Property.Dropdown({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Id',
|
||||
description: "The record's id",
|
||||
required: true,
|
||||
refreshers: ['elementType'],
|
||||
options: async ({ auth, elementType }) => {
|
||||
if (!auth || !elementType) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder:
|
||||
'Please select the element type and setup authentication to continue.',
|
||||
};
|
||||
}
|
||||
|
||||
let c = 0;
|
||||
let instance = null;
|
||||
while (!instance && c < 3) {
|
||||
instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
await sleep(1500);
|
||||
c++;
|
||||
}
|
||||
|
||||
if (!instance) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Authentication failed.',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, string>[];
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
operation: 'query',
|
||||
elementType: elementType as unknown as string,
|
||||
query: `SELECT * FROM ${elementType} LIMIT 100;`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.body.success)
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Request unsuccessful.',
|
||||
};
|
||||
|
||||
const element: string = elementType as unknown as string;
|
||||
|
||||
return {
|
||||
options: await Promise.all(response.body.result.map(async (record) => {
|
||||
return {
|
||||
label: await Modules[element]?.(record) || record['id'],
|
||||
value: record['id'] as string,
|
||||
};
|
||||
})),
|
||||
disabled: false,
|
||||
};
|
||||
},
|
||||
}),
|
||||
record: Property.DynamicProperties({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Record Fields',
|
||||
description: 'Add new fields to be created in the new record',
|
||||
required: true,
|
||||
refreshers: ['id', 'elementType'],
|
||||
props: async ({ auth, id, elementType }) => {
|
||||
if (!auth || !elementType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (!instance) return {};
|
||||
|
||||
let defaultValue: Record<string, unknown>;
|
||||
if (id && 'id') {
|
||||
const retrieve_response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, unknown>;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
operation: 'retrieve',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType as unknown as string,
|
||||
id: id as unknown as string,
|
||||
},
|
||||
});
|
||||
if (retrieve_response.body.result) {
|
||||
defaultValue = retrieve_response.body.result;
|
||||
} else {
|
||||
defaultValue = {};
|
||||
}
|
||||
} else {
|
||||
defaultValue = {};
|
||||
}
|
||||
|
||||
const describe_response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: { fields: Field[] };
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
operation: 'describe',
|
||||
elementType: elementType as unknown as string,
|
||||
},
|
||||
});
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
|
||||
if (describe_response.body.success) {
|
||||
let limit = 30; // Limit to show 30 input property, more than this will cause frontend unresponsive
|
||||
|
||||
const generateField = async (field: Field) => {
|
||||
const params = {
|
||||
displayName: field.label,
|
||||
description: `Field ${field.name} of object type ${elementType}`,
|
||||
required: field.mandatory,
|
||||
};
|
||||
|
||||
if (
|
||||
[
|
||||
'string',
|
||||
'text',
|
||||
'mediumtext',
|
||||
'phone',
|
||||
'url',
|
||||
'email',
|
||||
].includes(field.type.name)
|
||||
) {
|
||||
if (['mediumtext', 'url'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.LongText({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
});
|
||||
} else {
|
||||
fields[field.name] = Property.ShortText({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
['picklist', 'reference', 'owner'].includes(field.type.name)
|
||||
) {
|
||||
let options: DropdownState<string>;
|
||||
if (field.type.name === 'picklist') {
|
||||
options = {
|
||||
disabled: false,
|
||||
options: field.type.picklistValues ?? [],
|
||||
};
|
||||
} else if (field.type.name === 'owner') {
|
||||
options = await getRecordReference(
|
||||
auth,
|
||||
['Users']
|
||||
);
|
||||
} else if (field.type.refersTo) {
|
||||
options = await getRecordReference(
|
||||
auth,
|
||||
field.type.refersTo ?? []
|
||||
);
|
||||
} else {
|
||||
options = { disabled: false, options: [] };
|
||||
}
|
||||
|
||||
fields[field.name] = Property.StaticDropdown({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
options,
|
||||
});
|
||||
} else if (
|
||||
['double', 'integer', 'currency'].includes(field.type.name)
|
||||
) {
|
||||
fields[field.name] = Property.Number({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as number,
|
||||
});
|
||||
} else if (['boolean'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.Checkbox({
|
||||
displayName: field.label,
|
||||
description: `The fields to fill in the object type ${elementType}`,
|
||||
required: field.mandatory,
|
||||
defaultValue: defaultValue?.[field.name] ? true : false,
|
||||
});
|
||||
} else if (['date', 'datetime', 'time'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.DateTime({
|
||||
displayName: field.label,
|
||||
description: `The fields to fill in the object type ${elementType}`,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
required: field.mandatory,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const skipFields = [
|
||||
'id',
|
||||
];
|
||||
|
||||
// Prioritize mandatory fields
|
||||
for (const field of describe_response.body.result.fields) {
|
||||
if (skipFields.includes(field.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.mandatory) {
|
||||
await generateField(field);
|
||||
limit--;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's add the rest...
|
||||
for (const field of describe_response.body.result.fields) {
|
||||
if (skipFields.includes(field.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the rest of field to avoid unresponsive frontend
|
||||
if (limit < 0) break;
|
||||
|
||||
if (!field.mandatory) {
|
||||
await generateField(field);
|
||||
limit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run({ propsValue: { elementType, id, record }, auth }) {
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
|
||||
if (instance !== null) {
|
||||
const response = await httpClient.sendRequest<Record<string, unknown>[]>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
operation: 'update',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType,
|
||||
element: JSON.stringify({
|
||||
id: id,
|
||||
...record,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
console.debug({
|
||||
operation: 'update',
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
elementType: elementType,
|
||||
element: JSON.stringify({
|
||||
id: id,
|
||||
...record,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,635 @@
|
||||
import {
|
||||
AuthenticationType,
|
||||
HttpHeaders,
|
||||
HttpMessageBody,
|
||||
HttpMethod,
|
||||
HttpRequest,
|
||||
httpClient,
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
DropdownState,
|
||||
DynamicPropsValue,
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import * as crypto from 'crypto-js';
|
||||
import { Challenge, Instance } from './models';
|
||||
import { vtigerAuth } from '..';
|
||||
|
||||
export const isBaseUrl = (urlString: string): boolean => {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
return !url.pathname || url.pathname === '/';
|
||||
} catch (error) {
|
||||
// Handle invalid URLs here, e.g., return false or throw an error
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const md5 = (contents: string) => crypto.MD5(contents).toString();
|
||||
export const calculateAuthKey = (
|
||||
challengeToken: string,
|
||||
accessKey: string
|
||||
): string => crypto.MD5(challengeToken + accessKey).toString(crypto.enc.Hex);
|
||||
|
||||
export const instanceLogin = async (
|
||||
instance_url: string,
|
||||
username: string,
|
||||
password: string,
|
||||
debug = false
|
||||
) => {
|
||||
const endpoint = `${instance_url}/webservice.php`;
|
||||
const challenge = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Challenge;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${endpoint}?operation=getchallenge&username=${username}`,
|
||||
});
|
||||
|
||||
const accessKey = calculateAuthKey(challenge.body.result.token, password);
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Instance;
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
url: `${endpoint}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
operation: 'login',
|
||||
username,
|
||||
accessKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (debug) {
|
||||
console.debug('>>>>>>>>>>>> LOGIN', response.body, {
|
||||
method: HttpMethod.POST,
|
||||
url: `${endpoint}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: {
|
||||
operation: 'login',
|
||||
username,
|
||||
accessKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (response.body.success) {
|
||||
return response.body.result;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type Operation =
|
||||
| 'create'
|
||||
| 'retrieve'
|
||||
| 'delete'
|
||||
| 'update'
|
||||
| 'query'
|
||||
| 'listtypes';
|
||||
|
||||
export const Operations: Record<Operation, BodyParams> = {
|
||||
listtypes: {
|
||||
method: HttpMethod.GET,
|
||||
},
|
||||
create: {
|
||||
method: HttpMethod.POST,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
retrieve: {
|
||||
method: HttpMethod.GET,
|
||||
},
|
||||
delete: {
|
||||
method: HttpMethod.POST,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
update: {
|
||||
method: HttpMethod.POST,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
query: {
|
||||
method: HttpMethod.GET,
|
||||
},
|
||||
};
|
||||
|
||||
export const prepareHttpRequest = (
|
||||
instanceUrl: string,
|
||||
sessionName: string,
|
||||
operation: Operation,
|
||||
record: Record<string, string>
|
||||
) => {
|
||||
const data: Record<string, string> = {
|
||||
operation,
|
||||
sessionName,
|
||||
...record,
|
||||
};
|
||||
if ('element' in record) data['element'] = JSON.stringify(record['element']);
|
||||
|
||||
const httpRequest: HttpRequest<HttpMessageBody> = {
|
||||
url: `${instanceUrl}/webservice.php`,
|
||||
method: Operations[operation].method,
|
||||
headers: Operations[operation].headers,
|
||||
};
|
||||
|
||||
if (Operations[operation].method === HttpMethod.GET) {
|
||||
httpRequest['queryParams'] = data;
|
||||
} else if (Operations[operation].method === HttpMethod.POST) {
|
||||
httpRequest['body'] = data;
|
||||
}
|
||||
|
||||
return httpRequest;
|
||||
};
|
||||
|
||||
interface BodyParams {
|
||||
method: HttpMethod;
|
||||
headers?: HttpHeaders;
|
||||
}
|
||||
|
||||
export const Modules: Record<string, (record: Record<string, string>) => Promise<string>> = {
|
||||
Contacts: async (record) => `${record['email']}`, // firstname,lastname
|
||||
Documents: async (record) => `${record['notes_title']}`, // title
|
||||
Faq: async (record) => `${record['faq_no']}`, // question
|
||||
// HelpDesk: async (record) => `${record['ticket_no']}`, // this module not exist
|
||||
Invoice: async (record) => `${record['invoice_no']}`, // subject
|
||||
Leads: async (record) => `${record['lead_no']}: ${record['firstname']} ${record['lastname']}`, // firstname,lastname
|
||||
LineItem: async (record) => `${record['productid']}`, // no label field
|
||||
ProductTaxes: async (record) => `#${record['taxid']} pid: ${record['productid']}`, // no label field
|
||||
// ProjectTask: async (record) => `${record['projecttaskname']}`, // this module not exist
|
||||
SalesOrder: async (record) => `${record['salesorder_no']}`, // subject
|
||||
Tax: async (record) => `${record['taxname']}`, // taxlabel
|
||||
Users: async (record) => `${record['user_name']}`, // first_name,last_name
|
||||
};
|
||||
|
||||
export async function refreshModules(auth: VTigerAuthValue){
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/restapi/v1/vtiger/default/listtypes?fieldTypeList=null`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
},
|
||||
});
|
||||
|
||||
if(response.body.success !== true){
|
||||
throw new Error('Failed to retrieve module types');
|
||||
}
|
||||
|
||||
const types = response.body.result.types;
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
const element = types[i];
|
||||
|
||||
let labelFields = '';
|
||||
let isModuleLabelUnknown = false;
|
||||
Modules[element] ??= async (record) => {
|
||||
if(labelFields !== '') return labelFields;
|
||||
if(isModuleLabelUnknown) return '';
|
||||
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/restapi/v1/vtiger/default/describe?elementType=${element}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.body.success) return '';
|
||||
|
||||
const result = response.body.result;
|
||||
const lf = result.labelFields;
|
||||
if (Array.isArray(lf)) {
|
||||
labelFields = (lf[0] ?? '') as string;
|
||||
} else if (typeof lf === 'string') {
|
||||
labelFields = lf.includes(',') ? lf.split(',')[0] : lf;
|
||||
} else {
|
||||
labelFields = '';
|
||||
}
|
||||
|
||||
if(labelFields === '') {
|
||||
if(!result.fields?.length){
|
||||
isModuleLabelUnknown = true;
|
||||
return '';
|
||||
}
|
||||
|
||||
labelFields = result.fields[0].name;
|
||||
}
|
||||
|
||||
return record[labelFields];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const elementTypeProperty = Property.Dropdown({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Module Type',
|
||||
description: 'The module / element type',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async (props) => {
|
||||
const { auth } = props;
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Please setup authentication to continue',
|
||||
};
|
||||
}
|
||||
|
||||
await refreshModules(auth);
|
||||
|
||||
const modules = Object.keys(Modules).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
return {
|
||||
disabled: false,
|
||||
options: modules.map((module) => ({
|
||||
label: module,
|
||||
value: module,
|
||||
}))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export interface Field {
|
||||
name: string;
|
||||
dblabel: string;
|
||||
label: string;
|
||||
default: string;
|
||||
mandatory: boolean;
|
||||
type: {
|
||||
name: string;
|
||||
length?: string;
|
||||
refersTo?: string[];
|
||||
picklistValues?: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export type VTigerAuthValue = AppConnectionValueForAuthProperty<typeof vtigerAuth>;
|
||||
|
||||
export const recordIdProperty = () =>
|
||||
Property.DynamicProperties({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Record Fields',
|
||||
description: 'Add new fields to be created in the new record',
|
||||
required: true,
|
||||
refreshers: ['elementType'],
|
||||
props: async ({ auth, elementType }) => {
|
||||
if (!auth || !elementType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (!instance) return {};
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, string>[];
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
operation: 'query',
|
||||
elementType: elementType as unknown as string,
|
||||
query: `SELECT * FROM ${elementType} LIMIT 100;`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.body.success) return {};
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
const _type: string = elementType as unknown as string;
|
||||
const _module: CallableFunction = Modules[_type];
|
||||
|
||||
fields['id'] = Property.StaticDropdown<string>({
|
||||
displayName: 'Id',
|
||||
description: "The record's id",
|
||||
required: true,
|
||||
options: {
|
||||
options: response.body.result.map((r) => ({
|
||||
label: _module?.(r) || r['id'],
|
||||
value: r['id'],
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return fields;
|
||||
},
|
||||
});
|
||||
|
||||
export const FieldMapping = {
|
||||
autogenerated: Property.ShortText,
|
||||
string: Property.ShortText,
|
||||
text: Property.ShortText,
|
||||
double: Property.Number,
|
||||
integer: Property.Number,
|
||||
mediumtext: Property.LongText,
|
||||
phone: Property.LongText,
|
||||
url: Property.LongText,
|
||||
email: Property.LongText,
|
||||
picklist: Property.StaticDropdown,
|
||||
reference: Property.StaticDropdown,
|
||||
currency: Property.Number,
|
||||
boolean: Property.Checkbox,
|
||||
owner: Property.StaticDropdown,
|
||||
date: Property.DateTime,
|
||||
datetime: Property.DateTime,
|
||||
file: Property.File,
|
||||
time: Property.DateTime,
|
||||
};
|
||||
|
||||
export async function getRecordReference(
|
||||
auth: AppConnectionValueForAuthProperty<typeof vtigerAuth>,
|
||||
modules: string[]
|
||||
): Promise<DropdownState<string>> {
|
||||
const module = modules[0]; //Limit to the first reference for now
|
||||
const vtigerInstance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (vtigerInstance === null)
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
};
|
||||
|
||||
const httpRequest = prepareHttpRequest(
|
||||
auth.props.instance_url,
|
||||
vtigerInstance.sessionId ?? vtigerInstance.sessionName,
|
||||
'query' as Operation,
|
||||
{ query: `SELECT * FROM ${module};` }
|
||||
);
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, any>[];
|
||||
}>(httpRequest);
|
||||
|
||||
if (response.body.success) {
|
||||
return {
|
||||
disabled: false,
|
||||
options: await Promise.all(response.body.result.map(async (record) => {
|
||||
return {
|
||||
label: await Modules[module]?.(record) || record['id'],
|
||||
value: record['id'] as string,
|
||||
};
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
export const recordProperty = () =>
|
||||
Property.DynamicProperties({
|
||||
auth: vtigerAuth,
|
||||
displayName: 'Record Fields',
|
||||
description: 'Add new fields to be created in the new record',
|
||||
required: true,
|
||||
refreshers: ['elementType'],
|
||||
props: async ({ auth, id, elementType }) => {
|
||||
if (!auth || !elementType) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
return generateElementFields(
|
||||
auth,
|
||||
elementType as unknown as string,
|
||||
{}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const queryRecords = async (
|
||||
auth: AppConnectionValueForAuthProperty<typeof vtigerAuth>,
|
||||
elementType: string,
|
||||
page = 0,
|
||||
limit = 100
|
||||
) => {
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (!instance) return [];
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, unknown>[];
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
operation: 'query',
|
||||
elementType: elementType as unknown as string,
|
||||
query: `SELECT * FROM ${elementType} LIMIT ${page}, ${limit};`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body.success) {
|
||||
return response.body.result;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const countRecords = async (
|
||||
auth: VTigerAuthValue,
|
||||
elementType: string
|
||||
) => {
|
||||
const instance = await instanceLogin(
|
||||
auth.props.instance_url,
|
||||
auth.props.username,
|
||||
auth.props.password
|
||||
);
|
||||
if (!instance) return 0;
|
||||
|
||||
const response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: { count: string }[];
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/webservice.php`,
|
||||
queryParams: {
|
||||
sessionName: instance.sessionId ?? instance.sessionName,
|
||||
operation: 'query',
|
||||
elementType: elementType as unknown as string,
|
||||
query: `SELECT count(*) FROM ${elementType};`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body.success) {
|
||||
return Number.parseInt(response.body.result[0].count);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const generateElementFields = async (
|
||||
auth: VTigerAuthValue,
|
||||
elementType: string,
|
||||
defaultValue: Record<string, unknown>,
|
||||
skipMandatory = false
|
||||
): Promise<DynamicPropsValue> => {
|
||||
const describe_response = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: { fields: Field[] };
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${auth.props.instance_url}/restapi/v1/vtiger/default/describe`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
},
|
||||
queryParams: {
|
||||
elementType: elementType,
|
||||
},
|
||||
});
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
|
||||
if (describe_response.body.success) {
|
||||
let limit = 30; // Limit to show 30 input property, more than this will cause frontend unresponsive
|
||||
|
||||
const generateField = async (field: Field) => {
|
||||
const params = {
|
||||
displayName: field.label,
|
||||
description: `Field ${field.name} of object type ${elementType}`,
|
||||
required: !skipMandatory ? field.mandatory : false,
|
||||
};
|
||||
|
||||
if (
|
||||
['string', 'text', 'mediumtext', 'phone', 'url', 'email'].includes(
|
||||
field.type.name
|
||||
)
|
||||
) {
|
||||
if (['mediumtext', 'url'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.LongText({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
});
|
||||
} else {
|
||||
fields[field.name] = Property.ShortText({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
});
|
||||
}
|
||||
} else if (['picklist', 'reference', 'owner'].includes(field.type.name)) {
|
||||
let options: DropdownState<string>;
|
||||
if (field.type.name === 'picklist') {
|
||||
options = {
|
||||
disabled: false,
|
||||
options: field.type.picklistValues ?? [],
|
||||
};
|
||||
} else if (field.type.name === 'owner') {
|
||||
options = await getRecordReference(
|
||||
auth,
|
||||
['Users']
|
||||
);
|
||||
} else if (field.type.refersTo) {
|
||||
options = await getRecordReference(
|
||||
auth,
|
||||
field.type.refersTo ?? []
|
||||
);
|
||||
} else {
|
||||
options = { disabled: false, options: [] };
|
||||
}
|
||||
|
||||
fields[field.name] = Property.StaticDropdown({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
options,
|
||||
});
|
||||
} else if (['double', 'integer', 'currency'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.Number({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as number,
|
||||
});
|
||||
} else if (['boolean'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.Checkbox({
|
||||
displayName: field.label,
|
||||
description: `The fields to fill in the object type ${elementType}`,
|
||||
required: !skipMandatory ? field.mandatory : false,
|
||||
defaultValue: defaultValue?.[field.name] ? true : false,
|
||||
});
|
||||
} else if (['date', 'datetime', 'time'].includes(field.type.name)) {
|
||||
fields[field.name] = Property.DateTime({
|
||||
displayName: field.label,
|
||||
description: `The fields to fill in the object type ${elementType}`,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
required: !skipMandatory ? field.mandatory : false,
|
||||
});
|
||||
} else if(params.required) {
|
||||
// Add the mandatory field for unknown input type, but with text input
|
||||
fields[field.name] = Property.ShortText({
|
||||
...params,
|
||||
defaultValue: defaultValue?.[field.name] as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const skipFields = [
|
||||
'id',
|
||||
'modifiedtime',
|
||||
'createdtime',
|
||||
'modifiedby',
|
||||
'created_user_id',
|
||||
];
|
||||
|
||||
// Prioritize mandatory fields
|
||||
for (const field of describe_response.body.result.fields) {
|
||||
if (skipFields.includes(field.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.mandatory) {
|
||||
await generateField(field);
|
||||
limit--;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's add the rest...
|
||||
for (const field of describe_response.body.result.fields) {
|
||||
if (skipFields.includes(field.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the rest of field to avoid unresponsive frontend
|
||||
if (limit < 0) break;
|
||||
|
||||
if (!field.mandatory) {
|
||||
await generateField(field);
|
||||
limit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
else throw new Error("Failed to get module description");
|
||||
|
||||
return fields;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface Challenge {
|
||||
token: string; // Challenge token to be used for login.
|
||||
serverTime: string; // Current Server time
|
||||
expireTime: string;
|
||||
}
|
||||
|
||||
export interface Instance {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
userId: string;
|
||||
version: string;
|
||||
vtigerVersion: string;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
time_zone: string;
|
||||
hour_format: string;
|
||||
date_format: string;
|
||||
is_admin: string;
|
||||
call_duration: string;
|
||||
other_event_duration: string;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
AuthenticationType,
|
||||
DedupeStrategy,
|
||||
HttpMethod,
|
||||
Polling,
|
||||
httpClient,
|
||||
pollingHelper,
|
||||
} from '@activepieces/pieces-common';
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
createTrigger,
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
TriggerStrategy,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { vtigerAuth } from '../..';
|
||||
import {
|
||||
elementTypeProperty,
|
||||
instanceLogin,
|
||||
prepareHttpRequest,
|
||||
} from '../common';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const newOrUpdatedRecord = createTrigger({
|
||||
auth: vtigerAuth,
|
||||
name: 'new_or_updated_record',
|
||||
displayName: 'New or Updated Record',
|
||||
description:
|
||||
'Triggers when a new record is introduced or a record is updated.',
|
||||
props: {
|
||||
elementType: elementTypeProperty,
|
||||
watchBy: Property.StaticDropdown({
|
||||
displayName: 'Watch By',
|
||||
description: 'Column to watch for trigger',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ value: 'createdtime', label: 'Created Time' },
|
||||
{ value: 'modifiedtime', label: 'Modified Time' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
syncType: Property.StaticDropdown({
|
||||
displayName: 'Sync Scope',
|
||||
description: 'Records visibility scope for sync',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ value: 'application', label: 'Application (all records)' },
|
||||
{ value: 'userandgroup', label: "User's groups" },
|
||||
{ value: 'user', label: 'User only' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'application',
|
||||
}),
|
||||
limit: Property.Number({
|
||||
displayName: 'Limit',
|
||||
description: 'Enter the maximum number of records to return.',
|
||||
defaultValue: 100,
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
sampleData: {
|
||||
success: true,
|
||||
result: [
|
||||
{
|
||||
id: '3x291',
|
||||
createdtime: '2020-07-22 12:46:55',
|
||||
modifiedtime: '2020-07-22 12:46:55',
|
||||
},
|
||||
],
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
async test(ctx) {
|
||||
return await pollingHelper.test(polling, { ...ctx });
|
||||
},
|
||||
async onEnable(ctx) {
|
||||
await pollingHelper.onEnable(polling, { ...ctx });
|
||||
},
|
||||
async onDisable(ctx) {
|
||||
await pollingHelper.onDisable(polling, { ...ctx });
|
||||
},
|
||||
async run(ctx) {
|
||||
return await pollingHelper.poll(polling, { ...ctx });
|
||||
},
|
||||
});
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof vtigerAuth>,
|
||||
{ elementType?: string; watchBy?: string; limit?: number; syncType?: string }
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
|
||||
const items = await fetchRecords({ auth, propsValue, lastFetchEpochMS });
|
||||
|
||||
return (items ?? []).map((item) => {
|
||||
return {
|
||||
epochMilliSeconds: dayjs(
|
||||
propsValue.watchBy === 'createdtime'
|
||||
? item['createdtime']
|
||||
: item['modifiedtime']
|
||||
).valueOf(),
|
||||
data: item,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const fetchRecords = async ({
|
||||
auth,
|
||||
propsValue,
|
||||
lastFetchEpochMS,
|
||||
}: {
|
||||
auth: AppConnectionValueForAuthProperty<typeof vtigerAuth>;
|
||||
propsValue: Record<string, unknown>;
|
||||
lastFetchEpochMS: number;
|
||||
}) => {
|
||||
const elementType = propsValue['elementType'] as string;
|
||||
const limit = (propsValue['limit'] as number) ?? 100;
|
||||
const syncType = (propsValue['syncType'] as string) ?? 'application';
|
||||
|
||||
const baseUrl = `${auth.props.instance_url}/restapi/v1/vtiger/default`;
|
||||
|
||||
// Vtiger expects UNIX timestamp (seconds)
|
||||
let modifiedTimeSec = Math.floor((lastFetchEpochMS || 0) / 1000);
|
||||
|
||||
const updatedIds: string[] = [];
|
||||
let more = true;
|
||||
let safety = 0;
|
||||
|
||||
while (more && updatedIds.length < limit && safety < 10) {
|
||||
const syncResp = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: { updated: string[]; deleted: string[]; more: boolean; lastModifiedTime: number };
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseUrl}/sync`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
},
|
||||
queryParams: {
|
||||
modifiedTime: String(modifiedTimeSec),
|
||||
elementType,
|
||||
syncType,
|
||||
},
|
||||
});
|
||||
|
||||
if (!syncResp.body?.success) break;
|
||||
|
||||
const res = syncResp.body.result;
|
||||
more = res.more === true;
|
||||
modifiedTimeSec = res.lastModifiedTime || modifiedTimeSec;
|
||||
|
||||
for (const id of res.updated ?? []) {
|
||||
if (updatedIds.length < limit) updatedIds.push(id);
|
||||
}
|
||||
|
||||
safety++;
|
||||
}
|
||||
|
||||
if (updatedIds.length === 0) return [];
|
||||
|
||||
const idsToFetch = updatedIds.slice(0, limit);
|
||||
|
||||
const results: Record<string, any>[] = [];
|
||||
for (const id of idsToFetch) {
|
||||
const retrieveResp = await httpClient.sendRequest<{
|
||||
success: boolean;
|
||||
result: Record<string, any>;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${baseUrl}/retrieve`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.props.username,
|
||||
password: auth.props.password,
|
||||
},
|
||||
queryParams: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (retrieveResp.body?.success && retrieveResp.body.result) {
|
||||
results.push(retrieveResp.body.result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
Reference in New Issue
Block a user