dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Salesforce]カスタムオブジェクトのメタデータをCSVに変換する

動機

開発時、特定のカスタムオブジェクトの項目一覧をさっと確認したい。
基本的にメタデータを git 管理しているので、src/objects/MyObj__c.object のようなローカルの XML ファイルをパースして
人が読める形式に加工できれば十分。

Node 界隈のパッケージとか使えば簡単にできるんじゃないかなと思って試してみたらできたので、メモ。


作る

想定しているディレクトリ構成は以下。

├── src
│   ├── package.xml
│   └── objects
│       └── MyObj__c.object
├── build.xml
├── package.json
└── index.js

必要なパッケージをインストールする。

$ npm install -D fs-extra
$ npm install -D xml2js
$ npm install -D json2csv

コードは以下のようになった。

const fs = require('fs-extra');
const xml2js  = require('xml2js');
const json2csv = require('json2csv');
const csv2md = require('csv2md');

const parser = new xml2js.Parser();
const PATH_TO_TARGET = 'src/objects/MyObj__c.object';
const CSV_FIELDS = [
  'label',
  'fullName',
  'type',
  'required',
  // 'externalId',
  // 'caseSensitive',
  // 'length',
  // 'trackTrending',
  // 'unique',
];
const dataList = [];

fs.readFile(__dirname + '/' + PATH_TO_TARGET, function(err, data) {
  parser.parseString(data, function (err, result) {
    // console.dir(result);

    for (const field of result.CustomObject.fields) {
      const data = {};
      // console.log(field);
      for (const csvField of CSV_FIELDS) {
        data[csvField] = field[csvField] ? field[csvField][0] : null;
      }
      // console.log(data);
      dataList.push(data);
    }
    // console.log('Done');
  });

  const csv = json2csv({ data: dataList, fields: CSV_FIELDS });
  console.log(csv);
});

CSV_FIELDS で定義しているプロパティは以下を参照。
https://developer.salesforce.com/docs/atlas.ja-jp.204.0.api_meta.meta/api_meta/customobject.htm

項目によって出力されるものとそうでないものがあるので、とりあえず最低限必要なものだけにしてみた。

試してみる

こういうメタデータに対して

