Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,5 @@ docs/.env.production.local

docs/npm-debug.log*
docs/yarn-debug.log*
docs/yarn-error.log*
docs/yarn-error.log*
.impeccable/
65 changes: 42 additions & 23 deletions src/m365/pp/commands/aibuildermodel/aibuildermodel-get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import { pid } from '../../../../utils/pid.js';
import { powerPlatform } from '../../../../utils/powerPlatform.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './aibuildermodel-get.js';
import command, { options } from './aibuildermodel-get.js';
import { session } from '../../../../utils/session.js';
import { settingsNames } from '../../../../settingsNames.js';
import { accessToken } from '../../../../utils/accessToken.js';

describe(commands.AIBUILDERMODEL_GET, () => {
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
//#region Mocked Responses
const validEnvironment = '4be50206-9576-4237-8b17-38d8aadfaa36';
const validId = '3a081d91-5ea8-40a7-8ac9-abbaa3fcb893';
Expand Down Expand Up @@ -79,6 +80,7 @@ describe(commands.AIBUILDERMODEL_GET, () => {
sinon.stub(accessToken, 'assertAccessTokenType').returns();
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => {
if (settingName === 'prompt') {
return false;
Expand Down Expand Up @@ -126,24 +128,41 @@ describe(commands.AIBUILDERMODEL_GET, () => {
assert.notStrictEqual(command.description, null);
});

it('fails validation if id is not a valid guid.', async () => {
const actual = await command.validate({
options: {
environmentName: validEnvironment,
id: 'Invalid GUID'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if id is not a valid guid.', () => {
const actual = commandOptionsSchema.safeParse({
environmentName: validEnvironment,
id: 'Invalid GUID'
});
assert.strictEqual(actual.success, false);
});

it('passes validation if required options specified (id)', () => {
const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId });
assert.strictEqual(actual.success, true);
});

it('passes validation if required options specified (name)', () => {
const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, name: validName });
assert.strictEqual(actual.success, true);
});
Comment thread
waldekmastykarz marked this conversation as resolved.

it('fails validation with unknown options', () => {
const actual = commandOptionsSchema.safeParse({
environmentName: validEnvironment,
id: validId,
unknownOption: 'value'
});
assert.strictEqual(actual.success, false);
});

it('passes validation if required options specified (id)', async () => {
const actual = await command.validate({ options: { environmentName: validEnvironment, id: validId } }, commandInfo);
assert.strictEqual(actual, true);
it('fails validation if both id and name are specified', () => {
const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment, id: validId, name: validName });
assert.strictEqual(actual.success, false);
});

it('passes validation if required options specified (name)', async () => {
const actual = await command.validate({ options: { environmentName: validEnvironment, name: validName } }, commandInfo);
assert.strictEqual(actual, true);
it('fails validation if neither id nor name is specified', () => {
const actual = commandOptionsSchema.safeParse({ environmentName: validEnvironment });
assert.strictEqual(actual.success, false);
});

it('throws error when multiple AI builder models with same name were found', async () => {
Expand Down Expand Up @@ -174,10 +193,10 @@ describe(commands.AIBUILDERMODEL_GET, () => {
});

await assert.rejects(command.action(logger, {
options: {
options: commandOptionsSchema.parse({
environmentName: validEnvironment,
name: validName
}
})
}), new CommandError("Multiple AI builder models with name 'CLI 365 AI Builder Model' found. Found: 69703efe-4149-ed11-bba2-000d3adf7537, 3a081d91-5ea8-40a7-8ac9-abbaa3fcb893."));
});

Expand All @@ -202,7 +221,7 @@ describe(commands.AIBUILDERMODEL_GET, () => {

sinon.stub(cli, 'handleMultipleResultsFound').resolves(aiBuilderModelResponse.value[0]);

await command.action(logger, { options: { verbose: true, environment: validEnvironment, name: validName } });
await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment, name: validName }) });
assert(loggerLogSpy.calledWith(aiBuilderModelResponse.value[0]));
});

Expand All @@ -220,10 +239,10 @@ describe(commands.AIBUILDERMODEL_GET, () => {
});

await assert.rejects(command.action(logger, {
options: {
options: commandOptionsSchema.parse({
environmentName: validEnvironment,
name: validName
}
})
}), new CommandError(`The specified AI builder model '${validName}' does not exist.`));
});

Expand All @@ -240,7 +259,7 @@ describe(commands.AIBUILDERMODEL_GET, () => {
throw `Invalid request ${opts.url}`;
});

await command.action(logger, { options: { verbose: true, environmentName: validEnvironment, name: validName } });
await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment, name: validName }) });
assert(loggerLogSpy.calledWith(aiBuilderModelResponse.value[0]));
});

Expand All @@ -257,7 +276,7 @@ describe(commands.AIBUILDERMODEL_GET, () => {
throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, environmentName: validEnvironment, id: validId } });
await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment, id: validId }) });
assert(loggerLogSpy.calledWith(aiBuilderModelResponse.value[0]));
});

Expand All @@ -281,7 +300,7 @@ describe(commands.AIBUILDERMODEL_GET, () => {
}
});

