Skip to content

Commit

Permalink
Fix up google Actions
Browse files Browse the repository at this point in the history
  • Loading branch information
TheIsrael1 committed Aug 14, 2024
1 parent d2e8158 commit 3a6a233
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 26 deletions.
22 changes: 22 additions & 0 deletions packages/core/src/data-access/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,26 @@ export class DataLayer {
data: update,
});
}

async getRecordsByFieldName({
fieldName,
connectionId,
}: {
fieldName: string;
connectionId: string;
}) {
return this.db.record.findMany({
where: {
syncTable: {
dataIntegration: {
connectionId,
},
},
data: {
path: [fieldName],
not: Prisma.JsonNull,
},
},
});
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export { IntegrationPlugin } from './plugin';
export { IntegrationCredentialType } from './types';
export { FieldTypes, DataIntegration, DataIntegrationCredential } from '@prisma-app/client';
export { IntegrationAuth } from './authenticator';
export * from './utils';

class IntegrationFramework {
//global events grouped by plugin
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Prisma } from '@prisma/client';
import { z } from 'zod';
import _ from 'lodash';

import { SchemaFieldOptions } from '../types';

type TypedInstance<T> = {
type: T;
id: string;
};

/**
* Extract schema options for the provided schema - Builds a map of fieldname to field options
* @param schema - schema
* @param dataCtx - data context - this is an object of options for each field that isn't enum (meaning string fields) but should have options
* @returns schema options
* */
export function extractSchemaOptions({
schema,
dataCtx,
}: {
schema: z.ZodObject<any>;
dataCtx?: Record<string, SchemaFieldOptions>;
}): Record<string, SchemaFieldOptions> {
return Object.entries(schema.shape).reduce((acc, [field, schema]) => {
setFieldOptions({ schema, acc, field, dataCtxList: dataCtx?.[field] });

return acc;
}, {} as Record<string, SchemaFieldOptions>);
}

/**
* Set field options for the schema - Recursively populate the accumulator with field options for the provided field
* if the schema is an enum or an array of enums, or an array of strings and has dataCtxList
* @param schema - schema
* @param acc - accumulator
* @param field - field
* @param dataCtxList - data context list - custom options for the field
* @returns void
* */
function setFieldOptions({
schema,
acc,
field,
dataCtxList,
}: {
schema: any;
acc: Record<string, SchemaFieldOptions>;
field: string;
dataCtxList?: SchemaFieldOptions;
}) {
if (schema instanceof z.ZodEnum) {
acc[field] = {
options: schema.options.map((value: string) => ({
value,
label: _.capitalize(value),
})),
};
} else if (schema instanceof z.ZodArray) {
if (schema.element instanceof z.ZodEnum) {
acc[field] = {
options: schema.element.options.map((value: string) => ({
value,
label: _.capitalize(value),
})),
};
} else if (schema.element instanceof z.ZodString && dataCtxList) {
acc[field] = dataCtxList || { options: undefined };
}
} else if (schema instanceof z.ZodString && dataCtxList) {
acc[field] = dataCtxList || { options: undefined };
} else if (schema instanceof z.ZodOptional) {
setFieldOptions({ schema: schema._def.innerType, dataCtxList, field, acc });
} else {
acc[field] = { options: undefined };
}
}
6 changes: 5 additions & 1 deletion packages/future-google/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"scripts": {
"analyze": "size-limit --why",
"build": "dts build",
"build": "dts build --rollupTypes",
"lint": "dts lint",
"prepare": "dts build",
"size": "size-limit",
Expand Down Expand Up @@ -42,8 +42,11 @@
"@googleapis/oauth2": "^1.0.7",
"@googleapis/people": "^3.0.9",
"async-retry-ng": "^2.0.1",
"base64-js": "^1.5.1",
"core": "workspace:*",
"google-auth-library": "^9.13.0",
"jsdom": "^24.1.1",
"marked": "^14.0.0",
"postal-mime": "^2.2.7",
"zod": "^3.23.8"
},
Expand All @@ -52,6 +55,7 @@
"@size-limit/preset-small-lib": "^11.1.4",
"@tsconfig/recommended": "^1.0.7",
"@types/jest": "^29.5.12",
"@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.7",
"@types/node": "^22.1.0",
"dts-cli": "^2.0.5",
Expand Down
40 changes: 20 additions & 20 deletions packages/future-google/src/actions/send-email.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { DataLayer, IntegrationAction, extractSchemaOptions } from 'core';
import { z } from 'zod';

import { extractSchemaOptions } from '../../server-utils';
import { IntegrationAction, MakeAPI } from '../../types';
import gmailIcon from '../assets/gmail.svg';
// TODO: ADD ACTION ICON
// import gmailIcon from '../assets/gmail.svg';
import { createRawMessage, getSnippet, threadHasMessage } from '../helpers';
import { SEND_EMAIL_SCHEMA } from '../schemas';
import { CreateEmailsParams, EmailRequestBody, MakeClient } from '../types';

const gmailIcon = '';

export const SEND_EMAIL = ({
name,
makeApi,
makeClient,
createEmails,
dataAccess,
}: {
name: string;
makeApi: MakeAPI;
dataAccess: DataLayer;
makeClient: MakeClient;
createEmails: (params: CreateEmailsParams) => Promise<void>;
}): IntegrationAction<
Expand All @@ -29,7 +31,7 @@ export const SEND_EMAIL = ({
pluginName: name,
label: 'Send Email',
icon: {
type: 'plugin',
alt: 'Gmail',
icon: gmailIcon,
},
description: 'Send an email',
Expand All @@ -38,7 +40,7 @@ export const SEND_EMAIL = ({
async getSchemaOptions({ ctx }) {
const emailSet = new Set();
// shouldn't have to call this again if we already have the data
const people = await recordService.getRecordsByFieldName({ fieldName: 'email', workspaceId: ctx.workspaceId });
const people = await dataAccess.getRecordsByFieldName({ fieldName: 'email', connectionId: ctx.connectionId });
/**
* get list of pipelines
* get list of stages
Expand All @@ -60,9 +62,8 @@ export const SEND_EMAIL = ({

return schemaOptions;
},
executor: async ({ data, userId, workspaceId }) => {
const api = makeApi({ context: { workspaceId, userId } });
const client = await makeClient({ api });
executor: async ({ data, ctx: { connectionId } }) => {
const client = await makeClient({ connectionId });

const email = data;

Expand All @@ -76,7 +77,7 @@ export const SEND_EMAIL = ({
if (emailId) {
const messageObj = await client.getGmailMessage({ messageId: emailId });
let references = `${messageObj.references || ''} ${messageObj.messageId}`;
const replySubject = `Re: ${messageObj.subject}`;
// const replySubject = `Re: ${messageObj.subject}`;
rawMessage = createRawMessage(to, cc, bcc, subject || '', body, 'html', messageObj.messageId, references);
} else {
rawMessage = createRawMessage(to, cc, bcc, subject || '', body, 'html');
Expand Down Expand Up @@ -116,7 +117,7 @@ export const SEND_EMAIL = ({

if (!threadHasMessage(thread, messageId)) throw new Error('New message not in thread');

await createEmails({ emails: thread.messages, api, contacts: {} });
await createEmails({ emails: thread.messages, contacts: {} });

return { status: true, message: 'email sent', messageId, joinedEmail };
} catch (e) {
Expand All @@ -128,12 +129,12 @@ export const SEND_EMAIL = ({

export const SEND_BULK_EMAIL = ({
name,
makeApi,
dataAccess,
makeClient,
createEmails,
}: {
name: string;
makeApi: MakeAPI;
dataAccess: DataLayer;
makeClient: MakeClient;
createEmails: (params: CreateEmailsParams) => Promise<void>;
}): IntegrationAction<
Expand All @@ -146,15 +147,15 @@ export const SEND_BULK_EMAIL = ({
pluginName: name,
label: 'Send Bulk Email',
icon: {
type: 'plugin',
icon: gmailIcon,
alt: 'Gmail',
},
description: 'Send an email to multiple recipients',
schema: SEND_EMAIL_SCHEMA,
type: 'SEND_BULK_EMAIL',
async getSchemaOptions({ ctx }) {
const emailSet = new Set();
const people = await recordService.getRecordsByFieldName({ fieldName: 'email', workspaceId: ctx.workspaceId });
const people = await dataAccess.getRecordsByFieldName({ fieldName: 'email', connectionId: ctx.connectionId });

people.forEach(person => {
if ((person.data as any)?.email) {
Expand All @@ -170,16 +171,15 @@ export const SEND_BULK_EMAIL = ({

return schemaOptions;
},
executor: async ({ data, userId, workspaceId }) => {
executor: async ({ data, ctx: { connectionId } }) => {
const email = data;

try {
const result = await Promise.all(
email.to?.map(async emailTo => {
return SEND_EMAIL({ name, makeApi, makeClient, createEmails }).executor({
return SEND_EMAIL({ name, dataAccess, makeClient, createEmails }).executor({
data: { ...email, to: [emailTo] },
userId,
workspaceId,
ctx: { connectionId },
});
}),
);
Expand Down
13 changes: 13 additions & 0 deletions packages/future-google/src/assets/gmail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 61 additions & 1 deletion packages/future-google/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as base64 from 'base64-js';
import { JSDOM } from 'jsdom';
import { marked } from 'marked';

import { Labels } from './constants';
import { Email } from './types';
import { Email, MessagesByThread } from './types';

export const formatDate = (date: Date): string => {
date = new Date(date);
Expand All @@ -15,6 +19,62 @@ export const formatDate = (date: Date): string => {
return formattedDate;
};

export const threadHasMessage = (thread: MessagesByThread, messageId: string): boolean => {
for (const message of thread.messages) {
if (message.id == messageId) return true;
}
return false;
};

export function getSnippet(body: string, length: number = 100): string {
const dom = new JSDOM(body);
const plainTextBody = dom.window.document.body.textContent || '';

// Trim the plain text to the desired snippet length.
return plainTextBody.slice(0, length);
}

export function createRawMessage(
to: string[],
cc: string[] = [],
bcc: string[] = [],
subject: string,
body: string,
format: 'text' | 'html' = 'html',
inReplyTo?: string,
references?: string,
): string {
// Create the message headers.
const headers = [
`To: ${to.join(', ')}`,
`Cc: ${cc.join(', ')}`,
`Bcc: ${bcc.join(', ')}`,
`Subject: ${subject}`,
'MIME-Version: 1.0',
`Content-Type: ${format === 'html' ? 'text/html' : 'text/plain'}; charset=UTF-8`,
];

// Add In-Reply-To and References headers if provided.
if (inReplyTo) {
headers.push(`In-Reply-To: ${inReplyTo}`);
}
if (references) {
headers.push(`References: ${references}`);
}

// Create the message body.
const messageBody = marked.parse(body, { async: false }) as string;

// Combine the headers and body into a single string.
const message = headers.join('\n') + '\n\n' + messageBody;
// convert to html

// Encode the message in base64url.
const rawMessage = base64.fromByteArray(new TextEncoder().encode(message));

return rawMessage;
}

export const buildGetMessagesQuery = async ({
labels,
from,
Expand Down
7 changes: 3 additions & 4 deletions packages/future-google/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DataIntegration, IntegrationAuth, IntegrationPlugin } from 'core';
import { z } from 'zod';

import { SEND_BULK_EMAIL, SEND_EMAIL } from './actions/send-email';
import { GoogleClient } from './client';

type GoogleConfig = {
Expand Down Expand Up @@ -40,25 +41,23 @@ export class GoogleIntegration extends IntegrationPlugin {

async createEmails() {}


getActions() {
return {
SEND_EMAIL: SEND_EMAIL({
dataAccess: this.dataLayer,
dataAccess: this?.dataLayer!,
name: this.name,
makeClient: this.makeClient,
createEmails: this.createEmails,
}),
SEND_BULK_EMAIL: SEND_BULK_EMAIL({
dataAccess: this.dataLayer,
dataAccess: this?.dataLayer!,
name: this.name,
makeClient: this.makeClient,
createEmails: this.createEmails,
}),
};
}


defineEvents() {
this.events = {
GCAL_SUBSCRIBE: {
Expand Down
Loading

0 comments on commit 3a6a233

Please sign in to comment.