☃️

【Salesforce】Salesforceファイル連携の仕組みを図解する

に公開

はじめに

現在、私は保育業界向けの転職支援アプリの開発に携わっています。
このアプリでは、今年4月に履歴書作成機能、9月には職務経歴書作成機能をリリースし、多くの求職者の方にご利用いただいています。

(参考記事)
履歴書機能:https://www.hoikushibank-column.com/column/post_3559
職務経歴書機能:https://www.hoikushibank-column.com/column/post_3561

そして現在、新機能として『アプリで作った書類を、Salesforceへ自動連携する機能』の実装を進めています。
いざ調査を始めてみると、Salesforceのファイル連携(ContentVersion周り)の仕様が複雑だったので、備忘録も兼ねて図解付きでまとめてみようと思います。

背景:ユーザーの手間を減らし、体験をスムーズにしたい

まずは、この機能を実装するに至った背景について、私たちのビジネスモデルを交えて説明します。

1. キャリアアドバイザー型のサービスモデル

私たちが提供している「保育士バンク!」は、単なる求人検索サイトではなく、専任の担当者が転職活動をサポートする人材紹介(エージェント)型のサービスも提供しています。

サービスの裏側では、キャリアアドバイザーが求職者の方と面談を行い、希望に合った保育園を紹介しています。その際、求人への応募には履歴書・職務経歴書が必要となります。

また、アドバイザーは求職者の情報や選考進捗をSalesforceで一元管理しているため、求職者がアプリ上で作成した書類は、最終的にアドバイザーの手元(Salesforce)に届く必要があります。

2. これまでの課題:ツール間の分断

アプリで書類作成ができる機能自体は好評でしたが、作成後の提出フローには大きなUX上の課題がありました。

これまでは、アプリで作った書類をアドバイザーに渡すために、以下の手順が必要でした。

  1. アプリで履歴書を入力・PDF化して端末に保存
  2. 一度アプリを閉じて、LINEやメールアプリを立ち上げる
  3. 担当アドバイザーとのトークルームにPDFファイルを添付して送信
  4. (その後、アドバイザーが手動でSalesforceへアップロード)

このように、「アプリで作成したにも関わらず、送るためだけに別のツールへ移動しなければならない」という手間が発生していました。これでは体験としてシームレスではありませんし、アドバイザー側にも「Salesforceへの転記・アップロード」という業務コストがかかります。

そこで今回、「アプリ内の『送信』ボタンを押すだけで、Salesforceの該当レコードに履歴書が自動添付される」仕組みを開発し、ユーザー・アドバイザー双方の手間をゼロにすることを目指しました。

Salesforceのファイル連携の壁:3つのオブジェクト

Salesforce Filesのデータ構造は少し特殊です。『File』という単一のオブジェクトがあるわけではなく、主に以下の3つのオブジェクトによって構成されています。

1. オブジェクトの整理

Salesforceでファイルを扱う際、以下の3つがセットで動きます。それぞれの役割が明確に分かれています。

オブジェクト名 役割 イメージ
ContentVersion ファイルの実体データとバージョン情報 「履歴書_v1.pdf」「履歴書_v2.pdf」
ContentDocument ファイル全体を管理する親箱 「この履歴書ファイル」という概念
ContentDocumentLink 「誰」や「どのレコード」に共有されているかのリンク情報 「AさんのContactレコードに紐付いている」

2. 図解:この3つの関係性は?

関係性を整理すると以下のようになります。

ContentVersion を作成(Insert)すると、その親である ContentDocument はSalesforce側で自動的に作成されます。
通常、「このファイルを、特定の求職者のレコードに添付したい」という場合は、自動生成された ContentDocument のIDを取得し、それを使って ContentDocumentLink を作成する必要があります。

実装のポイント

実装にあたり、いくつか検討すべきポイントがありました。ここでは『紐付けの簡略化』と『データ転送方式』について解説します。

1. FirstPublishLocationId が救世主

当初は以下の3ステップが必要だと考えていました。

  1. ContentVersion を作成
  2. 作成された ContentDocumentId を取得
  3. ContentDocumentLink を作成してレコードと紐付け

しかし、ドキュメントを調査すると FirstPublishLocationId というプロパティを利用することで、この手順を短縮できることが分かりました。

ContentVersion を作成する際に、この項目に『紐付けたいレコードID』を指定するだけで、ファイルのアップロードとレコードへの紐付けが一度のリクエストで完了します。

2. Base64 vs Multipart/form-data

Salesforceへのファイルアップロードには、主に2つの方式があります。

  • Multipart/form-data: バイナリデータをそのまま送る方式。大容量ファイル向き。
  • Base64エンコード: ファイルをBase64エンコードして送る方式。

