@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn
|
||||
|
||||
# codegen
|
||||
generated
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
|
||||
.twenty
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
*.d.ts
|
||||
@@ -0,0 +1 @@
|
||||
24.5.0
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript"],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,18 @@
|
||||
|
||||
Updates Last interaction and Interaction status fields based on last email date
|
||||
|
||||
## Requirements
|
||||
- an `apiKey` - go to Settings > API & Webhooks to generate one
|
||||
|
||||
## Setup
|
||||
1. Add and synchronize app
|
||||
Add and synchronize app
|
||||
```bash
|
||||
cd packages/twenty-apps/community/last-email-interaction
|
||||
yarn auth
|
||||
yarn sync
|
||||
yarn twenty remote add
|
||||
yarn twenty install
|
||||
```
|
||||
2. Go to Settings > Integrations > Last email interaction > Settings and add required variables
|
||||
|
||||
## Flow
|
||||
- Checks if fields are created, if not, creates them on fly
|
||||
- Extracts the datetime of message and calculates the last interaction status
|
||||
- Fetches all users and companies connected to them and updates their Last interaction and Interaction status fields
|
||||
- Extracts the datetime of fetched message and calculates the last interaction status
|
||||
- Fetches all users and companies connected to the message and updates their Last interaction and Interaction status fields
|
||||
|
||||
## Todo:
|
||||
- update app with generated Twenty object once extending objects is possible
|
||||
## Notes
|
||||
- Upon install, creates fields to Person and Company objects
|
||||
- Every day at midnight app goes through all companies and people records and updates their Interaction status based on Last interaction date
|
||||
@@ -1,23 +0,0 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
|
||||
displayName: 'Last email interaction',
|
||||
description:
|
||||
'Updates Last interaction and Interaction status fields based on last received email',
|
||||
icon: "IconMailFast",
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
|
||||
isSecret: true,
|
||||
description: 'Required to send requests to Twenty',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
|
||||
isSecret: false,
|
||||
description: 'Optional, defaults to cloud API URL',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -9,19 +9,23 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"twenty-sdk": "0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"axios": "1.14.0",
|
||||
"twenty-sdk": "0.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"auth": "twenty auth login",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"logs": "twenty app logs",
|
||||
"create-entity": "twenty app add",
|
||||
"help": "twenty --help"
|
||||
"twenty": "twenty",
|
||||
"lint": "oxlint -c .oxlintrc.json .",
|
||||
"lint:fix": "oxlint --fix -c .oxlintrc.json .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"oxlint": "^0.16.0",
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '718ed9ab-53fc-49c8-8deb-0cff78ecf0d2',
|
||||
displayName: 'Last email interaction',
|
||||
description:
|
||||
'Updates Last interaction and Interaction status fields based on last received email',
|
||||
icon: "IconMailFast",
|
||||
defaultRoleUniversalIdentifier: '7a66af97-5056-45b2-96a9-c89f0fd181d1',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '9378751e-c23b-4e84-887d-2905cb8359b4',
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
type: FieldType.SELECT,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
description: 'Indicates the health of relation',
|
||||
options: [
|
||||
{
|
||||
id: '39d54a6b-5a0e-4209-9a59-2a2d7b8c462b',
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '7377d6c5-a75c-453e-a1a1-63fb9cba4e26',
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: 'a8b99246-237f-4715-b21f-94a3ae14994e',
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '1f05d528-eaab-4639-aba1-328050a87220',
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: '2f195c4c-1db1-4bbe-80b6-25c2f63168b0',
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
type: FieldType.DATE_TIME,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
||||
description: 'Date when the last interaction happened',
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { defineField, FieldType, STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'fa342e26-9742-4db8-85b4-4d78ba18482f',
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
type: FieldType.SELECT,
|
||||
objectUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
description: 'Indicates the health of relation',
|
||||
options: [
|
||||
{
|
||||
id: '1dfbfa99-35fb-43af-8c14-74e682a8121b',
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: '955788e8-6d64-45ba-80ea-a1a5446a0ae7',
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
id: '7b84ca72-fac5-4c6d-ab08-b148e4b3efdf',
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: '04dea3e5-ec26-41cf-b23f-37abab67827a',
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
defineField,
|
||||
FieldType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
|
||||
export default defineField({
|
||||
universalIdentifier: 'bec14de7-6683-4784-91ba-62d83b5f30f7',
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
type: FieldType.DATE_TIME,
|
||||
objectUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier,
|
||||
description: 'Date when the last interaction happened',
|
||||
});
|
||||
@@ -1,270 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { type FunctionConfig } from 'twenty-sdk';
|
||||
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
|
||||
const TWENTY_URL =
|
||||
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
|
||||
? `${process.env.TWENTY_API_URL}/rest`
|
||||
: 'https://api.twenty.com/rest';
|
||||
|
||||
const create_last_interaction = (id: string) => {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `${TWENTY_URL}/metadata/fields`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
type: 'DATE_TIME',
|
||||
objectMetadataId: `${id}`,
|
||||
name: 'lastInteraction',
|
||||
label: 'Last interaction',
|
||||
description: 'Date when the last interaction happened',
|
||||
icon: 'IconCalendarClock',
|
||||
defaultValue: null,
|
||||
isNullable: true,
|
||||
settings: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const create_interaction_status = (id: string) => {
|
||||
return {
|
||||
method: 'POST',
|
||||
url: `${TWENTY_URL}/metadata/fields`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
type: 'SELECT',
|
||||
objectMetadataId: `${id}`,
|
||||
name: 'interactionStatus',
|
||||
label: 'Interaction status',
|
||||
description: 'Indicates the health of relation',
|
||||
icon: 'IconProgress',
|
||||
defaultValue: null,
|
||||
isNullable: true,
|
||||
settings: {},
|
||||
options: [
|
||||
{
|
||||
color: 'green',
|
||||
label: 'Recent',
|
||||
value: 'RECENT',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
color: 'yellow',
|
||||
label: 'Active',
|
||||
value: 'ACTIVE',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
color: 'sky',
|
||||
label: 'Cooling',
|
||||
value: 'COOLING',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
color: 'gray',
|
||||
label: 'Dormant',
|
||||
value: 'DORMANT',
|
||||
position: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const calculateStatus = (date: string) => {
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
const now = Date.now();
|
||||
const messageDate = Date.parse(date);
|
||||
const deltaTime = now - messageDate;
|
||||
return deltaTime < 7 * day
|
||||
? 'RECENT'
|
||||
: deltaTime < 30 * day
|
||||
? 'ACTIVE'
|
||||
: deltaTime < 90 * day
|
||||
? 'COOLING'
|
||||
: 'DORMANT';
|
||||
};
|
||||
|
||||
const updateInteractionStatus = async (objectName: string, id: string, messageDate: string, status: string) => {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
url: `${TWENTY_URL}/${objectName}/${id}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
data: {
|
||||
lastInteraction: messageDate,
|
||||
interactionStatus: status
|
||||
}
|
||||
};
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
if (response.status === 200) {
|
||||
console.log('Successfully updated company last interaction field');
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRelatedCompanyId = async (id: string) => {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/people/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const req = await axios.request(options);
|
||||
if (req.status === 200 && req.data.person.companyId !== null && req.data.person.companyId !== undefined) {
|
||||
return req.data.person.companyId;
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const main = async (params: {
|
||||
properties: Record<string, any>;
|
||||
recordId: string;
|
||||
userId: string;
|
||||
}): Promise<object | undefined> => {
|
||||
if (TWENTY_API_KEY === '') {
|
||||
console.log("Function exited as API key or URL hasn't been set properly");
|
||||
return {};
|
||||
}
|
||||
const { properties, recordId } = params;
|
||||
// Check if fields are created
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/metadata/objects`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
const objects = response.data.data.objects;
|
||||
const company_object = objects.find(
|
||||
(object: any) => object.nameSingular === 'company',
|
||||
);
|
||||
const company_last_interaction = company_object.fields.find(
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const company_interaction_status = company_object.fields.find(
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
const person_object = objects.find(
|
||||
(object: any) => object.nameSingular === 'person',
|
||||
);
|
||||
const person_last_interaction = person_object.fields.find(
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const person_interaction_status = person_object.fields.find(
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
// If not, create them
|
||||
if (company_last_interaction === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_last_interaction(company_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created company last interaction field');
|
||||
}
|
||||
}
|
||||
if (company_interaction_status === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_interaction_status(company_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created company interaction status field');
|
||||
}
|
||||
}
|
||||
if (person_last_interaction === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_last_interaction(person_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created person last interaction field');
|
||||
}
|
||||
}
|
||||
if (person_interaction_status === undefined) {
|
||||
const response2 = await axios.request(
|
||||
create_interaction_status(person_object.id),
|
||||
);
|
||||
if (response2.status === 201) {
|
||||
console.log('Successfully created person interaction status field');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the timestamp of message
|
||||
const messageDate = properties.after.receivedAt;
|
||||
const interactionStatus = calculateStatus(messageDate);
|
||||
|
||||
// Get the details of person and related company
|
||||
const messageOptions = {
|
||||
method: 'GET',
|
||||
url: `${TWENTY_URL}/messages/${recordId}?depth=1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TWENTY_API_KEY}`,
|
||||
},
|
||||
};
|
||||
const messageDetails = await axios.request(messageOptions);
|
||||
const peopleIds: string[] = [];
|
||||
for (const participant of messageDetails.data.messages
|
||||
.messageParticipants) {
|
||||
peopleIds.push(participant.personId);
|
||||
}
|
||||
|
||||
const companiesIds = [];
|
||||
for (const id of peopleIds) {
|
||||
companiesIds.push(await fetchRelatedCompanyId(id));
|
||||
}
|
||||
// Update the field value depending on the timestamp
|
||||
for (const id of peopleIds) {
|
||||
await updateInteractionStatus("people", id, messageDate, interactionStatus);
|
||||
}
|
||||
|
||||
for (const id of companiesIds) {
|
||||
await updateInteractionStatus("companies", id, messageDate, interactionStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(error.message);
|
||||
return {};
|
||||
}
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const config: FunctionConfig = {
|
||||
universalIdentifier: '683966a0-b60a-424e-86b1-7448c9191bde',
|
||||
name: 'test',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'f4f1e127-87f0-4dcf-99fe-8061adf5cbe6',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'message.created',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '4c17878f-b6b3-4d0a-8de6-967b1cb55002',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'message.updated',
|
||||
},
|
||||
],
|
||||
};
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
const fetchAllPeople = async () => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
/*
|
||||
lastInteraction: {
|
||||
is: 'NOT_NULL',
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
lastInteraction: true,
|
||||
interactionStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result.people) {
|
||||
throw new Error('Could not find any people');
|
||||
}
|
||||
return result.people.edges;
|
||||
}
|
||||
|
||||
const fetchAllCompanies = async () => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
companies: {
|
||||
__args: {
|
||||
filter: {
|
||||
/* how to fetch fields added in fields folder?
|
||||
lastInteraction: {
|
||||
is: 'NOT_NULL',
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
lastInteraction: true,
|
||||
interactionStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result.companies) {
|
||||
throw new Error('Could not find any companies');
|
||||
}
|
||||
return result.companies.edges;
|
||||
}
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handler = async () => {
|
||||
const people = await fetchAllPeople();
|
||||
for (const person of people) {
|
||||
const interactionStatus = calculateStatus(person.node.lastInteraction as string);
|
||||
if (interactionStatus !== person.node.interactionStatus) {
|
||||
await updatePerson(person.node.id, {interactionStatus: interactionStatus});
|
||||
}
|
||||
}
|
||||
const companies = await fetchAllCompanies();
|
||||
for (const company of companies) {
|
||||
const interactionStatus = calculateStatus(company.node.lastInteraction as string);
|
||||
if (interactionStatus !== company.node.interactionStatus) {
|
||||
await updateCompany(company.node.id, {interactionStatus: interactionStatus});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'c79f1f30-f369-4264-9e5b-c183577bc709',
|
||||
name: 'on-cron-job',
|
||||
description:
|
||||
'Runs daily at midnight and updates all companies and people with correct interaction status',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
cronTriggerSettings: {
|
||||
pattern: '0 0 * * *', // runs daily at midnight
|
||||
},
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
DatabaseEventPayload,
|
||||
defineLogicFunction,
|
||||
ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
|
||||
const fetchMessageParticipants = async (messageId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
messageParticipants: {
|
||||
__args: {
|
||||
filter: {
|
||||
messageId: {
|
||||
eq: messageId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
personId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let people: string[] = [];
|
||||
if (result.messageParticipants === undefined) {
|
||||
return people;
|
||||
}
|
||||
for (const person of result.messageParticipants.edges) {
|
||||
if (person.node.personId !== undefined) {
|
||||
people.push(person.node.personId);
|
||||
}
|
||||
}
|
||||
return people;
|
||||
};
|
||||
|
||||
const fetchRelatedCompany = async (personId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: personId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
company: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (
|
||||
result.people === undefined ||
|
||||
result.people.edges[0].node.company === undefined
|
||||
) {
|
||||
throw new Error(`Failed to fetch related company of person ${personId}`);
|
||||
}
|
||||
return result.people.edges[0].node.company.id;
|
||||
};
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Message = {
|
||||
receivedAt: string;
|
||||
};
|
||||
|
||||
type MessageCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Message>
|
||||
>;
|
||||
|
||||
const handler = async (
|
||||
event: MessageCreatedEvent,
|
||||
): Promise<object | undefined> => {
|
||||
const { properties, recordId } = event;
|
||||
const interactionStatus = calculateStatus(properties.after.receivedAt);
|
||||
const peopleIds: string[] = [];
|
||||
peopleIds.push(...(await fetchMessageParticipants(recordId)));
|
||||
const updateData = {
|
||||
lastInteraction: properties.after.receivedAt,
|
||||
interactionStatus: interactionStatus,
|
||||
};
|
||||
for (const person of peopleIds) {
|
||||
const companyId = await fetchRelatedCompany(person);
|
||||
await updatePerson(person, updateData);
|
||||
await updateCompany(companyId, updateData);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '543432c2-6509-4ee9-90ec-15ffb4d7abfc',
|
||||
name: 'on-message-created',
|
||||
description: 'Triggered when new message is created',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'message.created',
|
||||
},
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import {
|
||||
DatabaseEventPayload,
|
||||
defineLogicFunction,
|
||||
ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { calculateStatus } from '../shared/calculate-status';
|
||||
|
||||
|
||||
const fetchMessageParticipants = async (messageId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
messageParticipants: {
|
||||
__args: {
|
||||
filter: {
|
||||
messageId: {
|
||||
eq: messageId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
personId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let people: string[] = [];
|
||||
if (result.messageParticipants === undefined) {
|
||||
return people;
|
||||
}
|
||||
for (const person of result.messageParticipants.edges) {
|
||||
if (person.node.personId !== undefined) {
|
||||
people.push(person.node.personId);
|
||||
}
|
||||
}
|
||||
return people;
|
||||
};
|
||||
|
||||
const fetchRelatedCompany = async (personId: string) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.query({
|
||||
people: {
|
||||
__args: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: personId,
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
company: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (
|
||||
result.people === undefined ||
|
||||
result.people.edges[0].node.company === undefined
|
||||
) {
|
||||
throw new Error(`Failed to fetch related company of person ${personId}`);
|
||||
}
|
||||
return result.people.edges[0].node.company.id;
|
||||
};
|
||||
|
||||
const updateCompany = async (
|
||||
companyId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updateCompany: {
|
||||
__args: {
|
||||
id: companyId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updateCompany) {
|
||||
throw new Error(`Failed to update company ${companyId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePerson = async (
|
||||
personId: string,
|
||||
updateData: Record<string, string>,
|
||||
) => {
|
||||
const client = new CoreApiClient();
|
||||
const result = await client.mutation({
|
||||
updatePerson: {
|
||||
__args: {
|
||||
id: personId,
|
||||
data: updateData,
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!result.updatePerson) {
|
||||
throw new Error(`Failed to update person ${personId}`);
|
||||
}
|
||||
};
|
||||
|
||||
type Message = {
|
||||
receivedAt: string;
|
||||
};
|
||||
|
||||
type MessageCreatedEvent = DatabaseEventPayload<
|
||||
ObjectRecordCreateEvent<Message>
|
||||
>;
|
||||
|
||||
const handler = async (
|
||||
event: MessageCreatedEvent,
|
||||
): Promise<object | undefined> => {
|
||||
const { properties, recordId } = event;
|
||||
const interactionStatus = calculateStatus(properties.after.receivedAt);
|
||||
const peopleIds: string[] = [];
|
||||
peopleIds.push(...(await fetchMessageParticipants(recordId)));
|
||||
const updateData = {
|
||||
lastInteraction: properties.after.receivedAt,
|
||||
interactionStatus: interactionStatus,
|
||||
};
|
||||
for (const person of peopleIds) {
|
||||
const companyId = await fetchRelatedCompany(person);
|
||||
await updatePerson(person, updateData);
|
||||
await updateCompany(companyId, updateData);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '9bfcb3e4-9119-4b65-b6e9-d395d0764ce5',
|
||||
name: 'on-message-updated',
|
||||
description: 'Triggered when message is updated',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'message.updated',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export const calculateStatus = (date: string) => {
|
||||
const day = 1000 * 60 * 60 * 24;
|
||||
const now = Date.now();
|
||||
const messageDate = Date.parse(date);
|
||||
const deltaTime = now - messageDate;
|
||||
return deltaTime < 7 * day
|
||||
? 'RECENT'
|
||||
: deltaTime < 30 * day
|
||||
? 'ACTIVE'
|
||||
: deltaTime < 90 * day
|
||||
? 'COOLING'
|
||||
: 'DORMANT';
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user