Skip to content

Commit 13f57f0

Browse files
Migrates 'entra app' commands to Zod
1 parent 2cb88d5 commit 13f57f0

22 files changed

+1150
-1587
lines changed

src/m365/entra/commands/app/app-add.spec.ts

Lines changed: 181 additions & 176 deletions
Large diffs are not rendered by default.

src/m365/entra/commands/app/app-add.ts

Lines changed: 103 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,53 @@
11
import fs from 'fs';
22
import { v4 } from 'uuid';
3+
import { z } from 'zod';
34
import auth from '../../../../Auth.js';
4-
import GlobalOptions from '../../../../GlobalOptions.js';
55
import { Logger } from '../../../../cli/Logger.js';
6+
import { globalOptionsZod } from '../../../../Command.js';
67
import request, { CliRequestOptions } from '../../../../request.js';
78
import { accessToken } from '../../../../utils/accessToken.js';
89
import { AppCreationOptions, AppInfo, entraApp } from '../../../../utils/entraApp.js';
10+
import { optionsUtils } from '../../../../utils/optionsUtils.js';
11+
import { zod } from '../../../../utils/zod.js';
912
import GraphCommand from '../../../base/GraphCommand.js';
1013
import { M365RcJson } from '../../../base/M365RcJson.js';
1114
import commands from '../../commands.js';
12-
import { optionsUtils } from '../../../../utils/optionsUtils.js';
15+
16+
const entraApplicationPlatform = ['spa', 'web', 'publicClient'] as const;
17+
const entraAppScopeConsentBy = ['admins', 'adminsAndUsers'] as const;
18+
19+
const options = globalOptionsZod
20+
.extend({
21+
name: zod.alias('n', z.string().optional()),
22+
multitenant: z.boolean().optional(),
23+
redirectUris: zod.alias('r', z.string().optional()),
24+
platform: zod.alias('p', z.enum(entraApplicationPlatform).optional()),
25+
implicitFlow: z.boolean().optional(),
26+
withSecret: zod.alias('s', z.boolean().optional()),
27+
apisDelegated: z.string().optional(),
28+
apisApplication: z.string().optional(),
29+
uri: zod.alias('u', z.string().optional()),
30+
scopeName: z.string().optional(),
31+
scopeConsentBy: z.enum(entraAppScopeConsentBy).optional(),
32+
scopeAdminConsentDisplayName: z.string().optional(),
33+
scopeAdminConsentDescription: z.string().optional(),
34+
certificateFile: z.string().optional(),
35+
certificateBase64Encoded: z.string().optional(),
36+
certificateDisplayName: z.string().optional(),
37+
manifest: z.string().optional(),
38+
save: z.boolean().optional(),
39+
grantAdminConsent: z.boolean().optional(),
40+
allowPublicClientFlows: z.boolean().optional()
41+
})
42+
.and(z.any());
43+
44+
declare type Options = z.infer<typeof options> & AppCreationOptions;
1345

1446
interface CommandArgs {
1547
options: Options;
1648
}
1749

