Skip to content

Commit

Permalink
Fixes 'pa app export' without packageDisplayName. Closes #6215
Browse files Browse the repository at this point in the history
  • Loading branch information
milanholemans committed Aug 6, 2024
1 parent bf59841 commit ebbe35a
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 37 deletions.
24 changes: 15 additions & 9 deletions docs/docs/cmd/pa/app/app-export.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@ m365 pa app export [options]

```md definition-list
`-n, --name <name>`
: The name (GUID) of the Power Apps app to export
: The name (GUID) of the Power Apps app to export.

`-e, --environmentName <environmentName>`
: The name of the environment for which to export the app
: The name of the environment for which to export the app.

`--packageDisplayName [packageDisplayName]`
: The display name to use in the exported package
: The display name to use in the exported package.

`-d, --packageDescription [packageDescription]`
: The description to use in the exported package
: The description to use in the exported package.

`-c, --packageCreatedBy [packageCreatedBy]`
: The name of the person to be used as the creator of the exported package
: The name of the person to be used as the creator of the exported package.

`-s, --packageSourceEnvironment [packageSourceEnvironment]`
: The name of the source environment from which the exported package was taken
: The name of the source environment from which the exported package was taken.

`-p, --path [path]`
: The path to save the exported package to. If not specified the app will be exported in the current working directory
: The path to save the exported package to. If not specified the app will be exported in the current working directory.
```

<Global />
Expand All @@ -42,13 +42,19 @@ m365 pa app export [options]
Export the specified Power App as a ZIP file

```sh
m365 pa app export --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --name 3989cb59-ce1a-4a5c-bb78-257c5c39381d --packageDisplayName "PowerApp"
m365 pa app export --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --name 3989cb59-ce1a-4a5c-bb78-257c5c39381d
```

Export the specified Power App as a ZIP file with a custom package name

```sh
m365 pa app export --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --name 3989cb59-ce1a-4a5c-bb78-257c5c39381d --packageDisplayName "Assets app"
```

Export the specified Power App as a ZIP file with the package displayname, package description, the one who created it, the package source environment and the path

```sh
m365 pa app export --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --name 3989cb59-ce1a-4a5c-bb78-257c5c39381d --packageDisplayName "PowerApp" --packageDescription "Power App Description" --packageCreatedBy "John Doe" --packageSourceEnvironment "Contoso" --path "C:/Users/John/Documents"
m365 pa app export --environmentName Default-d87a7535-dd31-4437-bfe1-95340acd55c5 --name 3989cb59-ce1a-4a5c-bb78-257c5c39381d --packageDisplayName "Assets app" --packageDescription "App to track assets of people" --packageCreatedBy "John Doe" --packageSourceEnvironment "Contoso" --path "C:/Users/John/Documents"
```

## Response
Expand Down
67 changes: 48 additions & 19 deletions src/m365/pa/commands/app/app-export.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ describe(commands.APP_EXPORT, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;
let loggerLogToStderrSpy: sinon.SinonSpy;

const actualFilename = 'Power App.zip';
const packageDisplayName = 'Power App';
const packageDescription = 'Power App Description';
const packageCreatedBy = 'John Doe';
Expand Down Expand Up @@ -142,7 +140,7 @@ describe(commands.APP_EXPORT, () => {
}
};

const fileBlobResponse = {
const fileBlobResponse: any = {
type: 'Buffer',
data: [80, 75, 3, 4, 20, 0, 0, 0, 8, 0, 237, 115, 99, 86, 250, 76, 155, 216, 248, 3, 0, 0, 7, 8, 0, 0, 71, 0, 0, 0, 77, 105, 99, 114, 111, 115, 111, 102, 116, 46, 80, 111, 119, 101, 114, 65, 112, 112, 115, 47, 97, 112, 112, 115, 47, 49, 56, 48, 50, 54, 54, 51, 51, 48]
};
Expand Down Expand Up @@ -171,7 +169,6 @@ describe(commands.APP_EXPORT, () => {
log.push(msg);
}
};
loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr');
});

