2014-06-21

Google Apps ScriptからSalesforceのAPIを叩いてみる。【色々イジる編】

前回はGoogleAppsScriptを使ってSalesforceのaccess_token取得まで行いました。

今回は実際にレコード取得してセルに入れたり、データを更新したり

MetadataAPI使ってカスタムオブジェクト作っちゃったりと色々とやってみます!

 

1. レコード取得してデータを格納

コードはこんな感じ。
/**
 * クエリ発行してスプレッドシートに書き込み
 */
function query(q) {
  checkAuthorization();
  
  //クエリ発行
  var prop = PropertiesService.getUserProperties();
  var sessionInfo = JSON.parse(prop.getProperty("session_info"));
  var res = UrlFetchApp.fetch(sessionInfo.instance_url + "/services/data/v30.0/query/?q=" + q, {
    "method" : "GET",
    "headers" : {
      "Authorization": "Bearer " + sessionInfo.access_token
    }
  });
  
  //レスポンスをパースして変数に格納
  var queryResult = JSON.parse(res.getContentText());
  var fieldsArray = getFieldsFromQueryResult(queryResult);
  var records = [fieldsArray];
  
  queryResult.records.forEach(function(record){
    var pushRecord = [];
    fieldsArray.forEach(function(field) {
      pushRecord.push(record[field]);
    });
    records.push(pushRecord);
  });

  //シートに書きこみ  
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var range = SpreadsheetApp.getActiveRange();
  sheet.getRange(range.getRow(), range.getColumn(), records.length, fieldsArray.length).setValues(records);
    
}

/**
 * QueryResultから項目リストを取得
 */
function getFieldsFromQueryResult(qr) {
  var fields = Object.keys(qr.records[0]);
  fields.forEach(function(field, i){
    if (field == "attributes") {
      return fields.splice(i,1);
    }
  });
  return fields;
}

/**
 * 認証状態を確認し、access_tokenの有効期限が切れていたら
 * refresh_tokenでaccess_tokenを再取得する。
 */
function checkAuthorization() {
  var prop = PropertiesService.getUserProperties();
  var sessionInfo = JSON.parse(prop.getProperty("session_info"));
  var res = UrlFetchApp.fetch(sessionInfo.instance_url + "/services/data/v30.0", {
    "method" : "GET",
    "headers" : {
      "Authorization": "Bearer " + sessionInfo.access_token
    },
    "muteHttpExceptions": true
  });
  
  if (res.getResponseCode() === 401) {
    var res = UrlFetchApp.fetch(
      ACCESS_TOKEN_URL, 
      {
        "method" : "POST",
        "payload" : {
        "grant_type": "refresh_token",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "refresh_token": sessionInfo.refresh_token
      },
      "muteHttpExceptions": true
    });
    
    if (res.getResponseCode() == 200) {
      var newSessionInfo = JSON.parse(res.getContentText());
      newSessionInfo.refresh_token = sessionInfo.refresh_token;
      prop.setProperty("session_info", res.getContentText());
    }
  }
}

UserPropertyの"session_info"にはaccess_token等を格納しています。

checkAuthorizationはaccess_tokenの有効期限が切れたらrefresh_tokenで更新する関数。

getFieldsFromQueryResultでヘッダ用にQueryResultから取得した

項目のAPI参照名のリストを取得してます。

 

レコード数多い場合はレスポンスのnextRecordsUrlを使ってループさせることになります。

 

2. データ更新

選択したセルの取引先(SalesforceIDで判別)を更新する役立たないサンプル↓
/**
 * 更新クエリ
 */
function updateAccount() {
  checkAuthorization();
  var prop = PropertiesService.getUserProperties();
  var sessionInfo = JSON.parse(prop.getProperty("session_info"))
  //選択したセルがSalesforceのAccountIDであることを想定
  var accountId = SpreadsheetApp.getActiveRange().getValue();
  var res = UrlFetchApp.fetch(
    sessionInfo.instance_url + "/services/data/v30.0/sobjects/Account/" + accountId +"?_HttpMethod=PATCH", 
    {
      "method" : "POST",
      "headers" : {
        "Authorization": "Bearer " + sessionInfo.access_token
      },
      "payload": JSON.stringify({
      "Name": "GoogleAppsScriptによって更新!"
      }),
      "contentType": "application/json; charset=utf-8"
    }
  );
  return res.getContentText();  
}

 

UrlFetchApp.fetchメソッドがPATCHに対応していないため、そのままではPATCHのAPIは叩けませんが

Salesforce様はサーバーサイドでPATCHに対応していないアプリケーションに対して考慮してくれていて

URLパラメータに_HttpMethod=PATCHというのを付与してPOSTすれば

