gulpで作るSalesforce開発環境


Atom + MavensMateでSalesforce開発環境を作成する - Qiita
http://qiita.com/yodroid/items/8331aa31f406d65c6dc4

Salesforce1 - 開発ツールカタログ - Qiita
http://qiita.com/yebihara0802/items/548e33c56e233e0d4551

にて、Salesforce開発環境としてForce.com IDE や MavesMate が紹介されていますが、いくら公式のAPIがそうなっているからといって、いい加減に全てのクラスを1フォルダにまとめることを強制されるような最低限文化的未満の開発環境は滅ぶべきだと思うのでgulpデプロイ環境で自由にフォルダを分割する方法について書きます。


## gulpからデプロイ

普通にフォルダの内容を拾ってデプロイするコードは以下になります。

```js
const gulp = require('gulp');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

gulp.task('deploy', () => {
  gulp.src('src/**', { base: '.' })
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```

## Stringからファイルを追加

[gulp-file](https://www.npmjs.com/package/gulp-file) を使ってgulpfile内でpackage.xmlを追加します。
pakcage.xmlの中身はtemplate stringで定義します。

```js
const gulp = require('gulp');
const file = require('gulp-file');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

const packagexml = `<Package>
  <types>
    <name>ApexClass</name>
    <members>MyClass1</members>
    <members>MyClass2</members>
  </types>
  <version>35.0</version>
</Package>`;

gulp.task('deploy', () => {
  gulp.src('src/**', { base: '.' })
    .pipe(file('src/package.xml', packagexml))
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```

## package.xml を生成

[salesforce-metadata-xml-builder](https://www.npmjs.com/package/salesforce-metadata-xml-builder) を使ってpackage.xmlをオブジェクトから生成します。

```js
const gulp = require('gulp');
const file = require('gulp-file');
const metadata = require('salesforce-metadata-xml-builder');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

const packagexml = metadata.Package({
  types: [{ name: 'ApexClass', members: ['*'] }],
  version: '35.0'
});

gulp.task('deploy', () => {
  gulp.src('src/**', { base: '.' })
    .pipe(file('src/package.xml', packagexml))
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```

salesforce-metadata-xml-builder は私が作ったパッケージで、JavaScriptのオブジェクトを受け取りデプロイ可能なXMLにして返します。
この例では package.xml しか作っていませんが、WSDLから型情報を取得して生成したコードなので他のメタデータにも使えるはずです。

## 複数のフォルダで開発

[merge-stream](https://www.npmjs.com/package/merge-stream) を使って複数のストリームをマージしてからデプロイします。

```js
const gulp = require('gulp');
const file = require('gulp-file');
const merge = require('merge-stream');
const metadata = require('salesforce-metadata-xml-builder');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

const packagexml = metadata.Package({
  types: [{ name: 'ApexClass', members: ['*'] }],
  version: '35.0'
});

gulp.task('deploy', () => {
  const src1 = gulp.src('src1/**', { base: '.' });
  const src2 = gulp.src('src2/**', { base: '.' });
  merge(src1, src2)
    .pipe(file('src/package.xml', packagexml))
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```


## メタデータをプログラムする

JavaScriptなので一定の規則があるならばループして生成することもできます。
簡単なパターンですが、取引先と取引先責任者に適当に項目を20個ずつ追加してみます。

```js
const gulp = require('gulp');
const through = require('through2');
const file = require('gulp-file');
const merge = require('merge-stream');
const metadata = require('salesforce-metadata-xml-builder');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

function makeFields() {
  return Array.apply(null, new Array(20)).map((n, i) => ({
    fullName: `Field${i}__c`,
    label: `Field${i}`,
    type: 'Text',
    length: 255
  }));
}

gulp.task('deploy', () => {
  const account = { fullName: 'Account', fields: makeFields() };
  const contact = { fullName: 'Contact', fields: makeFields() };
  const objects = [account, contact].map((object) => {
    const objectxml = metadata.CustomObject(object);
    return through.obj()
      .pipe(file(`src/objects/${object.fullName}.object`, objectxml, { src: true }));
  })
  const packagexml = metadata.Package({
    types: [{ name: 'CustomObject', members: ['*'] }],
    version: '35.0'
  });
  merge(objects)
    .pipe(file('src/package.xml', packagexml))
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```

## Apexクラス生成

メタデータのマスターをJavaScriptに持ち、そこから各データを生成するようにすれば、
JavaScript側でメタデータに応じたApexクラスを生成することもできます。

例えばプロファイル毎にテストを実行する必要がある際、プロファイルメタデータ情報が既にJavaScript上で定義されていれば、そこからプロファイルごとにテストメソッドを生成することができるでしょう。

* カスタム項目
* その項目へのFLSごとにプロファイル
* 作成した項目に対し、プロファイルごとに保存を行うテストクラス

を同時に作成・デプロイします。

テストの中身は面倒臭いのでなしで。
ここに貼り付けるために1ファイルにまとめてますが実際やるなら適宜ファイルを分割すべきでしょう。

```js
const gulp = require('gulp');
const through = require('through2');
const file = require('gulp-file');
const merge = require('merge-stream');
const metadata = require('salesforce-metadata-xml-builder');
const zip = require('gulp-zip');
const deploy = require('gulp-jsforce-deploy');

const TARGET_OBJECT = 'Account';
const TARGET_FIELD = 'TestField__c';

gulp.task('deploy', () => {
  // object
  const account = { fields: [{
    fullName: TARGET_FIELD,
    label: TARGET_FIELD.replace('__c', ''),
    type: 'Text',
    length: 255
  }] };
  const accountxml = metadata.CustomObject(account);
  const accountStream = through.obj()
    .pipe(file(`src/objects/${TARGET_OBJECT}.object`, accountxml, { src: true }));
  // profile
  const profiles = [
    { name: 'Unaccessible', readable: false, editable: false },
    { name: 'Read'        , readable: true , editable: false },
    { name: 'Write'       , readable: true , editable: true  },
  ];
  const profileStreams = profiles.map((pattern) => {
    const field = `${TARGET_OBJECT}.${TARGET_FIELD}`;
    const readable = pattern.readable;
    const editable = pattern.editable;
    const profilexml = metadata.Profile({
      custom: true,
      fieldPermissions: [{field, readable, editable}]
    });
    return through.obj()
      .pipe(file(`src/profiles/${pattern.name}.profile`, profilexml, {src: true}));
  });
  // apex
  const apexmetaxml = metadata.ApexClass({
    apiVersion: '35.0',
    status: 'Active'
  });
  const makeTestMethod = (profile) => {
    return `
  @isTest
  static void saveAs${profile.name}() {
    System.runAs(newUser('${profile.name}')) {
      ${TARGET_OBJECT} record = new ${TARGET_OBJECT}(
        Name = 'TESTNAME',
        ${TARGET_FIELD} = 'TEST'
      );
      insert record;
    }
  }
    `;
  };
  const apexclass = `@isTest
class MyTestClass {
  static Id getProfileIdByName(String name) {
    Profile[] records = [SELECT Id FROM Profile WHERE Name = :name];
    return records[0].Id;
  }
  static User newUser(String profileName) {
    return new User(
      UserName = 'testuser@example.test',
      ProfileId = getProfileIdByName(profileName),
      LastName = 'Test User',
      Email = 'testuser@example.test',
      Alias = 'Test',
      TimeZoneSidKey = 'Asia/Tokyo',
      LocaleSidKey = 'ja',
      EmailEncodingKey = 'UTF-8',
      LanguageLocaleKey = 'ja'
    );
  }
  ${profiles.map(makeTestMethod).join('')}
}
`;
  const apexStream = through.obj()
    .pipe(file('src/classes/MyTestClass.cls', apexclass, { src: true }))
    .pipe(file('src/classes/MyTestClass.cls-meta.xml', apexmetaxml));
  // package.xml
  const packagexml = metadata.Package({
    types: [
      { name: 'CustomObject', members: ['*'] },
      { name: 'Profile'     , members: ['*'] },
      { name: 'ApexClass'   , members: ['*'] }
    ],
    version: '35.0'
  });
  merge(accountStream, profileStreams, apexStream)
    .pipe(file('src/package.xml', packagexml))
    .pipe(zip('pkg.zip'))
    .pipe(deploy({
      username: process.env.SF_USERNAME,
      password: process.env.SF_PASSWORD
    }));
});
```

## まとめ

すべてのクラスを1つのフォルダに定義することを強制されるのは真っ当な開発環境ではないですし、
Salesforce社は今に至るまでこの状況を放置し、最近はさらにコンポーネント指向の概念まで導入しています。

開発者各位は各自で強く生きましょう。