1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

salesforce lwc/APEX/SFDX CLI関連

Last updated at Posted at 2022-07-26
目次

salesforce inspectorでmetadata取得
  • Salesforce inspectorをChromeに追加
  • 目標sf組織にログイン(システム管理者権限ユーザ)、salesforce inspectorを開く、右上のこのアイコン
    スクリーンショット 2023-01-31 10.59.00.png
  • 「download metadata」をクリック、次の画面でdownloadしたいコンポーネントを選び、「download metadata」をクリック、そしてdownload完了後「save download metadata」をクリック
    スクリーンショット 2023-01-31 11.00.06.png
    スクリーンショット 2023-01-31 11.01.10.png
    スクリーンショット 2023-01-31 11.01.45.png
  • download出来たzipファイルの中、package.xmlを取り出し、目標sf組織に繋ぐvscodeのsfプロジェクトフォルダに入れ、package.xml右クリックし、「Retrieve Source in Manifest from Org」をクリック、そしてpackage.xml中のコンポーネントはdownloadされる。
    DevToolsについて
salesforceライセンスとオブジェクト権限

lwc基礎tipsと開発環境設定関連
  • lwcがブラウザ側のCaching機能を使わ無い場合、クイック検索 →セッションの設定 →「キャッシュ」下の「パフォーマンスを向上させるためにブラウザの安全で永続的なキャッシュを有効にする」をoffにする。
  • sf環境のデバッグモードを有効したい場合、クイック検索 →デバッグモード →適用したいユーザ左にチェック入れ、「有効化」を押す。
  • lwc中apex使わず直接レコード操作行うには、jsコードにimport { getRecord, getFieldValue } from 'lightning/uiRecordApi';をimportし、const FIELD_IN_JSCODE = 'OBJECTNAME.FIELDNAME';でjsコード中のフィールとを定義、constの使用でフィールドを変更不可にする。
  • lwcが埋められたレコードのrecordIDを取得には、@api recordId;をjsコードに埋める。
  • lwc画面に、データの取得を確定後、該当lwcを表示させるには<template if:true={JS_FIELD.data}></template>で、JS_FIELD取得出来た場合のみ、template内部の画面を表示する。
  • html側でjsからのレコードを反復処理の場合<template for:each={js_field} for:item="property_name">を使用、じゃ側のコードは以下の様に:
    js側よく使う、命令型apexでレコードlistをjsフィールド渡す方法
    loadRecords() {
      	getAllRecords() //ここは大体apexメソッド使用し、レコードlist取得
      		.then(result => {
      			this.records = result;
      		})
      		.catch(error => {
      			this.error = error;
      		});
      }
    
    ワイヤードApexでjsフィールドをデコレートして、JSコードを大幅に簡略化できる。この場合、htmlに渡されたrecordsには.data成功結果と.error失敗結果両方含まれているため、<template if:true={records}>で画面表示コントロールしたい場合、<template if:true={records.data}>に置き換える。
    js側よく使う、ワイヤードapexでレコードlistをjsフィールド渡す方法
    @wire(getAllRecords) records;
    
lwcに選択リスト

lightning-comboboxコンポーネントで選択リスト実現

html
//選択リスト表示コントローラー、まずjsコードで表示用list作成、そしてコントローラーをtrueにする
<lightning-layout-item size="6" padding="around-small"> 
        <template if:true={loaded}>
          <lightning-combobox
            name="machine_code"
            label="カメラの機器コード"
            value={value}
            placeholder="カメラの機器コードを選び下し"
            options={options}
            onchange={handleChange}
          ></lightning-combobox>
        </template>
      </lightning-layout-item>
javascript
import { LightningElement, api, wire, track } from "lwc";
import getProductNoSmartVCAList from "@salesforce/apex/RMS_RegisterServiceCtrl.getProductNoSmartVCAList";
export default class RmsRegisterServiceCameraInf extends LightningElement {
  //カメラの機器コード選択リスト表示内容保存リスト
  @track machineCodeList = [];
  //カメラの機器コード選択リスト表示コントローラー
  loaded = false;
  //「カメラの機器コード」と「smartVCA対応」数値紐つくためのmap
  productNoSmartVCAMap = new Map();
  //カメラの機器コード選択リスト結果保存用テキスト
  machineCode = '';
  //選択されたカメラの機器コードに該当する「smartVCA対応」保存用数値
  smartVCA = 1;

  // ApexからList<ProductNoSmartVCA>受け取る
  @wire(getProductNoSmartVCAList) 
  wiredLabels({error, data}){
    if(data){
      for (var key in data) {
        //カメラの機器コード選択リストに内容追加
        this.machineCodeList.push({
          label: data[key].productNo, 
          value: data[key].productNo
          //value如果有重复的话会导致label没法正确选择;value如果是数字会导致选择之后没有显示被选项;;lightning-combobox label是选项上方的标题,每次选完有变化的是value;不在html加上<template if:true={loaded}>会导致html不能加载lightning-combobox的list
          //value: data[key].smartVCA.toString()
        });
        //「カメラの機器コード」と「smartVCA対応」数値を紐つくためのmapに内容追加
        this.productNoSmartVCAMap.set(
          data[key].productNo,
          data[key].smartVCA
        );
      };
      //カメラの機器コード選択リスト表示出来る様にする
      this.loaded = true;
    }
    if(error){
      this.selectOptions = undefined;
      this.error=error;
    }
  }
  
  //カメラの機器コード選択リストに表示内容を渡す
  get options() {
    return this.machineCodeList;
  }
  //カメラの機器コード選択後、選択結果と紐付いた「smartVCA対応」数値を保存
  handleChange(event){
    this.machineCode = event.target.value;
    this.smartVCA = this.productNoSmartVCAMap.get(this.machineCode);
    console.log('Value :'+ this.machineCode)
    //console.log('options.label :'+ event.target.options.find(opt => opt.value === event.detail.value).label)    
    //this.machineCode = event.target.value;
    console.log('smartVCA :'+ this.smartVCA)
  }

  //「smartVCA対応」数値が”0”(該当カメラにはAI対応機能がない)の場合、連動項目に”0”を固定
  get setDisabled() {
    //console.log("machine_code" + this.template.querySelector('[data-id="machine_code"]').options)
    return !(this.smartVCA != 0);
  }
  /* 画面入力を監視し、入力値に合わせて、disablesdを更新
  machineCode;
  machine_codeChange(event){
    this.machineCode = this.template.querySelector('[data-id="machine_code"]').value;
  }
  get setDisabled() {
    return !(this.machineCode != 'FD9166-HN');
  }
  */

注意点:

  • htmlに渡す項目は@trackの宣言が必要、js内部のみ使う項目にはアノテーションが不要、でも引用の時、this. の宣言が必要
  • htmlで選択listを実現したいとき、listを作成し、表示用の”label”と値”value”を設置し、このlistをhtmlに渡す。ただし、valueは重複したり、数字になった利すると、html側”label”の表示が消えたら、”value”との組み合わせがズレたりするので、そこが注意必要。
  • html側にjsの項目を渡すとき、js getメソッドを用意し、returnで項目を渡す。html側では{項目}パラメータで項目を引き取る。選択リストの場合、html側はoptionsでリストを取得、そしてvalueで選択された値を保存。
  • html項目が変わったとき、jsにそれを合わせて処理を追加の場合、html側目標項目にonchangeを追加し、js側でonchange項目(event)メソッドを作成
  • ページを開く時、html側でjsの処理が終わった後、その結果を表示したい場合、html項目に<template if:true={loaded}>を追加、そしてjsで先ずloadedをfalseにし、該当処理が終わった後、loadedをtrueに変える
apex
public class ProductNoSmartVCA {
    @AuraEnabled
    public String productNo { get; set; }
    @AuraEnabled
    public Decimal smartVCA { get; set; }
    public ProductNoSmartVCA() {
    }
  }
  /**
   * デバイスマスタID(device_mst_id)で
   * 機器マスタ(DEVICE_MST)型番(product_no)と
   * Sightsカメラ機能マスタ(SIGHTS_CAMERA_FUNCTION_MST)smartVCA対応(smart_vca)
   * を紐つく。
   *
   * @param null 
   * @return List<ProductNoSmartVCA>
   */
  @AuraEnabled(cacheable=true)
  public static List<ProductNoSmartVCA> getProductNoSmartVCAList() {    
    //List<Map<Integer,Integer>> productNoSmartVCAList = new List<Map<Integer, Integer>>();
    //Map<String, Integer> productNoSmartVCAMap = new Map<String, Integer>();
    List<ProductNoSmartVCA> productNoSmartVCAList = new List<ProductNoSmartVCA>();
    //ProductNoSmartVCA productNoSmartVCA = new ProductNoSmartVCA();

    List<DEVICE_MST__x> deviceMSTList = [
      SELECT
        device_mst_id__c, 
        product_no__c
      FROM DEVICE_MST__x
      order by device_mst_id__c
    ];
    List<SIGHTS_CAMERA_FUNCTION_MST__x> sightsCameraFunctionMSTList = [
      SELECT
        device_mst_id__c, 
        smart_vca__c
      FROM SIGHTS_CAMERA_FUNCTION_MST__x
      order by device_mst_id__c
    ];

    for(Integer i = 0; i < deviceMSTList.size(); i++) {
      ProductNoSmartVCA productNoSmartVCA = new ProductNoSmartVCA();
      productNoSmartVCA.productNo = deviceMSTList[i].product_no__c;
      productNoSmartVCA.smartVCA = sightsCameraFunctionMSTList[i].smart_vca__c;
      productNoSmartVCAList.add(productNoSmartVCA);
      //productNoSmartVCAList.add(new Map<Integer, Integer> {'label'=>deviceMSTList[i].product_no__c, 'value'=>sightsCameraFunctionMSTList[i].smart_vca__c});
    }
    return productNoSmartVCAList;
  }