PATCHとして処理を行ってくれるみたいです。(→リファレンス

 

3. カスタムオブジェクト作成

今回はCRUDベースではなくFileベースのMetadata APIを使いました。

XMLファイル作成→Zip圧縮→httpコール→ポーリング

という流れになります。

 

/**
 * カスタムオブジェクトGAS__cを作成する。
 */
function createObject() {
  //package.xml作成
  var package = Utilities.newBlob("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<Package xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<types><name>CustomObject</name><members>GAS__c</members>\
</types><version>30.0</version>\
</Package>").setName("hogehoge/package.xml");
 
  //オブジェクトXML作成 
  var objInfo = Utilities.newBlob("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<CustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<actionOverrides>\
<actionName>Accept</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>Clone</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>Delete</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>Edit</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>List</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>New</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>Tab</actionName>\
<type>Default</type>\
</actionOverrides>\
<actionOverrides>\
<actionName>View</actionName>\
<type>Default</type>\
</actionOverrides>\
<deploymentStatus>Deployed</deploymentStatus>\
<description>このオブジェクトはGASから作ってます</description>\
<enableActivities>false</enableActivities>\
<enableFeeds>false</enableFeeds>\
<enableHistory>false</enableHistory>\
<enableReports>false</enableReports>\
<label>GoogleAppsScriptから作ったオブジェクト</label>\
<nameField>\
<label>The Name</label>\
<type>Text</type>\
</nameField>\
<searchLayouts/>\
<sharingModel>ReadWrite</sharingModel>\
</CustomObject>").setName("hogehoge/objects/GAS__c.object");

  var zip = Utilities.zip([package, objInfo]);
  var driveZip = DriveApp.createFile(zip); // データをGドライブに格納
  var binary = Utilities.base64Encode(zip.getBytes());
  
  var metaBody = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\
<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" \
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\
<soap:Header><SessionHeader xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<sessionId>{{sessionId}}</sessionId></SessionHeader></soap:Header><soap:Body>\
<deploy xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<ZipFile>{{zipfile}}</ZipFile>\
<DeployOptions>\
<allowMissingFiles>false</allowMissingFiles>\
<autoUpdatePackage>false</autoUpdatePackage>\
<checkOnly>false</checkOnly>\
<ignoreWarnings>false</ignoreWarnings>\
<performRetrieve>false</performRetrieve>\
<purgeOnDelete>false</purgeOnDelete>\
<rollbackOnError>false</rollbackOnError>\
<runAllTests>false</runAllTests>\
<singlePackage>false</singlePackage>\
</DeployOptions>\
</deploy>\
</soap:Body></soap:Envelope>";
  
  //deployコール
  checkAuthorization();
  var prop = PropertiesService.getUserProperties();
  var sessionInfo = JSON.parse(prop.getProperty("session_info"));
  var orgId = sessionInfo.id.match(/https:\/\/.+\.com\/id\/([a-zA-Z\d]+)\/([a-zA-Z\d]+)/)[1];
  var result = UrlFetchApp.fetch(sessionInfo.instance_url + "/services/Soap/m/30.0/" + orgId, {
    "method" : "POST",
    "payload" : metaBody.replace("{{sessionId}}", sessionInfo.access_token).replace("{{zipfile}}", binary),
    "muteHttpExceptions": true,
    "headers" : {
      "SOAPAction" : "\"\""
    },
    "contentType" : "text/xml"
  });
  
  //レスポンスのXMLのパース
  var doc = XmlService.parse(result.getContentText());
  var contents = doc.getDescendants();
  var checkId = '';
  for (var i = 0; i < contents.length; i++ ) {
    if (contents[i].getType() == XmlService.ContentTypes.ELEMENT) {
      if (contents[i].asElement().getName() == "id") {
        var checkId = contents[i].asElement().getText();
        break;
      }
    }
  }

  //ステータスチェック(ポーリング)
  while(true) {
    Utilities.sleep(1000);
    var status = checkDeployStatus(checkId);
    if (status != "InProgress" && status != "Pending") {
      break;
    };
  }
}

/**
 * ポーリング用メソッド
 */
function checkDeployStatus(checkId) {
  
  var metaBody = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\
<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" \
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\
<soap:Header><SessionHeader xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<sessionId>{{sessionId}}</sessionId></SessionHeader></soap:Header><soap:Body>\
<checkDeployStatus xmlns=\"http://soap.sforce.com/2006/04/metadata\">\
<ID>{{id}}</ID>\
<includeDetails>true</includeDetails>\
</checkDeployStatus>\
</soap:Body></soap:Envelope>";
  
  var prop = PropertiesService.getUserProperties();
  var sessionInfo = JSON.parse(prop.getProperty("session_info"));
  var orgId = sessionInfo.id.match(/https:\/\/.+\.com\/id\/([a-zA-Z\d]+)\/([a-zA-Z\d]+)/)[1];
  var result = UrlFetchApp.fetch(
    sessionInfo.instance_url + "/services/Soap/m/30.0/" + orgId, {
    "method" : "POST",
    "payload" : metaBody.replace("{{sessionId}}", sessionInfo.access_token).replace("{{id}}", checkId),
    "muteHttpExceptions": true,
    "headers" : {
      "SOAPAction" : "\"\""
    },
    "contentType" : "text/xml"
  });

  var doc = XmlService.parse(result.getContentText());
  var contents = doc.getDescendants();
  var checkId = '';
  for (var i = 0; i < contents.length; i++ ) {
    if (contents[i].getType() == XmlService.ContentTypes.ELEMENT) {
      if (contents[i].asElement().getName() == "status") {
        return contents[i].asElement().getText();
      }
    }
  }
  return '';
}

 

注意点としてはzipの階層を

-hogehoge/objects/GAS__c

-hogehoge/package.xml

というように任意のディレクトリ(ここではhogehoge)をrootに持っていかないといけないところ。

root直でpackage.xmlを置いても No package.xml foundというエラーが出ちゃいます。

それさえ気をつければ、あとはforce.com IDEでよく見る階層構造でXMLを詰め込んでいくだけ!

 

上記サンプルはがっつりハードコーディングしてますが、スプレッドシートから値を引っ張ってきたり

GドライブにあるzipファイルをデプロイしたりGAS自体Googleのサービスと相性が良いので色々と応用は効きそう。

 

また、GASはチョー簡単にcronが出来るので、MetadataAPIを使えばretrieveして

GoogleDriveに突っ込んで任意の環境に自動デプロイするといったCIチックなことも出来ます(多分)。

 

ただし、一応制限あるっぽいので、SFDCと同じように注意が必要。

https://developers.google.com/apps-script/guides/services/quotas

 

このエントリーをはてなブックマークに追加