@adobe/helix-onedrive-support 5.2.2 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ {
2
+ "reporterEnabled": "spec,xunit",
3
+ "xunitReporterOptions": {
4
+ "output": "junit/test-results.xml"
5
+ }
6
+ }
package/.nycrc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "reporter": [
3
+ "lcov",
4
+ "text"
5
+ ],
6
+ "check-coverage": true,
7
+ "lines": 15,
8
+ "branches": 19,
9
+ "statements": 15
10
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## [6.1.1](https://github.com/adobe/helix-onedrive-support/compare/v6.1.0...v6.1.1) (2022-02-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update dependency @adobe/helix-fetch to v3.0.2 ([3c3a2f2](https://github.com/adobe/helix-onedrive-support/commit/3c3a2f2c70bc4e69a66b8a2558add6154289eaee))
7
+
8
+ # [6.1.0](https://github.com/adobe/helix-onedrive-support/compare/v6.0.1...v6.1.0) (2022-01-28)
9
+
10
+
11
+ ### Features
12
+
13
+ * add API for SP API ([#227](https://github.com/adobe/helix-onedrive-support/issues/227)) ([6ce5938](https://github.com/adobe/helix-onedrive-support/commit/6ce59387a777d4549964a8fb90b2e0ca2a4fd86e))
14
+
15
+ ## [6.0.1](https://github.com/adobe/helix-onedrive-support/compare/v6.0.0...v6.0.1) (2021-10-18)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **deps:** update dependency @adobe/helix-fetch to v3 ([#202](https://github.com/adobe/helix-onedrive-support/issues/202)) ([1b0b586](https://github.com/adobe/helix-onedrive-support/commit/1b0b58661458550a3d6b782ada364f2f48d69fb0))
21
+
22
+ # [6.0.0](https://github.com/adobe/helix-onedrive-support/compare/v5.2.2...v6.0.0) (2021-10-08)
23
+
24
+
25
+ * Support conflict behaviour (#197) ([b4a5c72](https://github.com/adobe/helix-onedrive-support/commit/b4a5c72ed76062c558ea175aaedff3475e892c34)), closes [#197](https://github.com/adobe/helix-onedrive-support/issues/197)
26
+
27
+
28
+ ### BREAKING CHANGES
29
+
30
+ * uploadDriveItem return value changed
31
+
32
+ * fix: test behaviour values
33
+
1
34
  ## [5.2.2](https://github.com/adobe/helix-onedrive-support/compare/v5.2.1...v5.2.2) (2021-10-08)
2
35
 
3
36
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adobe/helix-onedrive-support",
3
- "version": "5.2.2",
3
+ "version": "6.1.1",
4
4
  "description": "Helix OneDrive Support",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "scripts": {
8
- "test": " nyc --reporter=text --reporter=lcov --check-coverage --branches 19 --statements 15 --lines 15 mocha",
9
- "test-ci": "nyc --reporter=text --reporter=lcov --check-coverage --branches 19 --statements 15 --lines 15 mocha --reporter xunit --reporter-options output=./junit/test-results.xml && codecov",
8
+ "test": "nyc mocha",
9
+ "test-ci": "nyc mocha && codecov",
10
10
  "lint": "./node_modules/.bin/eslint .",
11
11
  "semantic-release": "semantic-release",
12
12
  "docs": "npx jsdoc2md -c .jsdoc.json --files 'src/*.js' > docs/API.md",
@@ -23,28 +23,29 @@
23
23
  },
24
24
  "homepage": "https://github.com/adobe/helix-onedrive-support#readme",
25
25
  "dependencies": {
26
- "@adobe/helix-fetch": "2.4.2",
26
+ "@adobe/helix-fetch": "3.0.2",
27
27
  "adal-node": "https://github.com/adobe-rnd/azure-activedirectory-library-for-nodejs.git#adobe"
28
28
  },
29
29
  "devDependencies": {
30
- "@adobe/eslint-config-helix": "1.2.0",
31
- "@semantic-release/changelog": "6.0.0",
32
- "@semantic-release/git": "10.0.0",
33
- "ajv": "8.6.3",
30
+ "@adobe/eslint-config-helix": "1.3.2",
31
+ "@semantic-release/changelog": "6.0.1",
32
+ "@semantic-release/git": "10.0.1",
33
+ "ajv": "8.9.0",
34
34
  "codecov": "3.8.3",
35
35
  "commitizen": "4.2.4",
36
36
  "cz-conventional-changelog": "3.3.0",
37
- "dotenv": "10.0.0",
38
- "eslint": "7.32.0",
37
+ "dotenv": "15.0.0",
38
+ "eslint": "8.8.0",
39
39
  "eslint-plugin-header": "3.1.1",
40
- "eslint-plugin-import": "2.24.2",
41
- "jsdoc-to-markdown": "7.0.1",
40
+ "eslint-plugin-import": "2.25.4",
41
+ "jsdoc-to-markdown": "7.1.1",
42
42
  "junit-report-builder": "3.0.0",
43
- "lint-staged": "11.1.2",
44
- "mocha": "9.1.2",
45
- "nock": "13.1.3",
43
+ "lint-staged": "12.3.2",
44
+ "mocha": "9.2.0",
45
+ "mocha-multi-reporters": "1.5.1",
46
+ "nock": "13.2.2",
46
47
  "nyc": "15.1.0",
47
- "semantic-release": "18.0.0"
48
+ "semantic-release": "19.0.2"
48
49
  },
49
50
  "lint-staged": {
50
51
  "*.js": "eslint"
@@ -56,5 +57,11 @@
56
57
  "ghooks": {
57
58
  "pre-commit": "npx lint-staged"
58
59
  }
60
+ },
61
+ "mocha": {
62
+ "spec": "test/**/*.test.js",
63
+ "require": "test/setup-env.js",
64
+ "reporter": "mocha-multi-reporters",
65
+ "reporter-options": "configFile=.mocha-multi.json"
59
66
  }
60
67
  }
package/src/OneDrive.d.ts CHANGED
@@ -60,6 +60,36 @@ export declare interface SubscriptionOptions {
60
60
  expiresIn?: number;
61
61
  }
62
62
 
63
+ export declare interface SharePointSite {
64
+ /**
65
+ * Return a file's properties.
66
+ * @param file file name
67
+ * @returns file properties
68
+ */
69
+ getFile(file: string): Promise<GraphResult>;
70
+
71
+ /**
72
+ * Return a folder's properties.
73
+ * @param folder folder name
74
+ * @returns folder properties
75
+ */
76
+ getFolder(folder: string): Promise<GraphResult>;
77
+
78
+ /**
79
+ * Return a file's contents, as a binary buffer.
80
+ * @param file file name
81
+ * @returns file contents
82
+ */
83
+ getFileContents(file: string): Promise<Buffer>;
84
+
85
+ /**
86
+ * Returns a list of children items in a folder
87
+ * @param folder folder name
88
+ * @returns list of files and folders
89
+ */
90
+ getFilesAndFolders(folder: string): Promise<GraphResult>;
91
+ }
92
+
63
93
  /**
64
94
  * Helper class that facilitates accessing one drive.
65
95
  */
@@ -203,7 +233,15 @@ export declare class OneDrive extends EventEmitter {
203
233
 
204
234
  deleteSubscription(id: string): Promise<GraphResult>;
205
235
 
206
- uploadDriveItem(buffer: Buffer, driveItem: string, relPath?: string);
236
+ /**
237
+ * Uploads a drive item.
238
+ *
239
+ * @param buffer contents of file
240
+ * @param driveItem parent item in combination with relPath or item itself
241
+ * @param relPath relative path
242
+ * @param conflictBehaviour replace, rename or fail, default is replace
243
+ */
244
+ uploadDriveItem(buffer: Buffer, driveItem: string, relPath?: string, conflictBehaviour?: string);
207
245
 
208
246
  /**
209
247
  * Returns the root item for a drive given its id.
@@ -220,4 +258,11 @@ export declare class OneDrive extends EventEmitter {
220
258
  * @returns {Promise<Array>} A return object with the values and a `@odata.deltaLink`.
221
259
  */
222
260
  fetchChanges(resource: string, token?: string);
261
+
262
+ /**
263
+ * Returns a site object exposing the SharePoint API (now called Graph API V1).
264
+ * @param siteURL site URL, in the format https://<tenant>.sharepoint.com/sites/<site>
265
+ * @return {Promise<SharePointSite} site object
266
+ */
267
+ getSite(siteURL: string): Promise<SharePointSite>;
223
268
  }
package/src/OneDrive.js CHANGED
@@ -20,6 +20,7 @@ const Workbook = require('./Workbook.js');
20
20
  const StatusCodeError = require('./StatusCodeError.js');
21
21
  const { driveItemFromURL, driveItemToURL } = require('./utils.js');
22
22
  const { splitByExtension, sanitize, editDistance } = require('./fuzzy-helper.js');
23
+ const SharePointSite = require('./SharePointSite.js');
23
24
 
24
25
  const { fetch, reset } = process.env.HELIX_FETCH_FORCE_HTTP1
25
26
  ? fetchAPI.context({
@@ -204,18 +205,26 @@ class OneDrive extends EventEmitter {
204
205
  if (this.refreshToken) {
205
206
  log.debug('acquire token with refresh token.');
206
207
  const resp = await context.acquireTokenWithRefreshToken(
207
- this.refreshToken, this.clientId, this.clientSecret, AZ_RESOURCE,
208
+ this.refreshToken,
209
+ this.clientId,
210
+ this.clientSecret,
211
+ AZ_RESOURCE,
208
212
  );
209
213
  return await this.augmentAndCacheResponse(resp);
210
214
  } else if (this.username && this.password) {
211
215
  log.debug('acquire token with ROPC.');
212
216
  return await context.acquireTokenWithUsernamePassword(
213
- AZ_RESOURCE, this.username, this.password, this.clientId,
217
+ AZ_RESOURCE,
218
+ this.username,
219
+ this.password,
220
+ this.clientId,
214
221
  );
215
222
  } else if (this.clientSecret) {
216
223
  log.debug('acquire token with client credentials.');
217
224
  return await context.acquireTokenWithClientCredentials(
218
- AZ_RESOURCE, this.clientId, this.clientSecret,
225
+ AZ_RESOURCE,
226
+ this.clientId,
227
+ this.clientSecret,
219
228
  );
220
229
  } else {
221
230
  const err = new StatusCodeError('No valid authentication credentials supplied.');
@@ -259,7 +268,11 @@ class OneDrive extends EventEmitter {
259
268
  const { log, authContext: context } = this;
260
269
  try {
261
270
  const resp = await context.acquireTokenWithAuthorizationCode(
262
- code, redirectUri, AZ_RESOURCE, this.clientId, this.clientSecret,
271
+ code,
272
+ redirectUri,
273
+ AZ_RESOURCE,
274
+ this.clientId,
275
+ this.clientSecret,
263
276
  );
264
277
  return await this.augmentAndCacheResponse(resp);
265
278
  } catch (e) {
@@ -489,7 +502,15 @@ class OneDrive extends EventEmitter {
489
502
  /**
490
503
  * @see https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0&tabs=http
491
504
  */
492
- async uploadDriveItem(buffer, driveItem, relPath = '') {
505
+ async uploadDriveItem(buffer, driveItem, relPath = '', conflictBehaviour = 'replace') {
506
+ const validConflictBehaviours = [
507
+ 'replace',
508
+ 'rename',
509
+ 'fail',
510
+ ];
511
+ if (!validConflictBehaviours.includes(conflictBehaviour)) {
512
+ throw new Error(`Bad confict behaviour: ${conflictBehaviour}, must be one of: ${validConflictBehaviours.join('/')}`);
513
+ }
493
514
  // eslint-disable-next-line no-param-reassign
494
515
  relPath = relPath.replace(/\/+$/, '');
495
516
  if (relPath) {
@@ -498,7 +519,7 @@ class OneDrive extends EventEmitter {
498
519
  }
499
520
 
500
521
  // PUT /drives/{drive-id}/items/{parent-id}:/{filename}:/content
501
- const uri = `/drives/${driveItem.parentReference.driveId}/items/${driveItem.id}${relPath}/content`;
522
+ const uri = `/drives/${driveItem.parentReference.driveId}/items/${driveItem.id}${relPath}/content?@microsoft.graph.conflictBehavior=${conflictBehaviour}`;
502
523
  const opts = {
503
524
  method: 'PUT',
504
525
  body: buffer,
@@ -506,7 +527,7 @@ class OneDrive extends EventEmitter {
506
527
  'Content-Type': 'application/octet-stream',
507
528
  },
508
529
  };
509
- return this.doFetch(uri, true, opts);
530
+ return this.doFetch(uri, false, opts);
510
531
  }
511
532
 
512
533
  /**
@@ -603,6 +624,35 @@ class OneDrive extends EventEmitter {
603
624
  }
604
625
  }
605
626
  }
627
+
628
+ async getSite(siteURL) {
629
+ this.log.debug(`getting site: (${siteURL})`);
630
+
631
+ const match = siteURL.match(/^https:\/\/(\S+).sharepoint.com\/sites\/([^/]+)\/(\S+)$/);
632
+ if (!match) {
633
+ throw new Error(`Site URL does not match (*.sharepoint.com/sites/.*): ${match}`);
634
+ }
635
+ const [, owner, site, root] = match;
636
+
637
+ try {
638
+ const accessToken = await this.getAccessToken();
639
+ return new SharePointSite({
640
+ owner,
641
+ site,
642
+ root,
643
+ clientId: this.clientId,
644
+ tenantId: accessToken.tenantId,
645
+ refreshToken: accessToken.refreshToken,
646
+ log: this.log,
647
+ });
648
+ } catch (e) {
649
+ if (e.statusCode === 401) {
650
+ // an inexistant share returns 401, we prefer to just say it wasn't found
651
+ throw new StatusCodeError(e.message, 404, e.details);
652
+ }
653
+ throw e;
654
+ }
655
+ }
606
656
  }
607
657
 
608
658
  module.exports = Object.assign(OneDrive, {
@@ -0,0 +1,139 @@
1
+ /*
2
+ * Copyright 2021 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ const fetchAPI = require('@adobe/helix-fetch');
14
+ const StatusCodeError = require('./StatusCodeError.js');
15
+
16
+ /* istanbul ignore next */
17
+ const { fetch } = process.env.HELIX_FETCH_FORCE_HTTP1
18
+ ? fetchAPI.context({
19
+ alpnProtocols: [fetchAPI.ALPN_HTTP1_1],
20
+ userAgent: 'helix-fetch', // static user agent for test recordings
21
+ })
22
+ /* istanbul ignore next */
23
+ : fetchAPI;
24
+
25
+ /**
26
+ * Helper class accessing folders and files using the SharePoint V1 API.
27
+ */
28
+ class SharePointSite {
29
+ constructor(opts) {
30
+ this._owner = opts.owner;
31
+ this._site = opts.site;
32
+ this._clientId = opts.clientId;
33
+ this._tenantId = opts.tenantId;
34
+ this._refreshToken = opts.refreshToken;
35
+ this._root = opts.root || '';
36
+ this._log = opts.log || console;
37
+ }
38
+
39
+ async getAccessToken() {
40
+ const { log } = this;
41
+ if (!this._accessToken || Date.now() >= this._expires) {
42
+ const url = `https://login.microsoftonline.com/${this._tenantId}/oauth2/v2.0/token`;
43
+ const resp = await fetch(url, {
44
+ method: 'POST',
45
+ body: new URLSearchParams({
46
+ client_id: this._clientId,
47
+ refresh_token: this._refreshToken,
48
+ grant_type: 'refresh_token',
49
+ scope: `https://${this._owner}.sharepoint.com/Sites.ReadWrite.All`,
50
+ }),
51
+ });
52
+ if (!resp.ok) {
53
+ const text = await resp.text();
54
+ log.error(`Error while getting a SharePoint API token: ${text}}`);
55
+ throw new StatusCodeError(text, resp.status);
56
+ }
57
+ const json = await resp.json();
58
+ this._accessToken = json.access_token;
59
+ this._expires = Date.now() + json.expires_in * 1000;
60
+ }
61
+ return this._accessToken;
62
+ }
63
+
64
+ _splitDirAndBase(file) {
65
+ const idx = file.lastIndexOf('/');
66
+ const [dir, base] = (idx < 0)
67
+ ? ['', file]
68
+ : [file.substring(0, idx), file.substring(idx + 1)];
69
+ return dir ? [`${this._root}/${dir}`, base] : [this._root, base];
70
+ }
71
+
72
+ _getRelativePath(folder) {
73
+ return folder ? `${this._root}/${folder}` : this._root;
74
+ }
75
+
76
+ async getFile(file) {
77
+ const [dir, base] = this._splitDirAndBase(file);
78
+ return this.doFetch(`/GetFolderByServerRelativeUrl('${dir}')/Files('${base}')?$expand=ModifiedBy`);
79
+ }
80
+
81
+ async getFolder(folder) {
82
+ const dir = this._getRelativePath(folder);
83
+ return this.doFetch(`/GetFolderByServerRelativeUrl('${dir}')`);
84
+ }
85
+
86
+ async getFileContents(file) {
87
+ const [dir, base] = this._splitDirAndBase(file);
88
+ return this.doFetch(`/GetFolderByServerRelativeUrl('${dir}')/Files('${base}')/$value`, true);
89
+ }
90
+
91
+ async getFilesAndFolders(folder) {
92
+ const dir = this._getRelativePath(folder);
93
+ return this.doFetch(`/GetFolderByServerRelativeUrl('${dir}')?$expand=Files/ModifiedBy,Folders`);
94
+ }
95
+
96
+ async doFetch(relUrl, rawResponseBody = false) {
97
+ const opts = { headers: {} };
98
+ const accessToken = await this.getAccessToken();
99
+ opts.headers.authorization = `Bearer ${accessToken}`;
100
+ if (!rawResponseBody) {
101
+ opts.headers.accept = 'application/json;odata=verbose';
102
+ }
103
+
104
+ const url = `https://${this._owner}.sharepoint.com/sites/${this._site}/_api/web${relUrl}`;
105
+ try {
106
+ const resp = await fetch(url, opts);
107
+ if (!resp.ok) {
108
+ const text = await resp.text();
109
+ let err;
110
+ try {
111
+ // try to parse json
112
+ err = StatusCodeError.fromErrorResponse(JSON.parse(text), resp.status);
113
+ } catch {
114
+ err = new StatusCodeError(text, resp.status);
115
+ }
116
+ throw err;
117
+ }
118
+ // check content type before trying to parse a response body as JSON
119
+ const contentType = resp.headers.get('content-type');
120
+ const json = contentType && contentType.startsWith('application/json');
121
+
122
+ // await result in order to be able to catch any error
123
+ return await (rawResponseBody || !json ? resp.buffer() : resp.json());
124
+ } catch (e) {
125
+ /* istanbul ignore else */
126
+ if (e instanceof StatusCodeError) {
127
+ throw e;
128
+ }
129
+ /* istanbul ignore next */
130
+ throw StatusCodeError.fromError(e);
131
+ }
132
+ }
133
+
134
+ get log() {
135
+ return this._log;
136
+ }
137
+ }
138
+
139
+ module.exports = SharePointSite;
@@ -58,7 +58,7 @@ class StatusCodeError extends Error {
58
58
  * @param {object} details underlying error
59
59
  */
60
60
  constructor(msg, statusCode, details) {
61
- super(msg);
61
+ super(msg?.value ?? msg);
62
62
  this.statusCode = statusCode;
63
63
  this.details = details;
64
64
  }