await assert.rejects(command.action(logger, { options: { environmentName: validEnvironment, name: validName } } as any),
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: validEnvironment, name: validName }) }),
new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`));
});
});
81 changes: 24 additions & 57 deletions src/m365/pp/commands/aibuildermodel/aibuildermodel-get.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { z } from 'zod';
import { cli } from '../../../../cli/cli.js';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { powerPlatform } from '../../../../utils/powerPlatform.js';
import { validation } from '../../../../utils/validation.js';
import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js';
import commands from '../../commands.js';

export const options = z.strictObject({
...globalOptionsZod.shape,
environmentName: z.string().alias('e'),
id: z.string().refine(val => validation.isValidGuid(val), {
error: 'The value must be a valid GUID.'
}).optional().alias('i'),
name: z.string().optional().alias('n'),
asAdmin: z.boolean().optional()
});

declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

export interface Options extends GlobalOptions {
environmentName: string;
id?: string;
name?: string;
asAdmin?: boolean;
}

class PpAiBuilderModelGetCommand extends PowerPlatformCommand {
public get name(): string {
return commands.AIBUILDERMODEL_GET;
Expand All @@ -28,58 +34,19 @@ class PpAiBuilderModelGetCommand extends PowerPlatformCommand {
return 'Get an AI builder model in the specified Power Platform environment.';
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
this.#initOptionSets();
this.#initValidators();
public get schema(): z.ZodType {
return options;
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
id: typeof args.options.id !== 'undefined',
name: typeof args.options.name !== 'undefined',
asAdmin: !!args.options.asAdmin
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-e, --environmentName <environmentName>'
},
{
option: '-i, --id [id]'
},
{
option: '-n, --name [name]'
},
{
option: '--asAdmin'
}
);
}

#initOptionSets(): void {
this.optionSets.push(
{ options: ['id', 'name'] }
);
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (args.options.id && !validation.isValidGuid(args.options.id as string)) {
return `${args.options.id} is not a valid GUID`;
public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined {
return schema
.refine(opts => [opts.id, opts.name].filter(x => x !== undefined).length === 1, {
error: `Specify either 'id' or 'name', but not both.`,
params: {
customCode: 'optionSet',
options: ['id', 'name']
}

return true;
}
);
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
Expand Down
20 changes: 17 additions & 3 deletions src/m365/pp/commands/aibuildermodel/aibuildermodel-list.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
Expand All @@ -10,10 +12,12 @@ import { powerPlatform } from '../../../../utils/powerPlatform.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './aibuildermodel-list.js';
import command, { options } from './aibuildermodel-list.js';
import { accessToken } from '../../../../utils/accessToken.js';

describe(commands.AIBUILDERMODEL_LIST, () => {
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
//#region Mocked Responses
const envUrl = "https://contoso-dev.api.crm4.dynamics.com";
const validEnvironment = "4be50206-9576-4237-8b17-38d8aadfaa36";
Expand Down Expand Up @@ -73,6 +77,8 @@ describe(commands.AIBUILDERMODEL_LIST, () => {
sinon.stub(session, 'getId').returns('');
sinon.stub(accessToken, 'assertAccessTokenType').returns();
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
Expand Down Expand Up @@ -115,6 +121,14 @@ describe(commands.AIBUILDERMODEL_LIST, () => {
assert.deepStrictEqual(command.defaultProperties(), ['msdyn_name', 'msdyn_aimodelid', 'createdon', 'modifiedon']);
});

it('fails validation with unknown options', () => {
const actual = commandOptionsSchema.safeParse({
environmentName: validEnvironment,
unknownOption: 'value'
});
assert.strictEqual(actual.success, false);
});

it('retrieves AI Builder models', async () => {
sinon.stub(powerPlatform, 'getDynamicsInstanceApiUrl').callsFake(async () => envUrl);

Expand All @@ -130,7 +144,7 @@ describe(commands.AIBUILDERMODEL_LIST, () => {
throw 'Invalid request';
});

await command.action(logger, { options: { verbose: true, environmentName: validEnvironment } });
await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, environmentName: validEnvironment }) });
assert(loggerLogSpy.calledWith(modelsResponse.value));

});
Expand All @@ -155,7 +169,7 @@ describe(commands.AIBUILDERMODEL_LIST, () => {
}
});

await assert.rejects(command.action(logger, { options: { environmentName: validEnvironment } } as any),
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ environmentName: validEnvironment }) }),
new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`));
});
});
42 changes: 12 additions & 30 deletions src/m365/pp/commands/aibuildermodel/aibuildermodel-list.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { z } from 'zod';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { globalOptionsZod } from '../../../../Command.js';
import { odata } from '../../../../utils/odata.js';
import { powerPlatform } from '../../../../utils/powerPlatform.js';
import PowerPlatformCommand from '../../../base/PowerPlatformCommand.js';
import commands from '../../commands.js';

export const options = z.strictObject({
...globalOptionsZod.shape,
environmentName: z.string().alias('e'),
asAdmin: z.boolean().optional()
});

declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
environmentName: string;
asAdmin?: boolean;
}

class PpAiBuilderModelListCommand extends PowerPlatformCommand {
public get name(): string {
return commands.AIBUILDERMODEL_LIST;
Expand All @@ -27,30 +31,8 @@ class PpAiBuilderModelListCommand extends PowerPlatformCommand {
return ['msdyn_name', 'msdyn_aimodelid', 'createdon', 'modifiedon'];
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
asAdmin: !!args.options.asAdmin
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-e, --environmentName <environmentName>'
},
{
option: '--asAdmin'
}
);
public get schema(): z.ZodType {
return options;
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
Expand Down
Loading
Loading