@adobe/helix-onedrive-support 6.0.1 → 6.1.0

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,10 @@
1
+ # [6.1.0](https://github.com/adobe/helix-onedrive-support/compare/v6.0.1...v6.1.0) (2022-01-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * 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))
7
+
1
8
  ## [6.0.1](https://github.com/adobe/helix-onedrive-support/compare/v6.0.0...v6.0.1) (2021-10-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adobe/helix-onedrive-support",
3
- "version": "6.0.1",
3
+ "version": "6.1.0",
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",
@@ -27,24 +27,25 @@
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.3.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": "14.2.0",
38
+ "eslint": "8.7.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.0",
42
42
  "junit-report-builder": "3.0.0",
43
- "lint-staged": "11.2.2",
44
- "mocha": "9.1.2",
45
- "nock": "13.1.3",
43
+ "lint-staged": "12.3.1",
44
+ "mocha": "9.1.4",
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
  */
@@ -228,4 +258,11 @@ export declare class OneDrive extends EventEmitter {
228
258
  * @returns {Promise<Array>} A return object with the values and a `@odata.deltaLink`.
229
259
  */
230
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>;
231
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) {
@@ -611,6 +624,35 @@ class OneDrive extends EventEmitter {
611
624
  }
612
625
  }
613
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
+ }
614
656
  }
615
657
 
616
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
  }