Skip to content

Commit

Permalink
better template variable support (#2637)
Browse files Browse the repository at this point in the history
  • Loading branch information
joe-ayoub-segment authored Dec 11, 2024
1 parent f653d94 commit 24983e8
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { extractVariables } from '../dynamic-fields'

const emailTemplate = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Email Template</title>
</head>
<body>
<!-- Header -->
<h1>Welcome to Test Email Template!</h1>
<!-- Main Content -->
<h2>Welcome, {{user.username}}!</h2>
<p>Thank you for joining <strong>{{user.leagueName}}</strong>! Your current leagues are:</p>
<ul>
{{#each user.currentLeagues}}
<li><strong>{{this.leagueName}}: {{this.leagueParticipants}} members</strong></li>
{{/each}}
</ul>
<a href="{{login_url}}">My Leagues</a>
<p>{{insert name "default=Customer"}}</p>
{{#if user.profile.male}}
<p>Dear Sir</p>
{{else if user.profile.female}}
<p>Dear Madame</p>
{{else}}
<p>Dear Customer</p>
{{/if}}
{{#if user.suspended}}
<p>Warning! Your account is suspended, please call: {{@root.supportPhone}}</p>
{{/if}}
{{#unless user.active}}
<p>Warning! Your account is suspended, please call: {{@root.supportPhone}}</p>
{{/unless}}
<p>
{{#greaterThan scoreOne scoreTwo}}
Congratulations, you have the high score today!
{{/greaterThan}}
</p>
<p>
{{#greaterThan scoreOne scoreTwo}}
Congratulations, you have the high score today!
{{else}}
You were close, but you didn't get the high score today.
{{/greaterThan}}
</p>
{{#lessThan scoreOne scoreTwo}}
<p>
{{#lessThan scoreOne scoreTwo}}
You were close, but you didn't get the high score today.
{{else}}
Congratulations, you have the high score today!
{{/lessThan}}
</p>
<p>
{{#equals customerCode winningCode}}
You have a winning code.
{{/equals}}
</p>
<p>
{{#equals customerCode winningCode}}
You have a winning code.
{{else}}
You do not have a winning code.
{{/equals}}
</p>
<p>
{{#notEquals currentDate appointmentDate}}
Your appointment is not today.
{{/notEquals}}
</p>
<p>
{{#notEquals currentDate appointmentDate}}
Your appointment is not today.
{{else}}
Your appointment is today.
{{/notEquals}}
</p>
<p>
{{#and favoriteFood favoriteDrink}}
Thank you for letting us know your dining preferences.
{{/and}}.
</p>
<p>
{{#and favoriteFood favoriteDrink}}
Thank you for letting us know your dining preferences.
{{else}}
If you finish filling out your dining preferences survey, we can deliver you recipes we think you'll be most interested in.
{{/and}}.
</p>
<p>
{{#or isRunner isCyclist}}
We think you might enjoy a map of trails in your area.
{{/or}}.
</p>
<p>
{{#or isRunner isCyclist}}
We think you might enjoy a map of trails in your area. You can find the map attached to this email.
{{else}}
We'd love to know more about the outdoor activities you enjoy. The survey linked below will take only a minute to fill out.
{{/or}}.
</p>
<p>
{{#greaterThan (length cartItems) 0}}
It looks like you still have some items in your shopping cart. Sign back in to continue checking out at any time.
{{else}}
Thanks for browsing our site. We hope you'll come back soon.
{{/greaterThan}}
</p>
<ol>
{{#each user.orderHistory}}
<li>You ordered: {{this.item}} on: {{this.date}}</li>
{{/each}}
</ol>
{{#each user.story}}
{{#if this.male}}
<p>{{this.date}}</p>
{{else if this.female}}
<p>{{this.item}}</p>
{{/if}}
{{/each}}
{{#each user.story}}
{{#if this.male}}
{{#if this.date}}
<p>{{this.date}}</p>
{{/if}}
{{#if this.item}}
<p>{{this.item}}</p>
{{/if}}
{{else if this.female}}
{{#if this.date}}
<p>{{this.date}}</p>
{{/if}}
{{#if this.item}}
<p>{{this.item}}</p>
{{/if}}
{{/if}}
{{/each}}
{{#if people}}
<p>People:</p>
{{#each people}}
<p>{{this.name}}</p>
{{/each}}
{{/if}}
<!-- Footer -->
<p>© 2024 Testing Templates blah, Inc. All rights reserved. {{formatDate timeStamp dateFormat}}</p>
</body>
</html>`

describe('Sendgrid.sendEmail', () => {
it('dynamic tokens should be extracted from template correctlly', async () => {
const tokens = extractVariables(emailTemplate)
expect(tokens).toMatchObject([
'user',
'login_url',
'name',
'supportPhone',
'scoreOne',
'scoreTwo',
'customerCode',
'winningCode',
'currentDate',
'appointmentDate',
'favoriteFood',
'favoriteDrink',
'isRunner',
'isCyclist',
'cartItems',
'people',
'timeStamp',
'dateFormat'
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,41 @@ function createErrorResponse(message?: string, code?: string): ErrorResponse {
}
}

export function extractVariables(content: string | undefined): string[] {
if (!content) {
return []
}

const removeIfStartsWith = ['if', 'unless', 'and', 'or', 'equals', 'notEquals', 'lessThan', 'greaterThan', 'each']

const removeIfMatch = ['else', 'if', 'this', 'insert', 'default', 'length', 'formatDate']

const regex1 = /{{(.*?)}}/g // matches handlebar expressions
const regex2 = /[#/]?"?[\w."]+"?/g // matches words. Deliberately includes " characters which will be removed later
const words = [
...new Set(
[...content.matchAll(regex1)]
.map((match) => match[1])
.join(' ')
.match(regex2)
)
]

const variables = [
...new Set(
words
.filter((w) => !removeIfMatch.some((item) => w.startsWith(item))) // remove words that start with any of the items in removeIfMatch
.filter((w) => !removeIfStartsWith.some((item) => w.startsWith(`#${item}`) || w.startsWith(`/${item}`))) // remove words that start with any of the items in removeIfStartsWith
.map((item) => (item.startsWith('root.') ? item.slice(5).trim() : item)) // remove root. from the start of the word
.map((item) => item.split('.')[0]) // remove everything after the first dot. for example: user.username -> user
.filter((item) => !item.includes('"')) // remove if word contains " (double quotes) as this implies it is a constant / string and not a variable
.filter((item) => isNaN(Number(item))) // remove numeric values
)
]

return variables
}

export async function dynamicTemplateData(request: RequestClient, payload: Payload): Promise<DynamicFieldResponse> {
interface ResultItem {
id: string
Expand Down Expand Up @@ -93,15 +128,12 @@ export async function dynamicTemplateData(request: RequestClient, payload: Paylo
return createErrorResponse('Returned template has no content')
}

const extractTokens = (content: string | undefined): string[] =>
[...(content ?? '').matchAll(/{{{?(\w+)}{0,3}}}/g)].map((match) => match[1])

const uniqueTokens: string[] = [
...new Set([
...extractTokens(version.html_content),
...extractTokens(version.plain_content),
...extractTokens(version.subject),
...extractTokens(version.thumbnail_url)
...extractVariables(version.html_content),
...extractVariables(version.plain_content),
...extractVariables(version.subject),
...extractVariables(version.thumbnail_url)
])
]

Expand Down Expand Up @@ -156,7 +188,10 @@ export async function dynamicGroupId(request: RequestClient): Promise<DynamicFie
} catch (err) {
const error = err as ResultError
const code = String(error?.response?.status ?? 500)
return createErrorResponse(error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicGroupId', code)
return createErrorResponse(
error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicGroupId',
code
)
}
}

Expand Down Expand Up @@ -192,7 +227,10 @@ export async function dynamicDomain(request: RequestClient): Promise<DynamicFiel
} catch (err) {
const error = err as ResultError
const code = String(error?.response?.status ?? 500)
return createErrorResponse(error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicDomain', code)
return createErrorResponse(
error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicDomain',
code
)
}
}

Expand Down Expand Up @@ -251,7 +289,10 @@ export async function dynamicTemplateId(request: RequestClient): Promise<Dynamic
} catch (err) {
const error = err as ResultError
const code = String(error?.response?.status ?? 500)
return createErrorResponse(error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicGetTemplates', code)
return createErrorResponse(
error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicGetTemplates',
code
)
}
}

Expand Down Expand Up @@ -281,6 +322,9 @@ export async function dynamicIpPoolNames(request: RequestClient): Promise<Dynami
} catch (err) {
const error = err as ResultError
const code = String(error?.response?.status ?? 500)
return createErrorResponse(error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicIpPoolNames', code)
return createErrorResponse(
error?.response?.data?.errors.map((error) => error.message).join(';') ?? 'Unknown error: dynamicIpPoolNames',
code
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ export const fields: Record<string, InputField> = {
type: 'string',
required: false
}
},
default: {
email: {
'@path': '$.properties.from_email'
},
name: {
'@path': '$.properties.from_name'
}
}
},
to: {
Expand Down Expand Up @@ -97,7 +89,7 @@ export const fields: Record<string, InputField> = {
type: 'string',
required: false
}
},
},
default: undefined
},
bcc: {
Expand Down

0 comments on commit 24983e8

Please sign in to comment.