  /**
   * デバイスマスタ(DEVICE_MST)テーブルのレコードlistを取得
   *
   * @param null 
   * @return List<DEVICE_MST__x> 
   */
  /*
  @AuraEnabled(cacheable=true)
  public static List<DEVICE_MST__x> getDeviceMSTList() {
    return [
      SELECT
        device_mst_id__c, 
        product_no__c //, 
        //AI_Flag__c
      FROM DEVICE_MST__x
      order by product_no__c
    ];
  }
  */
  
  /**
   * 申込情報登録時、カメラ情報「カメラの機器コード」項目で「デバイスマスタ(DEVICE_MST)テーブル」を検索、「AI設定の有/無フラグ」項目を返す
   *
   * @param string 画面で入力されたカメラの機器コード
   * @return int AI設定の有/無フラグ
   */
  /*  
  public static Integer getCameraAIConfigFlag(String machineCode) {
    System.debug('★★★★★ getCameraAIConfigFlag ★★★★★');
    System.debug('★' + 'カメラの機器コード : ' + machineCode);
    
    List<DEVICE_MST__x> deviceMSTList = [
        SELECT
          device_mst_id__c
          , AI_Flag__c
        FROM
          DEVICE_MST__x
        WHERE
          product_no__c = :machineCode
      ];
    System.debug('★★★★★ getCameraAIConfigFlag ★★★★★');
    System.debug('★' + '機器マスタList.size() : ' + deviceMSTList.size());
    if (deviceMSTList.size() == 1) {
      return deviceMSTList[0].AI_Flag__c;
    } else if (eviceMSTList.size() < 1) {
      //該当カメラの機器コードは未登録
      return 1;
    } else if (eviceMSTList.size() > 1) {
      //該当カメラの機器コードは重複登録
      return 1;
    }
  }
  */

注意点:

  • apexのsoqlでは普通のSQLみたいのinner join機能がないため、一つのkey columnで二つのオブジェクトtableを結合できない。
  • ループ内soqlの使用を避けるべき
  • そのため、オブジェクトlistをそれぞれ単独で取得、この時もし、key columnが一意の場合、order by 「key column」で二つのオブジェクトリスト順番を一致化出来る。
  • jsコードに検索結果を返す時、カスタムオブジェクトのlistを返した方が操作し易い
lwcページでレコード作成

apexコードは使わず、直接レコードの作成や変更できるが、ロジックの整理が困難なので、lwcを純粋のページにし、ダータベースの操作やロジックをapexで行った方がいい
account(取引先)レコード作成を例に、

accountCreator.html
<template>
    <lightning-card>
        <lightning-record-form
            object-api-name={objectApiName}
            fields={fields}
            onsuccess={handleSuccess}>
        </lightning-record-form>
    </lightning-card>
</template>

画面イメージ
スクリーンショット 2023-01-19 11.36.31.png

accountCreator.js
import { LightningElement } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import ACCOUNT_OBJECT from '@salesforce/schema/Account';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
export default class AccountCreator extends LightningElement {
    objectApiName = ACCOUNT_OBJECT;
    fields = [NAME_FIELD, REVENUE_FIELD, INDUSTRY_FIELD];
    handleSuccess(event) {
        const toastEvent = new ShowToastEvent({
            title: "Account created",
            message: "Record ID: " + event.detail.id,
            variant: "success"
        });
        this.dispatchEvent(toastEvent);
    }
}

onsuccess={handleSuccess}はレコード作成が成功の時、図の様なポップアップが表示される。表示内容詳細はhandleSuccess(event)で設定
スクリーンショット 2023-01-19 11.36.06.png

accountCraetor.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>

今回はapp内でlwcページを表示する為、xmlファイルに<target>lightning__AppPage</target>を追加。lwcページ表示出来る様に、該当lwcをオブジェクトの編集ページ、またAPPページに入れる必要がある。APPページの新規方法は:クイック検索「Lightning アプリケーションビルダー」→ 「新規」→ 「アプリケーションページ」。このAPPページはsfの「アプリケーションランチャー」で検索出来る。

lwcとapex使ってデータ操作

apexメソッドに@AuraEnabledをつけてLWCにも使用出来る様にする。

  • そのうち、(cacheable=true)をつけると、メソッドがキャッシュ可能なる。
  • この場合、データ操作言語 (DML) による操作は許可されません。そしてキャッシュが更新されるまで新しく追加または変更されたレコードのバージョンが返されません、手動更新が必要。
  • ただ、jsファイルで、命令的なApexコールすると、手動更新がでいない。命令的なApexコール例:
CallApexImperativeのgetContactsBornAfterメソッドをコール
import { LightningElement, api, wire } from 'lwc';
import getContactsBornAfter from '@salesforce/apex/ContactController.getContactsBornAfter';
export default class CallApexImperative extends LightningElement {
    @api minBirthDate;
    handleButtonClick() {
        getContactsBornAfter({ //imperative Apex call
            birthDate: this.minBirthDate
        })
            .then(contacts => {
                //code to execute if related contacts are returned successfully
            })
            .catch(error => {
                //code to execute if related contacts are not returned successfully
            });
    }
}

命令的なApexコールする以外、@wireを使用したApexコールの場合、以下の例で、account(取引先)のName、AnnualRevenue、Industryを取得し、lwcページで表示する。

account(取引先)のName、AnnualRevenue、Industryを取得apex class
public with sharing class AccountController {
    @AuraEnabled(cacheable=true)
    public static List<Account> getAccounts() {
        return [
            SELECT Name, AnnualRevenue, Industry
            FROM Account
            WITH SECURITY_ENFORCED
            ORDER BY Name
        ];
    }
}
apexメソッドで取得したデータ表示用のlist formを構成
import { LightningElement, wire } from 'lwc';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
const COLUMNS = [
    { label: 'Account Name', fieldName: NAME_FIELD.fieldApiName, type: 'text' },
    { label: 'Annual Revenue', fieldName: REVENUE_FIELD.fieldApiName, type: 'currency' },
    { label: 'Industry', fieldName: INDUSTRY_FIELD.fieldApiName, type: 'text' }
];
export default class AccountList extends LightningElement {
    columns = COLUMNS;
    // 結果を accounts プロパティに保存します。操作が成功すると、レコードは accounts.data でアクセス可能になります。失敗するとエラーが account.error に表示されます。
    // 命令的なApexコールする場合、「.catch(error =>」を使う必要がある、@wireデコレートの場合、errorプロパティ属性でアクセスできる
    @wire(getAccounts)
    accounts;
}
accountList.html
<template>
    <lightning-card>
        <template if:true={accounts.data}>
            <lightning-datatable
                key-field="Id"
                data={accounts.data}
                columns={columns}
            >
           </lightning-datatable>
        </template>
    </lightning-card>
</template>

コンポーネントをアプリケーションページで使用できるように

accountList.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>48.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>

参考: Apex を使用したデータの操作

サーバエラーの処理
  • @wireを使用してプロパティをデコレートしている場合、エラーは error プロパティ属性でアクセスできる、エラー情報の取得方法二つある、
    • @wireデコレートされたメソッド結果取得後、errorsのget関数でエラー情報取得
    wireApexProperty.js
    import { LightningElement, api, wire } from 'lwc';
    // reduceErrors ヘルパー関数を ldsUtils モジュールからインポート
    import { reduceErrors } from 'c/ldsUtils';
    import getContactsBornAfter from '@salesforce/apex/AccountController.getContactsBornAfter';
    export default class WireApexProperty extends LightningElement {
        @api minBirthDate;
        @wire(getContactsBornAfter, { birthDate: '$minBirthDate' })
        contacts;
        // getter で reduceErrors ヘルパー関数を使用して this.contacts.error の形式を設定し、この関数はエラーオブジェクトを受け取り、発生したすべてのエラーメッセージの配列を返す
        get errors() {
            return (this.contacts.error) ?
                reduceErrors(this.contacts.error) : [];
        }
    }
    
