プログラマ38の日記

主にプログラムメモです。

Salesforce: API50で追加されたCustomIndexメタデータのメモ

2020/9/29時点の備忘メモです。

 

SalesforceのAPI50(Winter'50)で「CustomIndex」メタデータが追加されます。

このメタデータについては、メタデータAPI名の取得が難しくワイルドカード指定で取得したほうがよさそうです。

 

メタデータAPI 「listMetadata」の取得結果では、インデックス項目のAPI名が取得できますが、実際の「CustomIndex」のAPI名は、オブジェクトのAPI名 + "_" + インデックス項目のAPI名 という指定のようです。(なので 「listMetadata」の結果でそのままリクエストできない)

 

さらに "__c" の文字列は "%5F%5Fc" となっていました。

取得できるのは、カスタム項目だけのようですが、これはどういう使い方になるのかリリース間近になったらあらためて確認しようと思います。

 

2020/10/19 追記、CustomIndexは、「listMetadata」の結果でそのままリクエストできないままでした。 以前、ApprovalRequestも同様な状態でしたが、修正されたのでしばらくすると修正されるかもしれません。

Salesforce:標準項目の選択リスト値をリリースする

標準項目の選択リスト値は今まで手動で行っていましたが、メタデータのデプロイでリリースができることを知ったのでメモとして残します。(API48時点での記載です)

 

メタデータの種類は「StandardValueSet」で、このメタデータは取得方法が他のメタデータと違います。
取得する場合は、次のヘルプから項目を選択することになります。

developer.salesforce.com

取得はAnt移行ツールが使いやすくretrieveタスクでのpackage.xmlの指定は次になります。

<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>AccountContactMultiRoles</members>
        <members>AccountContactRole</members>
        <members>AccountOwnership</members>
        <members>AccountRating</members>
        <members>AccountType</members>
        <members>AssetStatus</members>
        <members>CampaignMemberStatus</members>
        <members>CampaignStatus</members>
        <members>CampaignType</members>
        <members>CaseContactRole</members>
        <members>CaseOrigin</members>
        <members>CasePriority</members>
        <members>CaseReason</members>
        <members>CaseStatus</members>
        <members>CaseType</members>
        <members>ContactRole</members>
        <members>ContractContactRole</members>
        <members>ContractStatus</members>
        <members>EntitlementType</members>
        <members>EventSubject</members>
        <members>EventType</members>
        <members>FiscalYearPeriodName</members>
        <members>FiscalYearPeriodPrefix</members>
        <members>FiscalYearQuarterName</members>
        <members>FiscalYearQuarterPrefix</members>
        <members>IdeaCategory1</members>
        <members>IdeaMultiCategory</members>
        <members>IdeaStatus</members>
        <members>IdeaThemeStatus</members>
        <members>Industry</members>
        <members>LeadSource</members>
        <members>LeadStatus</members>
        <members>OpportunityCompetitor</members>
        <members>OpportunityStage</members>
        <members>OpportunityType</members>
        <members>OrderStatus</members>
        <members>OrderType</members>
        <members>PartnerRole</members>
        <members>Product2Family</members>
        <members>QuestionOrigin1</members>
        <members>QuickTextCategory</members>
        <members>QuickTextChannel</members>
        <members>QuoteStatus</members>
        <members>RoleInTerritory2</members>
        <members>SalesTeamRole</members>
        <members>Salutation</members>
        <members>ServiceContractApprovalStatus</members>
        <members>SocialPostClassification</members>
        <members>SocialPostEngagementLevel</members>
        <members>SocialPostReviewedStatus</members>
        <members>SolutionStatus</members>
        <members>TaskPriority</members>
        <members>TaskStatus</members>
        <members>TaskSubject</members>
        <members>TaskType</members>
        <members>WorkOrderLineItemStatus</members>
        <members>WorkOrderPriority</members>
        <members>WorkOrderStatus</members>
        <name>StandardValueSet</name>
    </types>
    <version>48.0</version>
</Package>

 

 上記以外にも次の標準項目の選択リスト値が取得できるようです。これ以外にも標準項目の選択リスト値は存在するのですが、どう指定すれば取得できるかわかりませんでした。

メタデータの指定 オブジェクト 項目
ラベル 名前 ラベル 項目
DigitalAssetStatus 納入商品 Asset DigitalAssetStatus DigitalAssetStatus
AssetRelationshipType 納入商品リレーション AssetRelationship リレーション種別 RelationshipType
ChannelProgramCategory チャネルプログラム項目 ChannelProgram カテゴリ Category
UnitOfMeasure ConsumptionSchedule ConsumptionSchedule 測定単位 UnitOfMeasure
ContactRequestReason 問い合わせ要求 ContactRequest 要求理由 RequestReason
ContactRequestStatus 問い合わせ要求 ContactRequest 要求の状況 Status
MilitaryService 個人 Individual 兵役 MilitaryService
PartnerFundRequestActivity パートナー資金リクエス PartnerFundRequest アクティビティ Activity
QuantityUnitOfMeasure 商品 Product2 基準数量単位 QuantityUnitOfMeasure
ScorecardMetricCategory スコアカード総計値項目 ScorecardMetric カテゴリ Category
StatusReason 納入商品 Asset 状況の理由 StatusReason

 

2020/7/12 納入商品に「状況の理由」 項目が追加されていました。

 

最後に

今までリードの状況/商談のフェーズ/ケースの状況など、リリースできないと思っていて手動で色々設定していました。この「StandardValueSet」でのデプロイを行えば、取引の開始済み/確度/完了フラグ といったその標準項目専用の属性もリリースできるのでとても便利だなと思いました。

Salesforce:Trigger.isDelete での before と after の違いではまったこと

今さらですが、Deleteトリガでの before と after の違いではまりました。

 

はまったこと

f:id:crmprogrammer38:20200525071847p:plain

f:id:crmprogrammer38:20200525072216p:plain

上記のように、オブジェクトBはオブジェクトAを参照しています。
オブジェクトBのオブジェクトAへの参照関係項目の「参照レコードが削除された場合の対処方法」は”この項目の値をクリアします。”としています。

この時にオブジェクトAを削除した時のDeleteトリガのbefore , after の違いで次でした。

before delete : オブジェクトBのオブジェクトAへの参照関係項目の値はセットされている

after delete:オブジェクトBのオブジェクトAへの参照関係項目の値がクリアされている。


なので、オブジェクトBの参照関係項目を使ってオブジェクトAが削除された時に関連するオブジェクトBも削除する場合、 before delete でオブジェクトBを検索して削除する必要がありました。

 

次のトリガの実行の順序は理解していてつもりですが、関連オブジェクトの参照関係項目の値のクリアについては正しく理解できていませんでした。

developer.salesforce.com

 

Salesforce: メタデータAPIの renameMetadata を使って 統一感のあるfullNameにする

急いで開発していて、設定の物理名(API名やdeveloper名など)はあまり考えている時間がなかったり、ネーミングルールを後から作ったため、それまでの設定がルールとあっていなかったりする場合があります。

 

メンテナンス性を考えてそれまでの設定の物理名を変更するのにメタデータAPIの renameMetadata が便利です。(ラベルの変更には、deploy もしくは、 updateMetadata を使います)

 

jaxws-ri-2.3.2 を使ったサンプルが次になります。
※Lightning Platform Web Services Connector (WSC) でも良かったのですがあまり面白みがなかったのでこちらにしました。

jaxws-ri の使い方については、以前記事を書きました。

 

crmprogrammer38.hatenablog.com

 

サンプルコード  対象のメタデータタイプは一部のものになります。

以下のソースは次の環境で動作します。 

java8,  jaxws-ri-2.3.2, wsimportはjava8のものを使いました。

オブジェクトの物理名はObject__cとして表記しています。

メタデータタイプは例として次をピックアップしました。

  • レコードタイプ
  • カスタム項目
  • リストビュー
  • 入力規則
  • ワークフロールール
  • メールアラート
  • 項目自動更新
  • Lightningレコードページ、ユーティリティバー、ホームページ
  • アクション
  • グローバルアクション
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;

import com.sforce.soap._2006._04.metadata.CustomField;
import com.sforce.soap._2006._04.metadata.FlexiPage;
import com.sforce.soap._2006._04.metadata.ListView;
import com.sforce.soap._2006._04.metadata.MetadataPortType;
import com.sforce.soap._2006._04.metadata.MetadataService;
import com.sforce.soap._2006._04.metadata.QuickAction;
import com.sforce.soap._2006._04.metadata.SaveResult;
import com.sforce.soap._2006._04.metadata.ValidationRule;
import com.sforce.soap._2006._04.metadata.WorkflowAlert;
import com.sforce.soap._2006._04.metadata.WorkflowFieldUpdate;
import com.sforce.soap._2006._04.metadata.WorkflowRule;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.SforceService;
import com.sforce.soap.partner.Soap;
import com.sun.xml.ws.api.message.Header;
import com.sun.xml.ws.api.message.Headers;
import com.sun.xml.ws.developer.WSBindingProvider;

public class Main {

  public static Soap soapBinding = new SforceService().getSoap();
  public static MetadataPortType metadataBinding = new MetadataService().getMetadata();

  public static String endpoint = "https://login.salesforce.com/services/Soap/u/48.0";
  public static String username = "sample@user.name";
  public static String password = "samplepassword";
  
  public static void main(String[] args) throws Exception {
    
    login();
    
    //レコードタイプ
    rename(RecordType.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //カスタム項目
    rename(CustomField.class.getSimpleName(), "Object__c.fullName1__c" , "Object__c.fullName2__c");
    //リストビュー
    rename(ListView.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //入力規則
    rename(ValidationRule.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //ワークフロールール
    rename(WorkflowRule.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //メールアラート
    rename(WorkflowAlert.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //項目自動更新
    rename(WorkflowFieldUpdate.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //Lightningレコードページ、ユーティリティバー、ホームページ
    rename(FlexiPage.class.getSimpleName(), "fullName1" , "fullName2");
    //アクション
    rename(QuickAction.class.getSimpleName(), "Object__c.fullName1" , "Object__c.fullName2");
    //グローバルアクション
    rename(QuickAction.class.getSimpleName(), "fullName1" , "fullName2");

  }
  
  public static void rename(String metadataType, String oldname, String newname) {
    boolean resultSuccess = false;
    String errorMessage = "";
    
    try{
      SaveResult sr = metadataBinding.renameMetadata(metadataType, oldname, newname);
      
      if(sr.isSuccess()){
        resultSuccess = true;
      } else {
        errorMessage = sr.getErrors().get(0).getMessage();
      }
    
    }catch(Exception e){
      errorMessage = e.getMessage();
    }
    
    
    LinkedHashMap<String, String> info= new LinkedHashMap<>();
    info.put("metadataType", metadataType);
    info.put("oldname", oldname);
    info.put("newname", newname);
    
    if( resultSuccess ){
      System.out.println("success : " + info );
    } else {
      System.out.println("error   : " + info + " error : " + errorMessage );
    }
  }
  
  
  public static void login() throws Exception{

    

    WSBindingProvider provider = (WSBindingProvider) soapBinding;

    Map<String,Object> reqContext = provider.getRequestContext();
    reqContext.put(WSBindingProvider.ENDPOINT_ADDRESS_PROPERTY,endpoint        );
    reqContext.put("javax.xml.ws.client.connectionTimeout", "180");
    reqContext.put("javax.xml.ws.client.receiveTimeout", "180");
    
    

    // Enable GZip compression
    Map<String, List<String>> httpHeaders = new HashMap<String, List<String>>();
    httpHeaders.put("Content-Encoding", Collections.singletonList("gzip"));
    httpHeaders.put("Accept-Encoding", Collections.singletonList("gzip"));
    reqContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders);

    LoginResult loginResult = soapBinding.login(username,password);

    String serverUrl = loginResult.getServerUrl();
    String metadataUrl = loginResult.getMetadataServerUrl();
    String sessionId = loginResult.getSessionId();

    reqContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serverUrl);

    com.sforce.soap.partner.SessionHeader sh = new com.sforce.soap.partner.SessionHeader();
    sh.setSessionId(sessionId);

    JAXBContext jaxbContext = null;
    try {
      jaxbContext = JAXBContext.newInstance(com.sforce.soap.partner.ObjectFactory.class);
    } catch (JAXBException e) {
    }

    ArrayList<Header> sessionheaderlst = new ArrayList<Header>();
    sessionheaderlst.add(Headers.create((JAXBContext) jaxbContext, sh));
    provider.setOutboundHeaders(sessionheaderlst);
    
    buildMetadataBinding(metadataUrl, sessionId);
    


  }
  
  public static void buildMetadataBinding(String metadataUrl, String sessionId) throws Exception{
    WSBindingProvider provider = (WSBindingProvider) metadataBinding;
    Map<String,Object> reqContext = provider.getRequestContext();
    reqContext.put(WSBindingProvider.ENDPOINT_ADDRESS_PROPERTY,metadataUrl        );
    reqContext.put("javax.xml.ws.client.connectionTimeout", "180");
    reqContext.put("javax.xml.ws.client.receiveTimeout", "180");
    // Enable GZip compression
    Map<String, List<String>> httpHeaders = new HashMap<String, List<String>>();
    httpHeaders.put("Content-Encoding", Collections.singletonList("gzip"));
    httpHeaders.put("Accept-Encoding", Collections.singletonList("gzip"));
    reqContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders);
    
    com.sforce.soap._2006._04.metadata.SessionHeader sh = new com.sforce.soap._2006._04.metadata.SessionHeader();
    sh.setSessionId(sessionId);

    JAXBContext jaxbContext = null;
    try {
      jaxbContext = JAXBContext.newInstance(com.sforce.soap._2006._04.metadata.ObjectFactory.class);
    } catch (JAXBException e) {
    }

    ArrayList<Header> sessionheaderlst = new ArrayList<Header>();
    sessionheaderlst.add(Headers.create((JAXBContext) jaxbContext, sh));
    provider.setOutboundHeaders(sessionheaderlst);
    
  }

}

最後に

よくやってしまいがちなのが、LightningレコードページでFlexiPage1などシステムで自動で付与されたものをそのまま使ってしまったりなどです。

また、ユーティリティバーについては、身に覚えがないのにUtilityBar1などとなってしまうので後から修正することも多いのかなと思います。

Salesforce:Lightning ホームページの割り当てに対応するメタデータ

ホームページの割り当てがメタデータでどう出力されているかのメモです。

 

まずホームページの割り当ては次の3パターンです。

  • デフォルトのホームページ
  • アプリケーション別割り当て
  • アプリケーションおよびプロファイル別割り当て

 

それぞれのメタデータについて次でした。

デフォルトのホームページ

f:id:crmprogrammer38:20200513080712p:plain

メタデータには出力されないようです。

 

アプリケーション別割り当て

f:id:crmprogrammer38:20200513080750p:plain
→アプリケーションのメタデータ、actionOverrides タグで standard-home に対して出力されます。

 

アプリケーションおよびプロファイル別割り当て

f:id:crmprogrammer38:20200513080830p:plain
→アプリケーションのメタデータ、profileActionOverrides タグで standard-home に対して出力されます。

 

最後に

調べている途中で気が付いたのですが、ホームページも、Lightningレコードページと同様に、「Flexipage」メタデータなので、Lightningレコードページの割り当てと同様のメタデータ出力になります。

Salesforce:Lightning 「コミュニティ レコードの詳細」でのforce:hasSObjectName の動き

結論としては、Lightningページでは、force:hasSObjectName インタフェースは利用できますが、

コミュニティ レコードの詳細ページでは使えませんでした。(Spring'20時点)

 

コミュニティのページで次のようなコンポーネントを作成した場合

 

<aura:component implements="force:hasSObjectName,force:hasRecordId,forceCommunity:availableForAllPageTypes" access="global" >
<aura:attribute name="sObjectName" type="String" />
<aura:attribute name="recordId" type="String" />

・・・・・

attributeの値は次のようになります。

 

var recordId = component.get("v.recordId");
値が正しくとれる。

var sObjectName = component.get("v.sObjectName");
値がnullになる。 

 

なので、コミュニティ レコードの詳細ページで、特定のオブジェクトの時の表示を行う際は別途Apexコントローラを用意して、Lightning Componentで呼び出しする必要があります。

[Apex Controller]

global class SampleController {

    

    @AuraEnabled

    public static String getSobjectName(String recordId){

        try{

            Id sobjid = (Id)recordId;

            return sobjid.getSobjectType().getDescribe().getName();

        }   catch(Exception e) {

            return null;

        }

    }

}

[JS Controller]

({

  doInit: function(component, event, helper) {

    var recordId = component.get("v.recordId");

    var fetchEventAction = component.get("c.getSobjectName");

    fetchEventAction.setParams({ recordId: recordId });

    fetchEventAction.setCallback(this, function(response) {

      var fieldName = response.getReturnValue();

      component.set("v.sObjectName", fieldName);

    });



    $A.enqueueAction(fetchEventAction);

  }

})

 

例えば、コミュニティで特定のオブジェクトの時はパス(lightning:path)を表示したいといった場合には、上記の方法で取得したオブジェクト名で制御をすることになります。

Salesforce: レコードのフォローを自動化する

レコードのフォローを自動化するには、

ConnectApi.ChatterUsers.follow(String communityId, String userId, String subjectId)

を使います。

 

実際使ってみて気づいた点をメモしておこうと思います。

 

  1. 内部ユーザが外部ユーザのuserIdを指定できるが、外部ユーザが内部ユーザのuserIdを指定するとエラーとなる。
  2. communityIdは、userIdのユーザ種別に応じてセットの内容を変える必要がある。

 

1.内部ユーザが外部ユーザのuserIdを指定できるが、外部ユーザが内部ユーザのuserIdを指定するとエラーとなる。

レコードの新規作成時に対象のユーザ(内部ユーザとコミュニティユーザ)がレコードをフォローするというトリガを作りました。
内部ユーザがトリガを実行すれば正しく動作しますが、コミュニティユーザがトリガが実行すると「System.NoAccessException: アクセス権がありません: アクセス権がないため要求を実行できません。」が発生しました。

コミュニティユーザはそのユーザは参照できるように設定してもこのエラーは解消できませんでした。

外部ユーザが外部ユーザのuserIdを指定する場合はエラーになりませんでした。

 

2.communityIdは、userIdのユーザ種別に応じてセットの内容を変える必要がある。

内部ユーザのレコードのフォローを自動化するには、

ConnectApi.ChatterUsers.follow(null , 内部ユーザのID, レコードのID);

となります。

 

内部ユーザのレコードのフォローを自動化するには、

 

ConnectApi.ChatterUsers.follow(外部ユーザのコミュニティID , 外部ユーザのID, レコードのID);

となります。

外部ユーザのコミュニティIDは、ネットワーク(Network)オブジェクトから取得したものをセットします。

 

最後に

レコードのフォローの自動化は、様々な状況に対応させるには難しさを感じました。
例えば、社内ユーザに限定して動作させたりといったように用途を限定すれば比較的に簡単に使えそうだなと思いました。