diff --git a/docs/manual/docs/cmd/graph/schemaextension/schemaextension-set.md b/docs/manual/docs/cmd/graph/schemaextension/schemaextension-set.md new file mode 100644 index 00000000000..8af5f4588fe --- /dev/null +++ b/docs/manual/docs/cmd/graph/schemaextension/schemaextension-set.md @@ -0,0 +1,59 @@ +# graph schemaextension set + +Updates a Microsoft Graph schema extension + +## Usage + +```sh +graph schemaextension set [options] +``` + +## Options + +Option|Description +------|----------- +`--help`|output usage information +`-i, --id `|The unique identifier for the schema extension definition +`--owner`|The ID of the Azure AD application that is the owner of the schema extension +`-d, --description [description]`|Description of the schema extension +`-s, --status [status]`|The lifecycle state of the schema extension. Accepted values are 'Available' or 'Deprecated' +`-t, --targetTypes [targetTypes]`|Comma-separated list of Microsoft Graph resource types the schema extension targets +`-p, --properties [properties]`|The collection of property names and types that make up the schema extension definition formatted as a JSON string +`-o, --output [output]`|Output type. `json|text`. Default `text` +`--verbose`|Runs command with verbose logging +`--debug`|Runs command with debug logging + +## Remarks + +The lifecycle state of the schema extension. The initial state upon creation is `InDevelopment`. +Possible states transitions are from `InDevelopment` to `Available` and `Available` to `Deprecated`. +The target types are the set of Microsoft Graph resource types (that support schema extensions) that this schema extension definition can be applied to. This option is specified as a comma-separated list. +When specifying the JSON string of properties on Windows, you have to escape double quotes in a specific way. Considering the following value for the _properties_ option: `{"Foo":"Bar"}`, +you should specify the value as \`"{""Foo"":""Bar""}"\`. +In addition, when using PowerShell, you should use the `--%` argument. + +## Examples + + Update the description of a schema extension + +```sh +graph schemaextension set --id MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --description "My schema extension" +``` + +Update the target types and properties of a schema extension + +```sh +graph schemaextension set --id contoso_MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --targetTypes Group --properties \`"[{""name"":""myProp1"",""type"":""Integer""},{""name"":""myProp2"",""type"":""String""}]\` +``` + +Update the properties of a schema extension in PowerShell + +```PowerShell +graph schemaextension set --id contoso_MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --properties --% \`"[{""name"":""myProp1"",""type"":""Integer""},{""name"":""myProp2"",""type"":""String""}]\` +``` + +Change the status of a schema extension to 'Available' + +```sh +graph schemaextension set --id contoso_MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --status Available +``` \ No newline at end of file diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml index dfc66db109a..765d637ac55 100644 --- a/docs/manual/mkdocs.yml +++ b/docs/manual/mkdocs.yml @@ -73,6 +73,7 @@ nav: - schemaextension add: 'cmd/graph/schemaextension/schemaextension-add.md' - schemaextension get: 'cmd/graph/schemaextension/schemaextension-get.md' - schemaextension remove: 'cmd/graph/schemaextension/schemaextension-remove.md' + - schemaextension set: 'cmd/graph/schemaextension/schemaextension-set.md' - OneDrive (onedrive): - report: - report activityfilecounts: 'cmd/onedrive/report/report-activityfilecounts.md' diff --git a/src/o365/graph/commands.ts b/src/o365/graph/commands.ts index 18283f89d9e..88b00a3fb4d 100644 --- a/src/o365/graph/commands.ts +++ b/src/o365/graph/commands.ts @@ -4,4 +4,5 @@ export default { SCHEMAEXTENSION_ADD: `${prefix} schemaextension add`, SCHEMAEXTENSION_GET: `${prefix} schemaextension get`, SCHEMAEXTENSION_REMOVE: `${prefix} schemaextension remove`, + SCHEMAEXTENSION_SET: `${prefix} schemaextension set` }; \ No newline at end of file diff --git a/src/o365/graph/commands/schemaextension/schemaextension-set.spec.ts b/src/o365/graph/commands/schemaextension/schemaextension-set.spec.ts new file mode 100644 index 00000000000..417d1a409e9 --- /dev/null +++ b/src/o365/graph/commands/schemaextension/schemaextension-set.spec.ts @@ -0,0 +1,452 @@ +import commands from '../../commands'; +import Command, { CommandOption, CommandValidate, CommandError } from '../../../../Command'; +import * as sinon from 'sinon'; +import appInsights from '../../../../appInsights'; +import auth from '../../../../Auth'; +const command: Command = require('./schemaextension-set'); +import * as assert from 'assert'; +import request from '../../../../request'; +import Utils from '../../../../Utils'; + +describe(commands.SCHEMAEXTENSION_SET, () => { + let vorpal: Vorpal; + let log: string[]; + let cmdInstance: any; + let cmdInstanceLogSpy: sinon.SinonSpy; + + before(() => { + sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve()); + sinon.stub(appInsights, 'trackEvent').callsFake(() => { }); + auth.service.connected = true; + }); + + beforeEach(() => { + vorpal = require('../../../../vorpal-init'); + log = []; + cmdInstance = { + commandWrapper: { + command: command.name + }, + action: command.action(), + log: (msg: string) => { + log.push(msg); + } + }; + cmdInstanceLogSpy = sinon.spy(cmdInstance, 'log'); + (command as any).items = []; + }); + + afterEach(() => { + Utils.restore([ + vorpal.find, + request.patch + ]); + }); + + after(() => { + Utils.restore([ + auth.restoreAuth, + appInsights.trackEvent + ]); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.equal(command.name.startsWith(commands.SCHEMAEXTENSION_SET), true); + }); + + it('has a description', () => { + assert.notEqual(command.description, null); + }); + + it('updates schema extension', (done) => { + sinon.stub(request, 'patch').callsFake((opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/schemaExtensions/ext6kguklm2_TestSchemaExtension`) { + return Promise.resolve({ + "id": "ext6kguklm2_TestSchemaExtension", + "description": "Test Description", + "targetTypes": [ + "Group" + ], + "status": "InDevelopment", + "owner": "b07a45b3-f7b7-489b-9269-da6f3f93dff0", + "properties": [ + { + "name": "MyInt", + "type": "Integer" + }, + { + "name": "MyString", + "type": "String" + } + ] + }); + } + + return Promise.reject('Invalid request'); + }); + + cmdInstance.action = command.action(); + cmdInstance.action({ + options: { + debug: false, + id: 'ext6kguklm2_TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + status: 'Available', + properties: '[{"name":"MyInt","type":"Integer"},{"name":"MyString","type":"String"}]' + } + }, () => { + try { + assert.equal(log.length, 0); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('updates schema extension (debug)', (done) => { + sinon.stub(request, 'patch').callsFake((opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/schemaExtensions/ext6kguklm2_TestSchemaExtension`) { + return Promise.resolve(); + } + + return Promise.reject('Invalid request'); + }); + + cmdInstance.action = command.action(); + cmdInstance.action({ + options: { + debug: true, + id: 'ext6kguklm2_TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + status: 'Available', + properties: '[{"name":"MyInt","type":"Integer"},{"name":"MyString","type":"String"}]' + } + }, () => { + try { + assert(cmdInstanceLogSpy.calledWith("Schema extension successfully updated.")); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('updates schema extension (verbose)', (done) => { + sinon.stub(request, 'patch').callsFake((opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/schemaExtensions/ext6kguklm2_TestSchemaExtension`) { + return Promise.resolve(); + } + + return Promise.reject('Invalid request'); + }); + + cmdInstance.action = command.action(); + cmdInstance.action({ + options: { + verbose: true, + debug: false, + id: 'ext6kguklm2_TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + status: 'Available' + } + }, () => { + try { + assert(cmdInstanceLogSpy.calledWith(vorpal.chalk.green('DONE'))); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('handles error correctly', (done) => { + sinon.stub(request, 'patch').callsFake((opts) => { + return Promise.reject('An error has occurred'); + }); + + cmdInstance.action = command.action(); + cmdInstance.action({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Integer"},{"name":"MyString","type":"String"}]' + } + }, (err?: any) => { + try { + assert.equal(JSON.stringify(err), JSON.stringify(new CommandError('An error has occurred'))); + done(); + } + catch (e) { + done(e); + } + }); + }); + + it('fails validation if the id is not specified', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: null + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if owner is not specified', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if the owner is not a valid GUID', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'invalid', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Integer"},{"name":"MyString","type":"String"}]' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if no update information is specified', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if properties is not valid JSON string', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: 'foobar' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if properties have no valid type', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Foo"},{"name":"MyString","type":"String"}]' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if a specified property has missing type', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt"},{"name":"MyString","type":"String"}]' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if a specified property has missing name', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"type":"Integer"},{"name":"MyString","type":"String"}]' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if properties JSON string is not an array', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '{}' + } + }); + assert.notEqual(actual, true); + }); + + it('fails validation if status is not valid', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: 'Test Description', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + status: 'invalid' + } + }); + assert.notEqual(actual, true); + }); + + it('passes validation if required parameters are set and at least one property to update (description) is specified', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + description: 'test', + } + }); + assert.equal(actual, true); + }); + + it('passes validation if the property type is Binary', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: null, + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Binary"}]' + } + }); + assert.equal(actual, true); + }); + + it('passes validation if the property type is Boolean', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: null, + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Boolean"}]' + } + }); + assert.equal(actual, true); + }); + + it('passes validation if the property type is DateTime', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: null, + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"DateTime"}]' + } + }); + assert.equal(actual, true); + }); + + it('passes validation if the property type is Integer', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: null, + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"Integer"}]' + } + }); + assert.equal(actual, true); + }); + + it('passes validation if the property type is String', () => { + const actual = (command.validate() as CommandValidate)({ + options: { + debug: false, + id: 'TestSchemaExtension', + description: null, + owner: 'b07a45b3-f7b7-489b-9269-da6f3f93dff0', + targetTypes: 'Group', + properties: '[{"name":"MyInt","type":"String"}]' + } + }); + assert.equal(actual, true); + }); + + it('supports debug mode', () => { + const options = (command.options() as CommandOption[]); + let containsOption = false; + options.forEach(o => { + if (o.option === '--debug') { + containsOption = true; + } + }); + assert(containsOption); + }); + + it('has help referring to the right command', () => { + const cmd: any = { + log: (msg: string) => { }, + prompt: () => { }, + helpInformation: () => { } + }; + const find = sinon.stub(vorpal, 'find').callsFake(() => cmd); + cmd.help = command.help(); + cmd.help({}, () => { }); + assert(find.calledWith(commands.SCHEMAEXTENSION_SET)); + }); + + it('has help with examples', () => { + const _log: string[] = []; + const cmd: any = { + log: (msg: string) => { + _log.push(msg); + }, + prompt: () => { }, + helpInformation: () => { } + }; + sinon.stub(vorpal, 'find').callsFake(() => cmd); + cmd.help = command.help(); + cmd.help({}, () => { }); + let containsExamples: boolean = false; + _log.forEach(l => { + if (l && l.indexOf('Examples:') > -1) { + containsExamples = true; + } + }); + Utils.restore(vorpal.find); + assert(containsExamples); + }); +}); \ No newline at end of file diff --git a/src/o365/graph/commands/schemaextension/schemaextension-set.ts b/src/o365/graph/commands/schemaextension/schemaextension-set.ts new file mode 100644 index 00000000000..f9bb17848b2 --- /dev/null +++ b/src/o365/graph/commands/schemaextension/schemaextension-set.ts @@ -0,0 +1,261 @@ +import commands from '../../commands'; +import request from '../../../../request'; +import GlobalOptions from '../../../../GlobalOptions'; +import { + CommandOption, CommandValidate +} from '../../../../Command'; +import GraphCommand from '../../../base/GraphCommand'; +import Utils from '../../../../Utils'; + +const vorpal: Vorpal = require('../../../../vorpal-init'); + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + id: string; + owner: string; + description?: string; + status?: string; + targetTypes?: string; + properties?: string; +} + +class GraphSchemaExtensionSetCommand extends GraphCommand { + public get name(): string { + return `${commands.SCHEMAEXTENSION_SET}`; + } + + public get description(): string { + return 'Updates a Microsoft Graph schema extension'; + } + + public getTelemetryProperties(args: CommandArgs): any { + const telemetryProps: any = super.getTelemetryProperties(args); + // Do not include in telemetry on purpose: id, owner, description, properties + if (args.options.status) { + telemetryProps.status = args.options.status; + } + if (args.options.targetTypes) { + telemetryProps.targetTypes = args.options.targetTypes; + } + return telemetryProps; + } + + public commandAction(cmd: CommandInstance, args: CommandArgs, cb: () => void): void { + if (this.verbose) { + cmd.log(`Updating schema extension with id '${args.options.id}'...`); + } + + // The default request body always contains owner + const body: { + owner: string; + description?: string; + status?: string; + targetTypes?: string[]; + properties?: any; + } = { + owner: args.options.owner + }; + + // Add the description to request body if any + if (args.options.description) { + if (this.debug) { + cmd.log(`Will update description to '${args.options.description}'...`); + } + body.description = args.options.description; + } + + // Add the status to request body if any + if (args.options.status) { + if (this.debug) { + cmd.log(`Will update status to '${args.options.status}'...`); + } + body.status = args.options.status; + } + + // Add the target types to request body if any + const targetTypes: string[] = args.options.targetTypes + ? args.options.targetTypes.split(',').map(t => t.trim()) + : []; + if (targetTypes.length > 0) { + if (this.debug) { + cmd.log(`Will update targetTypes to '${args.options.targetTypes}'...`); + } + body.targetTypes = targetTypes; + } + + // Add the properties to request body if any + const properties: any = args.options.properties + ? JSON.parse(args.options.properties) + : null; + if (properties) { + if (this.debug) { + cmd.log(`Will update properties to '${args.options.properties}'...`); + } + body.properties = properties; + } + + const requestOptions: any = { + url: `${this.resource}/v1.0/schemaExtensions/${args.options.id}`, + headers: { + accept: 'application/json;odata.metadata=none', + 'content-type': 'application/json' + }, + body, + json: true + }; + + request + .patch(requestOptions) + .then((res: any): void => { + if (this.debug) { + cmd.log("Schema extension successfully updated."); + } + + if (this.verbose) { + cmd.log(vorpal.chalk.green('DONE')); + } + + cb(); + }, (err: any) => this.handleRejectedODataJsonPromise(err, cmd, cb)); + } + + public options(): CommandOption[] { + const options: CommandOption[] = [ + { + option: '-i, --id ', + description: `The unique identifier for the schema extension definition` + }, + { + option: '--owner ', + description: `The ID of the Azure AD application that is the owner of the schema extension` + }, + { + option: '-d, --description [description]', + description: 'Description of the schema extension' + }, + { + option: '-s, --status [status]', + description: `The lifecycle state of the schema extension. Accepted values are 'Available' or 'Deprecated'` + }, + { + option: '-t, --targetTypes [targetTypes]', + description: `Comma-separated list of Microsoft Graph resource types the schema extension targets` + }, + { + option: '-p, --properties [properties]', + description: `The collection of property names and types that make up the schema extension definition formatted as a JSON string` + } + ]; + + const parentOptions: CommandOption[] = super.options(); + return options.concat(parentOptions); + } + + public validate(): CommandValidate { + return (args: CommandArgs): boolean | string => { + if (!args.options.id) { + return 'Required option id is missing'; + } + + if (!args.options.owner) { + return 'Required option owner is missing'; + } + + if (!Utils.isValidGuid(args.options.owner)) { + return `The specified owner '${args.options.owner}' is not a valid App Id`; + } + + if (!args.options.status && !args.options.properties && !args.options.targetTypes && !args.options.description) { + return `No updates were specified. Please specify at least one argument among --status, --targetTypes, --description or --properties` + } + + const validStatusValues = ['Available', 'Deprecated']; + if (args.options.status && validStatusValues.indexOf(args.options.status) < 0) { + return `Status option is invalid. Valid statuses are: Available or Deprecated`; + } + + if (args.options.properties) { + return this.validateProperties(args.options.properties); + } + + return true; + }; + } + + private validateProperties(propertiesString: string): boolean | string { + let properties: any = null; + try { + properties = JSON.parse(propertiesString); + } + catch (e) { + return 'The specified properties is not a valid JSON string'; + } + + // If the properties object is not an array + if (properties.length === undefined) { + return 'The specified properties JSON string is not an array'; + } + + for (let i: number = 0; i < properties.length; i++) { + const property: any = properties[i]; + if (!property.name) { + return `Property ${JSON.stringify(property)} misses name`; + } + if (!this.isValidPropertyType(property.type)) { + return `${property.type} is not a valid property type. Valid types are: Binary, Boolean, DateTime, Integer and String`; + } + } + + return true; + } + + private isValidPropertyType(propertyType: string): boolean { + if (!propertyType) { + return false; + } + + return ['Binary', 'Boolean', 'DateTime', 'Integer', 'String'].indexOf(propertyType) > -1; + } + + public commandHelp(args: {}, log: (help: string) => void): void { + const chalk = vorpal.chalk; + log(vorpal.find(this.name).helpInformation()); + log( + ` Remarks: + + The lifecycle state of the schema extension. + The initial state upon creation is ${chalk.grey("InDevelopment")}. + Possible states transitions are from ${chalk.grey("InDevelopment")} to ${chalk.grey("Available")} and ${chalk.grey("Available")} to ${chalk.grey("Deprecated")}. + + The target types are the set of Microsoft Graph resource types (that support + schema extensions) that this schema extension definition can be applied to + This option is specified as a comma-separated list. + + When specifying the JSON string of properties on Windows, you + have to escape double quotes in a specific way. Considering the following + value for the properties option: {"Foo":"Bar"}, + you should specify the value as ${chalk.grey('\`"{""Foo"":""Bar""}"\`')}. + In addition, when using PowerShell, you should use the --% argument. + + Examples: + + Update the description of a schema extension + ${this.name} --id MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --description "My schema extension" + + Update the target types and properties of a schema extension + ${this.name} --id contoso_MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --description "My schema extension" --targetTypes Group --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --properties \`"[{""name"":""myProp1"",""type"":""Integer""},{""name"":""myProp2"",""type"":""String""}]\` + + Update the properties of a schema extension in PowerShell + ${this.name} --id MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --% --properties \`"[{""name"":""myProp1"",""type"":""Integer""},{""name"":""myProp2"",""type"":""String""}]\` + + Change the status of a schema extension to 'Available' + ${this.name} --id contoso_MySchemaExtension --owner 62375ab9-6b52-47ed-826b-58e47e0e304b --status Available + +`); + } +} + +module.exports = new GraphSchemaExtensionSetCommand(); \ No newline at end of file