    • @wireデコレートされたメソッドパラメーターとしてエラー (エラーがあった場合) を含むオブジェクトを受け取る
    wireApexFunction.js
    import { LightningElement, api, wire } from 'lwc';
    // reduceErrors ヘルパー関数を ldsUtils モジュールからインポート
    import { reduceErrors } from 'c/ldsUtils';
    import getContactsBornAfter from '@salesforce/apex/AccountController.getContactsBornAfter';
    export default class WireApexFunction extends LightningElement {
        @api minBirthDate;
        // errorsという名前のプロパティを定義
        errors;
        //  結び付けられた関数がエラーを受け取るたびに、reduceErrorsヘルパー関数を使用してエラーの形式を設定し、この関数は発生したすべてのエラーの配列を返す
        @wire(getContactsBornAfter, { birthDate: '$minBirthDate' })
        wiredContacts({data, error}) {
            if (error)
                this.errors = reduceErrors(error);
        }
    }
    
  • @wireを使用してプロパティをデコレートし無い、命令的に関数をコールする場合のエラー処理
callApexImperative.js
  import { LightningElement, api, wire } from 'lwc';
  import { reduceErrors } from 'c/ldsUtils';
  import getContactsBornAfter from '@salesforce/apex/AccountController.getContactsBornAfter';
  export default class CallApexImperative extends LightningElement {
      @api minBirthDate;
      //errors という名前のプロパティを定義
      errors;
      //getContactsBornAfter関数を命令的に呼び出し、関数はpromiseを返す、promiseが解決された場合、contactsを処理;promiseが却下された場合、reduceErrorsヘルパー関数を使用して受け取ったエラーの形式を設定し、errorsプロパティに保存
      handleButtonClick() {
          getContactsBornAfter({
              birthDate: this.minBirthDate
          })
              .then(contacts => {
                  // code to execute if the promise is resolved
              })
              .catch(error => {
                  this.errors = reduceErrors(error); // code to execute if the promise is rejected
              });
      }
  }

accountList.html にエラー情報表示用templateを追加
<template if:true={errors}>
    <p>{errors}</p>
</template>

スクリーンショット 2023-01-24 12.11.47.png

apexメソッドに、以下のコード入れは、強制的にエラーを作成できる

メソッドの本文をコメントにし、コードをその場所に追加して強制的にエラーを発生
throw new AuraHandledException('Forced error');
LWCコンポーネントのテストフォルダー

LWCを作成の時__tests__と言うテストフォルダーが自動生成される、この中にはjsコード為のJestコードを入れる事ができるが、Jestテストではsf組織と接続出来無いため、テスト出来るのはjsのロジックだけです。
スクリーンショット 2023-01-24 14.48.56.png
このテストフォルダーを無理矢理にsf組織にdeployすると、エラーが発生になる、そのため、ローカル環境い.forceignoreファイルの下に、**/__tests__/**を入れる事で、lwcコンポーネントをsf環境へdeployの時、フォルダー内ののテストフォルダーを無視出来る。
参考記事: Jest テストの記述

salesforceデータを読み取りlwcをテストする方法
  • 汎用ワイヤーアダプター
    詳細は下の 参考記事 を参照
  • Lightning データサービス (LDS) ワイヤーアダプター。カスタムオブジェクトや標準オブジェクトにすばやくアクセスできる。__test__フォルダ下でテスト用レコードjsonファイルを用意し、sf環境データとして、lwc資材をテスト出来る。詳細は 参考記事 を参照。
    テストの時注意必要のは、もしテストコードでjsonレコードファイルを読み込み時、require('./data/getRecord.json');を使用の場合、lwcテスト画面の①の部分で虫マークをクリック必要がある。もし②の部分の虫マークでテスト始めたら、jsonファイルが見つから無いエラーが出る。
    スクリーンショット 2023-01-24 17.23.31.png
  • Apex ワイヤーアダプター
    上の「Lightning データサービス (LDS) ワイヤーアダプター」テストに似ているが、apex class使用されたlwcをテストする方法。
    参考記事: ワイヤサービスの Jest テストの記述
jsコードのtroubleshooting
  • chrome上devtools開き、「sources」タブに入り、lwc資材のjsコードならlighting/n /modules/cの下にある、もしコードは図の様に1行だけの場合、左下の{}をクリックし、formattedのコードが表示される。
    スクリーンショット 2023-01-25 10.41.35.png

  • lwcのframeworkコードをdevtoolsから無視させるため、「ignore list」に/aura_prod.*.js$/components/.*.js$を追加する。
    スクリーンショット 2023-01-25 10.56.18.png

  • devtools使って、コードにbreakpoint入れる操作はUse Breakpointsを参考

子lwcから親lwcとの連動
  • 子lwcのボタンクリックで親lwc値をコントロール、コード例の中、「count」値のIncrement(+)とDecrement(-)はこれで操作する。

    • 先ず子lwcを親lwcに含まれる、<c-子lwc名>下の「numerator.html(親)」を参照。
    • そして子jsコードの中に、子画面ボタンと紐付けたメソッドに中にthis.dispatchEvent(new CustomEvent('イベント名'));で親側で受けたイベントを定義、そして親側の<c-子lwc名>の中、onイベント名={親ベント名}で親イベントと紐つく。
      下の「numerator.html(親)」と「controls.html(子)」「controls.js(子)」を参照
  • 子lwcのイベント中のデータを親lwcに渡す、コード例の中、「count」値のMultiply(x2, x3)はこれで操作する。

    • この場合先ず子lwcの中、イベント中のデータを定義、<lightning-button>の中data-factor="数値"を定義、event.target.dataset.factorでfactorを取得し、CustomEventのdetailに渡すthis.dispatchEvent(new CustomEvent('multiply', { detail: factor }));
      コード例の「controls.html(子)」「controls.js(子)」を参照
    • そして親側で同じくonmultiply={handleMultiply}でイベント受け取り、そすてhandleMultiply(event) { const factor = event.detail;event.detailを受け取る。ここで注意必要のは、上のボタンクリックの例では、単純に親lwcイベント呼ぶ為、親jsコードにeventを渡す必要が無いが、データを渡す場合handleMultiply(event)の様に、eventを渡す必要がある
      コード例の「numerator.html(親)」「numerator.js(親)」を参照

スクリーンショット 2023-01-27 11.37.01.png

numerator.html(親)
<template>
    <lightning-card title="Numerator" icon-name="action:manage_perm_sets">    
        <p class="slds-text-align_center slds-var-m-vertical_medium">
          Count: <lightning-formatted-number value={counter}></lightning-formatted-number>
        </p>
        <!-- ここの<c-controls>は”controls”と言うlwcを子として入れる 
        onadd={handleIncrement}は Count のプラス
        onsubtract={handleDecrement}は Count のマイナス
        onmultiply={handleMultiply}>は Count の掛け算
        -->
        <c-controls
            class="slds-show slds-is-relative"
            onadd={handleIncrement}
            onsubtract={handleDecrement}
            onmultiply={handleMultiply}>
        </c-controls>
    </lightning-card>
</template>
numerator.js(親)
import { LightningElement } from 'lwc';

export default class Numerator extends LightningElement {
    counter = 0;
    handleIncrement() {
      this.counter++;
    }
    handleDecrement() {
      this.counter--;
    }
    handleMultiply(event) {
        const factor = event.detail;
        this.counter *= factor;
    }
}
numerator.js-meta.xml(親)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
      <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>
controls.html(子)
<template>
    <lightning-card title="Controls" icon-name="action:upload">
        <lightning-layout>
          <lightning-layout-item flexibility="auto" padding="around-small">
            <lightning-button
              label="Subtract"
              icon-name="utility:dash"
              onclick={handleSubtract}>
            </lightning-button>
          </lightning-layout-item>
          <lightning-layout-item flexibility="auto" padding="around-small">
            <lightning-button
              label="2"
              data-factor="2"
              icon-name="utility:close"
              onclick={handleMultiply}>
            </lightning-button>
            <lightning-button
              label="3"
              data-factor="3"
              icon-name="utility:close"
              onclick={handleMultiply}>
            </lightning-button>
          </lightning-layout-item>
          <lightning-layout-item flexibility="auto" padding="around-small">
            <lightning-button
              label="Add"
              icon-name="utility:add"
              onclick={handleAdd}
              icon-position="right">
            </lightning-button>
          </lightning-layout-item>
        </lightning-layout>
      </lightning-card>
</template>
controls.js(子)
import { LightningElement } from 'lwc';

export default class Controls extends LightningElement {
    handleAdd() {
        this.dispatchEvent(new CustomEvent('add'));
      }
    handleSubtract() {
        this.dispatchEvent(new CustomEvent('subtract'));
      }
    handleMultiply(event) {
        const factor = event.target.dataset.factor;
        this.dispatchEvent(new CustomEvent('multiply', {
          detail: factor
        }));
      }
}
  • Bubbling機能で、子イベントを親に渡す、「x0~6」ボタンをクリックの時、botton→ controls→ numeratorで「count」をコントロール。
    スクリーンショット 2023-01-27 18.57.55.png
「botton」lwcを含めるcontrols.html
<!-- 「x2」[x3]ボタン作成コードを以下のと入れ替え、「x0~6」のボタンを自動生成、ボタンは「botton」lwcで定義 -->
<lightning-layout-item flexibility="auto" padding="around-small" onbuttonclick={handleMultiply}>
  <template for:each={factors} for:item="factor">
    <c-button
      key={factor}
      label={factor}
      data-factor={factor}
      icon="utility:close">
    </c-button>
  </template>
</lightning-layout-item>
controls.jsに以下のpropertyを追加
// 「x0~6」のボタンを自動生成
factors = [0,2,3,4,5,6];  
button.html ボタン画面表示を定義
<template>
    <lightning-button
    label={label}
    data-factor={label}
    icon-name={icon}
    onclick={handleButton}>
  </lightning-button>
</template>
button.js
import { LightningElement, api } from 'lwc';

export default class Button extends LightningElement {
    @api label;
    @api icon;
    handleButton() {
      this.dispatchEvent(new CustomEvent('buttonclick',{
        // ここの定義は、botton lwcがクリックされた時、controls.htmlの「onbuttonclick={handleMultiply}」を呼ぶ
        bubbles: true
      }));
    }
}
親lwcから子lwcとの連動
  • publicのプロパティの更新、@apiデコレーターを使って、lwcコンポーネントのプロパティを暴露(public)させる、そして親から子のpublicプロパティを更新
    スクリーンショット 2023-01-30 11.33.40.png
numerator.js(augmentorの子)
// 先ず本来のnumerator.jsのcounterに @api デコレーターをつける、以下の様に
  @api counter = 0;
augmentor.html(numeratorの親)
<template>
    <lightning-card title="Augmentor" icon-name="action:download">
        <lightning-layout>
          <lightning-layout-item flexibility="auto" padding="around-small">
            <lightning-input
              label="Set Starting Counter"
              type="number"
              min="0"
              max="1000000"
              value={startCounter}
              onchange={handleStartChange}>
            </lightning-input>
          </lightning-layout-item>
        </lightning-layout>
        <!-- ここでnumeratorを子コンポーネントとして入れる、
             そしてnumerator.jsのcounterプロパティが @api がある為、startCounterの値を渡せる -->
        <c-numerator
          class="slds-show slds-is-relative"
          counter={startCounter}>
        </c-numerator>
      </lightning-card>
</template>
augmentor.js(numeratorの親)
import { LightningElement } from 'lwc';

export default class Augmentor extends LightningElement {
    startCounter = 0;
    handleStartChange(event) {
      // ここのparseIntは入力string値を10ベースのintに変換。ここでstring入れる時、keyboard入力は「英字」に必要がある、「日本語」の場合表示され無い
      this.startCounter = parseInt(event.target.value);
    }
}
augmentor.xml(numeratorの親)
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>55.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
      <target>lightning__AppPage</target>
    </targets>
</LightningComponentBundle>
  • 親コンポーネントから子コンポーネントのpublicメソッドを呼ぶ、@apiをメソッドにつけると、このメソッドもpublicになり、親から呼ぶ事ができる。
    スクリーンショット 2023-01-30 11.51.52.png
numerator.js(augmentorの子)
    // 以下メソッド追加、numeratorのcounterプロパティを直接1000000になる
    @api
    maximizeCounter() {
      this.counter += 1000000;
    }
augmentor.html(numeratorの親)
<!-- このボタンをlightning-inputの後ろに入れ -->
        <lightning-button 
          class="slds-var-p-vertical_xx-small"
          label="Add 1m To Counter"
          onclick={handleMaximizeCounter}>
        </lightning-button>
augmentor.js(numeratorの親)
  // 以下のメソッドを追加、template.querySelectorでnumeratorコンポーネントを取得し、そのmaximizeCounter()を呼ぶ
  handleMaximizeCounter() {
    this.template.querySelector('c-numerator').maximizeCounter();
  }
  • publicのgetterとsetterで子のprivateプロパティを制御
numerator.html
      <!-- 以下内容を<P>countの前に追加 -->
      <p class="slds-text-align_center slds-var-m-vertical_medium">
        Prior Count: <lightning-formatted-number value={priorCount}></lightning-formatted-number>
      </p>
numerator.js
  // @api counter = 0;  counterをコメントアウト

  // 以下内容を追加
  // 現在の数値表す_currentCountと前一個の数値表示用priorCountを用意、そして本来public counterプロパティに対する取得、と変更をpublicメソッドに変える、jsの中にparameterとmethod調用方法がほぼ同じ
  _currentCount = 0;
  priorCount = 0;
  @api
  get counter() {
    return this._currentCount;
  }
  set counter(value) {
    this.priorCount = this._currentCount;
    this._currentCount = value;
  }
REST API

参考資料:REST API を使用する

http calloutコード書き方
  • 先ずアクセスしたいエンドポイントをsf環境に追加のは必要、「設定」→「リモートサイトの設定」→「新規」→end point情報追加
    スクリーンショット 2023-01-12 16.47.06.png

  • apexコードでそのend pointにrequest投げる。
    コードtestの時は「開発者コンソール」「Debug (デバッグ)」「Open Execute Anonymous Window (実行匿名ウィンドウを開く)」でテスト用ウインドを開き。「open log」にチェック入れexecuteすると、直接実行logが表示される
    スクリーンショット 2023-01-12 16.54.06.png
    そして「debug only」にチェック入れると、”System.debug()”の内容だけが表示される
    スクリーンショット 2023-01-12 17.05.52.png

簡単なapex http get calloutコード
Http http = new Http();
HttpRequest request = new HttpRequest();
//end point設置
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
//requestメソッド設置
request.setMethod('GET');
HttpResponse response = http.send(request);
// If the request is successful, parse the JSON response.
// 返されるresponseは {"animals":["majestic badger","fluffy bunny","scary bear","chicken"]} の様なjsonコード
if(response.getStatusCode() == 200) {
    // Deserialize the JSON string into collections of primitive data types.
    Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
    // Cast the values in the 'animals' key as a list
    List<Object> animals = (List<Object>) results.get('animals');
    //”System.debug()”を使ってlogに出力文字は”|DEBUG|”が付いている
    System.debug('Received the following animals:');
    for(Object animal: animals) {
        System.debug(animal);
    }
}
  • http postメソッドでrequest投げる時、投げるjsonコードの”Content-Type”の入力も必要
簡単なapex http post calloutコード
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json;charset=UTF-8');
// Set the body as a JSON object
request.setBody('{"name":"mighty moose"}');
HttpResponse response = http.send(request);
// Parse the JSON response
if(response.getStatusCode() != 201) {
    System.debug('The status code returned was not expected: ' + response.getStatusCode() + ' ' + response.getStatus());
} else {
    System.debug(response.getBody());
}
  • jsonをオブジェクトへの解析は JSON2Apex で可能
http callout testコード書き方

apex testコードでは本番のend pointへのアクセス出来ないが、予想の正しいend pointからのresponseを作成し、疑似コールアウトをcalloutに返す。この場合、apex test classTest.setMockメソッドを使う

  • classでhttp calloutコードの書き方
classでhttp calloutコードの書き方
public class AnimalsCallouts {
    public static HttpResponse makeGetCallout() {
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
        request.setMethod('GET');
        HttpResponse response = http.send(request);
        // If the request is successful, parse the JSON response.
        if(response.getStatusCode() == 200) {
            // Deserializes the JSON string into collections of primitive data types.
            Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
            // Cast the values in the 'animals' key as a list
            List<Object> animals = (List<Object>) results.get('animals');
            System.debug('Received the following animals:');
            for(Object animal: animals) {
                System.debug(animal);
            }
        }
        return response;
    }
    public static HttpResponse makePostCallout() {
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals');
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        request.setBody('{"name":"mighty moose"}');
        HttpResponse response = http.send(request);
        // Parse the JSON response
        if(response.getStatusCode() != 201) {
            System.debug('The status code returned was not expected: ' +
                response.getStatusCode() + ' ' + response.getStatus());
        } else {
            System.debug(response.getBody());
        }
        return response;
    }        
}
  • 疑似コールアウトは二つの方法で作成できる、一つは静的リソース、もう一つは疑似インターフェース
    • 静的リソースの場合、まずテスト用の静的リソース作成、「開発者コンソール」→[File (ファイル)」→「New (新規)」→「Static Resource (静的リソース)」、「MIME タイプ」に”text/plain”を選択。入れるresponse bodyは1行に収まるべき。
    • 疑似インターフェースの場合、まず上静的リソースの代わり、responseを返すapex classの作成が必要(今回は AnimalsHttpCalloutMock を作成)、そしてTest.setMock(HttpCalloutMock.class, mock);Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock());に変える。
疑似コールアウトは静的リソース場合のtestコード(getメソッドテスト)
@isTest
private class AnimalsCalloutsTest {
    @isTest 
    static  void testGetCallout() {
    // Create the mock response based on a static resource
    StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
    // 取得したい静的リソース名を入れる
    mock.setStaticResource('GetAnimalResource');
    mock.setStatusCode(200);
    mock.setHeader('Content-Type', 'application/json;charset=UTF-8');
    // 本来end pointのresponseを疑似コールアウトに入れ替える
    Test.setMock(HttpCalloutMock.class, mock);
    // 実際responseを投げるapex http calloutメソッドを呼ぶ 
    HttpResponse result = AnimalsCallouts.makeGetCallout();
    // Verify mock response is not null
    System.assertNotEquals(null,result, 'The callout returned a null response.');
    // Verify status code
    System.assertEquals(200,result.getStatusCode(), 'The status code is not 200.');
    // Verify content type   
    System.assertEquals('application/json;charset=UTF-8',
      result.getHeader('Content-Type'),
      'The content type value is not expected.');  
    // Verify the array contains 3 items     
    Map<String, Object> results = (Map<String, Object>) 
      JSON.deserializeUntyped(result.getBody());
    List<Object> animals = (List<Object>) results.get('animals');
    System.assertEquals(3, animals.size(), 'The array should only contain 3 items.');          
    }   
}
疑似コールアウトは疑似インターフェース場合のtestコード(postメソッドテスト)
@isTest 
static void testPostCallout() {
    // Set mock callout class 
    Test.setMock(HttpCalloutMock.class, new AnimalsHttpCalloutMock()); 
    // This causes a fake response to be sent
    // from the class that implements HttpCalloutMock. 
    HttpResponse response = AnimalsCallouts.makePostCallout();
    // Verify that the response received contains fake values
    String contentType = response.getHeader('Content-Type');
    System.assert(contentType == 'application/json');
    String actualValue = response.getBody();
    System.debug(response.getBody());
    String expectedValue = '{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}';
    System.assertEquals(expectedValue, actualValue);
    System.assertEquals(200, response.getStatusCode());
}
疑似インターフェースを実装クラス
@isTest
global class AnimalsHttpCalloutMock implements HttpCalloutMock {
    // Implement this interface method
    global HTTPResponse respond(HTTPRequest request) {
        // Create a fake response
        HttpResponse response = new HttpResponse();
        response.setHeader('Content-Type', 'application/json');
        response.setBody('{"animals": ["majestic badger", "fluffy bunny", "scary bear", "chicken", "mighty moose"]}');
        response.setStatusCode(200);
        return response; 
    }
}
  • テストの時「Always Run Asynchronously (常に非同期に実行)」の選択について補充説明。非同期 Apexは、複数の処理を後で別個のスレッドで実行するために使用します。非同期処理とは、ユーザーがタスクの終了まで待つことなく、タスクを「バックグラウンドで」実行するプロセスまたは機能です。そして、このオプションが有効になっていない場合は、1 つのクラスのテストのみが含まれるテスト実行が同期して実行されます。複数のクラスが含まれるテスト実行は、このオプションが有効になっているかどうかに関係なく非同期に実行されます。
    スクリーンショット 2023-01-12 18.14.49.png
  • 参考記事:Apex REST コールアウト HttpCalloutMock インターフェースの実装による HTTP コールアウトのテスト
apex web サービス
  • Apex クラスを rest Web サービスとして公開。sample code中Apex REST のベースエンドポイントは、
    https://<sf組織インスタンス>.my.salesforce.com/services/apexrest/Account/* です。URL 対応付けでは大文字と小文字が区別される。
get request受けるsample code
// 一個のREST エンドポイント(apex class)中に、get、post、putなどのメソッドは一回のみ使用できる
@RestResource(urlMapping='/Account/*')
global with sharing class MyRestResource {
    @HttpGet
    global static Account getRecord() {
        // Add your code
    }
}
  • 作ったrest end pointを動作確認の時、workBenchを使用できる。画面に入って、「Environment (環境)」で環境選択(Playgroundの場合は”Production (本番)”)、環境に合わせたアカウントを登録、そして「utilities (ユーティリティ)」を”REST Explorer”選択、メソッドを選び、相対パス/services/apexrest/<apexコードに合わせ>を入れる。実行結果を見たい場合「show/hide raw response」をクリック
    スクリーンショット 2023-01-13 12.37.20.png
CaseのGet、Post、Delete、Put、Patchメソッド例
@RestResource(urlMapping='/Cases/*')
global with sharing class CaseManager {
    @HttpGet
    global static Case getCaseById() {
        RestRequest request = RestContext.request;
        // grab the caseId from the end of the URL
        String caseId = request.requestURI.substring(
          request.requestURI.lastIndexOf('/')+1);
        Case result =  [SELECT CaseNumber,Subject,Status,Origin,Priority
                        FROM Case
                        WHERE Id = :caseId];
        return result;
    }
    @HttpPost
    global static ID createCase(String subject, String status,
        String origin, String priority) {
        Case thisCase = new Case(
            Subject=subject,
            Status=status,
            Origin=origin,
            Priority=priority);
        insert thisCase;
        return thisCase.Id;
    }   
    @HttpDelete
    global static void deleteCase() {
        RestRequest request = RestContext.request;
        String caseId = request.requestURI.substring(
            request.requestURI.lastIndexOf('/')+1);
        Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
        delete thisCase;
    }     
    @HttpPut
    global static ID upsertCase(String subject, String status,
        String origin, String priority, String id) {
        Case thisCase = new Case(
                Id=id,
                Subject=subject,
                Status=status,
                Origin=origin,
                Priority=priority);
        // Match case by Id, if present.
        // Otherwise, create new case.
        upsert thisCase;
        // Return the case ID.
        return thisCase.Id;
    }
    @HttpPatch
    global static ID updateCaseFields() {
        RestRequest request = RestContext.request;
        String caseId = request.requestURI.substring(
            request.requestURI.lastIndexOf('/')+1);
        Case thisCase = [SELECT Id FROM Case WHERE Id = :caseId];
        // Deserialize the JSON string into name-value pairs
        Map<String, Object> params = (Map<String, Object>)JSON.deserializeUntyped(request.requestbody.tostring());
        // Iterate through each parameter field and value
        for(String fieldName : params.keySet()) {
            // Set the field and value on the Case sObject
            thisCase.put(fieldName, params.get(fieldName));
        }
        update thisCase;
        return thisCase.Id;
    }    
}
  • workBench以外、コマンドラインcURLを使って、end pointアクセスできる。windowsやMacやLinuxのコマンドを入力前、目標組織に接続用アプリ作成必要、「アプリケーションマネージャ」→「新規接続アプリケーション」→「OAuth 設定の有効化」チェック入れ →「コールバック URL」に”https://”入れ →「選択した OAuth 範囲」に”選択した OAuth 範囲”入れる。
    スクリーンショット 2023-01-13 16.02.17.png
    もし下のコマンド入れて、{ "error" : "invalid_grant", "error_description" : "authentication failure" *Connection #0 to host login.salesforce.com left intact }が出たら、接続アプリ「manage」で「IP 制限の緩和」を”IP 制限の緩和”に変えたら多分できるようになる。
sf組織からaccess_token取得コマンド(最後の”-H”は返すjsonの改行)
curl -v https://login.salesforce.com/services/oauth2/token -d "grant_type=password" -d "client_id=<your_consumer_key>" -d "client_secret=<your_consumer_secret>" -d "username=<your_username>" -d "password=<your_password_and_security_token>" -H "X-PrettyPrint:1"
  • 上のコマンドで「access_token」取得した後、Getメソッドで取得したいレコードIDとユーザ認証用のaccess_tokenをコマンドに入れる。ここの「yourInstance」は組織の識別名、例えPlaygroundホームページURLはhttps://resilient-fox-gcanmz-dev-ed.lightning.force.com/lightning/page/homeの場合、yourInstance == resilient-fox-gcanmz-dev-ed
sf組織からレコード情報取得コマンド
curl https://<yourInstance>.my.salesforce.com/services/apexrest/Cases/<Record_ID> -H "Authorization: Bearer <access_token>" -H "X-PrettyPrint:1"
  • Apex RESTテストコード書き方
上記apex restクラスのテストコード
@IsTest
private class CaseManagerTest {
    @isTest static void testGetCaseById() {
        Id recordId = createTestRecord();
        // Set up a test request
        RestRequest request = new RestRequest();
        request.requestUri =
            'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
            + recordId;
        request.httpMethod = 'GET';
        RestContext.request = request;
        // Call the method to test
        Case thisCase = CaseManager.getCaseById();
        // Verify results
        System.assert(thisCase != null);
        System.assertEquals('Test record', thisCase.Subject);
    }
    @isTest static void testCreateCase() {
        // Call the method to test
        ID thisCaseId = CaseManager.createCase(
            'Ferocious chipmunk', 'New', 'Phone', 'Low');
        // Verify results
        System.assert(thisCaseId != null);
        Case thisCase = [SELECT Id,Subject FROM Case WHERE Id=:thisCaseId];
        System.assert(thisCase != null);
        System.assertEquals(thisCase.Subject, 'Ferocious chipmunk');
    }
    @isTest static void testDeleteCase() {
        Id recordId = createTestRecord();
        // Set up a test request
        RestRequest request = new RestRequest();
        request.requestUri =
            'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
            + recordId;
        request.httpMethod = 'DELETE';
        RestContext.request = request;
        // Call the method to test
        CaseManager.deleteCase();
        // Verify record is deleted
        List<Case> cases = [SELECT Id FROM Case WHERE Id=:recordId];
        System.assert(cases.size() == 0);
    }
    @isTest static void testUpsertCase() {
        // 1. Insert new record
        ID case1Id = CaseManager.upsertCase(
                'Ferocious chipmunk', 'New', 'Phone', 'Low', null);
        // Verify new record was created
        System.assert(Case1Id != null);
        Case case1 = [SELECT Id,Subject FROM Case WHERE Id=:case1Id];
        System.assert(case1 != null);
        System.assertEquals(case1.Subject, 'Ferocious chipmunk');
        // 2. Update status of existing record to Working
        ID case2Id = CaseManager.upsertCase(
                'Ferocious chipmunk', 'Working', 'Phone', 'Low', case1Id);
        // Verify record was updated
        System.assertEquals(case1Id, case2Id);
        Case case2 = [SELECT Id,Status FROM Case WHERE Id=:case2Id];
        System.assert(case2 != null);
        System.assertEquals(case2.Status, 'Working');
    }    
    @isTest static void testUpdateCaseFields() {
        Id recordId = createTestRecord();
        RestRequest request = new RestRequest();
        request.requestUri =
            'https://yourInstance.my.salesforce.com/services/apexrest/Cases/'
            + recordId;
        request.httpMethod = 'PATCH';
        request.addHeader('Content-Type', 'application/json');
        request.requestBody = Blob.valueOf('{"status": "Working"}');
        RestContext.request = request;
        // Update status of existing record to Working
        ID thisCaseId = CaseManager.updateCaseFields();
        // Verify record was updated
        System.assert(thisCaseId != null);
        Case thisCase = [SELECT Id,Status FROM Case WHERE Id=:thisCaseId];
        System.assert(thisCase != null);
        System.assertEquals(thisCase.Status, 'Working');
    }  
    // Helper method
    static Id createTestRecord() {
        // Create test record
        Case caseTest = new Case(
            Subject='Test record',
            Status='New',
            Origin='Phone',
            Priority='Medium');
        insert caseTest;
        return caseTest.Id;
    }          
}
apex class通じ、APIをアクセス
  • salesforceのApex class通じ、APIをアクセス

  • 先ず「指定ログイン情報」を作成、
    スクリーンショット 2022-07-26 22.42.19.png

  • 次Apex classの中request作成、send()メソッドでresponseを貰う、

    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setTimeout(120000);
    
    //setEndpoint()で「指定ログイン情報」/「アクセスパス」の形で設置
    //以下の例では、「RMS_APIGW」と言う指定ログイン情報のapiをアクセス、apiの具体のパスは:「/devices/cameras/standard_img/approval」
    request.setEndpoint(
      'callout:RMS_APIGW/api/devices/cameras/standard_img/approval'
    );
    
    request.setMethod('POST');
    request.setHeader('Authorization', IdToken);
    //受信可能なレスポンスデータのメディアタイプを指定
    request.setHeader('Accept', 'application/json');
      
    response = http.send(request);
    System.debug('★' + 'response : ' + response);
    
    return response;
    
  • apiの設定により、requestにbodyが必要と必要無い場合がある、

    • 必要の場合(大体json形式)
      String json =
        '{' +
          '"項目名1" : "' + 値1 + '",' +
          '"項目名2" : "' + 値2 + '",' +
          '"項目名3" : "' + 値3 + '",' +
          '"サブjson" : {' +
            '"サブ項目名1" : "' + サブ値1 + '",' +
            '"サブ項目名2" : "' + サブ値2 + '"' +
         '}' +
       '}';
       request.setBody(json);
      
    • 必要無い場合(大体pathで情報含む)
      例えば:apiの具体のパスは:「/devices/cameras/[項目]/standard_img/approval」の場合、request.setEndpoint()には以下のように設定:
      request.setEndpoint(
        'callout:RMS_APIGW/api/devices/cameras/' + 
        [項目] + 
        '/standard_img/approval'
      );
      

参考:コールアウトエンドポイントとしての指定ログイン情報

salesforceコンポーネントとmetadataの対照
  • salesforce資材名
    各salesorce資材やカスタマイズ設置が「変更セット」で表示された名前。下のコンポーネント一覧をコピー出来る、そのままexclにペースト、整理できる。
    スクリーンショット 2023-06-29 13.43.49.png
  • metaData保存パス:
    ローカル環境salesforce project中、資材の保存パス。force-app/main/default/の下(vscodeの例)
    スクリーンショット 2023-06-28 15.15.10.png
  • metaData取得パス:
    vscode「org Browser」上salesfroce metadataの保存先(vscodeの例)
    スクリーンショット 2023-06-28 15.16.28.png
  • package.xml 項目名:
    metadataをdeployや取得用のpackage.xml中で、<name>中に入れる資材名 package.xml参考ファイル
    スクリーンショット 2023-06-28 15.17.21.png
salesforce資材名 metaData保存パス metaData取得パス package.xml 項目名 追加説明
サイト custom sites CustomSite
リモートサイト remote site settings RemoteSiteSetting
Aura コンポーネントバンドル aura aura components AuraDefinitionBundle
visualforce page pages visualforce pages ApexPage
Apex クラス classes apex classes ApexClass
Apex トリガ triggers apex triggers ApexTrigger
カスタムオブジェクト objects custom objects CustomObject
カスタム項目 objects/オブジェクト名/fields objects/オブジェクト名/fields CustomField custom objectと同時にdownload
標準項目選択リストカスタマイズ後の値 standard value sets StandardValueSet 参考記事
ページレイアウト objects/オブジェクト名/layouts layouts Layout record画面のオブジェクトを編集で開く
lightingページ flexipages flexipages FlexiPage record画面の編集ページで開く & オブジェクト詳細「Lightning レコードページ」画面
リストビュー objects/オブジェクト名/listViews custom objects ListView custom objectと同時にdownload
ボタン、リンク、およびアクション quickActions QuickAction record画面のオブジェクトを編集で開く
レコードタイプ objects/オブジェクト名/recordTypes custom objects RecordType
入力規則 objects/オブジェクト名/validationRules custom objects ValidationRule
カスタムメタデータ型 custom objects CustomObject 参考記事
カスタムメタデータ型の項目 fields custom objects CustomField custom objectと同時にdownload
カスタムメタデータ型のレコード custom metadatas CustomMetadata 参考記事
標準項目タブと表示ラベルの名称変更 objectTranslations Custom Object Translation CustomObjectTranslation metadataをdeployの時オブジェクトを分けてdeployする 参考記事
タブ tabs custom tabs CustomTab
ケースの割り当てルール assignmentRules Assignment Rules AssignmentRules metadata通じての直接削除でき無い、環境上手動で必要
フロー flows flows Flow flowsはフロー本体を保存用、flowDefinitionsはバージョン記録用
カスタム表示ラベル labels custom labels CustomLabel(CustomLabelsは全部まとめ用)
アセットファイル contentassets content assets ContentAsset
アプリケーション applications custom applications CustomApplication
メールテンプレート email email templates EmailTemplate
ワークフローメールアラート workflows workflows Workflow
レポート作成スナップショット analyticSnapshots Analytic Snapshot AnalyticSnapshot ”*”で全て選ぶは有効しない

上記資材がsalesforce画面上のイメージ:

  • オブジェクト、「設定」の「オブジェクトマネージャ」画面
    スクリーンショット 2023-06-28 15.19.46.png
  • カスタム項目、オブジェクト詳細画面に入った後、「項目とリレーション」をクリック
    スクリーンショット 2023-06-28 15.22.08.png
  • アクション、オブジェクト詳細画面に入った後、「ボタン、リンク、およびアクション」をクリック
    スクリーンショット 2023-06-28 15.24.33.png
  • ページレイアウト、オブジェクト詳細画面に入った後、「ページレイアウト」をクリック
    スクリーンショット 2023-06-28 15.26.31.png
  • 入力規則、オブジェクト詳細画面に入った後、「入力規則」をクリック
    スクリーンショット 2023-06-28 15.27.56.png
  • リストビュー、オブジェクト詳細画面に入った後、「リストビューボタンレイアウト」をクリック
    スクリーンショット 2023-06-28 15.28.38.png
  • Apex クラス & Apex トリガ、「設定」の「Apex クラス」と「Apex トリガ」画面
    スクリーンショット 2023-06-28 15.30.47.png
  • フロー、「設定」の「フロー」画面
    スクリーンショット 2023-06-28 15.33.46.png
  • lightingページ、オブジェクト詳細画面に入った後、「Lightning レコードページ」をクリック
    スクリーンショット 2023-06-28 15.37.55.png
  • 標準項目タブと表示ラベルの名称変更、「設定」の「タブと表示ラベルの名称変更」画面、その内「標準タブ」は標準オブジェクトの表示名と項目の表示名をカスタマイズ
    スクリーンショット 2023-06-28 17.05.37.png
  • アセットファイル、「設定」の「アセットファイル」画面
    スクリーンショット 2023-06-29 14.35.03.png
  • アプリケーション、「設定」の「アプリケーションマネージャ」画面
    スクリーンショット 2023-06-29 14.35.39.png
  • タブ、「設定」の「タブ」画面
    スクリーンショット 2023-06-29 14.36.25.png
  • カスタム表示ラベル、「設定」の「カスタム表示ラベル」画面
    スクリーンショット 2023-06-29 14.37.02.png
  • レポートタイプ、「設定」の「レポートタイプ」画面
    スクリーンショット 2023-06-29 14.38.05.png
  • メールテンプレート、「設定」の「Classic メールテンプレート」画面
    スクリーンショット 2023-06-29 14.38.42.png
  • Aura コンポーネントバンドル、「設定」の「Lightning コンポーネント」画面
    スクリーンショット 2023-06-29 14.40.29.png
  • ワークフローメールアラート、「設定」の「メールアラート」画面
    スクリーンショット 2023-06-29 14.43.04.png
  • レポート作成スナップショット、「設定」の「レポート作成スナップショット」画面
    スクリーンショット 2023-06-29 14.43.28.png
metadataでプロファイルをdeploy時の注意点

追加説明:
spring26(約2025年)プロファイル中のアクセス権限まわりが権限セットに移行され、プロファイルはレコードタイプやログインIPなどの権限を持つ 参考URL

結論から言う:

  1. salesforceはmetadataの形でA環境のプロファイルをB環境に移すのは推奨し無い
    • metadataでも、変更セットでも、プロファイルの移行が効率よくないので;
    • 効率が良い方法は:まず本番環境で空っぽのプロファイルを作成、そして本番からsandboxを作る。登録時間帯やIPのみこの空っぽプロファイルからコピーを取得し、コントロールする。オブジェクトやシステム権限などは権限セットでコントロール。
  2. salesforce環境(webページ)でプロファイル設置する時、項目レベルセキュリティーは画面遷移ある為、時間がかかる上、抜け漏れしやすい。でも他の内容は画面遷移がないため、設定の手間が少ない。加えて、プロファイルのmetadataで取得出来る内容は少ない、オフィシャル説明にもハッキリ書いていない。
    以上の原因で、metadataでプロファイルをdeployする場合、「項目レベルセキュリティー」と「オブジェクトアクセス」と「タグ」と「アプリ」の設定だけmetadataで取得し、目標環境にdeployして、他の部分は手動で追加。
    • sfdxでは、”プロファイル”と言うフォルダが無いので、package.xmlでプロファイルのmetadataを取得する、package.xml例↓ 標準オブジェクト含むversion
    //"*"でカスタム・外部オブジェクトの設定を取得出来るが、標準オブジェクトの設定を取得ため、
    //手動追加が必要”Account”の様に、全ての標準オブジェクトを含むpackage.xmlファイルは上の”標準オブジェクト含むversion”を参考
    <?xml version="1.0" encoding="UTF-8"?>
    <Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
         <members>Account</members>
         <members>*</members>
         <name>CustomObject</name>
    </types>
    <types> 
         <members>*</members>
         <name>CustomApplication</name>
    </types>
    <types> 
         <members>*</members>
         <name>CustomTab</name>
    </types>
    <types>
         <members>*</members>
         <name>Profile</name>
    </types>
    <version>53.0</version>
    </Package>
    
  3. 標準オブジェクトのmetadata取得の時、もし標準オブジェクトは何の変更がなあい場合は、そのmetadataは取得し無い。
  4. もし既に目標環境の中には”ABCプロファイル”が存在した場合、”ABCプロファイル”のmetadataをdeployすると、metadataの中にある項目は目標環境”ABCプロファイル”の項目を上書きする;metadataの中にない項目なら、上書きし無い
  5. 補充説明
metadataで権限セットをdeploy時の注意点

以下図のように、「dreamhouse」と言う権限セットのmetadata取得出来るかどうかを検証。
スクリーンショット 2023-03-21 15.19.48.png
結果として:

  • 取得出来る
    • 割り当てられたアプリケーション
    • オブジェクト設定
      • 標準オブジェクト
        • タブの設定
        • オブジェクト権限
        • 項目権限
    • アプリケーション権限
    • Apex クラスアクセス
    • Visualforce ページのアクセス
    • フローアクセス
    • システム権限
  • 取得できない
    • 割り当てられた接続アプリケーション
    • 指定ログイン情報アクセス
  • 未確認
    • (未確認)外部データソースアクセス
    • (未確認)カスタム権限
    • (未確認)カスタムメタデータ型
    • (未確認)カスタム設定の定義
    • (未確認)サービスプロバイダ
metadataで「フロー」資材をdeploy / 削除
  • (deploy)取得先該当フローが無効化状態

    • 最新versionのフロー資材が取得、フローAPI名.flow-meta.xml ファイルにObsoleteステータスが表示されます
    • 資材追加用package.xml構成例:API名が「test03」のフロー資材を追加
      フローdeploy用package.xml例
      <?xml version="1.0" encoding="UTF-8"?>
      <Package xmlns="http://soap.sforce.com/2006/04/metadata">
      <types>
        <members>test03</members>
        <name>flow</name>
      </types>
      <version>53.0</version>
      </Package>
      
    • deploy結果:
      • deploy先環境で該当フロー存在しない場合
        • 該当フローが作成され、version番号1に設定、フローが無効化されます。
      • deploy先環境で該当フロー存在する場合
        • 該当フロー有効化されたversionが最新versionの場合、versionが追い番され、deploy資材になります
        • 該当フロー有効化されたversionが最新versionでは無い場合、該当フロー最新のversionがdeploy資材に上書きされます
  • (deploy)取得先該当フローが有効化状態

    • 有効化されたversionの資材が取得、フローAPI名.flow-meta.xml ファイルにActiveステータスが表示されます
    • deploy結果:
      • deploy先環境で該当フロー存在しない場合:該当フローが作成され、version番号1に設定、フローが有効化されます。
      • deploy先環境で該当フロー存在する場合:該当フローversionが追い番され、且つ最新versionが有効化されます。
  • salesforce組織上「フロー」資材を削除

    • フロー資材を削除の時、一つのフロー丸ごと削除はできません、フローのversion番号を指定し、version別々削除する必要があります。フローの全versionが削除後、フロー自体も消します
      例:API名「test03」のフローには三つのversionが存在します、該当フローを削除するため、三つのversion全部削除必要があります
      フロー削除用package.xml例
      <?xml version="1.0" encoding="UTF-8"?>
      <Package xmlns="http://soap.sforce.com/2006/04/metadata">
      <types>
        <members>test03-1</members>
        <members>test03-2</members>
        <members>test03-3</members>
        <name>flow</name>
      </types>
      </Package>
      

    image.png

  • 追加説明

    • salesforce「フロー」をmetadataで組織へdeployするには、以下の二つパーツと絡む
      • 「flow」一つのフローの中身、version情報とか付いていない、
      • 「flowDefinitions」フローのversion情報とかをつ
    • vscode、githubでmetadata管理、deploy(所謂CI/CD)を行う場合
      • 「flowDefinitions」フォルダをgitignoreに入れ、触らない方が便利
      • フローの削除や特定versionの削除操作は、SF環境でやった方がいい;もしsfdx使って、ローカル環境でpackage.xmlの方法でSF環境上のフローの特定versionの削除操作を行う場合;sfdxGUIのフローは図の様に、綺麗に名前で分けるではなく、「flow name-versionXXX」の羅列表示になってしまう、かなり醜くなる、余り進めない。そうなった場合、も一回sfdx環境を構築したら図の様な表示に戻る。
        スクリーンショット 2022-10-15 12.26.09.png
      • 「flowDefinitions」作用の検証
        検証用:テストtask5796検証用フロー(testTask5796)作成
        flowDifinitionをdeploy資材に入れる
        version1を有効化、他version存在なし
        flowDifinitionの<activeVersionNumber>を除去し
        flowとflowDifinition両方をdeploy
        結果:追い番なし、version1のまま、無効化に成る
        version1を無効化、他version存在なし
        flowDifinitionの<activeVersionNumber>を除去し
        flowとflowDifinition両方をdeploy
        結果:追い番あり、version2追い番、無効化に成る
        version1を有効化、他version2存在あり
        flowDifinitionの<activeVersionNumber>を除去し
        flowとflowDifinition両方をdeploy
        結果:追い番あり、version3追い番、無効化に成る
        version3を有効化、他version1・2存在あり
        flowDifinitionの<activeVersionNumber>を除去し
        flowとflowDifinition両方をdeploy
        結果:追い番あり、version4追い番、無効化に成る
        version全部無効化、flowファイルversion4と同じ中身、他version存在あり
        flowDifinitionの<activeVersionNumber>を除去し
        flowとflowDifinition両方をdeploy
        結果:追い番あり、version5追い番、無効化に成る
        flowDifinitionをdeploy資材に入れない
        version全部無効化、flowファイルversion5と同じ中身、他version存在あり
        flowDifinitionファイルdeployしない
        結果:追い番あり、version6追い番、有効化に成る
        version6を有効化、他version存在あり
        flowDifinitionファイルdeployしない
        結果:追い番あり、version7追い番、有効化に成る 
        
外部オブジェクト項目削除

外部オブジェクト項目を削除の時、sfdxコマンドでdistrictivePackage.xmlをdeployによっての削除方法はできない、環境上手動作業が必要。
そして削除後、該当項目API名後ろに”_del”が追加される。一度削除された項目を復元しても、”_del”のサブフィックスは自動的に消せない、そこも手動修正が必要。

apex class管理

apex classのmetadata管理の時、force-app/main/default/classesの下に更にフォルダを作って、違う用途のapex classを分ける。この場合、apex classの名前はグローバルで一意である必要。参考URL
フォルダ分けると、こんな感じ:
image (1).png

sf環境との接続エラー

sf環境接続後、もし、「org Browser」をリフレッシュやmetadataダウンロードの時、「Unexpected end of JSON input」のエラーが発生し、接続できなくなったの場合。

  • .sfdxフォルダーを確認、中には空っぽのjsonファイルがあるばい、それを削除 参考記事
  • sfdx force:org:listコマンドで接続状況を確認、もし接続正常の場合。vscodeを何分放置し、接続が治るはず
    スクリーンショット 2023-05-23 16.43.47.png
gitブランチ間の差分でmetadataのpackageファイルを作成

以下のコマンドで修正資源のみがリストされる差分package.xmlファイルを生成(修正資源のファイルパスが空白を含む時は(2)で実施)

(1) sfdx force:source:manifest:create --sourcepath `git diff 変更前ブランチ名 変更後ブランチ名 --name-only  --diff-filter=MRA | awk '{printf ","$1;} '` --manifestname ./manifest/package.xml

(2) sfdx force:source:manifest:create -p "`git diff 変更前ブランチ名 変更後ブランチ名 --name-only --diff-filter=MRA | awk '{ o = NF > 1 ? "'\''" $0 "'\''" : $0; s = length(s) > 0 ? s "," o : s = o } END{ print s }'`" -n ./manifest/package.xml
deplpoy検証

manifest/package.xmlの内容は目標組織にdeploy出来るかどうかを検証、-cを取り除くと普通のdeployコマンド

sfdx force:source:deploy -c --manifest manifest/package.xml  --postdestructivechanges manifest/destructiveChangesPost.xml -u ユーザ名(省略可能)
// 今回デプロイするApexテスト + デプロイ先に存在するすべてのApexテストを実行する
// 共通クラスの修正をしているような場合はこれで他に影響が出ていないことを確認する
sfdx force:source:deploy -c --manifest manifest/package.xml  --postdestructivechanges manifest/destructiveChangesPost.xml -l RunLocalTests -u ユーザ名(省略可能)

// 指定したApexテストのみを実行する
sfdx force:source:deploy -c --manifest manifest/package.xml  --postdestructivechanges manifest/destructiveChangesPost.xml -l RunSpecifiedTests -r APEXテストクラス名01,APEXテストクラス名02 -u ユーザ名(省略可能)
vscode上、apex classテストコード走る

vscode上、apex classテストコード走る時、テストコードはリモート組織上で走っている、その為、先ず新しいテストコードを組織へdeployする必要がある

apexでレコード大量作成

chatGPTに実現したいことを詳しく説明すれば、コードを作ってくれる

「行動」「ToDo」作成と削除の例
public class CreateDeleteClass {
    public static void createEvents() { //「行動」(event)作成用
        DateTime sampleDateTime01 = DateTime.newInstance(2023, 5, 24, 10, 0, 0); // サンプルの日時を設定
		DateTime sampleDateTime02 = DateTime.newInstance(2023, 5, 25, 10, 0, 0); // サンプルの日時を設定
        Id ownerId = '0059D000004SHxdQAG'; // 参照所有者ユーザレコード
        Id whatId = '0069D00000SaodSQAR' ; // 参照商談レコード
        
        List<Event> records = new List<Event>(); // 399個行動レコード作成
        for (Integer i = 0; i < 400; i++) {
            Event obj = new Event(
                Subject = 'Sample Event ' + i,
                StartDateTime = sampleDateTime01,
                EndDateTime = sampleDateTime01.addHours(1),
                OwnerId = ownerId,
                WhatId = whatId                                
                );
            records.add(obj);
        }
 		insert records;
    }


    public static void deleteEvents() { //  「行動」(event) 削除用
        DateTime sampleDateTime01 = DateTime.newInstance(2023, 5, 24, 10, 0, 0); // 削除したい特定の日付を指定
		List<Event> eventsToDelete = [SELECT Id FROM Event WHERE StartDateTime >= :sampleDateTime01];
		delete eventsToDelete;
    }
     
    
    public static void createTasks() { //  「Todo」(task) 作成用
        List<Task> tasks = new List<Task>();
        Id ownerId = '0059D000004SHxdQAG';
        Id whatId = '0069D00000SaodSQAR' ;
        Date dateToSet  = Date.newInstance(2023, 5, 24);
        for (Integer i = 0; i < 400; i++) {
            Task task = new Task(
                Subject = 'Sample Task ' + i,
                ActivityDate = dateToSet,
                OwnerId = ownerId,
                WhatId = whatId
            );
            tasks.add(task);
        }
		insert tasks;
    }
        

    public static void deleteTasks() { //  「Todo」(task) 削除用
        Date dateToSet  = Date.newInstance(2023, 5, 24);// 削除したい特定の日付を指定
		List<Task> tasksToDelete = [SELECT Id FROM Task WHERE ActivityDate >= :dateToSet];
		delete tasksToDelete;
	}
}

apex class保存後、メソッドを呼ぶ場合、開発者コンソールを開き、「Debug」タブの「Open Execute Anonymous Window」を開き、class名.method名入れ、「execute」でメソッドを実行。
スクリーンショット 2023-05-22 18.00.30.png
スクリーンショット 2023-05-22 18.00.44.png

apex classテストクラス作成時、参照関係の書き方

例:
テスト用contact作成時、参照関係になるaccountも作成され;その参照関係を保つため、
contact.AccountId = account.Id

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?