From fac09276a0a1bb34106ca8f316d12de5b769e235 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Sat, 28 Oct 2023 19:53:49 +0200 Subject: [PATCH] Adds the 'external item add' command. Closes #5530 --- docs/docs/cmd/external/item/item-add.mdx | 145 +++++ docs/src/config/sidebars.js | 13 + src/m365/external/commands.ts | 5 + .../external/commands/item/item-add.spec.ts | 569 ++++++++++++++++++ src/m365/external/commands/item/item-add.ts | 185 ++++++ 5 files changed, 917 insertions(+) create mode 100644 docs/docs/cmd/external/item/item-add.mdx create mode 100644 src/m365/external/commands.ts create mode 100644 src/m365/external/commands/item/item-add.spec.ts create mode 100644 src/m365/external/commands/item/item-add.ts diff --git a/docs/docs/cmd/external/item/item-add.mdx b/docs/docs/cmd/external/item/item-add.mdx new file mode 100644 index 00000000000..afc4941735f --- /dev/null +++ b/docs/docs/cmd/external/item/item-add.mdx @@ -0,0 +1,145 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# external item add + +Creates external item + +## Usage + +```sh +m365 external item add [options] +``` + +## Options + +```md definition-list +`-i, --id ` +: The ID of the item to create + +`--externalConnectionId ` +: The ID of the external connection on which to create the item + +`--content ` +: External item content + +`--contentType [contentType]` +: Type of content to load. Allowed values: `text` (default), `html` + +`--acls ` +: List of permissions to set on the item, in the format `accessType1,type1,value1;accessType2,type2,value2`, eg. `grant,everyone,everyone` +``` + + +## Remarks + +When creating an external item, the options you specify will map to the external item properties, eg. `--author "Steve"` will set the `author` property of the external item to `Steve`. + +If in your schema you have a multi-value property, to specify its value you need two things: + +- you need to separate multiple values using `;#`, eg. `--author "Steve;#Bill"` +- you need to specify the type of the property as defined in the schema, eg. `--author@odata.type "Collection(String)"` + +When creating items you can specify one or more permissions that define who can access the external item. The format of the permissions is `accessType1,type1,value1;accessType2,type2,value2`, eg. `grant,everyone,everyone`. + +You can use the following access types: + +- `grant` - grants access to the specified users or groups +- `deny` - denies access to the specified users or groups + +You can use the following types: + +- `everyone` - everyone +- `group` - an Entra group +- `user` - an Entra user +- `everyoneExceptGuests` - everyone except external users +- `externalGroup` - a group defined in the external system from which you're importing the content + +For more information about using these options, see the Microsoft Graph API documentation and documentation about Microsoft Graph connectors. + +## Examples + +Creates an external item with simple properties that everyone is allowed to access + +```sh +m365 external item add --id "pnp-ensure-siteassets-library" --connectionId "samplesolutiongallery" --content "Ensure that the Site Assets library is created." --title "Ensure the Site Assets Library is created" --description "Ensure that the Site Assets library is created." --authors "Phil Harding" --acls "grant,everyone,everyone" +``` + +Creates an external item with multi-value properties accessible only to users from the specified Entra group + +```sh +m365 external item add --id "pnp-ensure-siteassets-library" --connectionId "samplesolutiongallery" --content "Ensure that the Site Assets library is created." --title "Ensure the Site Assets Library is created" --description "Ensure that the Site Assets library is created." --authors@odata.type "Collection(String)" --authors "Phil Harding;#Steve Smith" --acls "grant,group,Super users" +``` + +## Response + + + + + ```json + { + "id": "pnp-ensure-siteassets-library", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "title": "Ensure the Site Assets Library is created", + "description": "Ensure that the Site Assets library is created.", + "authors": "Phil Harding" + }, + "content": { + "value": "Ensure that the Site Assets library is created.", + "type": "text" + }, + "activities": [] + } + ``` + + + + + ```text + acl : [{"type":"everyone","value":"everyone","accessType":"grant"}] + activities: [] + content : {"value":"Ensure that the Site Assets library is created.","type":"text"} + id : pnp-ensure-siteassets-library + properties: {"title":"Ensure the Site Assets Library is created","description":"Ensure that the Site Assets library is created.","authors":"Phil Harding"} + ``` + + + + + ```csv + id + pnp-ensure-siteassets-library + ``` + + + + + ```md + # m365 external item add --id "pnp-ensure-siteassets-library" --connectionId "samplesolutiongallery" --content "Ensure that the Site Assets library is created." --title "Ensure the Site Assets Library is created" --description "Ensure that the Site Assets library is created." --authors "Phil Harding" --acls "grant,everyone,everyone" + + Date: 2023-10-28 + + ## pnp-ensure-siteassets-library + + Property | Value + ---------|------- + id | pnp-ensure-siteassets-library + ``` + + + + +## More information + +- Microsoft Graph connectors access control list [https://learn.microsoft.com/graph/connecting-external-content-manage-items#access-control-list](https://learn.microsoft.com/graph/connecting-external-content-manage-items#access-control-list) +- Create, update, and delete items added by your application via Microsoft Graph connectors [https://learn.microsoft.com/graph/connecting-external-content-manage-items](https://learn.microsoft.com/graph/connecting-external-content-manage-items) +- Use external groups to manage permissions to Microsoft Graph connectors data sources [https://learn.microsoft.com/graph/connecting-external-content-external-groups](https://learn.microsoft.com/graph/connecting-external-content-external-groups) +- Create externalItem [https://learn.microsoft.com/graph/api/externalconnectors-externalconnection-put-items?view=graph-rest-1.0&tabs=http](https://learn.microsoft.com/graph/api/externalconnectors-externalconnection-put-items?view=graph-rest-1.0&tabs=http) diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index d55c8c52e0e..52e54bfcbf9 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -578,6 +578,19 @@ const sidebars = { } ] }, + { + 'External content (external)': [ + { + item: [ + { + type: 'doc', + label: 'item add', + id: 'cmd/external/item/item-add' + } + ] + } + ] + }, { 'File (file)': [ { diff --git a/src/m365/external/commands.ts b/src/m365/external/commands.ts new file mode 100644 index 00000000000..0c6a66e1473 --- /dev/null +++ b/src/m365/external/commands.ts @@ -0,0 +1,5 @@ +const prefix: string = 'external'; + +export default { + ITEM_ADD: `${prefix} item add` +}; \ No newline at end of file diff --git a/src/m365/external/commands/item/item-add.spec.ts b/src/m365/external/commands/item/item-add.spec.ts new file mode 100644 index 00000000000..4e76b7ba8a1 --- /dev/null +++ b/src/m365/external/commands/item/item-add.spec.ts @@ -0,0 +1,569 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { CommandError } from '../../../../Command.js'; +import { Cli } from '../../../../cli/Cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import command from './item-add.js'; + +describe(commands.ITEM_ADD, () => { + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + logger = { + log: async () => { }, + logRaw: async () => { }, + logToStderr: async () => { } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.put + ]); + loggerLogSpy.resetHistory(); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ITEM_ADD); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('adds an external item with simple properties', async () => { + const externalItem = { + "id": "ticket1", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee": "Steve" + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [] + }; + sinon.stub(request, 'put').callsFake(async (opts: any) => { + if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/connection/items/ticket1`) { + return externalItem; + } + throw 'Invalid request'; + }); + const options: any = { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Something went wrong', + contentType: 'text', + acls: 'grant,everyone,everyone', + ticketTitle: 'Something went wrong ticket', + priority: 'high', + assignee: 'Steve' + }; + await command.action(logger, { options } as any); + assert(loggerLogSpy.calledWith(externalItem)); + }); + + it('adds an external item with a collection properties', async () => { + const externalItem = { + "id": "ticket1", + "acl": [ + { + "type": "group", + "value": "Admins", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee@odata.type": "Collection(String)", + "assignee": ["Steve", "Brian"] + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [] + }; + sinon.stub(request, 'put').callsFake(async (opts: any) => { + if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/connection/items/ticket1`) { + return externalItem; + } + throw 'Invalid request'; + }); + const options: any = { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Something went wrong', + acls: 'grant,group,Admins', + ticketTitle: 'Something went wrong ticket', + priority: 'high', + // this is how assignee@odata.type is parsed by minimist + 'assignee@odata': { + type: 'Collection(String)' + }, + assignee: 'Steve;#Brian' + }; + await command.action(logger, { options } as any); + assert(loggerLogSpy.calledWith(externalItem)); + }); + + it('outputs properties in csv output', async () => { + const externalItem = { + "id": "ticket1", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee@odata.type": "Collection(String)", + "assignee": [ + "Steve", + "Brian" + ] + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [] + }; + sinon.stub(request, 'put').callsFake(async (opts: any) => { + if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/connection/items/ticket1`) { + return externalItem; + } + throw 'Invalid request'; + }); + const options: any = { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Something went wrong', + contentType: 'text', + acls: 'grant,everyone,everyone', + ticketTitle: 'Something went wrong ticket', + priority: 'high', + "assignee@odata.type": "Collection(String)", + assignee: 'Steve;#Brian', + output: 'csv' + }; + await command.action(logger, { options } as any); + const extendedItem = { + "id": "ticket1", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee@odata.type": "Collection(String)", + "assignee": ["Steve", "Brian"] + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [], + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee@odata.type": "Collection(String)", + "assignee": "Steve, Brian" + }; + assert(loggerLogSpy.calledWith(extendedItem)); + }); + + it('outputs properties in md output', async () => { + const externalItem = { + "id": "ticket1", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee": "Steve" + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [] + }; + sinon.stub(request, 'put').callsFake(async (opts: any) => { + if (opts.url === `https://graph.microsoft.com/v1.0/external/connections/connection/items/ticket1`) { + return externalItem; + } + throw 'Invalid request'; + }); + const options: any = { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Something went wrong', + contentType: 'text', + acls: 'grant,everyone,everyone', + ticketTitle: 'Something went wrong ticket', + priority: 'high', + assignee: 'Steve', + output: 'md' + }; + await command.action(logger, { options } as any); + const extendedItem = { + "id": "ticket1", + "acl": [ + { + "type": "everyone", + "value": "everyone", + "accessType": "grant" + } + ], + "properties": { + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee": "Steve" + }, + "content": { + "value": "Something went wrong", + "type": "text" + }, + "activities": [], + "ticketTitle": "Something went wrong ticket", + "priority": "high", + "assignee": "Steve" + }; + assert(loggerLogSpy.calledWith(extendedItem)); + }); + + it('correctly handles error', async () => { + sinon.stub(request, 'put').callsFake(() => { + throw { + "error": { + "code": "Error", + "message": "An error has occurred", + "innerError": { + "request-id": "9b0df954-93b5-4de9-8b99-43c204a8aaf8", + "date": "2018-04-24T18:56:48" + } + } + }; + }); + + const options: any = { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Something went wrong', + contentType: 'text', + acls: 'grant,everyone,everyone', + ticketTitle: 'Something went wrong ticket', + priority: 'high', + assignee: 'Steve' + }; + await assert.rejects(command.action(logger, { options } as any), + new CommandError(`An error has occurred`)); + }); + + //#region validation + it('fails validation when invalid contentType specified', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'invalid', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.notStrictEqual(actual, false); + }); + + it('passes validation when contentType is text', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation when contentType is html', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'html', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation when one acl with other than 3 elements', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone', + name: 'Test item' + } + }, commandInfo); + assert.notStrictEqual(actual, false); + }); + + it('fails validation when multiple acls specified where one is with other than 3 elements', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone;grant,everyone', + name: 'Test item' + } + }, commandInfo); + assert.notStrictEqual(actual, false); + }); + + it('passes validation for a single correct acl', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for multiple correct acls', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone;grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation for invalid acl access type', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'invalid,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.notStrictEqual(actual, false); + }); + + it('passes validation for acl access type grant', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for acl access type deny', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'deny,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation for invalid acl type', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,invalid,everyone', + name: 'Test item' + } + }, commandInfo); + assert.notStrictEqual(actual, false); + }); + + it('passes validation for acl type user', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,user,steve@contoso.com', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for acl type grant', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,group,Users', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for acl type everyone', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyone,everyone', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for acl type everyoneExceptGuests', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,everyoneExceptGuests,everyoneExceptGuests', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation for acl type externalGroup', async () => { + const actual = await command.validate({ + options: { + id: 'ticket1', + externalConnectionId: 'connection', + content: 'Hello world', + contentType: 'text', + acls: 'grant,externalGroup,Users', + name: 'Test item' + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + //#endregion + + it('allows unknown properties', () => { + const allowUnknownOptions = command.allowUnknownOptions(); + assert.strictEqual(allowUnknownOptions, true); + }); + + //#region options + it('supports specifying id', () => { + const containsOption = command.options + .some(o => o.option.indexOf('--id') > -1); + assert(containsOption); + }); + + it('supports specifying externalConnectionId', () => { + const containsOption = command.options + .some(o => o.option.indexOf('--externalConnectionId') > -1); + assert(containsOption); + }); + + it('supports specifying content', () => { + const containsOption = command.options + .some(o => o.option.indexOf('--content') > -1); + assert(containsOption); + }); + + it('supports specifying contentType', () => { + const containsOption = command.options + .some(o => o.option.indexOf('--contentType') > -1); + assert(containsOption); + }); + + it('supports specifying acls', () => { + const containsOption = command.options + .some(o => o.option.indexOf('--acls') > -1); + assert(containsOption); + }); + //#endregion +}); diff --git a/src/m365/external/commands/item/item-add.ts b/src/m365/external/commands/item/item-add.ts new file mode 100644 index 00000000000..bbc96e24432 --- /dev/null +++ b/src/m365/external/commands/item/item-add.ts @@ -0,0 +1,185 @@ +import { ExternalConnectors } from '@microsoft/microsoft-graph-types/microsoft-graph'; +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + id: string; + externalConnectionId: string; + content: string; + contentType?: string; + acls: string; +} + +class ExternalItemAddCommand extends GraphCommand { + private static contentType: string[] = ['text', 'html']; + + public get name(): string { + return commands.ITEM_ADD; + } + + public get description(): string { + return 'Creates external item'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + contentType: typeof args.options.contentType + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '--id ' + }, + { + option: '--externalConnectionId ' + }, + { + option: '--content ' + }, + { + option: '--contentType [contentType]', + autocomplete: ExternalItemAddCommand.contentType + }, + { + option: '--acls ' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.contentType && + ExternalItemAddCommand.contentType.indexOf(args.options.contentType) < 0) { + return `${args.options.contentType} is not a valid value for contentType. Allowed values are ${ExternalItemAddCommand.contentType.join(', ')}`; + } + + // verify that each value for ACLs consists of three parts + // and that the values are correct + const acls: string[] = args.options.acls.split(';'); + for (let i = 0; i < acls.length; i++) { + const acl: string[] = acls[i].split(','); + if (acl.length !== 3) { + return `The value ${acls[i]} for option acls is not in the correct format. The correct format is "accessType,type,value", eg. "grant,everyone,everyone"`; + } + + const accessTypeValues = ['grant', 'deny']; + if (accessTypeValues.indexOf(acl[0]) < 0) { + return `The value ${acl[0]} for option acls is not valid. Allowed values are ${accessTypeValues.join(', ')}}`; + } + + const aclTypeValues = ['user', 'group', 'everyone', 'everyoneExceptGuests', 'externalGroup']; + if (aclTypeValues.indexOf(acl[1]) < 0) { + return `The value ${acl[1]} for option acls is not valid. Allowed values are ${aclTypeValues.join(', ')}}`; + } + } + + return true; + } + ); + } + + public allowUnknownOptions(): boolean | undefined { + return true; + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const acls: ExternalConnectors.Acl[] = args.options.acls + .split(';') + .map(acl => { + const aclParts: string[] = acl.split(','); + return { + accessType: aclParts[0] as any, + type: aclParts[1] as any, + value: aclParts[2] + }; + }); + + const requestBody: ExternalConnectors.ExternalItem = { + id: args.options.id, + content: { + value: args.options.content, + type: args.options.contentType as any ?? 'text' + }, + acl: acls, + properties: {} + }; + + // we need to rewrite the @odata properties to the correct format + // because . in @odata.type is interpreted by minimist as a child property + // we also need to extract multiple values for collections into arrays + this.rewriteCollectionProperties(args.options); + this.addUnknownOptionsToPayload(requestBody.properties, args.options); + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/external/connections/${args.options.externalConnectionId}/items/${args.options.id}`, + headers: { + accept: 'application/json;odata.metadata=none', + 'content-type': 'application/json' + }, + responseType: 'json', + data: requestBody + }; + + try { + const externalItem: any = await request.put(requestOptions); + + if (args.options.output === 'csv' || args.options.output === 'md') { + // for CSV and md, we need to bring the properties to the main object + // and convert arrays to comma-separated strings or they will be dropped + // from the output + Object.getOwnPropertyNames(externalItem.properties).forEach(name => { + if (Array.isArray(externalItem.properties[name])) { + externalItem[name] = externalItem.properties[name].join(', '); + } + else { + externalItem[name] = externalItem.properties[name]; + } + }); + } + + await logger.log(externalItem); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private rewriteCollectionProperties(options: any): void { + Object.getOwnPropertyNames(options).forEach(name => { + if (!name.endsWith('@odata')) { + return; + } + + options[`${name}.type`] = options[name].type; + delete options[name]; + + // convert the value of a collection to an array + const nameWithoutOData: string = name.substring(0, name.indexOf('@odata')); + if (options[nameWithoutOData]) { + options[nameWithoutOData] = options[nameWithoutOData].split(';#'); + } + }); + } +} + +export default new ExternalItemAddCommand();