Skip to content

Commit

Permalink
Introduces zod validation. Closes pnp#5639
Browse files Browse the repository at this point in the history
  • Loading branch information
waldekmastykarz committed Jul 5, 2024
1 parent 10b9b79 commit 3c1893f
Show file tree
Hide file tree
Showing 12 changed files with 1,304 additions and 1,171 deletions.
1,461 changes: 611 additions & 850 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,16 @@
"typescript": "^5.5.3",
"update-notifier": "^7.0.0",
"uuid": "^9.0.1",
"yaml": "^2.4.5"
"yaml": "^2.4.5",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@actions/core": "^1.10.1",
"@microsoft/microsoft-graph-types": "^2.40.0",
"@types/adm-zip": "^0.5.5",
"@types/jmespath": "^0.15.2",
"@types/json-schema": "^7.0.15",
"@types/json-to-ast": "^2.1.4",
"@types/minimist": "^1.2.5",
"@types/mocha": "^10.0.7",
Expand All @@ -297,6 +300,7 @@
"@types/sinon": "^17.0.3",
"@types/update-notifier": "^6.0.8",
"@types/uuid": "^9.0.8",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.15.0",
"c8": "^9.1.0",
Expand Down
20 changes: 10 additions & 10 deletions src/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { AzureCloudInstance, DeviceCodeResponse } from '@azure/msal-common';
import type * as Msal from '@azure/msal-node';
import assert from 'assert';
import type clipboard from 'clipboardy';
import type NodeForge from 'node-forge';
import type { AuthServer } from './AuthServer.js';
import { CommandError } from './Command.js';
import { FileTokenStorage } from './auth/FileTokenStorage.js';
import { TokenStorage } from './auth/TokenStorage.js';
import { msalCachePlugin } from './auth/msalCachePlugin.js';
import { cli } from './cli/cli.js';
import { Logger } from './cli/Logger.js';
import { cli } from './cli/cli.js';
import config from './config.js';
import { ConnectionDetails } from './m365/commands/ConnectionDetails.js';
import request from './request.js';
import { settingsNames } from './settingsNames.js';
import { browserUtil } from './utils/browserUtil.js';
import * as accessTokenUtil from './utils/accessToken.js';
import { ConnectionDetails } from './m365/commands/ConnectionDetails.js';
import assert from 'assert';
import { browserUtil } from './utils/browserUtil.js';

