スクラッチ組織定義ファイルのsettingsの正体は設定メタデータ

スクラッチ組織定義ファイルにある`settings`の意味がよく分かってなかった。
処理を追ってみると、`settings`は設定メタデータがそのまま指定できるものだった。

[設定 | メタデータ API 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.244.0.api_meta.meta/api_meta/meta_settings.htm)

# 例

例えばSalesforce DXプロジェクトを作成したときに作成されるスクラッチ組織定義ファイルの`settings`:

```json:config/project-scratch-def.json
  "settings": {
    "lightningExperienceSettings": {
      "enableS1DesktopEnabled": true
    },
    "mobileSettings": {
      "enableS1EncryptedStoragePref2": false
    }
  }
```

これは:

- `LightningExperienceSettings`メタデータの`enableS1DesktopEnabled`項目を有効化
    - [LightningExperienceSettings | メタデータ API 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.244.0.api_meta.meta/api_meta/meta_lightningexperiencesettings.htm)
    - > 組織で Lightning Experience が有効化されているか (true)、否か (false) を示します。有効化されると、この設定をユーザインターフェースや API によって無効にすることはできません。「組織の Lightning Experience の有効化」を参照してください。
- `MobileSettings`メタデータの`enableS1EncryptedStoragePref2`を無効化
    - [MobileSettings | メタデータ API 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.244.0.api_meta.meta/api_meta/meta_mobilesettings.htm)
    - > Salesforce モバイル Web がパフォーマンスを改善するために安全で永続的なブラウザキャッシュを使用するか (true)、否か (false) を示します。デフォルト値は true です。
API バージョン 47.0 以降で利用できます。

という設定になる。

例えばもし管理者による代理ログインを有効化したい場合は、`SecuritySettings`メタデータの`enableAdminLoginAsAnyUser`項目を有効化する。

```diff_json:config/project-scratch-def.json
  "settings": {
+   "securitySettings": {
+     "enableAdminLoginAsAnyUser": true
+   },
    "lightningExperienceSettings": {
      "enableS1DesktopEnabled": true
    },
    "mobileSettings": {
      "enableS1EncryptedStoragePref2": false
    }
  }
```

