Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

Commit

Permalink
Merge pull request #147 from salesforcecli/sh/update-for-sdr
Browse files Browse the repository at this point in the history
feat: updates predeploy and postretrieve hooks for source plugin
  • Loading branch information
WillieRuemmele authored Mar 3, 2022
2 parents fb32a22 + 6b765ca commit d69afeb
Show file tree
Hide file tree
Showing 5 changed files with 1,509 additions and 2,648 deletions.
80 changes: 5 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

[![NPM](https://img.shields.io/npm/v/plugin-metadata-hook-demo.svg?label=plugin-metadata-hook-demo)](https://www.npmjs.com/package/plugin-metadata-hook-demo) [![CircleCI](https://circleci.com/gh/salesforcecli/plugin-metadata-hook-demo/tree/master.svg?style=shield)](https://circleci.com/gh/salesforcecli/plugin-metadata-hook-demo/tree/master) [![Downloads/week](https://img.shields.io/npm/dw/plugin-metadata-hook-demo.svg)](https://npmjs.org/package/plugin-metadata-hook-demo) [![License](https://img.shields.io/badge/License-BSD%203--Clause-brightgreen.svg)](https://raw.githubusercontent.com/salesforcecli/plugin-metadata-hook-demo/master/LICENSE.txt)

Demo using Salesforce CLI hooks to replace metadata values with an environment variable during a deploy.
Demo using Salesforce CLI hooks to replace metadata values with an environment variable before a deploy and after a retrieve.

See the file src/hooks/predeploy/metadataReplace.ts to view the hook code.
See [metadataReplaceDeploy.ts](./src/hooks/predeploy/metadataReplaceDeploy.ts) for the sample predeploy hook code.
See [metadataReplaceRetrieve.ts](./src/hooks/postretrieve/metadataReplaceRetrieve.ts) for the sample postretrieve hook code.

To use this demo: build and link the plugin and then push or pull custom object metadata files. Their description fields will be updated to maintain a different description between local and remote obejcts.
To use this demo: build and link the plugin and then deploy/push or retrieve/pull custom object metadata files. Their description fields will be updated to maintain a different description between local and remote obejcts.

## Getting Started

Expand Down Expand Up @@ -37,78 +38,7 @@ To verify

## About the Predeploy Hook TypeScript Code

The example for creating a `predeploy` Salesforce CLI hook shows how to replace the description of a metadata type with the value of an environment variable. The hook runs only when pushing files to an org with the `force:source:push` command. See the [metadataReplace.ts](src/hooks/predeploy/metadataReplaceDeploy.ts) TypeScript file for the code described in this section so you can follow along. The process to create a hook is similar to the [oclif](https://oclif.io/docs/hooks) process.

Import the `Hook` and `Command` classes.

```
import { Command, Hook } from '@oclif/config';
```

Then declare the types you use in your code.

```
type HookFunction = (this: Hook.Context, options: HookOptions) => any;
type HookOptions = {
Command: Command.Class,
argv: string[],
commandId: string,
result?: PreDeployResult
}
type PreDeployResult = {
[aggregateName: string]: {
mdapiFilePath: string;
workspaceElements: {
fullName: string;
metadataName: string;
sourcePath: string;
state: string;
deleteSupported: boolean;
}[];
};
};
```

The `HookOptions` type contains the values that are returned after the hook fires:

- `Command`: The class name of the command that ran, such as `PushCommand`.
- `argv`: String array of the arguments that were passed to the command, such as `-m ApexClass` or `-o`.
- `commandId`: The CLI command that ran, such as `force:source:push`.
- `result`: An object that contains information about what just happened.

The `PreDeployResult` type describes the result object for a `predeploy` hook. Each hook type [returns a different `result` type](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_customize.htm). For example, the `predeploy` hook fires after the CLI converts your source files to Metadata API format but before it sends the files to the org. It returns an array of the converted metadata types and the associated source format files.

Most of the property names of the various result types describe themselves, such as `PostOrgCreateResult.expirationDate` and `PreRetrieveResult.packageXmlPath`. But a quick word about the `aggregateName` and `workspaceElements` properties:

- `aggregateName` refers to a single representation in metadata format of, for example, a custom object.
- `workspaceElements` is an array of source format files for the same custom object, each file describing the associated fields, layouts, and so on.

Use these returned values in your code to implement your logic. For example, this code checks for the CLI command that fired the hook:

```
if (options.commandId === 'force:source:push') {
```

This code iterates through the `result` and executes the `updateObjectDesription` function on each element to update the description of both the object in the org and the local source file:

```
if (options.result) {
Object.keys(options.result).forEach(mdapiElementName => {
console.log('Updating the ' + mdapiElementName + ' object');
let mdapiElement = options.result![mdapiElementName]!;
// Update the object in the org (the metadata that is being deployed)
updateObjectDescription(mdapiElement.mdapiFilePath);
// Update the object locally
updateObjectDescription(mdapiElement.workspaceElements[0].sourcePath);
});
}
}
};
```
The example for creating a `predeploy` Salesforce CLI hook shows how to replace the description of a CustomObject with the value of an environment variable. The hook runs only when deploying files to an org with `force:source:deploy`, `force:source:push`, or `force:source:delete` commands. See the [metadataReplaceDeploy.ts](./src/hooks/predeploy/metadataReplaceDeploy.ts) TypeScript file for the code described in this section so you can follow along. The process to create a hook is similar to the [oclif](https://oclif.io/docs/hooks) process.

## Debugging your plugin

Expand Down
16 changes: 6 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
{
"name": "plugin-metadata-hook-demo",
"description": "Demo using Salesforce CLI hooks to replace metadata values during a deploy.",
"version": "48.8.0",
"version": "54.3.0",
"author": "Salesforce",
"bugs": "https://github.com/salesforcecli/plugin-metadata-hook-demo/issues",
"dependencies": {
"@oclif/config": "^1",
"@salesforce/command": "^3.1.0",
"@salesforce/core": "^2.23.1",
"@types/xml2js": "^0.4.8",
"lint-staged": "^11.0.0",
"salesforce-alm": "^51.6.26",
"@salesforce/command": "^4.2.2",
"@salesforce/core": "^2.35.3",
"@salesforce/source-deploy-retrieve": "^5.12.0",
"tslib": "^2",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@oclif/dev-cli": "^1",
"@oclif/plugin-command-snapshot": "^2.0.0",
"@salesforce/cli-plugins-testkit": "^1.1.5",
"@salesforce/dev-config": "2.1.2",
"@salesforce/plugin-command-reference": "^1.3.3",
"@types/chai": "^4",
"chai": "^4",
"@types/xml2js": "^0.4.9",
"eslint": "7.27.0",
"eslint-config-prettier": "8.3.0",
"husky": "^4.3.8",
Expand All @@ -31,7 +27,7 @@
"shx": "0.3.3",
"ts-node": "^9.1.1",
"tslint": "^6.1.3",
"typescript": "^3.8.3"
"typescript": "^4.3.2"
},
"engines": {
"node": ">=12.0.0"
Expand Down
93 changes: 14 additions & 79 deletions src/hooks/postretrieve/metadataReplaceRetrieve.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,31 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { Builder, parseString } from 'xml2js';
import { readFileSync, writeFileSync } from 'fs';
import { Builder, parseStringPromise } from 'xml2js';
import { Command, Hook } from '@oclif/config';
import { FileResponse } from '@salesforce/source-deploy-retrieve';

type HookFunction = (this: Hook.Context, options: HookOptions) => any;

type HookOptions = {
Command: Command.Class;
argv: string[];
commandId: string;
result?: PostRetrieveResult;
};

type PostRetrieveResult = {
[aggregateName: string]: {
mdapiFilePath: string;
};
result?: FileResponse[];
};

export const hook: HookFunction = async function (options) {
console.log('PostRetrieve Hook Running');

// Run only on the pull command, not the retrieve command
// if (options.commandId === 'force:source:pull') {
if (options.result) {
for (const mdapiElementName of Object.keys(options.result)) {
console.log('Updating the ' + mdapiElementName + ' object');
let mdapiElement = options.result![mdapiElementName]!;

// Update the object locally so that the pull does not overwrite the local description
await retainObjectDescription(
mdapiElementName,
mdapiElement.mdapiFilePath
);
for (const fileResponse of options.result) {
const { type, filePath } = fileResponse;
if (type === 'CustomObject' && filePath) {
console.log(`Updating the description for object: ${filePath}`);
const objFileContents = readFileSync(filePath, 'utf-8');
const objJson = await parseStringPromise(objFileContents);
objJson.CustomObject.description = 'PostRetrieve description';
const xml = new Builder().buildObject(objJson);
writeFileSync(filePath, xml);
}
}
}
};
// };

export default hook;

async function retainObjectDescription(objectName: string, objectPath: string) {
// Find the current local description
let localDescription: string | undefined;
if (!process.env.SFDX_ORG_PATH) {
console.log(
'Error: set the SFDX_ORG_PATH environment variable to allow local descriptions to be read'
);
return;
}
const localFilePath = path.join(
process.env.SFDX_ORG_PATH,
'force-app',
'main',
'default',
'objects',
objectName,
objectName + '.object-meta.xml'
);
const localXml = await fs.readFile(localFilePath, 'utf-8');

const localJson = await new Promise<any>((resolve, reject) =>
parseString(localXml, (err, result) => {
if (err) reject(err);
else resolve(result);
})
);

// Grab the current description
if (localJson.CustomObject && localJson.CustomObject.description) {
localDescription = localJson.CustomObject.description;
}

// Update the incoming Metadata's description
const incomingXml = await fs.readFile(objectPath, 'utf-8');
const incomingJson = await new Promise<any>((resolve, reject) =>
parseString(incomingXml, (err, result) => {
if (err) reject(err);
else resolve(result);
})
);

// Replace the description of the object being pulled with the value of the current description
if (
incomingJson.CustomObject &&
incomingJson.CustomObject.description &&
localDescription
) {
incomingJson.CustomObject.description = localDescription;
}

const xml = new Builder().buildObject(incomingJson);

await fs.writeFile(objectPath, xml, 'utf-8');
}
72 changes: 25 additions & 47 deletions src/hooks/predeploy/metadataReplaceDeploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command, Hook } from '@oclif/config';
import * as fs from 'fs';
import { Builder, parseString } from 'xml2js';
import { writeFileSync } from 'fs';
import { Builder } from 'xml2js';
import { SourceComponent } from '@salesforce/source-deploy-retrieve';

// tslint:disable-next-line:no-any
type HookFunction = (this: Hook.Context, options: HookOptions) => any;
Expand All @@ -9,57 +10,34 @@ type HookOptions = {
Command: Command.Class;
argv: string[];
commandId: string;
result?: PreDeployResult;
result?: SourceComponent[];
};

type PreDeployResult = {
[aggregateName: string]: {
mdapiFilePath: string;
workspaceElements: {
fullName: string;
metadataName: string;
sourcePath: string;
state: string;
deleteSupported: boolean;
}[];
};
};
// Overly simple type for this basic example
type CustomObjectXml = {
CustomObject: {
description: string;
}
}

export const hook: HookFunction = async (options) => {
console.log('PreDepoy Hook Running');

if (options.result) {
Object.keys(options.result).forEach((mdapiElementName) => {
console.log('Updating the ' + mdapiElementName + ' object');
const mdapiElement = options.result![mdapiElementName]!;

// Update the object in the org (the metadata that is being deployed)
updateObjectDescription(mdapiElement.mdapiFilePath);

// Update the object locally
updateObjectDescription(mdapiElement.workspaceElements[0].sourcePath);
});
}
};

function updateObjectDescription(objectPath: string) {
fs.readFile(objectPath, 'utf-8', (err, data) => {
if (err) throw err;

if (data) {
parseString(data, (error, json) => {
if (error) throw error;

// Replace the description of the object being pushed with the value of an environment variable
if (json.CustomObject) {
json.CustomObject.description =
process.env.SFDX_NEW_METADATA_VALUE || 'Default new description';
const srcComponents = options.result;
for (const srcComponent of srcComponents) {
if (srcComponent.type.name === 'CustomObject' && srcComponent.xml) {
const desc = process.env.SFDX_NEW_METADATA_VALUE || 'Default new description';
console.log(`Updating the description for object: ${srcComponent.name} to: ${desc}`);
const customObjectXml = srcComponent.parseXmlSync() as CustomObjectXml;
if (customObjectXml) {
customObjectXml.CustomObject.description = desc;

// Update the object file locally
const xml = new Builder({ attrkey: '@_xmlns' }).buildObject(customObjectXml);
writeFileSync(srcComponent.xml, xml);
}

const xml = new Builder().buildObject(json);

fs.writeFile(objectPath, xml, () => {});
});
}
}
});
}
}
};
Loading

0 comments on commit d69afeb

Please sign in to comment.