From 5f1d5b1b6e243d36cde1f0011c46094af51e8e36 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Fri, 5 Apr 2024 20:03:33 +0200 Subject: [PATCH] Adds command 'm365 notebook add'. Closes #3100 --- .../cmd/onenote/notebook/notebook-add.mdx | 169 ++++++++++++ docs/src/config/sidebars.ts | 5 + src/m365/onenote/commands.ts | 1 + .../commands/notebook/notebook-add.spec.ts | 246 ++++++++++++++++++ .../onenote/commands/notebook/notebook-add.ts | 170 ++++++++++++ 5 files changed, 591 insertions(+) create mode 100644 docs/docs/cmd/onenote/notebook/notebook-add.mdx create mode 100644 src/m365/onenote/commands/notebook/notebook-add.spec.ts create mode 100644 src/m365/onenote/commands/notebook/notebook-add.ts diff --git a/docs/docs/cmd/onenote/notebook/notebook-add.mdx b/docs/docs/cmd/onenote/notebook/notebook-add.mdx new file mode 100644 index 00000000000..84a9daafed1 --- /dev/null +++ b/docs/docs/cmd/onenote/notebook/notebook-add.mdx @@ -0,0 +1,169 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# onenote notebook add + +Create a new OneNote notebook. + +## Usage + +```sh +m365 onenote notebook add [options] +``` + +## Options + +```md definition-list +`-n, --name ` +: Name of the notebook. Notebook names must be unique. The name cannot contain more than 128 characters or contain the following characters: `?*/:<>` + +`--userId [userId]` +: Id of the user. Specify either `userId`, `userName`, `groupId`, `groupName` or `webUrl`, but not multiple. + +`--userName [userName]` +: Name of the user. Specify either `userId`, `userName`, `groupId`, `groupName` or `webUrl`, but not multiple. + +`--groupId [groupId]` +: Id of the SharePoint group. Specify either `userId`, `userName`, `groupId`, `groupName` or `webUrl`, but not multiple. + +`--groupName [groupName]` +: Name of the SharePoint group. Specify either `userId`, `userName`, `groupId`, `groupName` or `webUrl`, but not multiple. + +`-u, --webUrl [webUrl]` +: URL of the SharePoint site. Specify either `userId`, `userName`, `groupId`, `groupName` or `webUrl`, but not multiple. +``` + + + +## Examples + +Create a Microsoft OneNote notebook for the currently logged in user + +```sh +m365 onenote notebook add --name "Private Notebook" +``` + +Create a Microsoft OneNote notebook in a group specified by id. + +```sh +m365 onenote notebook add --name "Private Notebook" --groupId 233e43d0-dc6a-482e-9b4e-0de7a7bce9b4 +``` + +Create a Microsoft OneNote notebook in a group specified by displayName. + +```sh +m365 onenote notebook add --name "Private Notebook" --groupName "MyGroup" +``` + +Create a Microsoft OneNote notebook for a user specified by name + +```sh +m365 onenote notebook add --name "Private Notebook" --userName user1@contoso.onmicrosoft.com +``` + +Create a Microsoft OneNote notebook for a user specified by id + +```sh +m365 onenote notebook add --name "Private Notebook" --userId 2609af39-7775-4f94-a3dc-0dd67657e900 +``` + +Creates a Microsoft OneNote notebooks for a site + +```sh + m365 onenote notebook add --name "Private Notebook" --webUrl https://contoso.sharepoint.com/sites/testsite +``` + +## Response + + + + + ```json + { + "id": "1-08554ffd-b769-4a4a-9563-faaa3191f253", + "self": "https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-08554ffd-b769-4a4a-9563-faaa3191f253", + "createdDateTime": "2024-04-05T17:58:27Z", + "displayName": "Private Notebook", + "lastModifiedDateTime": "2024-04-05T17:58:27Z", + "isDefault": false, + "userRole": "Owner", + "isShared": false, + "sectionsUrl": "https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-08554ffd-b769-4a4a-9563-faaa3191f253/sections", + "sectionGroupsUrl": "https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-08554ffd-b769-4a4a-9563-faaa3191f253/sectionGroups", + "createdBy": { + "user": { + "id": "fe36f75e-c103-410b-a18a-2bf6df06ac3a", + "displayName": "John Doe" + } + }, + "lastModifiedBy": { + "user": { + "id": "fe36f75e-c103-410b-a18a-2bf6df06ac3a", + "displayName": "John Doe" + } + }, + "links": { + "oneNoteClientUrl": { + "href": "onenote:https://contoso-my.sharepoint.com/personal/john_contoso_onmicrosoft_com/Documents/Notebooks/Private Notebook" + }, + "oneNoteWebUrl": { + "href": "https://contoso-my.sharepoint.com/personal/john_contoso_onmicrosoft_com/Documents/Notebooks/Private Notebook" + } + } + } + ``` + + + + + ```text + createdBy : {"user":{"id":"fe36f75e-c103-410b-a18a-2bf6df06ac3a","displayName":"John Doe"}} + createdDateTime : 2024-04-05T17:58:36Z + displayName : Private Notebook + id : 1-f4bba32b-9b3e-4892-8df4-931a15f7eb6f + isDefault : false + isShared : false + lastModifiedBy : {"user":{"id":"fe36f75e-c103-410b-a18a-2bf6df06ac3a","displayName":"John Doe"}} + lastModifiedDateTime: 2024-04-05T17:58:36Z + links : {"oneNoteClientUrl":{"href":"onenote:https://contoso-my.sharepoint.com/personal/john_contoso_onmicrosoft_com/Documents/Notebooks/Private Notebook"},"oneNoteWebUrl":{"href":"https://contoso-my.sharepoint.com/personal/john_contoso_onmicrosoft_com/Documents/Notebooks/Private Notebook"}} + sectionGroupsUrl : https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-f4bba32b-9b3e-4892-8df4-931a15f7eb6f/sectionGroups + sectionsUrl : https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-f4bba32b-9b3e-4892-8df4-931a15f7eb6f/sections + self : https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-f4bba32b-9b3e-4892-8df4-931a15f7eb6f + userRole : Owner + ``` + + + + + ```csv + id,self,createdDateTime,displayName,lastModifiedDateTime,isDefault,userRole,isShared,sectionsUrl,sectionGroupsUrl + 1-272a5791-2c95-45cf-b27d-7f68e07f6149,https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-272a5791-2c95-45cf-b27d-7f68e07f6149,2024-04-05T18:00:28Z,Private Notebook,2024-04-05T18:00:28Z,,Owner,,https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-272a5791-2c95-45cf-b27d-7f68e07f6149/sections,https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-272a5791-2c95-45cf-b27d-7f68e07f6149/sectionGroups + ``` + + + + + ```md + # onenote notebook add --name "Private Notebook" + + Date: 05/04/2024 + + ## Private Notebook (1-fa279d79-4701-43a2-9593-c2abfbe6999f) + + Property | Value + ---------|------- + id | 1-fa279d79-4701-43a2-9593-c2abfbe6999f + self | https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-fa279d79-4701-43a2-9593-c2abfbe6999f + createdDateTime | 2024-04-05T18:00:58Z + displayName | Private Notebook + lastModifiedDateTime | 2024-04-05T18:00:58Z + isDefault | false + userRole | Owner + isShared | false + sectionsUrl | https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-fa279d79-4701-43a2-9593-c2abfbe6999f/sections + sectionGroupsUrl | https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-fa279d79-4701-43a2-9593-c2abfbe6999f/sectionGroups + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index da14243a264..e0b1e70e70e 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1089,6 +1089,11 @@ const sidebars: SidebarsConfig = { 'OneNote (onenote)': [ { notebook: [ + { + type: 'doc', + label: 'notebook add', + id: 'cmd/onenote/notebook/notebook-add' + }, { type: 'doc', label: 'notebook list', diff --git a/src/m365/onenote/commands.ts b/src/m365/onenote/commands.ts index aafd73d72fa..b4242e721f7 100644 --- a/src/m365/onenote/commands.ts +++ b/src/m365/onenote/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'onenote'; export default { + NOTEBOOK_ADD: `${prefix} notebook add`, NOTEBOOK_LIST: `${prefix} notebook list`, PAGE_LIST: `${prefix} page list` }; \ No newline at end of file diff --git a/src/m365/onenote/commands/notebook/notebook-add.spec.ts b/src/m365/onenote/commands/notebook/notebook-add.spec.ts new file mode 100644 index 00000000000..a193726f317 --- /dev/null +++ b/src/m365/onenote/commands/notebook/notebook-add.spec.ts @@ -0,0 +1,246 @@ +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'; +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 './notebook-add.js'; +import { entraGroup } from '../../../../utils/entraGroup.js'; +import { spo } from '../../../../utils/spo.js'; + +describe(commands.NOTEBOOK_ADD, () => { + const name = 'My Notebook'; + const addResponse = { + id: '1-2ae2e5d0-2857-4b1a-99d3-cc5426799438', + self: 'https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-2ae2e5d0-2857-4b1a-99d3-cc5426799438', + createdDateTime: '2024-04-05T17:30:28Z', + displayName: name, + lastModifiedDateTime: '2024-04-05T17:30:28Z', + isDefault: false, + userRole: 'Owner', + isShared: false, + sectionsUrl: 'https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-2ae2e5d0-2857-4b1a-99d3-cc5426799438/sections', + sectionGroupsUrl: 'https://graph.microsoft.com/v1.0/users/fe36f75e-c103-410b-a18a-2bf6df06ac3a/onenote/notebooks/1-2ae2e5d0-2857-4b1a-99d3-cc5426799438/sectionGroups', + createdBy: { + user: { + id: 'fe36f75e-c103-410b-a18a-2bf6df06ac3a', + displayName: 'John Doe' + } + }, + lastModifiedBy: { + user: { + id: 'fe36f75e-c103-410b-a18a-2bf6df06ac3a', + displayName: 'John Doe' + } + }, + links: { + oneNoteClientUrl: { + href: 'onenote:https://contoso2-my.sharepoint.com/personal/john_contoso2_onmicrosoft_com/Documents/Notebooks/Dummy' + }, + oneNoteWebUrl: { + href: 'https://contoso2-my.sharepoint.com/personal/john_contoso2_onmicrosoft_com/Documents/Notebooks/Dummy' + } + } + }; + + let log: string[]; + 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.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.post + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.NOTEBOOK_ADD); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if name contains invalid characters', async () => { + const actual = await command.validate({ options: { name: 'My notebook /' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if name is longer than 128 characters', async () => { + const longString = 'x'.repeat(129); + const actual = await command.validate({ options: { name: longString } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if webUrl is not a valid webUrl', async () => { + const actual = await command.validate({ options: { name: name, webUrl: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the userId is not a valid GUID', async () => { + const actual = await command.validate({ options: { name: name, userId: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the groupId is not a valid GUID', async () => { + const actual = await command.validate({ options: { name: name, groupId: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if no option but name specified', async () => { + const actual = await command.validate({ options: { name: name } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('adds notebook for the currently logged in user', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/me/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { name: name, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('adds notebook for user by id', async () => { + const userId = '2609af39-7775-4f94-a3dc-0dd67657e900'; + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { name: name, userId: userId, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('adds notebook in group by id', async () => { + const groupId = '233e43d0-dc6a-482e-9b4e-0de7a7bce9b4'; + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { name: name, groupId: groupId, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('adds notebook in group by name', async () => { + const groupId = '233e43d0-dc6a-482e-9b4e-0de7a7bce9b4'; + const groupName = 'My group'; + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId); + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { name: name, groupName: groupName, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('adds notebook for site', async () => { + const siteUrl = 'https://contoso.sharepoint.com/sites/testsite'; + const siteId = 'contoso.sharepoint.com,2C712604-1370-44E7-A1F5-426573FDA80A,2D2244C3-251A-49EA-93A8-39E1C3A060FE'; + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/sites/${siteId}/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + sinon.stub(spo, 'getSpoGraphSiteId').resolves(siteId); + + await command.action(logger, { options: { name: name, webUrl: siteUrl, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('adds notebook for user by name', async () => { + const userName = 'john@contoso.com'; + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${userName}/onenote/notebooks`) { + return addResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { name: name, userName: userName, verbose: true } }); + assert(loggerLogSpy.calledWith(addResponse)); + }); + + it('handles error when adding notebook fails when it already exists', async () => { + const error = { + error: { + code: '20117', + message: 'An item with this name already exists in this location.', + innerError: { + date: '2024-04-05T17:49:42', + 'request-id': '47cd5f47-2158-4c43-ae0a-22e3b9073e7d', + 'client-request-id': '47cd5f47-2158-4c43-ae0a-22e3b9073e7d' + } + } + }; + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/me/onenote/notebooks`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { name: name, verbose: true } } as any), new CommandError(error.error.message)); + }); +}); diff --git a/src/m365/onenote/commands/notebook/notebook-add.ts b/src/m365/onenote/commands/notebook/notebook-add.ts new file mode 100644 index 00000000000..75366bf3620 --- /dev/null +++ b/src/m365/onenote/commands/notebook/notebook-add.ts @@ -0,0 +1,170 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { entraGroup } from '../../../../utils/entraGroup.js'; +import { validation } from '../../../../utils/validation.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; +import { spo } from '../../../../utils/spo.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + name: string; + userId?: string; + userName?: string; + groupId?: string; + groupName?: string; + webUrl?: string; +} + +class OneNoteNotebookAddCommand extends GraphCommand { + public get name(): string { + return commands.NOTEBOOK_ADD; + } + + public get description(): string { + return 'Create a new OneNote notebook'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + userId: typeof args.options.userId !== 'undefined', + userName: typeof args.options.userName !== 'undefined', + groupId: typeof args.options.groupId !== 'undefined', + groupName: typeof args.options.groupName !== 'undefined', + webUrl: typeof args.options.webUrl !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-n, --name ' + }, + { + option: '--userId [userId]' + }, + { + option: '--userName [userName]' + }, + { + option: '--groupId [groupId]' + }, + { + option: '--groupName [groupName]' + }, + { + option: '-u, --webUrl [webUrl]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + // check name for invalid characters + if (args.options.name.length > 128) { + return 'The specified name is too long. It should be less than 128 characters'; + } + + if (/[?*/:<>|'"]/.test(args.options.name)) { + return `The specified name contains invalid characters. It cannot contain ?*/:<>|'". Please remove them and try again.`; + } + + if (args.options.userId && !validation.isValidGuid(args.options.userId as string)) { + return `${args.options.userId} is not a valid GUID`; + } + + if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { + return `${args.options.groupId} is not a valid GUID`; + } + + if (args.options.webUrl) { + return validation.isValidSharePointUrl(args.options.webUrl); + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ + options: ['userId', 'userName', 'groupId', 'groupName', 'webUrl'], + runsWhen: (args) => { + const options = [args.options.userId, args.options.userName, args.options.groupId, args.options.groupName, args.options.webUrl]; + return options.some(item => item !== undefined); + } + }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + if (this.verbose) { + await logger.logToStderr(`Creating OneNote notebook ${args.options.name}`); + } + + const requestUrl = await this.getRequestUrl(args); + const requestOptions: CliRequestOptions = { + url: requestUrl, + headers: { + accept: 'application/json;odata.metadata=none', + 'content-type': "application/json" + }, + responseType: 'json', + data: { + displayName: args.options.name + } + }; + + const response = await request.post(requestOptions); + await logger.log(response); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private async getRequestUrl(args: CommandArgs): Promise { + let endpoint: string = `${this.resource}/v1.0/`; + + if (args.options.userId) { + endpoint += `users/${args.options.userId}`; + } + else if (args.options.userName) { + endpoint += `users/${args.options.userName}`; + } + else if (args.options.groupId) { + endpoint += `groups/${args.options.groupId}`; + } + else if (args.options.groupName) { + const groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupName); + endpoint += `groups/${groupId}`; + } + else if (args.options.webUrl) { + const siteId = await spo.getSpoGraphSiteId(args.options.webUrl); + endpoint += `sites/${siteId}`; + } + else { + endpoint += 'me'; + } + endpoint += '/onenote/notebooks'; + return endpoint; + } +} + +export default new OneNoteNotebookAddCommand(); \ No newline at end of file