[SecuritySettings | メタデータ API 開発者ガイド | Salesforce Developers](https://developer.salesforce.com/docs/atlas.ja-jp.244.0.api_meta.meta/api_meta/meta_securitysettings.htm)

# 仕組み

`settings`がどのようにスクラッチ組織に反映されるか処理の流れを追ってみる。

先に書くと大まかな流れは次のようになっていた:

1. スクラッチ組織定義ファイルをもとに`ScratchOrgInfo`オブジェクトレコードを作成(=スクラッチ組織作成)
1. `settings`から設定メタデータのXMLファイルを生成
1. 設定メタデータのXMLファイルをスクラッチ組織にデプロイ

スクラッチ組織の作成は`sf org create scratch`コマンドで行うので、orgプラグインの実装を追う。

`sf org create scratch`コマンドの実体はこれ:

https://github.com/salesforcecli/plugin-org/blob/2.9.14/src/commands/org/create/scratch.ts

`run()`メソッドの中で`scratchOrgCreate`を呼んでる:

```ts:src/commands/org/create/scratch.ts
      　const { username, scratchOrgInfo, authFields, warnings } = await scratchOrgCreate(createCommandOptions);
```

`scratchOrgCreate`は`@salesforce/core`パッケージ(`sfdx-core`プロジェクト)にあるので、そっちを見ていく。

https://github.com/forcedotcom/sfdx-core/blob/4.3.4/src/org/scratchOrgCreate.ts

`scratchOrgCreate`を追っていくと次の処理がある:

```ts:src/org/scratchOrgCreate.ts
  const [scratchOrgInfoRequestResult, signupTargetLoginUrlConfig] = await Promise.all([
    // creates the scratch org info in the devhub
    requestScratchOrgCreation(hubOrg, scratchOrgInfo, settingsGenerator),
    getSignupTargetLoginUrl(),
  ]);
```

呼んでる`requestScratchOrgCreation`は`ScratchOrgInfo`オブジェクトのレコードを作成する関数:

https://github.com/forcedotcom/sfdx-core/blob/4.3.4/src/org/scratchOrgInfoApi.ts#L274

`ScratchOrgInfo`オブジェクトのレコードを作成するとスクラッチ組織が作成されるようになってる。
参考: [Salesforce DXでAPIからスクラッチ組織を作成する - Qiita](https://qiita.com/stomita/items/94caff0d48e44508f049)
スクラッチ組織ができたら後続の処理で設定をデプロイする:

```ts:src/org/scratchOrgCreate.ts
  const [authInfo] = await Promise.all([
    resolveUrl(scratchOrgAuthInfo),
    deploySettings(
      scratchOrg,
      settingsGenerator,
      apiversion ??
        configAggregator.getPropertyValue(OrgConfigProperties.ORG_API_VERSION) ??
        (await scratchOrg.retrieveMaxApiVersion()),
      // some of our "wait" time has already been used.  Calculate how much remains that we can spend on the deployment.
      Duration.milliseconds(wait.milliseconds - (Date.now() - startTimestamp))
    ),
  ]);
```

`deploySettings`はここ:

https://github.com/forcedotcom/sfdx-core/blob/4.3.4/src/org/scratchOrgInfoApi.ts#L395

```ts:src/org/scratchOrgInfoApi.ts
export const deploySettings = async (
  scratchOrg: Org,
  orgSettings: SettingsGenerator,
  apiVersion: string,
  timeout: Duration = Duration.minutes(10)
): Promise<void> => {
  const logger = await Logger.child('scratchOrgInfoApi-deploySettings');
  if (orgSettings.hasSettings()) {
    // deploy the settings to the newly created scratch org
    logger.debug(`deploying scratch org settings with apiVersion ${apiVersion}`);

    try {
      await orgSettings.createDeploy();
      await orgSettings.deploySettingsViaFolder(scratchOrg, apiVersion, timeout);
      // updating the revision num to zero during org:creation if source members are created during org:create.
      // This only happens for some specific scratch org definition file.
      await updateRevisionCounterToZero(scratchOrg);
      logger.trace('Settings deployed to org');
    } catch (error) {
      throw SfError.wrap(error as Error);
    }
  }
};
```

`SettingGenerator`の`createDeploy()`を呼んでる:

```ts:src/org/scratchOrgSettingsGenerator.ts
  public async createDeploy(): Promise<void> {
    const settingsDir = path.join(this.shapeDirName, 'settings');
    const objectsDir = path.join(this.shapeDirName, 'objects');
    await Promise.all([
      this.writeSettingsIfNeeded(settingsDir),
      this.writeObjectSettingsIfNeeded(objectsDir, this.allRecordTypes, this.allBusinessProcesses),
    ]);
  }
```

`writeSettingsIfNeeded()`はこちら:

```ts:src/org/scratchOrgSettingsGenerator.ts
  private async writeSettingsIfNeeded(settingsDir: string): Promise<void> {
    if (this.settingData) {
      await Promise.all(
        Object.entries(this.settingData).map(([item, value]) => {
          const typeName = upperFirst(item);
          const fname = typeName.replace('Settings', '');
          const fileContent = js2xmlparser.parse(typeName, value);
          return this.writer.addToStore(fileContent, path.join(settingsDir, fname + '.settings'));
        })
      );
    }
  }
```

`js2xmlparser.parse()`を呼んだりしてるのでXMLを作ってる様子。

`createDeploy()`のあと`deploySettingsViaFolder()`メソッドを呼んで設定をスクラッチ組織にデプロイしている。

```ts:src/org/scratchOrgSettingsGenerator.ts
  public async deploySettingsViaFolder(
    scratchOrg: Org,
    apiVersion: string,
    timeout: Duration = Duration.minutes(10)
  ): Promise<void> {
    ...
    const connection = scratchOrg.getConnection();
    logger.debug(`deploying to apiVersion: ${apiVersion}`);
    connection.setApiVersion(apiVersion);
    const { id } = await connection.deploy(this.writer.buffer, {});
    ...
```

# まとめ

`settings`は[スクラッチ組織定義ファイルのドキュメント](https://developer.salesforce.com/docs/atlas.ja-jp.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_def_file.htm)でも明確な説明がなくて、何を書けばいいかよくわからずググってそれらしい情報を探して試してを繰り返していた。
プラグインの実装を確認して正体が設定メタデータとはっきりしたので、これからはメタデータのドキュメントをもとに`settings`を記述できるようになった。