Fix hacktoberfest applications (#15613)
As title, make them syncable with twenty-cli:0.2.0 <img width="1025" height="693" alt="image" src="https://github.com/user-attachments/assets/a18f8ba3-b6fc-40e9-84dd-446ff8deeb04" />
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: 'b53627f5-ca60-478c-bc43-c7ab4904e34a',
|
||||
displayName: 'Activity Summary',
|
||||
description:
|
||||
'A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '304b7d5d-e2bb-4444-9b04-6b3ae8b73730',
|
||||
description: 'Twenty API Key',
|
||||
isSecret: true,
|
||||
},
|
||||
DAYS_AGO: {
|
||||
universalIdentifier: '040a3097-9cee-4f74-b957-c2f9bf636c3f',
|
||||
description:
|
||||
'How far back into the past we want to summarise – defaults to the past 7 days',
|
||||
value: '7',
|
||||
isSecret: false,
|
||||
},
|
||||
SLACK_HOOK_URL: {
|
||||
universalIdentifier: 'fd16e370-934c-4267-83b4-7d88259bf7e1',
|
||||
description: 'Slack hook URL for sending message to channel',
|
||||
isSecret: true,
|
||||
},
|
||||
DISCORD_WEBHOOK_URL: {
|
||||
universalIdentifier: 'f3741075-d525-4988-ba42-55d519c6fd76',
|
||||
description:
|
||||
'Discord webhook URL for sending message to channel of a server',
|
||||
isSecret: true,
|
||||
},
|
||||
FB_GRAPH_TOKEN: {
|
||||
universalIdentifier: 'fb907f49-74ac-4aa5-ba45-cfc9250ecc44',
|
||||
description: 'For Facebook auth',
|
||||
isSecret: true,
|
||||
},
|
||||
WHATSAPP_RECIPIENT_PHONE_NUMBER: {
|
||||
universalIdentifier: 'c856ee5d-44bf-42f4-9a39-2553a94af518',
|
||||
description: 'Phone number for receiving WhatsApp message',
|
||||
isSecret: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "activity-summary",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -12,36 +13,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "b53627f5-ca60-478c-bc43-c7ab4904e34a",
|
||||
"name": "Activity Summary",
|
||||
"description": "A TypeScript-based reporting bot that summarizes activity from your Twenty CRM workspace and sends daily/periodic reports to Slack, Discord, and WhatsApp. Meet Kylian Mbaguette, your friendly CRM activity reporter!",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"description": "Twenty API Key",
|
||||
"isSecret": true
|
||||
},
|
||||
"DAYS_AGO": {
|
||||
"description": "How far back into the past we want to summarise – defaults to the past 7 days",
|
||||
"value": "7",
|
||||
"isSecret": false
|
||||
},
|
||||
"SLACK_HOOK_URL": {
|
||||
"description": "Slack hook URL for sending message to channel",
|
||||
"isSecret": true
|
||||
},
|
||||
"DISCORD_WEBHOOK_URL": {
|
||||
"description": "Discord webhook URL for sending message to channel of a server",
|
||||
"isSecret": true
|
||||
},
|
||||
"FB_GRAPH_TOKEN": {
|
||||
"description": "For Facebook auth",
|
||||
"isSecret": true
|
||||
},
|
||||
"WHATSAPP_RECIPIENT_PHONE_NUMBER": {
|
||||
"description": "Phone number for receiving WhatsApp message",
|
||||
"isSecret": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "c5b0e3f7-cbbd-4bd6-b01c-150d52cf2ce9",
|
||||
"name": "summarise-and-send",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "36e1c4c7-8664-4d6d-a88f-ac56f1bd0651",
|
||||
"type": "cron",
|
||||
"schedule": "0 9 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
+36
-20
@@ -1,15 +1,16 @@
|
||||
import { summariseOpportunityCreation } from "./opportunity-creation-summariser";
|
||||
import { summarisePeopleCreation } from "./people-creation-summariser";
|
||||
import { sendToDiscord, sendToSlack, sendToWhatsApp } from "./senders";
|
||||
import { summariseTaskCreation } from "./task-creation-summariser";
|
||||
import { summariseOpportunityCreation } from './opportunity-creation-summariser';
|
||||
import { summarisePeopleCreation } from './people-creation-summariser';
|
||||
import { sendToDiscord, sendToSlack, sendToWhatsApp } from './senders';
|
||||
import { summariseTaskCreation } from './task-creation-summariser';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (): Promise<object> => {
|
||||
let date: string | Date = new Date()
|
||||
date.setDate(new Date().getDate() - Number(process.env.DAYS_AGO))
|
||||
date = date.toISOString().substring(0, 10)
|
||||
const peopleCreationSummary = await summarisePeopleCreation(date)
|
||||
const opportunityCreationSummary = await summariseOpportunityCreation(date)
|
||||
const taskCreationSummary = await summariseTaskCreation(date)
|
||||
let date: string | Date = new Date();
|
||||
date.setDate(new Date().getDate() - Number(process.env.DAYS_AGO));
|
||||
date = date.toISOString().substring(0, 10);
|
||||
const peopleCreationSummary = await summarisePeopleCreation(date);
|
||||
const opportunityCreationSummary = await summariseOpportunityCreation(date);
|
||||
const taskCreationSummary = await summariseTaskCreation(date);
|
||||
|
||||
let body = {
|
||||
daysAgo: Number(process.env.DAYS_AGO),
|
||||
@@ -19,19 +20,19 @@ export const main = async (): Promise<object> => {
|
||||
discord: {},
|
||||
whatsapp: {},
|
||||
slack: {},
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.SLACK_HOOK_URL) {
|
||||
const slackBody = await sendToSlack({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
slack: slackBody,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.DISCORD_WEBHOOK_URL) {
|
||||
@@ -39,26 +40,41 @@ export const main = async (): Promise<object> => {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
discord: discordBody,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.FB_GRAPH_TOKEN && process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER) {
|
||||
if (
|
||||
process.env.FB_GRAPH_TOKEN &&
|
||||
process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER
|
||||
) {
|
||||
const whatsappBody = await sendToWhatsApp({
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
})
|
||||
});
|
||||
|
||||
body = {
|
||||
...body,
|
||||
whatsapp: whatsappBody,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
return body;
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'c5b0e3f7-cbbd-4bd6-b01c-150d52cf2ce9',
|
||||
name: 'summarise-and-send',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '36e1c4c7-8664-4d6d-a88f-ac56f1bd0651',
|
||||
type: 'cron',
|
||||
pattern: '0 9 * * *',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+124
-118
@@ -1,93 +1,97 @@
|
||||
export const sendToSlack = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
}) => {
|
||||
const { peopleCreationSummary, opportunityCreationSummary, taskCreationSummary } = params
|
||||
|
||||
const slackMessage = {
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days`,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "🧑💻 People & Companies",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": peopleCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "🎯 Opportunities",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": opportunityCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "📋 Tasks",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": taskCreationSummary,
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const response = await fetch(process.env.SLACK_HOOK_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(slackMessage),
|
||||
})
|
||||
|
||||
return {
|
||||
formattedMesage: slackMessage,
|
||||
webhookStatus: response.status,
|
||||
}
|
||||
}
|
||||
|
||||
export const sendToDiscord = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}) => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params
|
||||
} = params;
|
||||
|
||||
const slackMessage = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days`,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '🧑💻 People & Companies',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: peopleCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '🎯 Opportunities',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: opportunityCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: '📋 Tasks',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: taskCreationSummary,
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.SLACK_HOOK_URL ?? '', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(slackMessage),
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMesage: slackMessage,
|
||||
webhookStatus: response.status,
|
||||
};
|
||||
};
|
||||
|
||||
export const sendToDiscord = async (params: {
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}) => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params;
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
**🧑💻 People & Companies**
|
||||
@@ -97,36 +101,35 @@ ${peopleCreationSummary}
|
||||
${opportunityCreationSummary}
|
||||
|
||||
**📋 Tasks**
|
||||
${taskCreationSummary}`
|
||||
|
||||
${taskCreationSummary}`;
|
||||
|
||||
const body = {
|
||||
username: "Twenty Bot",
|
||||
username: 'Twenty Bot',
|
||||
content: formattedMesage,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.DISCORD_WEBHOOK_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
const response = await fetch(process.env.DISCORD_WEBHOOK_URL ?? '', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
webhookStatus: response.status,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const sendToWhatsApp = async (params: {
|
||||
peopleCreationSummary: string
|
||||
opportunityCreationSummary: string
|
||||
taskCreationSummary: string
|
||||
peopleCreationSummary: string;
|
||||
opportunityCreationSummary: string;
|
||||
taskCreationSummary: string;
|
||||
}): Promise<object> => {
|
||||
const {
|
||||
peopleCreationSummary,
|
||||
opportunityCreationSummary,
|
||||
taskCreationSummary,
|
||||
} = params
|
||||
} = params;
|
||||
const formattedMesage = `Bonjour! 🥖 Je m'appelle Kylian Mbaguette. Over the last ${process.env.DAYS_AGO} days:
|
||||
|
||||
*🧑💻 People & Companies*
|
||||
@@ -136,31 +139,34 @@ ${peopleCreationSummary}
|
||||
${opportunityCreationSummary}
|
||||
|
||||
*📋 Tasks*
|
||||
${taskCreationSummary}`
|
||||
${taskCreationSummary}`;
|
||||
|
||||
const response = await fetch('https://graph.facebook.com/v22.0/828771160324576/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.FB_GRAPH_TOKEN}`
|
||||
const response = await fetch(
|
||||
'https://graph.facebook.com/v22.0/828771160324576/messages',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.FB_GRAPH_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER,
|
||||
type: 'text',
|
||||
text: {
|
||||
preview_url: true,
|
||||
body: formattedMesage,
|
||||
},
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"messaging_product": "whatsapp",
|
||||
"recipient_type": "individual",
|
||||
"to": process.env.WHATSAPP_RECIPIENT_PHONE_NUMBER,
|
||||
"type": "text",
|
||||
"text": {
|
||||
"preview_url": true,
|
||||
"body": formattedMesage,
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
const responseBody = await response.json()
|
||||
const responseBody = await response.json();
|
||||
|
||||
return {
|
||||
formattedMesage,
|
||||
webhookStatus: response.status,
|
||||
webhookResponse: responseBody,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -14,9 +14,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"Activity Summary@workspace:.":
|
||||
"activity-summary@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "Activity Summary@workspace:."
|
||||
resolution: "activity-summary@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
twenty-sdk: "npm:0.0.3"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '028754f1-3235-43b9-9427-fa6a62dbd473',
|
||||
displayName: 'AI Meeting Transcript',
|
||||
description:
|
||||
'Automatically process meeting transcripts to extract insights, action items, and follow-ups',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '1359d05c-4947-4673-809f-abd55bede365',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Twenty API key',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: 'dbe83355-b574-445c-92c0-5c2b94a61ddb',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Twenty API URL',
|
||||
},
|
||||
OPENAI_API_KEY: {
|
||||
universalIdentifier: '9559470d-15eb-4bc2-9cbc-3bc5c869d1fd',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'OpenAI API key for transcript analysis',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "ai-meeting-transcript",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -7,27 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "028754f1-3235-43b9-9427-fa6a62dbd473",
|
||||
"name": "AI Meeting Transcript",
|
||||
"description": "Automatically process meeting transcripts to extract insights, action items, and follow-ups",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Twenty API key"
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Twenty API URL"
|
||||
},
|
||||
"OPENAI_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "OpenAI API key for transcript analysis"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"openai": "^4.28.0",
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "dae52ab2-174f-4f81-a031-604ee2e81eba",
|
||||
"name": "ai-meeting-transcriptor",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "b011303d-2c24-44d4-9923-55eb060a1ff6",
|
||||
"type": "route",
|
||||
"path": "/webhook/transcript",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import OpenAI from 'openai';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
type TranscriptWebhookPayload = {
|
||||
transcript: string;
|
||||
@@ -289,3 +290,17 @@ export const main = async (
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'dae52ab2-174f-4f81-a031-604ee2e81eba',
|
||||
name: 'ai-meeting-transcriptor',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'b011303d-2c24-44d4-9923-55eb060a1ff6',
|
||||
type: 'route',
|
||||
path: '/webhook/transcript',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["**/*.ts"]
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -33,17 +33,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"AI Meeting Transcript@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "AI Meeting Transcript@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
axios: "npm:^1.12.2"
|
||||
openai: "npm:^4.28.0"
|
||||
twenty-sdk: "npm:^0.0.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"abort-controller@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "abort-controller@npm:3.0.0"
|
||||
@@ -62,6 +51,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ai-meeting-transcript@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "ai-meeting-transcript@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
axios: "npm:^1.12.2"
|
||||
openai: "npm:^4.28.0"
|
||||
twenty-sdk: "npm:^0.0.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"async-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-function@npm:1.0.0"
|
||||
|
||||
Binary file not shown.
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/object.schema.json",
|
||||
"universalIdentifier": "c8f4d3e1-2a7b-4e9f-8c1d-5a6b7e8f9c2a",
|
||||
"standardId": "c8f4d3e1-2a7b-4e9f-8c1d-5a6b7e8f9c2a",
|
||||
"nameSingular": "meeting",
|
||||
"namePlural": "meetings",
|
||||
"labelSingular": "Meeting",
|
||||
"labelPlural": "Meetings",
|
||||
"description": "Meetings imported from Fireflies with AI-generated summaries, sentiment, and action items.",
|
||||
"icon": "IconVideo"
|
||||
}
|
||||
|
||||
@@ -8,78 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "af5128d7-192e-4bd3-bcf2-fec1ad767440",
|
||||
"description": "Get fireflies meeting notes in Twenty",
|
||||
"env": {
|
||||
"FIREFLIES_API_KEY": {
|
||||
"description": "Fireflies API key for authentication",
|
||||
"isSecret": true
|
||||
},
|
||||
"FIREFLIES_WEBHOOK_SECRET": {
|
||||
"description": "Secret key for validating Fireflies webhooks",
|
||||
"isSecret": true
|
||||
},
|
||||
"FIREFLIES_PLAN_LEVEL": {
|
||||
"description": "Fireflies plan level: free, pro, enterprise",
|
||||
"value": "pro",
|
||||
"isSecret": false
|
||||
},
|
||||
"TWENTY_API_KEY": {
|
||||
"description": "Twenty CRM API key for authentication",
|
||||
"isSecret": true
|
||||
},
|
||||
"SERVER_URL": {
|
||||
"description": "Twenty CRM server URL",
|
||||
"value": "http://localhost:3000",
|
||||
"isSecret": false
|
||||
},
|
||||
"AUTO_CREATE_CONTACTS": {
|
||||
"description": "Automatically create contacts for unknown participants (true/false)",
|
||||
"value": "true",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_SUMMARY_STRATEGY": {
|
||||
"description": "Summary fetch strategy: immediate_only, immediate_with_retry, delayed_polling, or basic_only",
|
||||
"value": "immediate_with_retry",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_RETRY_ATTEMPTS": {
|
||||
"description": "Number of retry attempts when fetching summaries",
|
||||
"value": "30",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_RETRY_DELAY": {
|
||||
"description": "Delay in milliseconds between retry attempts",
|
||||
"value": "30000",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_POLL_INTERVAL": {
|
||||
"description": "Polling interval (ms) when using delayed polling strategy",
|
||||
"value": "60000",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_MAX_POLLS": {
|
||||
"description": "Maximum number of polling attempts when waiting for summaries",
|
||||
"value": "15",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_MAX_RETRY_ATTEMPTS": {
|
||||
"description": "Maximum number of retry attempts when waiting for summaries",
|
||||
"value": "30",
|
||||
"isSecret": false
|
||||
},
|
||||
"FIREFLIES_MAX_POLL_INTERVAL": {
|
||||
"description": "Maximum polling interval (ms) when waiting for summaries",
|
||||
"value": "600000",
|
||||
"isSecret": false
|
||||
},
|
||||
"DEBUG_LOGS": {
|
||||
"description": "Enable debug logging (true/false)",
|
||||
"value": "false",
|
||||
"isSecret": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"setup:fields": "tsx scripts/add-meeting-fields.ts",
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "0765206a-a58d-4ecf-a5a4-23d1d2095f4e",
|
||||
"name": "receive-fireflies-notes",
|
||||
"description": "Receives Fireflies webhooks, fetches meeting summaries, and stores them in Twenty.",
|
||||
"code": {
|
||||
"src": "serverlessFunctions/receive-fireflies-notes/src/receive-fireflies-notes.ts"
|
||||
},
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "7742d477-4057-436d-9298-565f2934cf1a",
|
||||
"type": "route",
|
||||
"path": "/webhook/fireflies",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
@@ -15,8 +15,7 @@
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
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',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'aae3f523-4c1f-4805-b3ee-afeb676c381e',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Required to send requests to Twenty',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '6d19bb04-45bb-46aa-a4e5-4a2682c7b19d',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Optional, defaults to cloud API URL',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "last-email-interaction",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2"
|
||||
}
|
||||
}
|
||||
+25
-7
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? '';
|
||||
const TWENTY_URL =
|
||||
@@ -103,7 +104,7 @@ export const main = async (params: {
|
||||
properties: Record<string, any>;
|
||||
recordId: string;
|
||||
userId: string;
|
||||
}): Promise<object> => {
|
||||
}): Promise<object | undefined> => {
|
||||
if (TWENTY_API_KEY === '') {
|
||||
console.log("Function exited as API key or URL hasn't been set properly");
|
||||
return {};
|
||||
@@ -121,22 +122,22 @@ export const main = async (params: {
|
||||
const response = await axios.request(options);
|
||||
const objects = response.data.data.objects;
|
||||
const company_object = objects.find(
|
||||
(object) => object.nameSingular === 'company',
|
||||
(object: any) => object.nameSingular === 'company',
|
||||
);
|
||||
const company_last_interaction = company_object.fields.find(
|
||||
(field) => field.name === 'lastInteraction',
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const company_interaction_status = company_object.fields.find(
|
||||
(field) => field.name === 'interactionStatus',
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
const person_object = objects.find(
|
||||
(object) => object.nameSingular === 'person',
|
||||
(object: any) => object.nameSingular === 'person',
|
||||
);
|
||||
const person_last_interaction = person_object.fields.find(
|
||||
(field) => field.name === 'lastInteraction',
|
||||
(field: any) => field.name === 'lastInteraction',
|
||||
);
|
||||
const person_interaction_status = person_object.fields.find(
|
||||
(field) => field.name === 'interactionStatus',
|
||||
(field: any) => field.name === 'interactionStatus',
|
||||
);
|
||||
// If not, create them
|
||||
if (company_last_interaction === undefined) {
|
||||
@@ -273,3 +274,20 @@ export const main = async (params: {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "718ed9ab-53fc-49c8-8deb-0cff78ecf0d2",
|
||||
"name": "Last email interaction",
|
||||
"description": "Updates Last interaction and Interaction status fields based on last received email",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Required to send requests to Twenty"
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Optional, defaults to cloud API URL"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2"
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '627280a0-cb5b-40d3-a2e3-3e34b92926c8',
|
||||
displayName: 'Browser Extension',
|
||||
description: '',
|
||||
applicationVariables: {
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '6cf6a57a-9708-4995-b6a5-65222ee1baf1',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Twenty API URL',
|
||||
},
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '05d12575-e96e-4e45-b019-80dcdb67dc80',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Twenty API Key',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+1
-16
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "browser-extension",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -9,21 +10,5 @@
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "627280a0-cb5b-40d3-a2e3-3e34b92926c8",
|
||||
"name": "Browser Extension",
|
||||
"description": "",
|
||||
"env": {
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Twenty API URL"
|
||||
},
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Twenty API Key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "cead3d1e-1fbd-4b09-86a9-f0bedf4d54fa",
|
||||
"name": "create-company",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "57ff5ea2-c4b7-458c-9296-27bad6acdaf9",
|
||||
"type": "route",
|
||||
"path": "/create/company",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+18
-4
@@ -1,6 +1,6 @@
|
||||
export const main = async (params: {
|
||||
name: string
|
||||
}): Promise<object> => {
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (params: { name: string }): Promise<object> => {
|
||||
const response = await fetch(`${process.env.TWENTY_API_URL}/rest/companies`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -8,7 +8,7 @@ export const main = async (params: {
|
||||
Authorization: `Bearer ${process.env.TWENTY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: params.name
|
||||
name: params.name,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -18,3 +18,17 @@ export const main = async (params: {
|
||||
|
||||
return (await response.json()) as object;
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'cead3d1e-1fbd-4b09-86a9-f0bedf4d54fa',
|
||||
name: 'create-company',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '57ff5ea2-c4b7-458c-9296-27bad6acdaf9',
|
||||
type: 'route',
|
||||
path: '/create/company',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "7d38261b-99c5-43e7-83d8-bdcedc2dffdb",
|
||||
"name": "create-person",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "ecf261b8-183b-4323-ab95-3b11009a0eae",
|
||||
"type": "route",
|
||||
"path": "/create/person",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -1,3 +1,5 @@
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (params: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
@@ -22,3 +24,17 @@ export const main = async (params: {
|
||||
|
||||
return (await response.json()) as object;
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: '7d38261b-99c5-43e7-83d8-bdcedc2dffdb',
|
||||
name: 'create-person',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'ecf261b8-183b-4323-ab95-3b11009a0eae',
|
||||
type: 'route',
|
||||
path: '/create/person',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "8e43b96b-49a1-4e21-b257-e432a757b09f",
|
||||
"name": "get-company",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "7a2bb8ad-6366-49ac-9f73-db9c4713c5af",
|
||||
"type": "route",
|
||||
"path": "/get/company",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
-2
@@ -1,3 +1,5 @@
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (params: {
|
||||
a: string;
|
||||
b: number;
|
||||
@@ -8,7 +10,19 @@ export const main = async (params: {
|
||||
// This is just an example
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
|
||||
|
||||
|
||||
return { message };
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: '8e43b96b-49a1-4e21-b257-e432a757b09f',
|
||||
name: 'get-company',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '7a2bb8ad-6366-49ac-9f73-db9c4713c5af',
|
||||
type: 'route',
|
||||
path: '/get/company',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "87ea9816-c9e5-4860-b49f-a5f0759800f7",
|
||||
"name": "get-person",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "54aec609-0518-4fb0-bd90-7cd21507fe11",
|
||||
"type": "route",
|
||||
"path": "/get/person",
|
||||
"httpMethod": "GET",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
-3
@@ -1,3 +1,5 @@
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
export const main = async (params: {
|
||||
a: string;
|
||||
b: number;
|
||||
@@ -8,7 +10,19 @@ export const main = async (params: {
|
||||
// This is just an example
|
||||
const message = `Hello, input: ${a} and ${b}`;
|
||||
|
||||
|
||||
|
||||
return { message };
|
||||
};
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: '87ea9816-c9e5-4860-b49f-a5f0759800f7',
|
||||
name: 'get-person',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '54aec609-0518-4fb0-bd90-7cd21507fe11',
|
||||
type: 'route',
|
||||
path: '/get/person',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# This file is generated by running "yarn install" inside your project.
|
||||
# Manual changes might be lost - proceed with caution!
|
||||
|
||||
__metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"@types/node@npm:^24.7.2":
|
||||
version: 24.10.0
|
||||
resolution: "@types/node@npm:24.10.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.16.0"
|
||||
checksum: 10c0/f82ed7194e16f5590ef7afdc20c6d09068c76d50278b485ede8f0c5749683536e3064ffa8def8db76915196afb3724b854aa5723c64d6571b890b14492943b46
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"browser-extension@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "browser-extension@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"undici-types@npm:~7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici-types@npm:7.16.0"
|
||||
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '1eadac4e-db9f-4cce-b20b-de75f41e34dc',
|
||||
displayName: 'Mailchimp synchronizer',
|
||||
description: '',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '0af17af3-66b8-40cf-b6e2-6a29a1da5464',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Required to send requests to Twenty',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '12949c1c-aed7-4a9f-bd06-9fd15f0bfa63',
|
||||
value: '',
|
||||
description: 'Optional, defaults to cloud API URL',
|
||||
},
|
||||
MAILCHIMP_API_KEY: {
|
||||
universalIdentifier: 'f10d4e8a-8055-4eb2-b9ad-efd69d43b1f0',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Required to send requests to Mailchimp',
|
||||
},
|
||||
MAILCHIMP_URL: {
|
||||
universalIdentifier: '6c8b6ac9-dd45-4f0b-a397-c4a38edccfd9',
|
||||
value: '',
|
||||
description: 'Required to send requests to Mailchimp',
|
||||
},
|
||||
IS_EMAIL_CONSTRAINT: {
|
||||
universalIdentifier: '62626c57-470f-4866-be1e-5b4d7ec09f9f',
|
||||
value: 'false',
|
||||
description:
|
||||
'Set to true if you want to add additional constraint (default is false)',
|
||||
},
|
||||
IS_PHONE_CONSTRAINT: {
|
||||
universalIdentifier: 'fac8ec5b-dade-46bf-b938-3dfdef0aa298',
|
||||
value: 'false',
|
||||
description:
|
||||
'Set to true if you want to add additional constraint (default is false)',
|
||||
},
|
||||
IS_COMPANY_CONSTRAINT: {
|
||||
universalIdentifier: '9ffd8e76-4ab2-42f9-8549-3622a5ae2343',
|
||||
value: 'false',
|
||||
description:
|
||||
'Set to true if you want to add additional constraint (default is false)',
|
||||
},
|
||||
IS_ADDRESS_CONSTRAINT: {
|
||||
universalIdentifier: '4b899eb6-517e-4afd-bbf8-88097900ea42',
|
||||
value: 'false',
|
||||
description:
|
||||
'Set to true if you want to add additional constraint (default is false)',
|
||||
},
|
||||
UPDATE_PERSON: {
|
||||
universalIdentifier: '9d753e1e-4408-40ca-b0f0-5c7e8625c2aa',
|
||||
value: 'false',
|
||||
description: 'Set to true if you want to update record if it exists',
|
||||
},
|
||||
MAILCHIMP_AUDIENCE_ID: {
|
||||
universalIdentifier: '5492f06f-bb29-4c93-9436-b4736a396376',
|
||||
value: '',
|
||||
description: 'Required to send requests to Mailchimp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "mailchimp-synchronizer",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -7,54 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "1eadac4e-db9f-4cce-b20b-de75f41e34dc",
|
||||
"name": "Mailchimp synchronizer",
|
||||
"description": "",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Required to send requests to Twenty"
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"value": "",
|
||||
"description": "Optional, defaults to cloud API URL"
|
||||
},
|
||||
"MAILCHIMP_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Required to send requests to Mailchimp"
|
||||
},
|
||||
"MAILCHIMP_URL": {
|
||||
"value": "",
|
||||
"description": "Required to send requests to Mailchimp"
|
||||
},
|
||||
"IS_EMAIL_CONSTRAINT": {
|
||||
"value": "false",
|
||||
"description": "Set to true if you want to add additional constraint (default is false)"
|
||||
},
|
||||
"IS_PHONE_CONSTRAINT": {
|
||||
"value": "false",
|
||||
"description": "Set to true if you want to add additional constraint (default is false)"
|
||||
},
|
||||
"IS_COMPANY_CONSTRAINT": {
|
||||
"value": "false",
|
||||
"description": "Set to true if you want to add additional constraint (default is false)"
|
||||
},
|
||||
"IS_ADDRESS_CONSTRAINT": {
|
||||
"value": "false",
|
||||
"description": "Set to true if you want to add additional constraint (default is false)"
|
||||
},
|
||||
"UPDATE_PERSON": {
|
||||
"value": "false",
|
||||
"description": "Set to true if you want to update record if it exists"
|
||||
},
|
||||
"MAILCHIMP_AUDIENCE_ID": {
|
||||
"value": "",
|
||||
"description": "Required to send requests to Mailchimp"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.1"
|
||||
}
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "83319670-775b-4862-b133-5c353e594151",
|
||||
"name": "mailchimp-synchronizer",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "e627ff6f-0a0c-48b2-bdbb-31967489ec96",
|
||||
"type": "databaseEvent",
|
||||
"eventName": "person.created"
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
-4
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
const TWENTY_API_URL: string =
|
||||
process.env.TWENTY_API_URL !== '' && process.env.TWENTY_API_URL !== undefined
|
||||
@@ -119,9 +120,9 @@ const prepareData = (
|
||||
phoneNumber: string,
|
||||
phoneCallingCode: string,
|
||||
companyName: string,
|
||||
address: mailchimpAddress,
|
||||
address: mailchimpAddress | null,
|
||||
): object => {
|
||||
const data = {};
|
||||
const data = {} as any;
|
||||
if (IS_EMAIL_CONSTRAINT) {
|
||||
data['email_channel'] = {
|
||||
email: email,
|
||||
@@ -194,11 +195,11 @@ export const main = async (params: {
|
||||
IS_PHONE_CONSTRAINT &&
|
||||
twentyRecord.phones.primaryPhoneNumber !== '' &&
|
||||
twentyRecord.phones.primaryPhoneCallingCode !== '';
|
||||
const company: object = IS_COMPANY_CONSTRAINT
|
||||
const company: any = IS_COMPANY_CONSTRAINT
|
||||
? await fetchCompanyData(properties.after.companyId)
|
||||
: null;
|
||||
const companyName: string = company['name'] !== '' ? company['name'] : null;
|
||||
const address: mailchimpAddress = IS_ADDRESS_CONSTRAINT
|
||||
const address: mailchimpAddress | null = IS_ADDRESS_CONSTRAINT
|
||||
? checkAddress(company['address'])
|
||||
: null;
|
||||
|
||||
@@ -265,3 +266,15 @@ export const main = async (params: {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: '83319670-775b-4862-b133-5c353e594151',
|
||||
name: 'mailchimp-synchronizer',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'e627ff6f-0a0c-48b2-bdbb-31967489ec96',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.created',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -5,14 +5,6 @@ __metadata:
|
||||
version: 8
|
||||
cacheKey: 10c0
|
||||
|
||||
"Mailchimp synchronizer@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "Mailchimp synchronizer@workspace:."
|
||||
dependencies:
|
||||
axios: "npm:^1.13.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"async-function@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-function@npm:1.0.0"
|
||||
@@ -217,6 +209,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mailchimp-synchronizer@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "mailchimp-synchronizer@workspace:."
|
||||
dependencies:
|
||||
axios: "npm:^1.13.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"math-intrinsics@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "math-intrinsics@npm:1.1.0"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || ^22.0.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "f15a0c72-f7b4-4d20-9e97-ade1122d4bd7",
|
||||
"name": "Meeting Transcript",
|
||||
"description": "AI Meeting Transcript Integration for Hacktoberfest",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "API key for the Twenty CRM instance (used for authentication)."
|
||||
},
|
||||
"AI_PROVIDER_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "API key for authenticating with the OpenAI-compatible service (supports OpenAI, Groq, and other providers)."
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": false,
|
||||
"value": "https://unpaid-interns.twenty.com",
|
||||
"description": "The base URL for the Twenty CRM server (e.g., https://your-instance.twenty.com)."
|
||||
},
|
||||
"WEBHOOK_SECRET_TOKEN": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Secret token used to authenticate incoming webhook requests."
|
||||
},
|
||||
"AI_PROVIDER_API_BASE_URL": {
|
||||
"isSecret": false,
|
||||
"value": "https://api.openai.com/v1",
|
||||
"description": "Base URL for OpenAI-compatible API. Defaults to OpenAI, but can be changed to use Groq (https://api.groq.com/openai/v1) or other providers."
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.0",
|
||||
"openai": "^4.67.0"
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/src/constants/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "b5e86982-b9ec-4c3d-8a02-6ea08a5b2d35",
|
||||
"name": "process-transcript",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "a25fcbbf-1a20-438a-b277-ee8ca9770499",
|
||||
"type": "route",
|
||||
"path": "/transcript",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
// <-- add this
|
||||
"types": ["node"], // Node types
|
||||
"lib": ["esnext"], // Modern JS libs
|
||||
"esModuleInterop": true, // <-- add this for axios & other CJS imports
|
||||
|
||||
// Other Outputs
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
// Stricter Typechecking Options
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Recommended Options
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: 'f15a0c72-f7b4-4d20-9e97-ade1122d4bd7',
|
||||
displayName: 'Meeting Transcript',
|
||||
description: 'AI Meeting Transcript Integration for Hacktoberfest',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'c5a4310b-6744-4fda-ad0a-d1c6fea0539b',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description:
|
||||
'API key for the Twenty CRM instance (used for authentication).',
|
||||
},
|
||||
AI_PROVIDER_API_KEY: {
|
||||
universalIdentifier: '7b0f965e-0192-41b5-b390-45a7e5a761b8',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description:
|
||||
'API key for authenticating with the OpenAI-compatible service (supports OpenAI, Groq, and other providers).',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '84311303-1220-440c-a4fb-0be2d74d267b',
|
||||
isSecret: false,
|
||||
value: 'https://unpaid-interns.twenty.com',
|
||||
description:
|
||||
'The base URL for the Twenty CRM server (e.g., https://your-instance.twenty.com).',
|
||||
},
|
||||
WEBHOOK_SECRET_TOKEN: {
|
||||
universalIdentifier: '187c39c9-8e2a-4086-94b3-59935d4e1a93',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description:
|
||||
'Secret token used to authenticate incoming webhook requests.',
|
||||
},
|
||||
AI_PROVIDER_API_BASE_URL: {
|
||||
universalIdentifier: '15974ed8-4efb-4ebc-9f53-5b0b36183fc4',
|
||||
isSecret: false,
|
||||
value: 'https://api.openai.com/v1',
|
||||
description:
|
||||
'Base URL for OpenAI-compatible API. Defaults to OpenAI, but can be changed to use Groq (https://api.groq.com/openai/v1) or other providers.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "meeting-transcript",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || ^22.0.0",
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.0",
|
||||
"openai": "^4.67.0"
|
||||
}
|
||||
}
|
||||
+134
-69
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import OpenAI from 'openai';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
type TranscriptWebhookPayload = {
|
||||
transcript: string;
|
||||
@@ -91,16 +92,12 @@ const lookupWorkspaceMemberByName = async (
|
||||
`,
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/graphql`,
|
||||
graphqlQuery,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
const response = await axios.post(`${baseUrl}/graphql`, graphqlQuery, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const edges = response.data?.data?.workspaceMembers?.edges;
|
||||
|
||||
@@ -117,7 +114,9 @@ const lookupWorkspaceMemberByName = async (
|
||||
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
|
||||
|
||||
if (fullName === searchName) {
|
||||
console.log(`✅ Found workspace member (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found workspace member (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -126,7 +125,9 @@ const lookupWorkspaceMemberByName = async (
|
||||
const firstName = edge.node.name?.firstName || '';
|
||||
if (firstName.toLowerCase() === searchName) {
|
||||
const lastName = edge.node.name?.lastName || '';
|
||||
console.log(`✅ Found workspace member (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found workspace member (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +136,9 @@ const lookupWorkspaceMemberByName = async (
|
||||
const lastName = edge.node.name?.lastName || '';
|
||||
if (lastName.toLowerCase() === searchName) {
|
||||
const firstName = edge.node.name?.firstName || '';
|
||||
console.log(`✅ Found workspace member (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found workspace member (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +149,9 @@ const lookupWorkspaceMemberByName = async (
|
||||
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
|
||||
|
||||
if (fullName.includes(searchName) || searchName.includes(fullName)) {
|
||||
console.log(`✅ Found workspace member (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found workspace member (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -158,15 +163,15 @@ const lookupWorkspaceMemberByName = async (
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
console.error(`❌ Failed to lookup workspace member "${name}": ${errorMessage}`);
|
||||
console.error(
|
||||
`❌ Failed to lookup workspace member "${name}": ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const lookupPersonByName = async (
|
||||
name: string,
|
||||
): Promise<string | null> => {
|
||||
const lookupPersonByName = async (name: string): Promise<string | null> => {
|
||||
const { apiKey, baseUrl } = getTwentyApiConfig();
|
||||
|
||||
try {
|
||||
@@ -188,16 +193,12 @@ const lookupPersonByName = async (
|
||||
`,
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/graphql`,
|
||||
graphqlQuery,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
const response = await axios.post(`${baseUrl}/graphql`, graphqlQuery, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const edges = response.data?.data?.people?.edges;
|
||||
|
||||
@@ -214,7 +215,9 @@ const lookupPersonByName = async (
|
||||
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
|
||||
|
||||
if (fullName === searchName) {
|
||||
console.log(`✅ Found person (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found person (exact): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -223,7 +226,9 @@ const lookupPersonByName = async (
|
||||
const firstName = edge.node.name?.firstName || '';
|
||||
if (firstName.toLowerCase() === searchName) {
|
||||
const lastName = edge.node.name?.lastName || '';
|
||||
console.log(`✅ Found person (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found person (first name): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +237,9 @@ const lookupPersonByName = async (
|
||||
const lastName = edge.node.name?.lastName || '';
|
||||
if (lastName.toLowerCase() === searchName) {
|
||||
const firstName = edge.node.name?.firstName || '';
|
||||
console.log(`✅ Found person (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found person (last name): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -243,7 +250,9 @@ const lookupPersonByName = async (
|
||||
const fullName = `${firstName} ${lastName}`.trim().toLowerCase();
|
||||
|
||||
if (fullName.includes(searchName) || searchName.includes(fullName)) {
|
||||
console.log(`✅ Found person (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`);
|
||||
console.log(
|
||||
`✅ Found person (partial): ${firstName} ${lastName} (ID: ${edge.node.id})`,
|
||||
);
|
||||
return edge.node.id;
|
||||
}
|
||||
}
|
||||
@@ -261,7 +270,10 @@ const lookupPersonByName = async (
|
||||
}
|
||||
};
|
||||
|
||||
const extractPersonNamesFromDescription = (description: string, participants: string[]): string[] => {
|
||||
const extractPersonNamesFromDescription = (
|
||||
description: string,
|
||||
participants: string[],
|
||||
): string[] => {
|
||||
const foundNames: string[] = [];
|
||||
|
||||
for (const participant of participants) {
|
||||
@@ -308,7 +320,9 @@ const linkNoteToPersonREST = async (
|
||||
const noteTargetId = response.data?.data?.createNoteTarget?.id;
|
||||
|
||||
if (noteTargetId) {
|
||||
console.log(`✅ Successfully linked note ${noteId} to person ${personId} (noteTarget: ${noteTargetId})`);
|
||||
console.log(
|
||||
`✅ Successfully linked note ${noteId} to person ${personId} (noteTarget: ${noteTargetId})`,
|
||||
);
|
||||
} else {
|
||||
console.warn(`⚠️ Note linking response received but no ID found`);
|
||||
}
|
||||
@@ -318,8 +332,12 @@ const linkNoteToPersonREST = async (
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
const status = error.response?.status;
|
||||
console.error(`❌ Failed to link note to person. Status: ${status}, Error: ${errorMessage}`);
|
||||
console.error(`Attempted to link noteId: ${noteId} to personId: ${personId}`);
|
||||
console.error(
|
||||
`❌ Failed to link note to person. Status: ${status}, Error: ${errorMessage}`,
|
||||
);
|
||||
console.error(
|
||||
`Attempted to link noteId: ${noteId} to personId: ${personId}`,
|
||||
);
|
||||
throw new Error(`Failed to link note to person: ${errorMessage}`);
|
||||
}
|
||||
throw error;
|
||||
@@ -348,16 +366,12 @@ const createNoteInTwenty = async (
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/rest/notes`,
|
||||
requestData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
const response = await axios.post(`${baseUrl}/rest/notes`, requestData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const responseJson = JSON.stringify(response.data);
|
||||
console.log('📦 Note API Response:', responseJson);
|
||||
@@ -409,15 +423,22 @@ const linkTaskToPersonREST = async (
|
||||
},
|
||||
},
|
||||
);
|
||||
console.log(`✅ Successfully linked task ${taskId} to person ${personId}`, response.data);
|
||||
console.log(
|
||||
`✅ Successfully linked task ${taskId} to person ${personId}`,
|
||||
response.data,
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage = error.response?.data
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
const status = error.response?.status;
|
||||
console.error(`❌ Failed to link task to person. Status: ${status}, Error: ${errorMessage}`);
|
||||
console.error(`Attempted to link taskId: ${taskId} to personId: ${personId}`);
|
||||
console.error(
|
||||
`❌ Failed to link task to person. Status: ${status}, Error: ${errorMessage}`,
|
||||
);
|
||||
console.error(
|
||||
`Attempted to link taskId: ${taskId} to personId: ${personId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -446,9 +467,13 @@ const createTaskInTwenty = async (
|
||||
const assigneeId = await lookupWorkspaceMemberByName(actionItem.assignee);
|
||||
if (assigneeId) {
|
||||
taskData.assigneeId = assigneeId;
|
||||
console.log(`✅ Task will be assigned to: ${actionItem.assignee} (${assigneeId})`);
|
||||
console.log(
|
||||
`✅ Task will be assigned to: ${actionItem.assignee} (${assigneeId})`,
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠️ Could not find workspace member "${actionItem.assignee}", task will be unassigned`);
|
||||
console.log(
|
||||
`⚠️ Could not find workspace member "${actionItem.assignee}", task will be unassigned`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,27 +485,28 @@ const createTaskInTwenty = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/rest/tasks`,
|
||||
taskData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
const response = await axios.post(`${baseUrl}/rest/tasks`, taskData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
console.log('📦 Task API Response:', JSON.stringify(response.data));
|
||||
|
||||
const taskId = response.data?.data?.createTask?.id;
|
||||
|
||||
if (!taskId) {
|
||||
console.error('❌ Failed to extract task ID from response:', response.data);
|
||||
console.error(
|
||||
'❌ Failed to extract task ID from response:',
|
||||
response.data,
|
||||
);
|
||||
throw new Error('Task created but ID not found in response');
|
||||
}
|
||||
|
||||
console.log(`✅ Task created successfully: ${taskId} - "${actionItem.title}"`);
|
||||
console.log(
|
||||
`✅ Task created successfully: ${taskId} - "${actionItem.title}"`,
|
||||
);
|
||||
|
||||
if (relatedPersonId) {
|
||||
await linkTaskToPersonREST(taskId, relatedPersonId);
|
||||
@@ -493,7 +519,9 @@ const createTaskInTwenty = async (
|
||||
? JSON.stringify(error.response.data, null, 2)
|
||||
: error.message;
|
||||
const status = error.response?.status;
|
||||
console.error(`❌ Failed to create task "${actionItem.title}". Status: ${status}, Error: ${errorMessage}`);
|
||||
console.error(
|
||||
`❌ Failed to create task "${actionItem.title}". Status: ${status}, Error: ${errorMessage}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to create task "${actionItem.title}": ${errorMessage}. Status: ${status}`,
|
||||
);
|
||||
@@ -515,7 +543,10 @@ const createTasksFromActionItems = async (
|
||||
const taskDescription = `${actionItem.description}\n\n*Related to meeting note: ${noteId}*`;
|
||||
console.log(`Creating task: "${actionItem.title}"`);
|
||||
|
||||
const mentionedPeople = extractPersonNamesFromDescription(actionItem.description, participants);
|
||||
const mentionedPeople = extractPersonNamesFromDescription(
|
||||
actionItem.description,
|
||||
participants,
|
||||
);
|
||||
console.log(`📝 People mentioned in task description:`, mentionedPeople);
|
||||
|
||||
const task = await createTaskInTwenty({
|
||||
@@ -531,17 +562,24 @@ const createTasksFromActionItems = async (
|
||||
if (personId) {
|
||||
await linkTaskToPersonREST(task.id, personId);
|
||||
} else {
|
||||
console.log(`⚠️ Could not find person "${personName}" in CRM, skipping link`);
|
||||
console.log(
|
||||
`⚠️ Could not find person "${personName}" in CRM, skipping link`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ No specific people mentioned, using relatedPersonId as fallback`);
|
||||
console.log(
|
||||
`⚠️ No specific people mentioned, using relatedPersonId as fallback`,
|
||||
);
|
||||
await linkTaskToPersonREST(task.id, relatedPersonId);
|
||||
}
|
||||
|
||||
console.log(`✅ Task linking complete: ${task.id}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Task creation failed for "${actionItem.title}":`, error instanceof Error ? error.message : error);
|
||||
console.error(
|
||||
`❌ Task creation failed for "${actionItem.title}":`,
|
||||
error instanceof Error ? error.message : error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +611,10 @@ const createTasksFromCommitments = async (
|
||||
await linkTaskToPersonREST(task.id, relatedPersonId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Commitment task creation failed for "${commitment.commitment}":`, error);
|
||||
console.error(
|
||||
`Commitment task creation failed for "${commitment.commitment}":`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,7 +737,9 @@ ${transcript}`;
|
||||
try {
|
||||
parsedResult = JSON.parse(content) as AnalysisResult;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`Failed to parse AI response as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsedResult.summary || typeof parsedResult.summary !== 'string') {
|
||||
@@ -732,7 +775,13 @@ export const main = async (
|
||||
throw new Error('Unauthorized webhook access: Invalid or missing token.');
|
||||
}
|
||||
|
||||
const { transcript, meetingTitle, meetingDate, relatedPersonId, participants } = params;
|
||||
const {
|
||||
transcript,
|
||||
meetingTitle,
|
||||
meetingDate,
|
||||
relatedPersonId,
|
||||
participants,
|
||||
} = params;
|
||||
|
||||
if (!transcript || typeof transcript !== 'string') {
|
||||
throw new Error('Transcript is required and must be a string');
|
||||
@@ -752,7 +801,9 @@ export const main = async (
|
||||
log('🤖 Starting transcript analysis...');
|
||||
|
||||
const analysis = await analyzeTranscript(transcript, openaiApiKey);
|
||||
log(`✅ Analysis complete: ${analysis.actionItems.length} action items, ${analysis.commitments.length} commitments`);
|
||||
log(
|
||||
`✅ Analysis complete: ${analysis.actionItems.length} action items, ${analysis.commitments.length} commitments`,
|
||||
);
|
||||
|
||||
log('📄 Creating note in Twenty CRM...');
|
||||
const note = await createNoteInTwenty(
|
||||
@@ -804,4 +855,18 @@ export const main = async (
|
||||
executionLogs: executionLogs,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'b5e86982-b9ec-4c3d-8a02-6ea08a5b2d35',
|
||||
name: 'process-transcript',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'a25fcbbf-1a20-438a-b277-ee8ca9770499',
|
||||
type: 'route',
|
||||
path: '/transcript',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
+11
-11
@@ -42,17 +42,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"Meeting Transcript@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "Meeting Transcript@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^20.0.0"
|
||||
axios: "npm:^1.7.0"
|
||||
openai: "npm:^4.67.0"
|
||||
typescript: "npm:^5.6.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"abort-controller@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "abort-controller@npm:3.0.0"
|
||||
@@ -315,6 +304,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"meeting-transcript@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "meeting-transcript@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^20.0.0"
|
||||
axios: "npm:^1.7.0"
|
||||
openai: "npm:^4.67.0"
|
||||
typescript: "npm:^5.6.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"mime-db@npm:1.52.0":
|
||||
version: 1.52.0
|
||||
resolution: "mime-db@npm:1.52.0"
|
||||
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '2b308f8c-6ff8-4838-9880-aa0271dfd8d8',
|
||||
displayName: 'Rollup engine',
|
||||
description: 'Rollup engine',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: '1dc3356a-f660-4097-a161-b1686ad00c74',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description:
|
||||
'Workspace API key used by the rollup engine to call the Twenty REST API.',
|
||||
},
|
||||
TWENTY_API_BASE_URL: {
|
||||
universalIdentifier: '274c512c-a870-4651-9617-2638e0def14c',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description:
|
||||
'Optional override for the REST base URL (defaults to https://app.twenty.com/rest).',
|
||||
},
|
||||
ROLLUP_ENGINE_CONFIG: {
|
||||
universalIdentifier: 'a4672cd9-4081-43af-9d3b-5a8a55a72613',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description:
|
||||
'Optional JSON override for rollup definitions. Leave blank to use the baked-in defaults.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "rollup-engine",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -7,27 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "2b308f8c-6ff8-4838-9880-aa0271dfd8d8",
|
||||
"name": "Rollup engine",
|
||||
"description": "Rollup engine",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Workspace API key used by the rollup engine to call the Twenty REST API."
|
||||
},
|
||||
"TWENTY_API_BASE_URL": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Optional override for the REST base URL (defaults to https://app.twenty.com/rest)."
|
||||
},
|
||||
"ROLLUP_ENGINE_CONFIG": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Optional JSON override for rollup definitions. Leave blank to use the baked-in defaults."
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"smoke": "node --import tsx scripts/run-rollup-smoke.ts",
|
||||
"setup:metadata": "node scripts/setup-company-rollup-fields.mjs"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { main as runRollups } from '../serverlessFunctions/calculaterollups/src/index.ts';
|
||||
import { main as runRollups } from '../serverlessFunctions/calculaterollups/src/index';
|
||||
|
||||
type Json = Record<string, unknown>;
|
||||
|
||||
@@ -48,7 +48,7 @@ const jsonResponse = (data: Json | Json[]) =>
|
||||
});
|
||||
|
||||
global.fetch = async (
|
||||
rawUrl: string | URL,
|
||||
rawUrl: any,
|
||||
init?: { method?: string; body?: unknown },
|
||||
): Promise<any> => {
|
||||
const url = typeof rawUrl === 'string' ? new URL(rawUrl) : rawUrl;
|
||||
@@ -62,7 +62,9 @@ global.fetch = async (
|
||||
if (url.pathname.endsWith('/opportunities') && method === 'GET') {
|
||||
const companyId = url.searchParams.get('filter[companyId]');
|
||||
const items = companyId
|
||||
? mockOpportunities.filter((opportunity) => opportunity.companyId === companyId)
|
||||
? mockOpportunities.filter(
|
||||
(opportunity) => opportunity.companyId === companyId,
|
||||
)
|
||||
: mockOpportunities;
|
||||
return jsonResponse({
|
||||
data: {
|
||||
@@ -81,10 +83,12 @@ global.fetch = async (
|
||||
return jsonResponse({ data: { companies: [{ id, ...payload }] } });
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled request in mock fetch: ${method} ${url.toString()}`);
|
||||
throw new Error(
|
||||
`Unhandled request in mock fetch: ${method} ${url.toString()}`,
|
||||
);
|
||||
};
|
||||
|
||||
function safeParse(body: unknown) {
|
||||
const safeParse = (body: unknown) => {
|
||||
if (!body) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -97,9 +101,9 @@ function safeParse(body: unknown) {
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const main = async () => {
|
||||
process.env.TWENTY_API_KEY = 'mock-api-key';
|
||||
process.env.TWENTY_API_BASE_URL = 'https://mock.twenty/api';
|
||||
|
||||
@@ -123,15 +127,22 @@ async function main() {
|
||||
lastOpportunityCloseDate: `${currentYear.toString().padStart(4, '0')}-03-05`,
|
||||
};
|
||||
|
||||
const companyUpdate = updatePayloads.find((payload) => payload.id === 'company-1');
|
||||
const companyUpdate = updatePayloads.find(
|
||||
(payload) => payload.id === 'company-1',
|
||||
);
|
||||
assert(companyUpdate, 'expected a PATCH payload for company-1');
|
||||
assert.deepStrictEqual(companyUpdate.payload, expectedPayload);
|
||||
|
||||
const opportunitiesRequest = requestLog.find(
|
||||
(entry) => entry.method === 'GET' && entry.url.includes('/opportunities'),
|
||||
);
|
||||
assert(opportunitiesRequest, 'expected at least one GET request for /opportunities');
|
||||
const filterParams = new URL(opportunitiesRequest.url).searchParams.getAll('filter');
|
||||
assert(
|
||||
opportunitiesRequest,
|
||||
'expected at least one GET request for /opportunities',
|
||||
);
|
||||
const filterParams = new URL(opportunitiesRequest.url).searchParams.getAll(
|
||||
'filter',
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
filterParams,
|
||||
['companyId[eq]:"company-1"'],
|
||||
@@ -146,7 +157,7 @@ async function main() {
|
||||
|
||||
console.log('\n--- Requests made ---');
|
||||
console.dir(requestLog, { depth: null });
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Smoke test failed', error);
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "c3ec36c8-5b1d-421f-9172-a9e035ab9c18",
|
||||
"name": "calculaterollups",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "eec8aaf2-b0cc-47fd-b522-8d4aa5fe4bd3",
|
||||
"type": "databaseEvent",
|
||||
"eventName": "opportunity.*"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "a3fea230-1121-44a6-b395-5811c3031f8e",
|
||||
"type": "cron",
|
||||
"schedule": " 0 2 * * *"
|
||||
},
|
||||
{
|
||||
"universalIdentifier": "d33b0fe4-4b2b-45c0-aa2f-e617fdbba484",
|
||||
"type": "route",
|
||||
"path": "/recalculate-all",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+37
-22
@@ -10,7 +10,10 @@ export const computeAggregations = (
|
||||
now: Date,
|
||||
) => {
|
||||
const baseFiltered = applyFilters(records, definition.childFilters, now);
|
||||
const result: Record<string, number | string | null | Record<string, unknown>> = {};
|
||||
const result: Record<
|
||||
string,
|
||||
number | string | null | Record<string, unknown>
|
||||
> = {};
|
||||
|
||||
definition.aggregations.forEach((aggregation) => {
|
||||
const scopedRecords = applyFilters(baseFiltered, aggregation.filters, now);
|
||||
@@ -40,7 +43,7 @@ export const computeAggregations = (
|
||||
return accumulator;
|
||||
}
|
||||
return {
|
||||
amount: accumulator.amount + numeric,
|
||||
amount: (accumulator.amount as number) + numeric,
|
||||
currency:
|
||||
typeof currencyRaw === 'string' && currencyRaw.trim().length > 0
|
||||
? currencyRaw
|
||||
@@ -50,7 +53,7 @@ export const computeAggregations = (
|
||||
{ amount: 0, currency: undefined as string | undefined },
|
||||
);
|
||||
result[aggregation.parentField] = {
|
||||
amountMicros: Math.round(roundForSum(total.amount)),
|
||||
amountMicros: Math.round(roundForSum(total.amount as number)),
|
||||
currencyCode: total.currency ?? '',
|
||||
};
|
||||
break;
|
||||
@@ -72,46 +75,58 @@ export const computeAggregations = (
|
||||
return accumulator;
|
||||
}
|
||||
return {
|
||||
total: accumulator.total + numeric,
|
||||
count: accumulator.count + 1,
|
||||
total: (accumulator.total as number) + numeric,
|
||||
count: (accumulator.count as number) + 1,
|
||||
};
|
||||
},
|
||||
{ total: 0, count: 0 },
|
||||
);
|
||||
result[aggregation.parentField] = count === 0 ? null : roundForSum(total / count);
|
||||
result[aggregation.parentField] =
|
||||
count === 0
|
||||
? null
|
||||
: roundForSum((total as number) / (count as number));
|
||||
break;
|
||||
}
|
||||
case 'MAX':
|
||||
case 'MIN': {
|
||||
if (!aggregation.childField) {
|
||||
throw new Error(`${aggregation.type} aggregation requires childField`);
|
||||
const childField = aggregation.childField;
|
||||
if (!childField) {
|
||||
throw new Error(
|
||||
`${aggregation.type} aggregation requires childField`,
|
||||
);
|
||||
}
|
||||
|
||||
const direction = aggregation.type === 'MAX' ? 1 : -1;
|
||||
let chosen: { raw: unknown; comparable: number | null } | null = null;
|
||||
scopedRecords.forEach((record) => {
|
||||
const rawValue = getNestedValue(record, aggregation.childField!);
|
||||
let chosen: { raw: unknown; comparable: number } | null = null;
|
||||
|
||||
for (const record of scopedRecords) {
|
||||
const rawValue = getNestedValue(record, childField);
|
||||
const comparable = toComparableNumber(rawValue);
|
||||
if (comparable === null) {
|
||||
return;
|
||||
}
|
||||
if (comparable === null) continue;
|
||||
|
||||
if (
|
||||
chosen === null ||
|
||||
(chosen.comparable !== null && direction * (comparable - chosen.comparable) > 0)
|
||||
direction * (comparable - chosen.comparable) > 0
|
||||
) {
|
||||
chosen = { raw: rawValue, comparable };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (chosen === null) {
|
||||
result[aggregation.parentField] = null;
|
||||
} else if (typeof chosen.raw === 'number') {
|
||||
result[aggregation.parentField] = chosen.raw;
|
||||
} else if (chosen.raw instanceof Date) {
|
||||
result[aggregation.parentField] = chosen.raw.toISOString().slice(0, 10);
|
||||
} else if (chosen.raw === null || chosen.raw === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
const raw = chosen.raw;
|
||||
|
||||
if (typeof raw === 'number') {
|
||||
result[aggregation.parentField] = raw;
|
||||
} else if (raw instanceof Date) {
|
||||
result[aggregation.parentField] = raw.toISOString().slice(0, 10);
|
||||
} else if (raw == null) {
|
||||
result[aggregation.parentField] = null;
|
||||
} else {
|
||||
const rawString = String(chosen.raw);
|
||||
const rawString = String(raw);
|
||||
const parsed = Date.parse(rawString);
|
||||
result[aggregation.parentField] = Number.isNaN(parsed)
|
||||
? rawString
|
||||
|
||||
+45
-14
@@ -28,18 +28,27 @@ const aggregationTypes = new Set<AggregationConfig['type']>([
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
export const validateRollupConfig = (config: unknown): asserts config is RollupConfig => {
|
||||
export const validateRollupConfig = (config: any): RollupConfig | void => {
|
||||
if (!Array.isArray(config)) {
|
||||
throw new Error('Rollup configuration must contain an array of rollup definitions');
|
||||
throw new Error(
|
||||
'Rollup configuration must contain an array of rollup definitions',
|
||||
);
|
||||
}
|
||||
|
||||
config.forEach((definition, definitionIndex) => {
|
||||
if (!isObject(definition)) {
|
||||
throw new Error(`Definition at index ${definitionIndex} must be an object`);
|
||||
throw new Error(
|
||||
`Definition at index ${definitionIndex} must be an object`,
|
||||
);
|
||||
}
|
||||
|
||||
const { parentObject, childObject, relationField, childFilters, aggregations } =
|
||||
definition as Partial<RollupDefinition>;
|
||||
const {
|
||||
parentObject,
|
||||
childObject,
|
||||
relationField,
|
||||
childFilters,
|
||||
aggregations,
|
||||
} = definition as Partial<RollupDefinition>;
|
||||
|
||||
if (typeof parentObject !== 'string' || parentObject.trim().length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} missing parentObject`);
|
||||
@@ -47,11 +56,16 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
if (typeof childObject !== 'string' || childObject.trim().length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} missing childObject`);
|
||||
}
|
||||
if (typeof relationField !== 'string' || relationField.trim().length === 0) {
|
||||
if (
|
||||
typeof relationField !== 'string' ||
|
||||
relationField.trim().length === 0
|
||||
) {
|
||||
throw new Error(`Definition ${definitionIndex} missing relationField`);
|
||||
}
|
||||
if (!Array.isArray(aggregations) || aggregations.length === 0) {
|
||||
throw new Error(`Definition ${definitionIndex} must declare at least one aggregation`);
|
||||
throw new Error(
|
||||
`Definition ${definitionIndex} must declare at least one aggregation`,
|
||||
);
|
||||
}
|
||||
|
||||
const filtersToValidate = [
|
||||
@@ -63,7 +77,8 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
);
|
||||
}
|
||||
|
||||
const { type, parentField, childField, filters } = aggregation as AggregationConfig;
|
||||
const { type, parentField, childField, filters } =
|
||||
aggregation as AggregationConfig;
|
||||
|
||||
if (!aggregationTypes.has(type)) {
|
||||
throw new Error(
|
||||
@@ -71,13 +86,19 @@ export const validateRollupConfig = (config: unknown): asserts config is RollupC
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof parentField !== 'string' || parentField.trim().length === 0) {
|
||||
if (
|
||||
typeof parentField !== 'string' ||
|
||||
parentField.trim().length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
`Aggregation ${aggregationIndex} in definition ${definitionIndex} missing parentField`,
|
||||
);
|
||||
}
|
||||
|
||||
if (type !== 'COUNT' && (typeof childField !== 'string' || childField.length === 0)) {
|
||||
if (
|
||||
type !== 'COUNT' &&
|
||||
(typeof childField !== 'string' || childField.length === 0)
|
||||
) {
|
||||
throw new Error(
|
||||
`Aggregation ${aggregationIndex} in definition ${definitionIndex} with type ${type} requires childField`,
|
||||
);
|
||||
@@ -132,7 +153,11 @@ const collectValuesByKey = (
|
||||
}
|
||||
|
||||
Object.entries(value).forEach(([currentKey, entryValue]) => {
|
||||
if (currentKey === key && typeof entryValue === 'string' && entryValue.trim().length > 0) {
|
||||
if (
|
||||
currentKey === key &&
|
||||
typeof entryValue === 'string' &&
|
||||
entryValue.trim().length > 0
|
||||
) {
|
||||
result.add(entryValue);
|
||||
return;
|
||||
}
|
||||
@@ -160,11 +185,17 @@ export const resolveRollupConfig = (): RollupConfig => {
|
||||
try {
|
||||
const parsed = JSON.parse(override) as unknown;
|
||||
validateRollupConfig(parsed);
|
||||
return parsed;
|
||||
return parsed as RollupConfig;
|
||||
} catch (error) {
|
||||
const reason =
|
||||
error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error';
|
||||
throw new Error(`Unable to parse rollup configuration override (reason: ${reason})`);
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: 'Unknown error';
|
||||
throw new Error(
|
||||
`Unable to parse rollup configuration override (reason: ${reason})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+61
-11
@@ -2,7 +2,8 @@ import { computeAggregations } from './aggregations';
|
||||
import { buildChildRecordIndex, TwentyClient } from './client';
|
||||
import { extractRelationValues, resolveRollupConfig } from './config';
|
||||
import { getNestedValue } from './filtering';
|
||||
import type { ExecutionSummaryItem, RollupDefinition } from './types';
|
||||
import type { ExecutionSummaryItem } from './types';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
@@ -60,17 +61,26 @@ const getApiCredentials = () => {
|
||||
return { apiKey, baseUrl };
|
||||
};
|
||||
|
||||
const formatSummary = (summaries: ExecutionSummaryItem[], durationMs: number) => ({
|
||||
const formatSummary = (
|
||||
summaries: ExecutionSummaryItem[],
|
||||
durationMs: number,
|
||||
) => ({
|
||||
status: 'ok',
|
||||
tookMs: durationMs,
|
||||
totals: {
|
||||
processed: summaries.reduce((accumulator, item) => accumulator + item.processed, 0),
|
||||
updated: summaries.reduce((accumulator, item) => accumulator + item.updated, 0),
|
||||
processed: summaries.reduce(
|
||||
(accumulator, item) => accumulator + item.processed,
|
||||
0,
|
||||
),
|
||||
updated: summaries.reduce(
|
||||
(accumulator, item) => accumulator + item.updated,
|
||||
0,
|
||||
),
|
||||
},
|
||||
details: summaries,
|
||||
});
|
||||
|
||||
export async function main(params: unknown): Promise<object> {
|
||||
export const main = async (params: unknown): Promise<any> => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const config = resolveRollupConfig();
|
||||
@@ -92,7 +102,9 @@ export async function main(params: unknown): Promise<object> {
|
||||
|
||||
const credentials = getApiCredentials();
|
||||
if (!credentials) {
|
||||
console.warn('[rollup] skipping execution because TWENTY_API_KEY is not set');
|
||||
console.warn(
|
||||
'[rollup] skipping execution because TWENTY_API_KEY is not set',
|
||||
);
|
||||
return { status: 'noop', reason: 'TWENTY_API_KEY not configured' };
|
||||
}
|
||||
|
||||
@@ -102,7 +114,9 @@ export async function main(params: unknown): Promise<object> {
|
||||
const summaries: ExecutionSummaryItem[] = [];
|
||||
|
||||
for (const definition of config) {
|
||||
const targetIds = fullRebuild ? undefined : relationCache.get(definition.relationField);
|
||||
const targetIds = fullRebuild
|
||||
? undefined
|
||||
: relationCache.get(definition.relationField);
|
||||
|
||||
if (!fullRebuild && (!targetIds || targetIds.size === 0)) {
|
||||
summaries.push({
|
||||
@@ -116,7 +130,11 @@ export async function main(params: unknown): Promise<object> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const childIndex = await buildChildRecordIndex(definition, client, targetIds);
|
||||
const childIndex = await buildChildRecordIndex(
|
||||
definition,
|
||||
client,
|
||||
targetIds,
|
||||
);
|
||||
|
||||
const updates: Array<{
|
||||
id: string;
|
||||
@@ -138,13 +156,21 @@ export async function main(params: unknown): Promise<object> {
|
||||
console.info(
|
||||
`[rollup] computed aggregates for ${definition.parentObject} ${parentId}: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
updates.push({ id: parentId, payload, context: { relationId: parentId } });
|
||||
updates.push({
|
||||
id: parentId,
|
||||
payload,
|
||||
context: { relationId: parentId },
|
||||
});
|
||||
});
|
||||
|
||||
let updatedCount = 0;
|
||||
for (const update of updates) {
|
||||
try {
|
||||
await client.updateObject(definition.parentObject, update.id, update.payload);
|
||||
await client.updateObject(
|
||||
definition.parentObject,
|
||||
update.id,
|
||||
update.payload,
|
||||
);
|
||||
updatedCount += 1;
|
||||
console.info(
|
||||
`[rollup] updated ${definition.parentObject} ${update.id} (relation ${update.context.relationId})`,
|
||||
@@ -204,4 +230,28 @@ export async function main(params: unknown): Promise<object> {
|
||||
: 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'c3ec36c8-5b1d-421f-9172-a9e035ab9c18',
|
||||
name: 'calculaterollups',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'eec8aaf2-b0cc-47fd-b522-8d4aa5fe4bd3',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'opportunity.*',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a3fea230-1121-44a6-b395-5811c3031f8e',
|
||||
type: 'cron',
|
||||
pattern: ' 0 2 * * *',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd33b0fe4-4b2b-45c0-aa2f-e617fdbba484',
|
||||
type: 'route',
|
||||
path: '/recalculate-all',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -239,15 +239,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"Rollup engine@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "Rollup engine@workspace:."
|
||||
dependencies:
|
||||
dotenv: "npm:^16.4.5"
|
||||
tsx: "npm:^4.20.6"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"abbrev@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "abbrev@npm:3.0.1"
|
||||
@@ -912,6 +903,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup-engine@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "rollup-engine@workspace:."
|
||||
dependencies:
|
||||
dotenv: "npm:^16.4.5"
|
||||
tsx: "npm:^4.20.6"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"safer-buffer@npm:>= 2.1.2 < 3.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "safer-buffer@npm:2.1.2"
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '0ed2bcb8-64ab-4ca1-b875-eeabf41b5f95',
|
||||
displayName: 'Stripe synchronizer',
|
||||
description: 'Plugin synchronizing data from Stripe to Twenty',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'b0d9569b-da3e-4dad-b7b1-36c96f0598b9',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Required to send requests to Twenty',
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: 'fa50e016-e045-497a-9cdf-0949e7ef9f7a',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
description: 'Optional, defaults to cloud API URL',
|
||||
},
|
||||
STRIPE_API_KEY: {
|
||||
universalIdentifier: '807d67d6-f720-49c4-a93e-ef16cf4fe919',
|
||||
isSecret: true,
|
||||
value: '',
|
||||
description: 'Required to send request to Stripe',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "stripe-synchronizer",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -7,27 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"universalIdentifier": "0ed2bcb8-64ab-4ca1-b875-eeabf41b5f95",
|
||||
"name": "stripe-synchronizer",
|
||||
"description": "Plugin synchronizing data from Stripe to Twenty",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Required to send requests to Twenty"
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"isSecret": false,
|
||||
"value": "",
|
||||
"description": "Optional, defaults to cloud API URL"
|
||||
},
|
||||
"STRIPE_API_KEY": {
|
||||
"isSecret": true,
|
||||
"value": "",
|
||||
"description": "Required to send request to Stripe"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.1"
|
||||
}
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "cd15a738-18a5-406e-8b83-959dc52ebe14",
|
||||
"name": "stripe",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "55f58e19-d832-43c4-9f8b-3f29fc05c162",
|
||||
"type": "route",
|
||||
"path": "/stripe",
|
||||
"httpMethod": "POST",
|
||||
"isAuthRequired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
+53
-27
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
const TWENTY_API_KEY: string = process.env.TWENTY_API_KEY ?? '';
|
||||
const TWENTY_API_URL: string =
|
||||
@@ -21,14 +22,14 @@ enum stripeStatus {
|
||||
|
||||
type stripeData = {
|
||||
quantity: number;
|
||||
}
|
||||
};
|
||||
|
||||
type stripeItems = {
|
||||
data: stripeData[];
|
||||
};
|
||||
|
||||
type stripeResponse = {
|
||||
customer: string,
|
||||
customer: string;
|
||||
items: stripeItems;
|
||||
status: stripeStatus;
|
||||
type: string;
|
||||
@@ -36,15 +37,15 @@ type stripeResponse = {
|
||||
|
||||
type stripeCustomer = {
|
||||
businessName: string;
|
||||
}
|
||||
};
|
||||
|
||||
type twentyObject = {
|
||||
id: string;
|
||||
nameSingular: string;
|
||||
fields: Record<string, any>[];
|
||||
}
|
||||
};
|
||||
|
||||
const getCompaniesObject = async (): Promise<twentyObject> => {
|
||||
const getCompaniesObject = async (): Promise<twentyObject | undefined> => {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -58,7 +59,7 @@ const getCompaniesObject = async (): Promise<twentyObject> => {
|
||||
const companyObject = response.data.data.objects.find(
|
||||
(object: twentyObject) => object.nameSingular === 'company',
|
||||
);
|
||||
return companyObject as twentyObject ?? {} as twentyObject;
|
||||
return (companyObject as twentyObject) ?? ({} as twentyObject);
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
@@ -129,8 +130,8 @@ const createFields = async (objectId: string, fieldName: string) => {
|
||||
label: 'Paused',
|
||||
value: stripeStatus.Paused,
|
||||
position: 8,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
@@ -152,7 +153,9 @@ const createFields = async (objectId: string, fieldName: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStripeCustomerData = async (customerID: string): Promise<stripeCustomer> => {
|
||||
const getStripeCustomerData = async (
|
||||
customerID: string,
|
||||
): Promise<stripeCustomer | undefined> => {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${STRIPE_API_URL}/${customerID}`,
|
||||
@@ -163,7 +166,9 @@ const getStripeCustomerData = async (customerID: string): Promise<stripeCustomer
|
||||
};
|
||||
try {
|
||||
const response = await axios(options);
|
||||
return response.status === 200 ? response.data as stripeCustomer : {} as stripeCustomer ;
|
||||
return response.status === 200
|
||||
? (response.data as stripeCustomer)
|
||||
: ({} as stripeCustomer);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw error;
|
||||
@@ -171,7 +176,10 @@ const getStripeCustomerData = async (customerID: string): Promise<stripeCustomer
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfCompanyExistsInTwenty = async (name: string) => {
|
||||
const checkIfCompanyExistsInTwenty = async (name: string | undefined) => {
|
||||
if (!name) {
|
||||
return {};
|
||||
}
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -195,7 +203,7 @@ const updateTwentyCompany = async (
|
||||
companyId: string,
|
||||
seats: number,
|
||||
subStatus: stripeStatus,
|
||||
): Promise<boolean> => {
|
||||
): Promise<boolean | undefined> => {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
@@ -219,7 +227,7 @@ const updateTwentyCompany = async (
|
||||
};
|
||||
|
||||
const createTwentyCustomer = async (
|
||||
customerName: string,
|
||||
customerName: string | undefined,
|
||||
seats: number,
|
||||
subStatus: string,
|
||||
) => {
|
||||
@@ -248,13 +256,14 @@ const createTwentyCustomer = async (
|
||||
|
||||
export const main = async (params: {
|
||||
properties: unknown;
|
||||
}): Promise<object> => {
|
||||
}): Promise<object | undefined> => {
|
||||
if (TWENTY_API_KEY === '' || STRIPE_API_KEY === '') {
|
||||
console.warn('Missing variables');
|
||||
return {};
|
||||
}
|
||||
|
||||
try { // TODO: add validation of signature key from Stripe
|
||||
try {
|
||||
// TODO: add validation of signature key from Stripe
|
||||
const { properties } = params;
|
||||
const stripe = properties as stripeResponse;
|
||||
const allowed_types = [
|
||||
@@ -268,38 +277,41 @@ export const main = async (params: {
|
||||
|
||||
const companyObject = await getCompaniesObject();
|
||||
if (
|
||||
companyObject.fields.find((field) => field.name === 'seats') === undefined
|
||||
companyObject?.fields.find((field) => field.name === 'seats') ===
|
||||
undefined
|
||||
) {
|
||||
const t: boolean = await createFields(companyObject.id, 'seats');
|
||||
if (t == false) {
|
||||
const t: boolean | undefined = companyObject?.id
|
||||
? await createFields(companyObject?.id, 'seats')
|
||||
: false;
|
||||
if (t === false) {
|
||||
console.error('Seats field creation failed');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (
|
||||
companyObject.fields.find((field) => field.name === 'subStatus') ===
|
||||
companyObject?.fields.find((field) => field.name === 'subStatus') ===
|
||||
undefined
|
||||
) {
|
||||
const t: boolean = await createFields(companyObject.id, 'subStatus');
|
||||
if (t == false) {
|
||||
const t: boolean | undefined = companyObject?.id
|
||||
? await createFields(companyObject?.id, 'subStatus')
|
||||
: false;
|
||||
if (t === false) {
|
||||
console.error('Sub status field creation failed');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const stripeCustomer: stripeCustomer = await getStripeCustomerData(
|
||||
stripe.customer,
|
||||
);
|
||||
if (stripeCustomer.businessName) {
|
||||
const stripeCustomer = await getStripeCustomerData(stripe.customer);
|
||||
if (stripeCustomer?.businessName) {
|
||||
console.warn('Set customer business name in Stripe');
|
||||
return {};
|
||||
}
|
||||
const twentyCustomer = await checkIfCompanyExistsInTwenty(
|
||||
stripeCustomer.businessName,
|
||||
stripeCustomer?.businessName,
|
||||
);
|
||||
if (Object.keys(twentyCustomer).length === 0) {
|
||||
const a = await createTwentyCustomer(
|
||||
stripeCustomer.businessName,
|
||||
stripeCustomer?.businessName,
|
||||
stripe.items.data[0].quantity,
|
||||
stripe.status.toUpperCase(),
|
||||
);
|
||||
@@ -326,3 +338,17 @@ export const main = async (params: {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'cd15a738-18a5-406e-8b83-959dc52ebe14',
|
||||
name: 'stripe',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '55f58e19-d832-43c4-9f8b-3f29fc05c162',
|
||||
type: 'route',
|
||||
path: '/stripe',
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"strict": true,
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
+942
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk/application';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
universalIdentifier: '8f3c5a1e-9b2d-4c7e-a5f3-1d8e9c2b4a6f',
|
||||
displayName: 'Webmetic Visitor Intelligence',
|
||||
description:
|
||||
'Automatically sync B2B website visitor data into Twenty CRM. Identify companies visiting your website and track engagement without forms or manual entry.',
|
||||
applicationVariables: {
|
||||
TWENTY_API_KEY: {
|
||||
universalIdentifier: 'eb3866a1-36df-42f3-bcb8-351120b74096',
|
||||
description: 'Twenty API key for authentication',
|
||||
isSecret: true,
|
||||
},
|
||||
TWENTY_API_URL: {
|
||||
universalIdentifier: '10dfe92a-b472-43e6-a33a-db89dcdd9bad',
|
||||
description: 'Twenty API URL (base URL of your Twenty instance)',
|
||||
isSecret: false,
|
||||
value: '',
|
||||
},
|
||||
WEBMETIC_API_KEY: {
|
||||
universalIdentifier: '28540b41-ab79-489b-953f-c1491adc28f2',
|
||||
description: 'Webmetic API key (get from hub.webmetic.de)',
|
||||
isSecret: true,
|
||||
},
|
||||
WEBMETIC_DOMAIN: {
|
||||
universalIdentifier: '74f7d252-539a-4eba-8d12-e7010c8128a1',
|
||||
description: 'Your website domain to track (e.g., example.com)',
|
||||
isSecret: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/appManifest.schema.json",
|
||||
"name": "webmetic-visitor-intelligence",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -8,28 +8,6 @@
|
||||
"yarn": ">=4.0.2"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"universalIdentifier": "8f3c5a1e-9b2d-4c7e-a5f3-1d8e9c2b4a6f",
|
||||
"name": "Webmetic Visitor Intelligence",
|
||||
"description": "Automatically sync B2B website visitor data into Twenty CRM. Identify companies visiting your website and track engagement without forms or manual entry.",
|
||||
"env": {
|
||||
"TWENTY_API_KEY": {
|
||||
"description": "Twenty API key for authentication",
|
||||
"isSecret": true
|
||||
},
|
||||
"TWENTY_API_URL": {
|
||||
"description": "Twenty API URL (base URL of your Twenty instance)",
|
||||
"isSecret": false,
|
||||
"value": ""
|
||||
},
|
||||
"WEBMETIC_API_KEY": {
|
||||
"description": "Webmetic API key (get from hub.webmetic.de)",
|
||||
"isSecret": true
|
||||
},
|
||||
"WEBMETIC_DOMAIN": {
|
||||
"description": "Your website domain to track (e.g., example.com)",
|
||||
"isSecret": false
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"twenty-sdk": "0.0.3"
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-cli/schemas/serverlessFunction.schema.json",
|
||||
"universalIdentifier": "f2d4b8e6-3a1c-4f7e-9b5d-8c3e2a1f6d4b",
|
||||
"name": "sync-visitor-data",
|
||||
"triggers": [
|
||||
{
|
||||
"universalIdentifier": "c5e9a3b7-2d8f-4c1e-a6b3-9f2e5d8c1a7b",
|
||||
"type": "cron",
|
||||
"schedule": "0 * * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
+110
-40
@@ -1,9 +1,13 @@
|
||||
import axios from 'axios';
|
||||
import { WebmeticResponse, WebmeticCompany } from './types';
|
||||
import { ensureWebsiteLeadObjectExists, ensureWebsiteLeadFieldsExist } from './schemaSetup';
|
||||
import { type WebmeticResponse } from './types';
|
||||
import {
|
||||
ensureWebsiteLeadObjectExists,
|
||||
ensureWebsiteLeadFieldsExist,
|
||||
} from './schemaSetup';
|
||||
import { type ServerlessFunctionConfig } from 'twenty-sdk/application';
|
||||
|
||||
// Rate limiting helper: delay between API calls to avoid hitting limits
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const RATE_LIMIT_DELAY = 800;
|
||||
|
||||
export const main = async (): Promise<object> => {
|
||||
@@ -15,16 +19,23 @@ export const main = async (): Promise<object> => {
|
||||
};
|
||||
|
||||
// Debug: Log all available environment variables
|
||||
log('Available environment variables: ' + Object.keys(process.env).filter(k => k.includes('TWENTY') || k.includes('WEBMETIC')).join(', '));
|
||||
log(
|
||||
'Available environment variables: ' +
|
||||
Object.keys(process.env)
|
||||
.filter((k) => k.includes('TWENTY') || k.includes('WEBMETIC'))
|
||||
.join(', '),
|
||||
);
|
||||
|
||||
const WEBMETIC_API_KEY = process.env.WEBMETIC_API_KEY;
|
||||
const WEBMETIC_DOMAIN = process.env.WEBMETIC_DOMAIN;
|
||||
const TWENTY_API_KEY = process.env.TWENTY_API_KEY;
|
||||
|
||||
// Base URL without /rest for metadata operations
|
||||
const TWENTY_API_BASE_URL = process.env.TWENTY_API_URL !== "" && process.env.TWENTY_API_URL !== undefined
|
||||
? process.env.TWENTY_API_URL
|
||||
: "http://localhost:3000";
|
||||
const TWENTY_API_BASE_URL =
|
||||
process.env.TWENTY_API_URL !== '' &&
|
||||
process.env.TWENTY_API_URL !== undefined
|
||||
? process.env.TWENTY_API_URL
|
||||
: 'http://localhost:3000';
|
||||
|
||||
// REST URL for data operations
|
||||
const TWENTY_API_URL = `${TWENTY_API_BASE_URL}/rest`;
|
||||
@@ -42,7 +53,7 @@ export const main = async (): Promise<object> => {
|
||||
WEBMETIC_API_KEY: !!WEBMETIC_API_KEY,
|
||||
WEBMETIC_DOMAIN: !!WEBMETIC_DOMAIN,
|
||||
TWENTY_API_KEY: !!TWENTY_API_KEY,
|
||||
detailedLog: logs.join('\n')
|
||||
detailedLog: logs.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,7 +71,7 @@ export const main = async (): Promise<object> => {
|
||||
return {
|
||||
success: false,
|
||||
error: `Field setup failed: ${error.message}`,
|
||||
detailedLog: logs.join('\n')
|
||||
detailedLog: logs.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,11 +97,13 @@ export const main = async (): Promise<object> => {
|
||||
Authorization: WEBMETIC_API_KEY,
|
||||
},
|
||||
params: webmeticParams,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
log(`✅ Webmetic API responded successfully`);
|
||||
log(`📦 Webmetic returned ${webmeticResponse.data.result?.length || 0} companies`);
|
||||
log(
|
||||
`📦 Webmetic returned ${webmeticResponse.data.result?.length || 0} companies`,
|
||||
);
|
||||
|
||||
const companies = webmeticResponse.data.result;
|
||||
|
||||
@@ -101,12 +114,17 @@ export const main = async (): Promise<object> => {
|
||||
companiesProcessed: 0,
|
||||
sessionsProcessed: 0,
|
||||
logCount: logs.length,
|
||||
detailedLog: logs.join('\n')
|
||||
detailedLog: logs.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
const totalSessions = companies.reduce((sum, c) => sum + c.sessions.length, 0);
|
||||
log(`\n📊 Found ${companies.length} companies with ${totalSessions} total sessions`);
|
||||
const totalSessions = companies.reduce(
|
||||
(sum, c) => sum + c.sessions.length,
|
||||
0,
|
||||
);
|
||||
log(
|
||||
`\n📊 Found ${companies.length} companies with ${totalSessions} total sessions`,
|
||||
);
|
||||
log('='.repeat(50));
|
||||
|
||||
// 2. Process each company
|
||||
@@ -122,10 +140,14 @@ export const main = async (): Promise<object> => {
|
||||
|
||||
for (const company of companies) {
|
||||
try {
|
||||
log(`\n🏢 Processing: ${company.company_name} (${company.sessions.length} sessions)`);
|
||||
log(
|
||||
`\n🏢 Processing: ${company.company_name} (${company.sessions.length} sessions)`,
|
||||
);
|
||||
|
||||
// Extract domain from company_url
|
||||
const domainMatch = company.company_url?.match(/^https?:\/\/(?:www\.)?([^\/]+)/);
|
||||
const domainMatch = company.company_url?.match(
|
||||
/^https?:\/\/(?:www\.)?([^/]+)/,
|
||||
);
|
||||
const domain = domainMatch ? domainMatch[1] : company.company_url;
|
||||
log(` Domain: ${domain}`);
|
||||
|
||||
@@ -148,12 +170,17 @@ export const main = async (): Promise<object> => {
|
||||
|
||||
const existingCompaniesResponse = await axios(existingCompaniesOptions);
|
||||
log(` ✅ Response status: ${existingCompaniesResponse.status}`);
|
||||
log(` 📦 Response data structure: ${JSON.stringify(Object.keys(existingCompaniesResponse.data || {}))}`);
|
||||
log(
|
||||
` 📦 Response data structure: ${JSON.stringify(Object.keys(existingCompaniesResponse.data || {}))}`,
|
||||
);
|
||||
|
||||
await sleep(RATE_LIMIT_DELAY); // Rate limiting delay
|
||||
|
||||
const existingCompanies = existingCompaniesResponse.data?.data?.companies || [];
|
||||
log(` Found ${existingCompanies.length} existing companies with this domain`);
|
||||
const existingCompanies =
|
||||
existingCompaniesResponse.data?.data?.companies || [];
|
||||
log(
|
||||
` Found ${existingCompanies.length} existing companies with this domain`,
|
||||
);
|
||||
|
||||
// Build company data with proper validation
|
||||
const companyData: any = {
|
||||
@@ -165,7 +192,12 @@ export const main = async (): Promise<object> => {
|
||||
};
|
||||
|
||||
// Only add address if at least one field has a value
|
||||
if (company.address || company.city || company.postal_code || company.country) {
|
||||
if (
|
||||
company.address ||
|
||||
company.city ||
|
||||
company.postal_code ||
|
||||
company.country
|
||||
) {
|
||||
companyData.address = {
|
||||
addressStreet1: company.address || '',
|
||||
addressCity: company.city || '',
|
||||
@@ -250,7 +282,9 @@ export const main = async (): Promise<object> => {
|
||||
companyId = newCompanyResponse.data?.data?.createCompany?.id;
|
||||
|
||||
if (!companyId) {
|
||||
log(` ⚠️ Warning: Could not extract company ID from response. Full response: ${JSON.stringify(newCompanyResponse.data)}`);
|
||||
log(
|
||||
` ⚠️ Warning: Could not extract company ID from response. Full response: ${JSON.stringify(newCompanyResponse.data)}`,
|
||||
);
|
||||
}
|
||||
|
||||
results.companiesCreated++;
|
||||
@@ -274,7 +308,9 @@ export const main = async (): Promise<object> => {
|
||||
: 'Direct';
|
||||
|
||||
// Extract pages visited (limit length to prevent DB errors)
|
||||
const pagesArray = session.user_data.map((ud) => ud.document_location);
|
||||
const pagesArray = session.user_data.map(
|
||||
(ud) => ud.document_location,
|
||||
);
|
||||
let pagesVisited = pagesArray.join(' → ');
|
||||
|
||||
// Truncate if too long (max ~1000 chars to be safe)
|
||||
@@ -289,13 +325,19 @@ export const main = async (): Promise<object> => {
|
||||
const scrollDepths = session.user_data
|
||||
.map((ud) => ud.scroll_depth)
|
||||
.filter((sd) => typeof sd === 'number' && !isNaN(sd));
|
||||
const averageScrollDepth = scrollDepths.length > 0
|
||||
? Math.round(scrollDepths.reduce((sum, sd) => sum + sd, 0) / scrollDepths.length)
|
||||
: 0;
|
||||
const averageScrollDepth =
|
||||
scrollDepths.length > 0
|
||||
? Math.round(
|
||||
scrollDepths.reduce((sum, sd) => sum + sd, 0) /
|
||||
scrollDepths.length,
|
||||
)
|
||||
: 0;
|
||||
|
||||
// Calculate total user events
|
||||
const totalUserEvents = session.user_data
|
||||
.reduce((sum, ud) => sum + (ud.user_events_count || 0), 0);
|
||||
const totalUserEvents = session.user_data.reduce(
|
||||
(sum, ud) => sum + (ud.user_events_count || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
// Use session_id as unique identifier (company name available via relation)
|
||||
const leadName = session.session_id;
|
||||
@@ -314,7 +356,9 @@ export const main = async (): Promise<object> => {
|
||||
const existingLeadsResponse = await axios(existingLeadsOptions);
|
||||
await sleep(RATE_LIMIT_DELAY); // Rate limiting delay
|
||||
|
||||
const existingLeads = existingLeadsResponse.data?.data?.websiteLeads || existingLeadsResponse.data;
|
||||
const existingLeads =
|
||||
existingLeadsResponse.data?.data?.websiteLeads ||
|
||||
existingLeadsResponse.data;
|
||||
log(` Found ${existingLeads?.length || 0} existing leads`);
|
||||
|
||||
if (!existingLeads || existingLeads.length === 0) {
|
||||
@@ -364,7 +408,9 @@ export const main = async (): Promise<object> => {
|
||||
leadData.companyId = companyId;
|
||||
}
|
||||
|
||||
log(` Creating WebsiteLead with data: ${JSON.stringify(leadData)}`);
|
||||
log(
|
||||
` Creating WebsiteLead with data: ${JSON.stringify(leadData)}`,
|
||||
);
|
||||
|
||||
const createOptions = {
|
||||
method: 'POST',
|
||||
@@ -378,7 +424,9 @@ export const main = async (): Promise<object> => {
|
||||
const createResponse = await axios(createOptions);
|
||||
await sleep(RATE_LIMIT_DELAY); // Rate limiting delay
|
||||
|
||||
log(` WebsiteLead creation response: ${createResponse.status}`);
|
||||
log(
|
||||
` WebsiteLead creation response: ${createResponse.status}`,
|
||||
);
|
||||
|
||||
results.websiteLeadsCreated++;
|
||||
log(` ✓ Created WebsiteLead: ${leadName}`);
|
||||
@@ -386,7 +434,10 @@ export const main = async (): Promise<object> => {
|
||||
log(` - WebsiteLead already exists: ${leadName}`);
|
||||
}
|
||||
} catch (sessionError: any) {
|
||||
const errorDetails = sessionError?.response?.data || sessionError?.message || 'Unknown error';
|
||||
const errorDetails =
|
||||
sessionError?.response?.data ||
|
||||
sessionError?.message ||
|
||||
'Unknown error';
|
||||
const errorMsg = `Error processing WebsiteLead for session ${session.session_id}: ${JSON.stringify(errorDetails, null, 2)}`;
|
||||
console.error(errorMsg);
|
||||
log(` ❌ ${errorMsg}`);
|
||||
@@ -395,9 +446,9 @@ export const main = async (): Promise<object> => {
|
||||
|
||||
results.companiesProcessed++;
|
||||
results.sessionsProcessed += company.sessions.length;
|
||||
|
||||
} catch (error: any) {
|
||||
const errorDetails = error?.response?.data || error?.message || 'Unknown error';
|
||||
const errorDetails =
|
||||
error?.response?.data || error?.message || 'Unknown error';
|
||||
const errorMsg = `Error processing company ${company.company_name}: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`;
|
||||
@@ -409,13 +460,21 @@ export const main = async (): Promise<object> => {
|
||||
|
||||
// Log additional diagnostic info
|
||||
if (error?.response) {
|
||||
log(` 🔍 HTTP Status: ${error.response.status} ${error.response.statusText}`);
|
||||
log(
|
||||
` 🔍 HTTP Status: ${error.response.status} ${error.response.statusText}`,
|
||||
);
|
||||
log(` 🔍 Request URL: ${error.config?.url || 'Unknown'}`);
|
||||
log(` 🔍 Request Method: ${error.config?.method?.toUpperCase() || 'Unknown'}`);
|
||||
log(` 🔍 Auth Header: ${error.config?.headers?.Authorization ? 'Present' : 'MISSING!'}`);
|
||||
log(
|
||||
` 🔍 Request Method: ${error.config?.method?.toUpperCase() || 'Unknown'}`,
|
||||
);
|
||||
log(
|
||||
` 🔍 Auth Header: ${error.config?.headers?.Authorization ? 'Present' : 'MISSING!'}`,
|
||||
);
|
||||
}
|
||||
|
||||
results.errors.push(`${errorMsg} - Details: ${JSON.stringify(errorDetails)}`);
|
||||
results.errors.push(
|
||||
`${errorMsg} - Details: ${JSON.stringify(errorDetails)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,11 +500,22 @@ export const main = async (): Promise<object> => {
|
||||
return {
|
||||
...results,
|
||||
logCount: logs.length,
|
||||
detailedLog: logs.join('\n')
|
||||
detailedLog: logs.join('\n'),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fatal error during sync:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const config: ServerlessFunctionConfig = {
|
||||
universalIdentifier: 'f2d4b8e6-3a1c-4f7e-9b5d-8c3e2a1f6d4b',
|
||||
name: 'sync-visitor-data',
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'c5e9a3b7-2d8f-4c1e-a6b3-9f2e5d8c1a7b',
|
||||
type: 'cron',
|
||||
pattern: '0 * * * *',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
|
||||
@@ -262,9 +262,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webmetic@workspace:.":
|
||||
"webmetic-visitor-intelligence@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "webmetic@workspace:."
|
||||
resolution: "webmetic-visitor-intelligence@workspace:."
|
||||
dependencies:
|
||||
"@types/node": "npm:^24.7.2"
|
||||
axios: "npm:^1.12.2"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
|
||||
Reference in New Issue
Block a user