interface Hash<TValue> {
[key: string]: TValue;
Expand All @@ -37,11 +37,11 @@ export interface InteractiveAuthorizationErrorResponse {
}

export enum CloudType {
Public,
USGov,
USGovHigh,
USGovDoD,
China
Public = 'Public',
USGov = 'USGov',
USGovHigh = 'USGovHigh',
USGovDoD = 'USGovDoD',
China = 'China'
}

export class Connection {
Expand Down Expand Up @@ -123,7 +123,7 @@ export class Auth {
private deviceCodeRequest?: Msal.DeviceCodeRequest;
private _connection: Connection;
private clientApplication: Msal.ClientApplication | undefined;
private static cloudEndpoints: any[] = [];
private static cloudEndpoints: { [key: string]: any } = {};

// A list of all connections, including the active one
private _allConnections: Connection[] | undefined;
Expand Down
77 changes: 74 additions & 3 deletions src/Command.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import assert from 'assert';
import chalk from 'chalk';
import sinon from 'sinon';
import { z } from 'zod';
import auth from './Auth.js';
import { cli } from './cli/cli.js';
import { Logger } from './cli/Logger.js';
import Command, {
CommandError
CommandError,
globalOptionsZod
} from './Command.js';
import { CommandOptionInfo } from './cli/CommandOptionInfo.js';
import { Logger } from './cli/Logger.js';
import { cli } from './cli/cli.js';
import { telemetry } from './telemetry.js';
import { accessToken } from './utils/accessToken.js';
import { pid } from './utils/pid.js';
Expand Down Expand Up @@ -126,6 +129,54 @@ class MockCommand4 extends Command {
}
}

class MockCommandWithSchema extends Command {
public get name(): string {
return 'mock-command';
}

public get description(): string {
return 'Mock command description';
}

public get schema(): z.ZodTypeAny | undefined {
return globalOptionsZod;
}

public optionsInfo: CommandOptionInfo[] = [
{
name: 'requiredOption',
required: true,
type: 'string'
},
{
name: 'optionalString',
required: false,
type: 'string'
},
{
name: 'optionalEnum',
required: false,
type: 'string',
autocomplete: ['a', 'b', 'c']
},
{
name: 'optionalNumber',
required: false,
type: 'number'
},
{
name: 'optionalBoolean',
required: false,
type: 'boolean'
}
];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async commandAction(logger: Logger, args: any): Promise<void> {
throw 'Exception';
}
}

describe('Command', () => {
let telemetryCommandName: any;
let logger: Logger;
Expand Down Expand Up @@ -309,6 +360,26 @@ describe('Command', () => {
assert.strictEqual(JSON.stringify(actual), expected);
});

it('correctly tracks properties based on schema', () => {
const command = new MockCommandWithSchema();
const args = {
options: {
requiredOption: 'abc',
optionalString: 'def',
optionalEnum: 'a',
optionalNumber: 1,
optionalBoolean: false
}
};
const telemetryProps = (command as any).getTelemetryProperties(args);
assert.deepStrictEqual(telemetryProps, {
optionalString: true,
optionalEnum: 'a',
optionalNumber: true,
optionalBoolean: false
});
});

it('adds unknown options to payload', () => {
const command = new MockCommand1();
const actual = {
Expand Down
58 changes: 56 additions & 2 deletions src/Command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os from 'os';
import { ZodTypeAny, z } from 'zod';
import auth from './Auth.js';
import GlobalOptions from './GlobalOptions.js';
import { CommandInfo } from './cli/CommandInfo.js';
Expand All @@ -12,6 +13,7 @@ import { accessToken } from './utils/accessToken.js';
import { md } from './utils/md.js';
import { GraphResponseError } from './utils/odata.js';
import { prompt } from './utils/prompt.js';
import { zod } from './utils/zod.js';

interface CommandOption {
option: string;
Expand Down Expand Up @@ -43,6 +45,14 @@ interface ODataError {
}
}

export const globalOptionsZod = z.object({
query: z.string().optional(),
output: zod.alias('o', z.enum(['csv', 'json', 'md', 'text', 'none']).optional()),
debug: z.boolean().default(false),
verbose: z.boolean().default(false)
});
export type GlobalOptionsZod = z.infer<typeof globalOptionsZod>;

export interface CommandArgs {
options: GlobalOptions;
}
Expand Down Expand Up @@ -74,6 +84,22 @@ export default abstract class Command {

public abstract get name(): string;
public abstract get description(): string;
public get schema(): ZodTypeAny | undefined {
return undefined;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getRefinedSchema(schema: ZodTypeAny): z.ZodEffects<any> | undefined {
return undefined;
}

public getSchemaToParse(): z.ZodTypeAny | undefined {
return this.getRefinedSchema(this.schema as z.ZodTypeAny) ?? this.schema;
}

// metadata for command's options
// used for building telemetry
public optionsInfo: CommandOptionInfo[] = [];

constructor() {
// These functions must be defined with # so that they're truly private
Expand Down Expand Up @@ -554,8 +580,36 @@ export default abstract class Command {
}

private getTelemetryProperties(args: any): any {
this.telemetry.forEach(t => t(args));
return this.telemetryProperties;
if (this.schema) {
const telemetryProperties: any = {};
this.optionsInfo.forEach(o => {
if (o.required) {
return;
}

if (typeof args.options[o.name] === 'undefined') {
return;
}

switch (o.type) {
case 'string':
telemetryProperties[o.name] = o.autocomplete ? args.options[o.name] : typeof args.options[o.name] !== 'undefined';
break;
case 'boolean':
telemetryProperties[o.name] = args.options[o.name];
break;
case 'number':
telemetryProperties[o.name] = typeof args.options[o.name] !== 'undefined';
break;
};
});

return telemetryProperties;
}
else {
this.telemetry.forEach(t => t(args));
return this.telemetryProperties;
}
}

public async getTextOutput(logStatement: any[]): Promise<string> {
Expand Down
5 changes: 4 additions & 1 deletion src/cli/CommandOptionInfo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export interface CommandOptionInfo {
autocomplete: string[] | undefined;
autocomplete?: string[];
long?: string;
name: string;
required: boolean;
short?: string;
// undefined as a temporary solution to avoid breaking changes
// eventually it should be required
type?: 'string' | 'boolean' | 'number';
}
Loading

0 comments on commit 3c1893f

Please sign in to comment.