<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
    <fields>
        <fullName>Account__c</fullName>
        <deleteConstraint>SetNull</deleteConstraint>
        <externalId>false</externalId>
        <label>取引先</label>
        <referenceTo>Account</referenceTo>
        <relationshipLabel>Myobj</relationshipLabel>
        <relationshipName>Myobjs</relationshipName>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Lookup</type>
    </fields>
    <fields>
        <fullName>Checkbox__c</fullName>
        <externalId>false</externalId>
        <label>元チェックボックス</label>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Date</type>
    </fields>
    <fields>
        <fullName>Discount_Percentage__c</fullName>
        <externalId>false</externalId>
        <label>Discount_Percentage</label>
        <precision>3</precision>
        <required>false</required>
        <scale>0</scale>
        <trackTrending>false</trackTrending>
        <type>Number</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Discount_Range__c</fullName>
        <externalId>false</externalId>
        <formula>IF((Discount_Percentage__c &gt;=5 &amp;&amp; Discount_Percentage__c&lt;=10) , &apos;Level-1&apos;,
IF((Discount_Percentage__c &gt;=10 &amp;&amp; Discount_Percentage__c&lt;=20), &apos;Level-2&apos;,
IF((Discount_Percentage__c &gt;=20 &amp;&amp; Discount_Percentage__c&lt;=30), &apos;Level-3&apos;,
IF((Discount_Percentage__c &gt;=30 &amp;&amp; Discount_Percentage__c&lt;=40), &apos;Level-4&apos;,



&apos;No Disount&apos;))))</formula>
        <formulaTreatBlanksAs>BlankAsZero</formulaTreatBlanksAs>
        <label>Discount_Range</label>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Lead__c</fullName>
        <deleteConstraint>SetNull</deleteConstraint>
        <externalId>false</externalId>
        <label>リード</label>
        <referenceTo>Lead</referenceTo>
        <relationshipLabel>Myobj</relationshipLabel>
        <relationshipName>Myobjs</relationshipName>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Lookup</type>
    </fields>
    <fields>
        <fullName>PicklistValues1__c</fullName>
        <externalId>false</externalId>
        <label>PicklistValues</label>
        <picklist>
            <picklistValues>
                <fullName>aaaaa</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>bbbbb</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ccccc</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>PicklistValues__c</fullName>
        <externalId>false</externalId>
        <label>PicklistValues</label>
        <picklist>
            <picklistValues>
                <fullName>aaaaaaaaaaa</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>bbbbbbbbbbb</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>cccccccccc</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ddddddddddd</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>eeeeeeeeee</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>ffffffffffffffff</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>gggggggggg</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>hhhhhhhhh</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>iiiiiiiiiii</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>jjjjjjjjjjjj</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>kkkkkkkkkk</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>lllllllllll</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>mmmmmmmmmm</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>nnnnnnnnnnnnn</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>oooooooo</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>Status__c</fullName>
        <externalId>false</externalId>
        <label>ステータス</label>
        <picklist>
            <picklistValues>
                <fullName>対応中</fullName>
                <default>false</default>
            </picklistValues>
            <picklistValues>
                <fullName>完了</fullName>
                <default>false</default>
            </picklistValues>
            <sorted>false</sorted>
        </picklist>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Picklist</type>
    </fields>
    <fields>
        <fullName>Text2__c</fullName>
        <externalId>false</externalId>
        <label>テキスト入力欄2</label>
        <length>100</length>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <fields>
        <fullName>Text__c</fullName>
        <externalId>false</externalId>
        <label>テキスト入力欄</label>
        <length>255</length>
        <required>false</required>
        <trackTrending>false</trackTrending>
        <type>Text</type>
        <unique>false</unique>
    </fields>
    <label>Myobj</label>
    <!-- 略 -->
</CustomObject>

結果がこう。

$ node index.js
"label","fullName","type","required"
"取引先","Account__c","Lookup","false"
"元チェックボックス","Checkbox__c","Date","false"
"Discount_Percentage","Discount_Percentage__c","Number","false"
"Discount_Range","Discount_Range__c","Text","false"
"リード","Lead__c","Lookup","false"
"PicklistValues","PicklistValues1__c","Picklist","false"
"PicklistValues","PicklistValues__c","Picklist","false"
"ステータス","Status__c","Picklist","false"
"テキスト入力欄2","Text2__c","Text","false"
"テキスト入力欄","Text__c","Text","false"


TODO

とりあえずで作ったので、ブラッシュアップしないといけない点は多々。

  • Name などの標準項目がメタデータとして出力されてないので調べる
    • retrieve する時の指定のしかたが悪いかも
  • src/objects 以下の全オブジェクトに対して実行できるようにしたい
  • CLI 化したい
    • 出力するプロパティや対象のオブジェクトは引数で指定できるようにしたい
  • 選択リスト(Picklist)などは選択肢の値もほしい
  • Markdown 形式にも対応したい
    • csv2md というのが使えるかと思ったけど、CLI としてしか使えない?

Markdown については、csv2md でもとりあえず以下のようにして変換することはできる。

$ npm i -D csv2md
$ node index.js | ./node_modules/csv2md/bin/csv2md
| label | fullName | type | required |
|---|---|---|---|
| 取引先 | Account__c | Lookup | false |
| 元チェックボックス | Checkbox__c | Date | false |
| Discount_Percentage | Discount_Percentage__c | Number | false |
| Discount_Range | Discount_Range__c | Text | false |
| リード | Lead__c | Lookup | false |
| PicklistValues | PicklistValues1__c | Picklist | false |
| PicklistValues | PicklistValues__c | Picklist | false |
| ステータス | Status__c | Picklist | false |
| テキスト入力欄2 | Text2__c | Text | false |
| テキスト入力欄 | Text__c | Text | false |