今回はBase64方式を採用しました。

理由:
Base64方式はファイルサイズが約37%増加し、SalesforceのAPI仕様上もサイズ上限(50MB)があります。しかし、今回扱う履歴書・職務経歴書のPDFファイルは大きくても数MB程度です。
Multipartリクエストはバウンダリの生成など実装がやや複雑になるのに対し、Base64であればシンプルに実装できるため、保守性と実装スピードを優先してこちらを選定しました。

実装コード:TypeScriptでの実装例

ここからは、具体的な実装例を紹介します。

ContentVersion作成のAPIリクエスト

ContentVersionを作成するには、以下のエンドポイントにPOSTリクエストを送ります。

POST /services/data/vxx.x/sobjects/ContentVersion

以下は、ファイルをアップロードし、指定したContactレコードに紐付けるTypeScriptの関数です。

/**
 * ContentVersionを作成し、指定レコードにファイルを紐付ける
 *
 * @param accessToken - Salesforceのアクセストークン
 * @param instanceUrl - SalesforceインスタンスのURL
 * @param fileBuffer - アップロードするファイルのバッファ
 * @param fileName - ファイル名
 * @param linkedRecordId - 紐付け先のレコードID
 */
async function createContentVersion(
  accessToken: string,
  instanceUrl: string,
  fileBuffer: Buffer,
  fileName: string,
  linkedRecordId: string
): Promise<string> {

  const requestBody = {
    Title: fileName,
    // PathOnClientはSalesforceがファイル形式(拡張子)を認識するために必須
    PathOnClient: fileName, 
    VersionData: fileBuffer.toString('base64'),
    FirstPublishLocationId: linkedRecordId,  // ← ここがポイント!
  }

  const response = await fetch(
    `${instanceUrl}/services/data/v59.0/sobjects/ContentVersion`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestBody),
    }
  )

  if (!response.ok) {
    const errorText = await response.text()
    console.error(`Salesforce Upload Error: ${errorText}`)
    throw new Error(`Failed to create ContentVersion: ${response.statusText}`)
  }

  const result = await response.json() as { id: string }
  return result.id  // 作成されたContentVersionのID
}

実行結果:Salesforce側での確認

上記のコードが正常に実行されると、指定したSalesforceレコードの関連リスト内のファイルに、アップロードしたPDFが反映されます。

応用:履歴書を更新したい場合

上記のコードは常に新規ファイルとしてアップロードされますが、要件によっては既存の履歴書を最新版に更新したいケースもあるかと思います。

その場合は、FirstPublishLocationId ではなく、ContentDocumentId を指定します。

{
  "Title": "履歴書_v2.pdf",
  "PathOnClient": "履歴書_v2.pdf",
  "VersionData": "...",
  "ContentDocumentId": "xxxxxxxxxxxx" // ←更新したい親DocumentのIDを指定
}

これにより、Salesforce上では同じファイルのバージョン2として扱われ、レコード上のプレビューも最新版に差し替わります。用途に応じて使い分けると便利です。

使用例

import fs from 'fs'

// 認証済みのアクセストークンとインスタンスURLがある前提
const accessToken = 'your-access-token'
const instanceUrl = 'https://your-org.my.salesforce.com'

// PDFファイルの読み込み
const pdfBuffer = fs.readFileSync('./履歴書.pdf')

// Salesforceにアップロード
const contactId = 'xxxxxxxxxxxx'  // 紐付け先のID
const contentVersionId = await createContentVersion(
  accessToken,
  instanceUrl,
  pdfBuffer,
  '履歴書.pdf',
  contactId
)

console.log(`ContentVersion作成完了: ${contentVersionId}`)

おわりに

現在、この機能は鋭意実装中です。

Salesforceの ContentVersion 周りは独特な仕様で最初は戸惑いました。特に ContentDocumentContentVersion の関係性や、紐付けのタイミングなどは、実際にAPIを叩いてみないと挙動が掴みにくい部分です。

しかし、FirstPublishLocationId という解決策を見つけたことで、APIリクエストを最小限に抑え、バックエンドの実装をシンプルに保つことができました。

この機能がリリースされれば、ユーザーはLINEやメールアプリに切り替えることなく、アプリ完結でスムーズに書類提出ができるようになるはずです。
CRMとアプリをシームレスに繋ぎ、より良いユーザー体験を提供できるよう、引き続き開発を進めていきます。

同じようにSalesforce連携で悩んでいる方の参考になれば幸いです。

参考リンク

https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentversion.htm
https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentdocument.htm
https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_contentdocumentlink.htm

nextbeat Tech Blog

Discussion