Template Content
Scripts
About the integration
How does the integration logic work?
Which fields are being synced?
Can I configure which fields are being synced?
Yes. You can change which fields are being synced and control quite many other things via the configuration.
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
Template Content
Scripts
This template synchronizes Jira Cloud Issues and Salesforce Cases, ensuring the following data is exchanged between Jira Cloud and Salesforce:
Configure API connections and event listeners:
Create connectors for Jira Cloud and Salesforce instances or use existing ones.
Add webhooks in Jira Cloud:
project = TEST
).Create custom fields
In Jira Cloud
Create a new text custom field named ScriptRunner Connect Sync Issue Key
in your Jira Cloud project to keep items in sync.
In Salesforce
ScriptRunner Connect Sync Issue Key
for the case object.API Name
of this field (e.g., ScriptRunner_Connect_Sync_Issue_Key__c
).Configure parameters
Parameters
and configure the following:STATUS_MAPPING
and PRIORITY_MAPPING
variables.JIRA_SYNC_CUSTOM_FIELD_NAME
matches the custom field created in Jira Cloud.API Name
of the Salesforce custom field to the SALESFORCE_SYNC_KEY_API_NAME
variable.PROJECT_KEY
and ISSUE_TYPE_NAME
for Jira Cloud.SUPPORTED_JIRA_FIELDS
unchanged, as it contains fields supported by the template.Create Salesforce event listeners
OnSalesforceCaseCreated Event:
Set up an outbound message for the Case object with these fields:
Set up a Flow for the event:
OnSalesforceCaseUpdated Event
Set up an outbound message for the Case object with these fields:
Set up a Flow for the event:
OnSalesforceCommentCreated Event
Set up an outbound message for the Case Comment object with these fields:
Set up a Flow for the event:
OnSalesforceFeedItemAdded Event
Set up an outbound message for the Feed Item object with these fields:
Set up a Flow for the event:
Optional: Sync existing Salesforce cases and Jira issues
500J9000000hwmOIAQ
).import { IssueCommentCreatedEvent } from '@sr-connect/jira-cloud/events';
import JiraCloud from "./api/jira/cloud";
import Salesforce from "./api/salesforce";
import { getEnvVars } from './Utils';
export default async function (event: IssueCommentCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue matches the user who set up the integration
if (myself.accountId === event.comment.author.accountId) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const { JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
const issueSyncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
const issue = await JiraCloud.Issue.getIssue({
issueIdOrKey: event.issue.key,
fields: [issueSyncField?.key ?? '']
});
const syncKey = issue.fields?.[issueSyncField?.id ?? ''];
if (!syncKey) {
throw Error(`Jira issue ${issue.key} is not synced.`);
}
// Create comment for Salesforce case
await Salesforce.fetch(`/services/data/v57.0/sobjects/Case/${syncKey}`, {
method: 'PATCH',
body: JSON.stringify({
Comments: event.comment.body,
}),
});
}
import { IssueCreatedEvent } from '@sr-connect/jira-cloud/events';
import Salesforce from "./api/salesforce";
import JiraCloud from "./api/jira/cloud";
import { getEnvVars, createSalesforceCase, updateSalesforceCase, processAttachment, Mapper } from './Utils';
export default async function (event: IssueCreatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue matches the user who set up the integration
if (myself.accountId === event.user.accountId) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const { STATUS_MAPPING, PRIORITY_MAPPING, SALESFORCE_SYNC_KEY_API_NAME, JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
// Collect values from the event
const summary = event.issue.fields.summary;
const status = event.issue.fields.status?.name;
const description = event.issue.fields.description;
const priority = event.issue.fields?.priority?.name;
const statusMapping = new Mapper(STATUS_MAPPING);
const priorityMapping = new Mapper(PRIORITY_MAPPING);
const caseId = await createSalesforceCase(Salesforce, {
Subject: 'Jira Cloud Issue: ' + summary,
Status: statusMapping.getSalesforceValue(status ?? ''),
Description: description ? description.toString() : '',
Priority: priorityMapping.getSalesforceValue(priority ?? ''),
});
// Add sync key value to Saleforce Case
await updateSalesforceCase(Salesforce, caseId, {
[SALESFORCE_SYNC_KEY_API_NAME]: caseId,
});
if (event.issue.fields.attachment?.length) {
const attachments = event.issue.fields.attachment ?? [];
// Add attachments to Salesforce case
await Promise.all(attachments.map((att) => processAttachment(JiraCloud, Salesforce, att, caseId)));
}
// Get sync key
const syncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!syncField || !syncField.id) {
throw Error('Sync custom field not found on Jira Cloud.');
}
// Add sync key to newly created Jira issue
await JiraCloud.Issue.editIssue({
issueIdOrKey: event.issue.key,
body: {
fields: {
[syncField.id]: caseId,
}
}
});
console.log('Salesforce case created: ', caseId);
}
import { IssueUpdatedEvent } from '@sr-connect/jira-cloud/events';
import Salesforce from "./api/salesforce";
import JiraCloud from "./api/jira/cloud";
import { getEnvVars, updateSalesforceCase, processAttachment, addSalesforceAttachment, Mapper } from './Utils';
export default async function (event: IssueUpdatedEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
// Get the current user
const myself = await JiraCloud.Myself.getCurrentUser();
// Check if the user who updated the issue matches the user who set up the integration
if (myself.accountId === event.user.accountId) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const { STATUS_MAPPING, PRIORITY_MAPPING, JIRA_SYNC_CUSTOM_FIELD_NAME, SUPPORTED_JIRA_FIELDS } = getEnvVars(context);
// Check if update concerns supported fields
if (!event.changelog?.items.some(cl => SUPPORTED_JIRA_FIELDS.includes(cl.field))) {
console.warn('No supported fields in update event.');
return;
}
const issueSyncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
const issue = await JiraCloud.Issue.getIssue({
issueIdOrKey: event.issue.key,
fields: [...SUPPORTED_JIRA_FIELDS, issueSyncField?.key ?? '']
});
const syncKey = issue.fields?.[issueSyncField?.id ?? ''];
if (!syncKey) {
throw Error(`Jira issue ${issue.key} is not synced.`);
}
// Process status update
if (event.changelog?.items.some(cl => cl.field === 'status')) {
const status = issue.fields?.status?.name ?? '';
const statusMapping = new Mapper(STATUS_MAPPING);
await updateSalesforceCase(Salesforce, syncKey, {
Status: statusMapping.getSalesforceValue(status),
});
console.log(`Status updated for Salesforce case ${syncKey}.`);
}
// Process summary update
if (event.changelog?.items.some(cl => cl.field === 'summary')) {
const summary = issue.fields?.summary;
await updateSalesforceCase(Salesforce, syncKey, {
Subject: summary,
});
console.log(`Subject updated for Salesforce case ${syncKey}.`);
}
// Process description update
const updatedDesc = event.changelog?.items.find(cl => cl.field === 'description')?.toString;
if (updatedDesc) {
await updateSalesforceCase(Salesforce, syncKey, {
Description: updatedDesc,
});
console.log(`Description updated for Salesforce case ${syncKey}.`);
}
// Process priority update
if (event.changelog?.items.some(cl => cl.field === 'priority')) {
const priority = issue.fields?.priority?.name ?? '';
const priorityMapping = new Mapper(PRIORITY_MAPPING);
await updateSalesforceCase(Salesforce, syncKey, {
Priority: priorityMapping.getSalesforceValue(priority),
});
console.log(`Process updated for Salesforce case ${syncKey}.`);
}
if (event.changelog?.items.some(cl => cl.field === 'Attachment')) {
const item = event.changelog?.items.find(cl => cl.field === 'Attachment');
if (!item?.fromString) {
const attachmentName = item?.toString ?? '';
const content = event.issue.fields?.attachment?.find(a => a.filename === attachmentName);
// Retrieving content of an attachment from Jira
const response = await JiraCloud.fetch(`/rest/api/2/attachment/content/${content?.id}`);
const body = await response.arrayBuffer();
// Processing attachment
await addSalesforceAttachment(Salesforce, syncKey, {
body,
attachmentName,
mimeType: content?.mimeType ?? ''
});
console.log(`Attachment added to Salesforce case: ${attachmentName}`);
}
}
}
import JiraCloud from "./api/jira/cloud";
import Salesforce from "./api/salesforce";
import { SalesforceGenericEvent } from '@sr-connect/salesforce/events';
import { getCurrentUser, getEnvVars, updateSalesforceCase, Mapper } from './Utils';
export default async function (event: SalesforceGenericEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { STATUS_MAPPING, PRIORITY_MAPPING, PROJECT_KEY, ISSUE_TYPE_NAME, SALESFORCE_SYNC_KEY_API_NAME, JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
// Case data from the event
const caseId = event.notifications.Notification.sObject['sf:Id'];
const caseSubject = event.notifications.Notification.sObject['sf:Subject'];
const caseStatus = event.notifications.Notification.sObject['sf:Status'];
const casePriority = event.notifications.Notification.sObject['sf:Priority'];
const caseDescription = event.notifications.Notification.sObject['sf:Description'];
const userId = event.notifications.Notification.sObject['sf:CreatedById'];
// Check if the user who triggered the event matches the user who set up the integration
if (userId === await getCurrentUser(Salesforce)) {
console.warn('Integration user triggered the event, skipping.');
return;
}
if (caseSubject) {
const priorityMapping = new Mapper(PRIORITY_MAPPING);
// Get sync key
const syncField = (await JiraCloud.Issue.Field.getFields()).find(f => f.name === JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!syncField || !syncField.id) {
throw Error('Sync custom field not found on Jira Cloud.');
}
// Create Jira Cloud issue with data from Salesforce event.
const issueCreated = await JiraCloud.Issue.createIssue({
body: {
fields: {
project: {
key: PROJECT_KEY,
},
issuetype: {
name: ISSUE_TYPE_NAME,
},
summary: `SF Case: ${caseSubject}`,
priority: {
name: casePriority && priorityMapping.getJiraCloudValue(casePriority),
},
[syncField.id]: caseId,
description: caseDescription && {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: caseDescription
}
]
},
]
}
}
}
});
if (caseStatus) {
const statusMapping = new Mapper(STATUS_MAPPING);
// Find Jira status if Trello list value is passed
const jiraStatus = statusMapping.getJiraCloudValue(caseStatus);
if (!jiraStatus) {
throw Error(`Failed to get correct Jira status for Salesforce status: ${caseStatus}.`);
}
// Find Jira transitions
const transition = await JiraCloud.Issue.Transition.getTransitions({
issueIdOrKey: issueCreated.key ?? ''
});
// Find Jira transition ID
const id = transition?.transitions?.find(t => t.name?.localeCompare(jiraStatus, 'en', { sensitivity: 'base' }) === 0)?.id;
if (!id) {
throw Error('Failed to retrieve transition id for Jira Status.');
}
// Perform Jira issue transition according to status/list mapping
await JiraCloud.Issue.Transition.performTransition({
issueIdOrKey: issueCreated.key ?? '',
body: {
transition: {
id
}
}
});
}
// Add sync key value to Saleforce Case
await updateSalesforceCase(Salesforce, caseId, {
[SALESFORCE_SYNC_KEY_API_NAME]: caseId,
});
// Print out issue key of the newly created issue.
console.log(`Issue created: ${issueCreated.key}.`);
} else {
console.error('Case data is not presented in the event object.');
}
}
import { SalesforceGenericEvent } from '@sr-connect/salesforce/events';
import JiraCloud from "./api/jira/cloud";
import Salesforce from "./api/salesforce";
import { getCurrentUser, getEnvVars, getSyncedJiraIssueKey, Mapper } from './Utils';
export default async function (event: SalesforceGenericEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { STATUS_MAPPING, PRIORITY_MAPPING, PROJECT_KEY, ISSUE_TYPE_NAME, JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
// Case data from the event
const caseId = event.notifications.Notification.sObject['sf:Id'];
const caseSubject = event.notifications.Notification.sObject['sf:Subject'];
const caseStatus = event.notifications.Notification.sObject['sf:Status'];
const casePriority = event.notifications.Notification.sObject['sf:Priority'];
const caseDescription = event.notifications.Notification.sObject['sf:Description'];
const userId = event.notifications.Notification.sObject['sf:LastModifiedById'];
// Check if the user who triggered the event matches the user who set up the integration
if (userId === await getCurrentUser(Salesforce)) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const jiraIssueKey = await getSyncedJiraIssueKey(JiraCloud, caseId, JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!jiraIssueKey) {
throw Error(`Issue key not found for sync key ${caseId}`);
}
const priorityMapping = new Mapper(PRIORITY_MAPPING);
// Update Jira Cloud issue with data from Salesforce event.
await JiraCloud.Issue.editIssue({
issueIdOrKey: jiraIssueKey,
body: {
fields: {
project: {
key: PROJECT_KEY,
},
issuetype: {
name: ISSUE_TYPE_NAME,
},
summary: `SF Case: ${caseSubject}`,
priority: {
name: casePriority && priorityMapping.getJiraCloudValue(casePriority),
},
description: caseDescription && {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: caseDescription
}
]
},
]
}
}
}
});
if (caseStatus) {
const statusMapping = new Mapper(STATUS_MAPPING);
// Find Jira status if Salesforce list value is passed
const jiraStatus = statusMapping.getJiraCloudValue(caseStatus);
if (!jiraStatus) {
throw Error(`Failed to get correct Jira status for Salesforce status: ${caseStatus}.`);
}
// Find Jira transitions
const transition = await JiraCloud.Issue.Transition.getTransitions({
issueIdOrKey: jiraIssueKey,
});
// Find Jira transition ID
const id = transition?.transitions?.find(t => t.name?.localeCompare(jiraStatus, 'en', { sensitivity: 'base' }) === 0)?.id;
if (!id) {
throw Error('Failed to retrieve transition id for Jira Status.');
}
// Perform Jira issue transition according to status/list mapping
await JiraCloud.Issue.Transition.performTransition({
issueIdOrKey: jiraIssueKey,
body: {
transition: {
id
}
}
});
}
}
import { SalesforceGenericEvent } from '@sr-connect/salesforce/events';
import Salesforce from "./api/salesforce";
import JiraCloud from "./api/jira/cloud";
import { htmlToPlainText, getSyncedJiraIssueKey, getEnvVars, getCurrentUser } from './Utils';
export default async function (event: SalesforceGenericEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
// Collect values from the event
const isFeedItem = event.notifications.Notification.sObject['$']['xsi:type'] === 'sf:FeedItem';
const caseId = event.notifications.Notification.sObject['sf:ParentId'];
const userId = event.notifications.Notification.sObject['sf:CreatedById'];
const feedItemBody = event.notifications.Notification.sObject['sf:Body'];
// Check if the user who triggered the event matches the user who set up the integration
if (userId === await getCurrentUser(Salesforce)) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const jiraIssueKey = await getSyncedJiraIssueKey(JiraCloud, caseId, JIRA_SYNC_CUSTOM_FIELD_NAME);
if (!jiraIssueKey) {
throw Error(`Issue key not found for sync key ${caseId}`);
}
// Check the type of comment
if (isFeedItem) {
const stringifiedFeedItemBody = htmlToPlainText(feedItemBody);
// Add coment to Jira Cloud issue
await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: stringifiedFeedItemBody,
}
]
},
]
}
}
});
} else {
const commentBody = event.notifications.Notification.sObject['sf:CommentBody'];
// Add coment to Jira Cloud issue
await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: commentBody,
}
]
},
]
}
}
});
}
}
import { SalesforceGenericEvent } from '@sr-connect/salesforce/events';
import { SalesforceApi } from "@managed-api/salesforce-v57-sr-connect";
import Salesforce from "./api/salesforce";
import JiraCloud from "./api/jira/cloud";
import { AddIssueAttachmentsRequest } from '@managed-api/jira-cloud-v3-core/types/issue/attachment';
import { htmlToPlainText, getSyncedJiraIssueKey, getEnvVars, getCurrentUser } from './Utils';
export default async function (event: SalesforceGenericEvent, context: Context): Promise<void> {
if (context.triggerType === 'MANUAL') {
console.error('This script is designed to be triggered externally or manually from the Event Listener. Please consider using Event Listener Test Event Payload if you need to trigger this script manually.');
return;
}
const { JIRA_SYNC_CUSTOM_FIELD_NAME } = getEnvVars(context);
const notification = event.notifications.Notification;
if (Array.isArray(notification)) {
await Promise.all(notification.map(n => processFeedItemEvent(n, JIRA_SYNC_CUSTOM_FIELD_NAME)));
} else {
await processFeedItemEvent(notification, JIRA_SYNC_CUSTOM_FIELD_NAME);
}
}
const processFeedItemEvent = async (notification: any, syncFieldName: string) => {
// Collect values from the event
const caseId = notification.sObject['sf:ParentId'];
const userId = notification.sObject['sf:CreatedById'];
const feedItemBody = notification.sObject['sf:Body'];
const feedItemType = notification.sObject['sf:Type'];
const feedItemId = notification.sObject['sf:Id'];
const relatedRecordId = notification.sObject['sf:RelatedRecordId'];
// Check if the user who triggered the event matches the user who set up the integration
if (userId === await getCurrentUser(Salesforce)) {
console.warn('Integration user triggered the event, skipping.');
return;
}
const jiraIssueKey = await getSyncedJiraIssueKey(JiraCloud, caseId, syncFieldName);
if (!jiraIssueKey) {
throw Error(`Issue key not found for sync key ${caseId}`);
}
// Check if item type is comment
if (feedItemType === 'TextPost') {
// Convert comment body HTML into a string
const stringifiedFeedItemBody = htmlToPlainText(feedItemBody);
// Add coment to Jira Cloud issue
await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: stringifiedFeedItemBody,
}
]
},
]
}
}
});
// Check if item type is attachment
} else if (feedItemType === 'ContentPost') {
// Fetch Feed item details
const details = await getSalesforceFeedItemDetails(Salesforce, feedItemId);
// Iterate through Feed item records and collect attachments content
const attachmentBody = await Promise.all(details.records.map((rec) => getSalesforceAttachmentContent(Salesforce, rec.RecordId)));
// Add all attachments from Feed item
await JiraCloud.Issue.Attachment.addAttachments({
issueIdOrKey: jiraIssueKey,
body: attachmentBody,
});
// Check if attachment has a comment
if (feedItemBody) {
// Convert comment body HTML into a string
const stringifiedFeedItemBody = htmlToPlainText(feedItemBody);
await JiraCloud.Issue.Comment.addComment({
issueIdOrKey: jiraIssueKey,
body: {
body: {
version: 1,
type: "doc",
content: [
{
type: "paragraph",
content: [
{
type: "text",
text: `Comment to the attachment ${relatedRecordId}: ${stringifiedFeedItemBody}`,
}
]
},
]
}
}
});
}
}
}
const getSalesforceFeedItemDetails = async (instance: SalesforceApi, feedItemId: string): Promise<{
records: {
RecordId: string;
}[];
}> => {
const soql_query = `SELECT Id, RecordId, Type FROM FeedAttachment WHERE FeedEntityId = '${feedItemId}'`; // feed item ID
const attachmentsResponse = await instance.fetch(`/services/data/v57.0/query?q=${soql_query}`);
if (attachmentsResponse.ok) {
return await attachmentsResponse.json();
} else {
throw new Error('Failed to get Salesforce FedItem details.');
}
}
const getSalesforceAttachmentContent = async (instance: SalesforceApi, recordId: string): Promise<AddIssueAttachmentsRequest['body'][0]> => {
const res = await instance.fetch(`/services/data/v57.0/sobjects/ContentVersion/${recordId}/VersionData`);
if (res.ok) {
return {
fileName: recordId,
content: await res.arrayBuffer()
};
} else {
throw new Error('Failed to get Salesforce attachment content.');
}
}
import { SalesforceApi } from "@managed-api/salesforce-v57-sr-connect";
import { JiraCloudApi } from '@managed-api/jira-cloud-v3-sr-connect';
import { UpdateSObjectRowRecordRequest } from "@managed-api/salesforce-v57-core/types/sObject/row";
import { PartialAttachment } from '@sr-connect/jira-cloud/types/shared-types';
export async function getCurrentUser(instance: SalesforceApi): Promise<string> {
const resp = await instance.fetch('/services/oauth2/userinfo');
if (resp.ok) {
return (await resp.json()).user_id;
} else {
throw new Error('Failed to get current user details from Salesforce');
}
}
type JiraCloudValue = string;
type SalesforceValue = string;
export class Mapper {
constructor(private mapping: Record<string, string>) { }
// Method to get value by key
getSalesforceValue(key: JiraCloudValue): SalesforceValue | undefined {
return this.mapping[key];
}
// Method to get key by value
getJiraCloudValue(value: SalesforceValue): JiraCloudValue | undefined {
return Object.keys(this.mapping).find(key => this.mapping[key] === value);
}
}
interface EnvVars {
STATUS_MAPPING: Record<JiraCloudValue, SalesforceValue>;
PRIORITY_MAPPING: Record<JiraCloudValue, SalesforceValue>;
PROJECT_KEY: string;
ISSUE_TYPE_NAME: string;
SALESFORCE_SYNC_KEY_API_NAME: string;
JIRA_SYNC_CUSTOM_FIELD_NAME: string;
SUPPORTED_JIRA_FIELDS: string[];
}
export function getEnvVars(context: Context) {
return context.environment.vars as EnvVars;
}
export interface SalesforceCaseRequestBody {
Subject?: string,
Status?: string,
Description?: string,
Priority?: string,
}
export async function createSalesforceCase(instance: SalesforceApi, body: SalesforceCaseRequestBody): Promise<string> {
const resp = await instance.fetch('/services/data/v57.0/sobjects/Case', {
method: 'POST',
body: JSON.stringify(body),
});
if (resp.ok) {
const payload = await resp.json();
return payload.id;
} else {
throw new Error('Failed to create Salesforce case.');
}
}
export async function updateSalesforceCase(instance: SalesforceApi, caseId: string, body: UpdateSObjectRowRecordRequest['body']): Promise<void> {
// Add sync key value to Saleforce Case
await instance.sObject.Row.updateRecord({
id: caseId,
sObject: 'Case',
body,
});
}
/**
* Find an issue that belongs to Salesforce case
*/
export async function getSyncedJiraIssueKey(instance: JiraCloudApi, syncKey: string, syncKeyName: string,): Promise<string> {
// You may want to use another means of storing Incident number in Jira Cloud side, for example use a custom field
const issues = (await instance.Issue.Search.searchByJql({
body: {
jql: `"${syncKeyName}" ~ "${syncKey}"`
}
})).issues ?? [];
// If no issues with that sync key is found throw an error
if (issues.length === 0) {
throw Error('No issue is found');
}
if (issues.length > 1) {
throw Error(`Found more than 1 matching issue with sync key (${syncKey}): ${issues.map(i => i.key).join(', ')}`)
}
if (!issues[0].key) {
throw Error('No issue key presented.');
}
return issues[0].key;
}
// Convert HTML into a text string
export function htmlToPlainText(html: string): string {
// Handle specific block-level elements by replacing them with newlines
const blockElements = ['p', 'div', 'section', 'header', 'footer', 'article'];
blockElements.forEach(tag => {
const regex = new RegExp(`<${tag}[^>]*>`, 'gi');
html = html.replace(regex, '\n');
html = html.replace(new RegExp(`</${tag}>`, 'gi'), '\n');
});
// Handle list items by adding newlines before and after each item
html = html.replace(/<li[^>]*>/gi, '\n- ');
html = html.replace(/<\/li>/gi, '');
// Replace other HTML tags with spaces
html = html.replace(/<\/?[^>]+(>|$)/g, "");
// Optionally, you can handle common HTML entities like < > etc.
const entities: { [key: string]: string } = {
' ': ' ',
'<': '<',
'>': '>',
'&': '&',
'"': '"',
''': "'"
};
html = html.replace(/&[^;]+;/g, (entity) => entities[entity] || entity);
// Trim and collapse multiple newlines into a single one
return html.trim().replace(/\n\s*\n/g, '\n');
}
// Process Jira Cloud attachment
export const processAttachment = async (
jiraInstance: JiraCloudApi,
salesforceInstance: SalesforceApi,
att: PartialAttachment & { id: number },
syncKey: string
) => {
// Retrieving content of an attachment from Jira
const response = await jiraInstance.fetch(`/rest/api/2/attachment/content/${att?.id}`);
const body = await response.arrayBuffer();
// Processing attachment
await addSalesforceAttachment(salesforceInstance, syncKey, {
body,
attachmentName: att.filename,
mimeType: att.mimeType ?? ''
});
console.log(`Attachment added to Salesforce case: ${att.filename}`);
}
// Convert and upload content to Salesforce Case
export const addSalesforceAttachment = async (instance: SalesforceApi, caseId: string, content: {
body: ArrayBuffer | string;
attachmentName: string;
mimeType: string;
}) => {
const { body, attachmentName, mimeType } = content;
const { uint8Array, boundary } = await getUint8ArrayOfFormDataFromBlob(new Blob([body]), attachmentName, mimeType);
const contentUploadResponse = await instance.fetch(`/services/data/v57.0/sobjects/ContentVersion`, {
method: 'POST',
body: uint8Array,
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`
}
});
if (contentUploadResponse.ok) {
const uploadedContent = await contentUploadResponse.json<{ id: string }>();
const feedItemCreatedResponse = await instance.fetch('/services/data/v57.0/sobjects/FeedItem', {
method: 'POST',
body: JSON.stringify({
ParentId: caseId,
Type: 'ContentPost',
RelatedRecordId: uploadedContent.id,
}),
});
if (!feedItemCreatedResponse.ok) {
throw new Error('Failed to add attachment to Salesforce.');
}
}
}
// Create FormData in Uint8Array representation from Blob
export async function getUint8ArrayOfFormDataFromBlob(content: Blob, fileName: string, mimeType: string): Promise<{
uint8Array: Uint8Array,
boundary: string;
}> {
const boundary = '----formdata-' + Math.random();
const chunks: any[] = [];
chunks.push(`--${boundary}\r\n`);
chunks.push(
`Content-Disposition: form-data; name="entity_content";\r\n`,
`Content-Type: application/json\r\n\r\n`,
JSON.stringify({
"ContentLocation": "S",
"Title": fileName,
"PathOnClient": fileName
}),
'\r\n'
);
chunks.push(`--${boundary}\r\n`);
chunks.push(
`Content-Type: ${mimeType}\r\n`,
`Content-Disposition: form-data; name="VersionData"; filename="${fileName}"\r\n\r\n`,
await content.arrayBuffer(),
'\r\n'
);
chunks.push(`--${boundary}--`);
const encoder = new TextEncoder();
const encodedChunks: ArrayBuffer[] = chunks.map(chunk => typeof chunk === 'string' ? encoder?.encode(chunk) : chunk);
const totalByteLength = encodedChunks.reduce((total, chunk) => total + chunk.byteLength, 0);
const uint8Array = new Uint8Array(totalByteLength);
let offset = 0;
for (const chunk of encodedChunks) {
uint8Array.set(new Uint8Array(chunk), offset);
offset += chunk.byteLength;
}
return {
uint8Array,
boundary
};
}