18-
interface Options extends GlobalOptions, AppCreationOptions {
19-
grantAdminConsent?: boolean;
20-
manifest?: string;
21-
save?: boolean;
22-
scopeAdminConsentDescription?: string;
23-
scopeAdminConsentDisplayName?: string;
24-
scopeConsentBy?: string;
25-
scopeName?: string;
26-
uri?: string;
27-
withSecret: boolean;
28-
}
29-
3050
class EntraAppAddCommand extends GraphCommand {
31-
private static entraApplicationPlatform: string[] = ['spa', 'web', 'publicClient'];
32-
private static entraAppScopeConsentBy: string[] = ['admins', 'adminsAndUsers'];
3351
private manifest: any;
3452
private appName: string = '';
3553

@@ -45,173 +63,91 @@ class EntraAppAddCommand extends GraphCommand {
4563
return true;
4664
}
4765

48-
constructor() {
49-
super();
50-
51-
this.#initTelemetry();
52-
this.#initOptions();
53-
this.#initValidators();
54-
this.#initOptionSets();
55-
}
56-
57-
#initTelemetry(): void {
58-
this.telemetry.push((args: CommandArgs) => {
59-
Object.assign(this.telemetryProperties, {
60-
apis: typeof args.options.apisDelegated !== 'undefined',
61-
implicitFlow: args.options.implicitFlow,
62-
multitenant: args.options.multitenant,
63-
platform: args.options.platform,
64-
redirectUris: typeof args.options.redirectUris !== 'undefined',
65-
scopeAdminConsentDescription: typeof args.options.scopeAdminConsentDescription !== 'undefined',
66-
scopeAdminConsentDisplayName: typeof args.options.scopeAdminConsentDisplayName !== 'undefined',
67-
scopeConsentBy: args.options.scopeConsentBy,
68-
scopeName: typeof args.options.scopeName !== 'undefined',
69-
uri: typeof args.options.uri !== 'undefined',
70-
withSecret: args.options.withSecret,
71-
certificateFile: typeof args.options.certificateFile !== 'undefined',
72-
certificateBase64Encoded: typeof args.options.certificateBase64Encoded !== 'undefined',
73-
certificateDisplayName: typeof args.options.certificateDisplayName !== 'undefined',
74-
grantAdminConsent: typeof args.options.grantAdminConsent !== 'undefined',
75-
allowPublicClientFlows: typeof args.options.allowPublicClientFlows !== 'undefined'
76-
});
77-
});
66+
public get schema(): z.ZodTypeAny | undefined {
67+
return options;
7868
}
7969

80-
#initOptions(): void {
81-
this.options.unshift(
82-
{
83-
option: '-n, --name [name]'
84-
},
85-
{
86-
option: '--multitenant'
87-
},
88-
{
89-
option: '-r, --redirectUris [redirectUris]'
90-
},
91-
{
92-
option: '-p, --platform [platform]',
93-
autocomplete: EntraAppAddCommand.entraApplicationPlatform
94-
},
95-
{
96-
option: '--implicitFlow'
97-
},
98-
{
99-
option: '-s, --withSecret'
100-
},
101-
{
102-
option: '--apisDelegated [apisDelegated]'
103-
},
104-
{
105-
option: '--apisApplication [apisApplication]'
106-
},
107-
{
108-
option: '-u, --uri [uri]'
109-
},
110-
{
111-
option: '--scopeName [scopeName]'
112-
},
113-
{
114-
option: '--scopeConsentBy [scopeConsentBy]',
115-
autocomplete: EntraAppAddCommand.entraAppScopeConsentBy
116-
},
117-
{
118-
option: '--scopeAdminConsentDisplayName [scopeAdminConsentDisplayName]'
119-
},
120-
{
121-
option: '--scopeAdminConsentDescription [scopeAdminConsentDescription]'
122-
},
123-
{
124-
option: '--certificateFile [certificateFile]'
125-
},
126-
{
127-
option: '--certificateBase64Encoded [certificateBase64Encoded]'
128-
},
129-
{
130-
option: '--certificateDisplayName [certificateDisplayName]'
131-
},
132-
{
133-
option: '--manifest [manifest]'
134-
},
135-
{
136-
option: '--save'
137-
},
138-
{
139-
option: '--grantAdminConsent'
140-
},
141-
{
142-
option: '--allowPublicClientFlows'
143-
}
144-
);
145-
}
146-
147-
#initValidators(): void {
148-
this.validators.push(
149-
async (args: CommandArgs) => {
150-
if (args.options.platform &&
151-
EntraAppAddCommand.entraApplicationPlatform.indexOf(args.options.platform) < 0) {
152-
return `${args.options.platform} is not a valid value for platform. Allowed values are ${EntraAppAddCommand.entraApplicationPlatform.join(', ')}`;
70+
public getRefinedSchema(schema: typeof options): z.ZodEffects<any> | undefined {
71+
return schema
72+
.refine(options => {
73+
if (options.redirectUris && !options.platform) {
74+
return false;
15375
}
154-
155-
if (args.options.redirectUris && !args.options.platform) {
156-
return `When you specify redirectUris you also need to specify platform`;
157-
}
158-
159-
if (args.options.platform && ['spa', 'web', 'publicClient'].indexOf(args.options.platform) > -1 && !args.options.redirectUris) {
160-
return `When you use platform spa, web or publicClient, you'll need to specify redirectUris`;
76+
return true;
77+
}, {
78+
message: 'When you specify redirectUris you also need to specify platform'
79+
})
80+
.refine(options => {
81+
if (options.platform && ['spa', 'web', 'publicClient'].includes(options.platform) && !options.redirectUris) {
82+
return false;
16183
}
162-
163-
if (args.options.certificateFile && args.options.certificateBase64Encoded) {
164-
return 'Specify either certificateFile or certificateBase64Encoded but not both';
84+
return true;
85+
}, {
86+
message: 'When you use platform spa, web or publicClient, you\'ll need to specify redirectUris'
87+
})
88+
.refine(options => {
89+
if (options.certificateFile && options.certificateBase64Encoded) {
90+
return false;
16591
}
166-
167-
if (args.options.certificateDisplayName && !args.options.certificateFile && !args.options.certificateBase64Encoded) {
168-
return 'When you specify certificateDisplayName you also need to specify certificateFile or certificateBase64Encoded';
92+
return true;
93+
}, {
94+
message: 'Specify either certificateFile or certificateBase64Encoded but not both'
95+
})
96+
.refine(options => {
97+
if (options.certificateDisplayName && !options.certificateFile && !options.certificateBase64Encoded) {
98+
return false;
16999
}
170-
171-
if (args.options.certificateFile && !fs.existsSync(args.options.certificateFile as string)) {
172-
return 'Certificate file not found';
100+
return true;
101+
}, {
102+
message: 'When you specify certificateDisplayName you also need to specify certificateFile or certificateBase64Encoded'
103+
})
104+
.refine(options => {
105+
if (options.certificateFile && !fs.existsSync(options.certificateFile)) {
106+
return false;
173107
}
174-
175-
if (args.options.scopeName) {
176-
if (!args.options.uri) {
177-
return `When you specify scopeName you also need to specify uri`;
108+
return true;
109+
}, {
110+
message: 'Certificate file not found'
111+
})
112+
.refine(options => {
113+
if (options.scopeName) {
114+
if (!options.uri) {
115+
return false;
178116
}
179-
180-
if (!args.options.scopeAdminConsentDescription) {
181-
return `When you specify scopeName you also need to specify scopeAdminConsentDescription`;
117+
if (!options.scopeAdminConsentDescription) {
118+
return false;
182119
}
183-
184-
if (!args.options.scopeAdminConsentDisplayName) {
185-
return `When you specify scopeName you also need to specify scopeAdminConsentDisplayName`;
120+
if (!options.scopeAdminConsentDisplayName) {
121+
return false;
186122
}
187123
}
188-
189-
if (args.options.scopeConsentBy &&
190-
EntraAppAddCommand.entraAppScopeConsentBy.indexOf(args.options.scopeConsentBy) < 0) {
191-
return `${args.options.scopeConsentBy} is not a valid value for scopeConsentBy. Allowed values are ${EntraAppAddCommand.entraAppScopeConsentBy.join(', ')}`;
192-
}
193-
194-
if (args.options.manifest) {
124+
return true;
125+
}, {
126+
message: 'When you specify scopeName you also need to specify uri, scopeAdminConsentDescription, and scopeAdminConsentDisplayName'
127+
})
128+
.refine(options => {
129+
if (options.manifest) {
195130
try {
196-
this.manifest = JSON.parse(args.options.manifest);
197-
if (!args.options.name && !this.manifest.name) {
198-
return `Specify the name of the app to create either through the 'name' option or the 'name' property in the manifest`;
131+
const manifest = JSON.parse(options.manifest);
132+
if (!options.name && !manifest.name) {
133+
return false;
199134
}
135+
this.manifest = manifest;
136+
return true;
200137
}
201138
catch (e) {
202-
return `Error while parsing the specified manifest: ${e}`;
139+
return false;
203140
}
204141
}
205-
206142
return true;
207-
},
208-
);
209-
}
210-
211-
#initOptionSets(): void {
212-
this.optionSets.push(
213-
{ options: ['name', 'manifest'] }
214-
);
143+
}, {
144+
message: 'Specify the name of the app to create either through the \'name\' option or the \'name\' property in the manifest'
145+
})
146+
.refine(options => {
147+
return options.name || options.manifest;
148+
}, {
149+
message: 'Specify either name or manifest'
150+
});
215151
}
216152

217153
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
@@ -230,7 +166,7 @@ class EntraAppAddCommand extends GraphCommand {
230166
});
231167
let appInfo: any = await entraApp.createAppRegistration({
232168
options: args.options,
233-
unknownOptions: optionsUtils.getUnknownOptions(args.options, this.options),
169+
unknownOptions: optionsUtils.getUnknownOptions(args.options, zod.schemaToOptions(this.schema!)),
234170
apis,
235171
logger,
236172
verbose: this.verbose,
@@ -343,7 +279,7 @@ class EntraAppAddCommand extends GraphCommand {
343279
if (args.options.redirectUris) {
344280
// take submitted redirectUris/platform as options
345281
// otherwise, they will be removed from the app
346-
v2Manifest.replyUrlsWithType = args.options.redirectUris.split(',').map(u => {
282+
v2Manifest.replyUrlsWithType = (args.options.redirectUris as string).split(',').map(u => {
347283
return {
348284
url: u.trim(),
349285
type: this.translatePlatformToType(args.options.platform!)

0 commit comments

Comments
 (0)