afterEach(() => {
Expand All @@ -195,12 +192,10 @@ describe(commands.APP_EXPORT, () => {
assert.notStrictEqual(command.description, null);
});

it('exports the specified App', async () => {
let index = 0;
sinon.stub(request, 'get').callsFake(async (opts) => {
it('exports the specified app correctly', async () => {
const getStub = sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === exportPackageResponse.headers.location) {
if (index === 0) {
index = 1;
if (getStub.calledOnce) {
return locationRunningResponse;
}
else {
Expand All @@ -226,17 +221,16 @@ describe(commands.APP_EXPORT, () => {

throw 'invalid request';
});
sinon.stub(fs, 'writeFileSync').returns();
const writeFileStub = sinon.stub(fs, 'writeFileSync').returns();

await assert.doesNotReject(command.action(logger, { options: { name: appId, environmentName: environmentName, packageDisplayName: packageDisplayName } }));
await command.action(logger, { options: { name: appId, environmentName: environmentName } });
assert(writeFileStub.calledOnceWithExactly(`./${appId}.zip`, fileBlobResponse, 'binary'));
});

it('exports the specified App (debug)', async () => {
let index = 0;
sinon.stub(request, 'get').callsFake(async (opts) => {
it('exports the specified app correctly with packageDisplayName', async () => {
const getStub = sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === exportPackageResponse.headers.location) {
if (index === 0) {
index = 1;
if (getStub.calledOnce) {
return locationRunningResponse;
}
else {
Expand All @@ -262,10 +256,45 @@ describe(commands.APP_EXPORT, () => {

throw 'invalid request';
});
sinon.stub(fs, 'writeFileSync').returns();
const writeFileStub = sinon.stub(fs, 'writeFileSync').returns();

await command.action(logger, { options: { name: appId, environmentName: environmentName, packageDisplayName: packageDisplayName } });
assert(writeFileStub.calledOnceWithExactly(`./${packageDisplayName}.zip`, fileBlobResponse, 'binary'));
});

it('exports the specified App correctly with all options', async () => {
const getStub = sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === exportPackageResponse.headers.location) {
if (getStub.calledOnce) {
return locationRunningResponse;
}
else {
return locationSuccessResponse;
}
}

if (opts.url === locationSuccessResponse.properties.packageLink.value) {
return fileBlobResponse;
}

throw 'invalid request';
});

sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments/${environmentName}/listPackageResources?api-version=2016-11-01`) {
return listPackageResourcesResponse;
}

if (opts.url === `https://api.bap.microsoft.com/providers/Microsoft.BusinessAppPlatform/environments/${environmentName}/exportPackage?api-version=2016-11-01`) {
return exportPackageResponse;
}

throw 'invalid request';
});
const writeFileStub = sinon.stub(fs, 'writeFileSync').returns();

await command.action(logger, { options: { verbose: true, name: appId, environmentName: environmentName, packageDisplayName: packageDisplayName, packageDescription: packageDescription, packageCreatedBy: packageCreatedBy, packageSourceEnvironment: packageSourceEnvironment, path: path } });
assert(loggerLogToStderrSpy.calledWith(`File saved to path '${path}/${actualFilename}'`));
assert(writeFileStub.calledOnceWithExactly(`${path}/${packageDisplayName}.zip`, fileBlobResponse, 'binary'));
});

it('fails validation if the name is not a GUID', async () => {
Expand Down Expand Up @@ -294,7 +323,7 @@ describe(commands.APP_EXPORT, () => {

sinon.stub(request, 'post').rejects(error);

await assert.rejects(command.action(logger, { options: { name: appId, environmentName: environmentName, packageDisplayName: packageDisplayName } } as any),
await assert.rejects(command.action(logger, { options: { name: appId, environmentName: environmentName, packageDisplayName: packageDisplayName } }),
new CommandError(error.error.message));
});
});
27 changes: 18 additions & 9 deletions src/m365/pa/commands/app/app-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface CommandArgs {
interface Options extends GlobalOptions {
name: string;
environmentName: string;
packageDisplayName: string;
packageDisplayName?: string;
packageDescription?: string;
packageCreatedBy?: string;
packageSourceEnvironment?: string;
Expand All @@ -40,6 +40,7 @@ class PaAppExportCommand extends PowerPlatformCommand {
this.#initTelemetry();
this.#initOptions();
this.#initValidators();
this.#initTypes();
}

#initTelemetry(): void {
Expand Down Expand Up @@ -95,13 +96,21 @@ class PaAppExportCommand extends PowerPlatformCommand {
);
}

#initTypes(): void {
this.types.string.push('name', 'environmentName', 'packageDisplayName', 'packageDescription', 'packageCreatedBy', 'packageSourceEnvironment', 'path');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
const location = await this.exportPackage(args, logger);
const packageLink = await this.getPackageLink(args, logger, location);
//Replace all illegal characters from the file name
const illegalCharsRegEx = /[\\\/:*?"<>|]/g;
const filename = args.options.packageDisplayName.replace(illegalCharsRegEx, '_');
const packageLink = await this.getPackageLink(logger, location);

let filename = args.options.name;
if (args.options.packageDisplayName) {
//Replace all illegal characters from the file name
const illegalCharsRegEx = /[\\\/:*?"<>|]/g;
filename = args.options.packageDisplayName.replace(illegalCharsRegEx, '_');
}

const requestOptions: CliRequestOptions = {
url: packageLink,
Expand All @@ -113,7 +122,7 @@ class PaAppExportCommand extends PowerPlatformCommand {
}
};

const file = await request.get<string>(requestOptions);
const file = await request.get<any>(requestOptions);

let path = args.options.path || './';

Expand Down Expand Up @@ -179,20 +188,20 @@ class PaAppExportCommand extends PowerPlatformCommand {
details: {
creator: args.options.packageCreatedBy,
description: args.options.packageDescription,
displayName: args.options.packageDisplayName,
displayName: args.options.packageDisplayName || args.options.name,
sourceEnvironment: args.options.packageSourceEnvironment
},
resources: resources
},
fullResponse: true
};

const response: any = await request.post<any>(requestOptions);
const response = await request.post<any>(requestOptions);

return response.headers.location;
}

private async getPackageLink(args: CommandArgs, logger: Logger, location: string): Promise<string> {
private async getPackageLink(logger: Logger, location: string): Promise<string> {
if (this.verbose) {
await logger.logToStderr('Retrieving the package link and waiting on the exported package.');
}
Expand Down

0 comments on commit ebbe35a

Please sign in to comment.