Add PBandGraphOauth scaffold and auth UI

This commit is contained in:
2025-12-19 05:42:29 +00:00
commit b51963f5d7
6264 changed files with 606430 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module Client
*/
import { GRAPH_API_VERSION, GRAPH_BASE_URL } from "./Constants";
import { CustomAuthenticationProvider } from "./CustomAuthenticationProvider";
import { GraphRequest } from "./GraphRequest";
import { HTTPClient } from "./HTTPClient";
import { HTTPClientFactory } from "./HTTPClientFactory";
import { ClientOptions } from "./IClientOptions";
import { Options } from "./IOptions";
import { validatePolyFilling } from "./ValidatePolyFilling";
export class Client {
/**
* @private
* A member which stores the Client instance options
*/
private config: ClientOptions = {
baseUrl: GRAPH_BASE_URL,
debugLogging: false,
defaultVersion: GRAPH_API_VERSION,
};
/**
* @private
* A member which holds the HTTPClient instance
*/
private httpClient: HTTPClient;
/**
* @public
* @static
* To create a client instance with options and initializes the default middleware chain
* @param {Options} options - The options for client instance
* @returns The Client instance
*/
public static init(options: Options): Client {
const clientOptions: ClientOptions = {};
for (const i in options) {
if (Object.prototype.hasOwnProperty.call(options, i)) {
clientOptions[i] = i === "authProvider" ? new CustomAuthenticationProvider(options[i]) : options[i];
}
}
return Client.initWithMiddleware(clientOptions);
}
/**
* @public
* @static
* To create a client instance with the Client Options
* @param {ClientOptions} clientOptions - The options object for initializing the client
* @returns The Client instance
*/
public static initWithMiddleware(clientOptions: ClientOptions): Client {
return new Client(clientOptions);
}
/**
* @private
* @constructor
* Creates an instance of Client
* @param {ClientOptions} clientOptions - The options to instantiate the client object
*/
private constructor(clientOptions: ClientOptions) {
validatePolyFilling();
for (const key in clientOptions) {
if (Object.prototype.hasOwnProperty.call(clientOptions, key)) {
this.config[key] = clientOptions[key];
}
}
let httpClient: HTTPClient;
if (clientOptions.authProvider !== undefined && clientOptions.middleware !== undefined) {
const error = new Error();
error.name = "AmbiguityInInitialization";
error.message = "Unable to Create Client, Please provide either authentication provider for default middleware chain or custom middleware chain not both";
throw error;
} else if (clientOptions.authProvider !== undefined) {
httpClient = HTTPClientFactory.createWithAuthenticationProvider(clientOptions.authProvider);
} else if (clientOptions.middleware !== undefined) {
httpClient = new HTTPClient(...[].concat(clientOptions.middleware));
} else {
const error = new Error();
error.name = "InvalidMiddlewareChain";
error.message = "Unable to Create Client, Please provide either authentication provider for default middleware chain or custom middleware chain";
throw error;
}
this.httpClient = httpClient;
}
/**
* @public
* Entry point to make requests
* @param {string} path - The path string value
* @returns The graph request instance
*/
public api(path: string): GraphRequest {
return new GraphRequest(this.httpClient, this.config, path);
}
}
+28
View File
@@ -0,0 +1,28 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module Constants
*/
/**
* @constant
* A Default API endpoint version for a request
*/
export const GRAPH_API_VERSION = "v1.0";
/**
* @constant
* A Default base url for a request
*/
export const GRAPH_BASE_URL = "https://graph.microsoft.com/";
/**
* To hold list of the service root endpoints for Microsoft Graph and Graph Explorer for each national cloud.
* Set(iterable:Object) is not supported in Internet Explorer. The consumer is recommended to use a suitable polyfill.
*/
export const GRAPH_URLS = new Set<string>(["graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", "graph.microsoft.de", "microsoftgraph.chinacloudapi.cn", "canary.graph.microsoft.com"]);
@@ -0,0 +1,63 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module CustomAuthenticationProvider
*/
import { GraphClientError } from "./GraphClientError";
import { AuthenticationProvider } from "./IAuthenticationProvider";
import { AuthProvider } from "./IAuthProvider";
/**
* @class
* Class representing CustomAuthenticationProvider
* @extends AuthenticationProvider
*/
export class CustomAuthenticationProvider implements AuthenticationProvider {
/**
* @private
* A member to hold authProvider callback
*/
private provider: AuthProvider;
/**
* @public
* @constructor
* Creates an instance of CustomAuthenticationProvider
* @param {AuthProviderCallback} provider - An authProvider function
* @returns An instance of CustomAuthenticationProvider
*/
public constructor(provider: AuthProvider) {
this.provider = provider;
}
/**
* @public
* @async
* To get the access token
* @returns The promise that resolves to an access token
*/
public async getAccessToken(): Promise<any> {
return new Promise((resolve: (accessToken: string) => void, reject: (error: any) => void) => {
this.provider(async (error: any, accessToken: string | null) => {
if (accessToken) {
resolve(accessToken);
} else {
if (!error) {
const invalidTokenMessage = "Access token is undefined or empty.\
Please provide a valid token.\
For more help - https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CustomAuthenticationProvider.md";
error = new GraphClientError(invalidTokenMessage);
}
const err = await GraphClientError.setGraphClientError(error);
reject(err);
}
});
});
}
}
+61
View File
@@ -0,0 +1,61 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphClientError
*/
/**
* @class
* Create GraphClientError object to handle client-side errors
* encountered within the JavaScript Client SDK.
* Whereas GraphError Class should be used to handle errors in the response from the Graph API.
*/
export class GraphClientError extends Error {
/**
* @public
* A custom error. This property should set be when the error is not of instanceOf Error/GraphClientError.
* Example =
* const client = MicrosoftGraph.Client.init({
* defaultVersion: "v1.0",
* authProvider: (done) => { done({TokenError:"AccessToken cannot be null"}, "<ACCESS_TOKEN>");
* });
*/
public customError?: any;
/**
* @public
* @static
* @async
* To set the GraphClientError object
* @param {any} error - The error returned encountered by the Graph JavaScript Client SDK while processing request
* @returns GraphClientError object set to the error passed
*/
public static setGraphClientError(error: any): GraphClientError {
let graphClientError: GraphClientError;
if (error instanceof Error) {
graphClientError = error;
} else {
graphClientError = new GraphClientError();
graphClientError.customError = error;
}
return graphClientError;
}
/**
* @public
* @constructor
* Creates an instance of GraphClientError
* @param {string} message? - Error message
* @returns An instance of GraphClientError
*/
public constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, GraphClientError.prototype);
}
}
+73
View File
@@ -0,0 +1,73 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphError
*/
/**
* @class
* Class for GraphError
* @NOTE: This is NOT what is returned from the Graph
* GraphError is created from parsing JSON errors returned from the graph
* Some fields are renamed ie, "request-id" => requestId so you can use dot notation
*/
export class GraphError extends Error {
/**
* @public
* A member holding status code of the error
*/
public statusCode: number;
/**
* @public
* A member holding code i.e name of the error
*/
public code: string | null;
/**
* @public
* A member holding request-id i.e identifier of the request
*/
public requestId: string | null;
/**
* @public
* A member holding processed date and time of the request
*/
public date: Date;
public headers?: Headers;
/**
* @public
* A member holding original error response by the graph service
*/
public body: any;
/**
* @public
* @constructor
* Creates an instance of GraphError
* @param {number} [statusCode = -1] - The status code of the error
* @param {string} [message] - The message of the error
* @param {Error} [baseError] - The base error
* @returns An instance of GraphError
*/
public constructor(statusCode = -1, message?: string, baseError?: Error) {
super(message || (baseError && baseError.message));
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, GraphError.prototype);
this.statusCode = statusCode;
this.code = null;
this.requestId = null;
this.date = new Date();
this.body = null;
this.stack = baseError ? baseError.stack : this.stack;
}
}
+117
View File
@@ -0,0 +1,117 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphErrorHandler
*/
import { GraphError } from "./GraphError";
import { GraphRequestCallback } from "./IGraphRequestCallback";
/**
* @interface
* Signature for the json represent of the error response from the Graph API
* https://docs.microsoft.com/en-us/graph/errors
* @property {[key: string] : string | number} - The Key value pair
*/
interface GraphAPIErrorResponse {
error: {
code: string;
message: string;
innerError: any;
};
}
/**
* @class
* Class for GraphErrorHandler
*/
export class GraphErrorHandler {
/**
* @private
* @static
* Populates the GraphError instance with Error instance values
* @param {Error} error - The error returned by graph service or some native error
* @param {number} [statusCode] - The status code of the response
* @returns The GraphError instance
*/
private static constructError(error: Error, statusCode?: number, rawResponse?: Response): GraphError {
const gError = new GraphError(statusCode, "", error);
if (error.name !== undefined) {
gError.code = error.name;
}
gError.body = error.toString();
gError.date = new Date();
gError.headers = rawResponse?.headers;
return gError;
}
/**
* @private
* @static
* @async
* Populates the GraphError instance from the Error returned by graph service
* @param {GraphAPIErrorResponse} graphError - The error possibly returned by graph service or some native error
* @param {number} statusCode - The status code of the response
* @returns A promise that resolves to GraphError instance
*
* Example error for https://graph.microsoft.com/v1.0/me/events?$top=3&$search=foo
* {
* "error": {
* "code": "SearchEvents",
* "message": "The parameter $search is not currently supported on the Events resource.",
* "innerError": {
* "request-id": "b31c83fd-944c-4663-aa50-5d9ceb367e19",
* "date": "2016-11-17T18:37:45"
* }
* }
* }
*/
private static constructErrorFromResponse(graphError: GraphAPIErrorResponse, statusCode: number, rawResponse?: Response): GraphError {
const error = graphError.error;
const gError = new GraphError(statusCode, error.message);
gError.code = error.code;
if (error.innerError !== undefined) {
gError.requestId = error.innerError["request-id"];
gError.date = new Date(error.innerError.date);
}
gError.body = JSON.stringify(error);
gError.headers = rawResponse?.headers;
return gError;
}
/**
* @public
* @static
* @async
* To get the GraphError object
* Reference - https://docs.microsoft.com/en-us/graph/errors
* @param {any} [error = null] - The error returned by graph service or some native error
* @param {number} [statusCode = -1] - The status code of the response
* @param {GraphRequestCallback} [callback] - The graph request callback function
* @returns A promise that resolves to GraphError instance
*/
public static async getError(error: any = null, statusCode = -1, callback?: GraphRequestCallback, rawResponse?: Response): Promise<GraphError> {
let gError: GraphError;
if (error && error.error) {
gError = GraphErrorHandler.constructErrorFromResponse(error, statusCode, rawResponse);
} else if (error instanceof Error) {
gError = GraphErrorHandler.constructError(error, statusCode, rawResponse);
} else {
gError = new GraphError(statusCode);
gError.body = error; // if a custom error is passed which is not instance of Error object or a graph API response
}
if (typeof callback === "function") {
callback(gError, null);
} else {
return gError;
}
}
}
+785
View File
@@ -0,0 +1,785 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphRequest
*/
import { GraphClientError } from "./GraphClientError";
import { GraphError } from "./GraphError";
import { GraphErrorHandler } from "./GraphErrorHandler";
import { oDataQueryNames, serializeContent, urlJoin } from "./GraphRequestUtil";
import { GraphResponseHandler } from "./GraphResponseHandler";
import { HTTPClient } from "./HTTPClient";
import { ClientOptions } from "./IClientOptions";
import { Context } from "./IContext";
import { FetchOptions } from "./IFetchOptions";
import { GraphRequestCallback } from "./IGraphRequestCallback";
import { MiddlewareControl } from "./middleware/MiddlewareControl";
import { MiddlewareOptions } from "./middleware/options/IMiddlewareOptions";
import { RequestMethod } from "./RequestMethod";
import { ResponseType } from "./ResponseType";
/**
* @interface
* Signature to representing key value pairs
* @property {[key: string] : string | number} - The Key value pair
*/
interface KeyValuePairObjectStringNumber {
[key: string]: string | number;
}
/**
* @interface
* Signature to define URL components
* @template http://graph.microsoft.com/VERSION/PATH?QUERYSTRING&OTHER_QUERY_PARAMS
*
* @property {string} host - The host to which the request needs to be made
* @property {string} version - Version of the graph endpoint
* @property {string} [path] - The path of the resource request
* @property {KeyValuePairObjectStringNumber} oDataQueryParams - The oData Query Params
* @property {KeyValuePairObjectStringNumber} otherURLQueryParams - The other query params for a request
* @property {string[]} otherURLQueryOptions - The non key-value query parameters. Example- '/me?$whatif'
*/
export interface URLComponents {
host: string;
version: string;
path?: string;
oDataQueryParams: KeyValuePairObjectStringNumber;
otherURLQueryParams: KeyValuePairObjectStringNumber;
otherURLQueryOptions?: string[];
}
/**
* @class
* A Class representing GraphRequest
*/
export class GraphRequest {
/**
* @private
* A member variable to hold HTTPClient instance
*/
private httpClient: HTTPClient;
/**
* @private
* A member variable to hold client options
*/
private config: ClientOptions;
/**
* @private
* A member to hold URL Components data
*/
private urlComponents: URLComponents;
/**
* @private
* A member to hold custom header options for a request
*/
private _headers: HeadersInit;
/**
* @private
* A member to hold custom options for a request
*/
private _options: FetchOptions;
/**
* @private
* A member to hold the array of middleware options for a request
*/
private _middlewareOptions: MiddlewareOptions[];
/**
* @private
* A member to hold custom response type for a request
*/
private _responseType: ResponseType;
/**
* @public
* @constructor
* Creates an instance of GraphRequest
* @param {HTTPClient} httpClient - The HTTPClient instance
* @param {ClientOptions} config - The options for making request
* @param {string} path - A path string
*/
public constructor(httpClient: HTTPClient, config: ClientOptions, path: string) {
this.httpClient = httpClient;
this.config = config;
this.urlComponents = {
host: this.config.baseUrl,
version: this.config.defaultVersion,
oDataQueryParams: {},
otherURLQueryParams: {},
otherURLQueryOptions: [],
};
this._headers = {};
this._options = {};
this._middlewareOptions = [];
this.parsePath(path);
}
/**
* @private
* Parses the path string and creates URLComponents out of it
* @param {string} path - The request path string
* @returns Nothing
*/
private parsePath = (path: string): void => {
// Strips out the base of the url if they passed in
if (path.indexOf("https://") !== -1) {
path = path.replace("https://", "");
// Find where the host ends
const endOfHostStrPos = path.indexOf("/");
if (endOfHostStrPos !== -1) {
// Parse out the host
this.urlComponents.host = "https://" + path.substring(0, endOfHostStrPos);
// Strip the host from path
path = path.substring(endOfHostStrPos + 1, path.length);
}
// Remove the following version
const endOfVersionStrPos = path.indexOf("/");
if (endOfVersionStrPos !== -1) {
// Parse out the version
this.urlComponents.version = path.substring(0, endOfVersionStrPos);
// Strip version from path
path = path.substring(endOfVersionStrPos + 1, path.length);
}
}
// Strip out any leading "/"
if (path.charAt(0) === "/") {
path = path.substr(1);
}
const queryStrPos = path.indexOf("?");
if (queryStrPos === -1) {
// No query string
this.urlComponents.path = path;
} else {
this.urlComponents.path = path.substr(0, queryStrPos);
// Capture query string into oDataQueryParams and otherURLQueryParams
const queryParams = path.substring(queryStrPos + 1, path.length).split("&");
for (const queryParam of queryParams) {
this.parseQueryParameter(queryParam);
}
}
};
/**
* @private
* Adds the query parameter as comma separated values
* @param {string} propertyName - The name of a property
* @param {string|string[]} propertyValue - The vale of a property
* @param {IArguments} additionalProperties - The additional properties
* @returns Nothing
*/
private addCsvQueryParameter(propertyName: string, propertyValue: string | string[], additionalProperties: IArguments): void {
// If there are already $propertyName value there, append a ","
this.urlComponents.oDataQueryParams[propertyName] = this.urlComponents.oDataQueryParams[propertyName] ? this.urlComponents.oDataQueryParams[propertyName] + "," : "";
let allValues: string[] = [];
if (additionalProperties.length > 1 && typeof propertyValue === "string") {
allValues = Array.prototype.slice.call(additionalProperties);
} else if (typeof propertyValue === "string") {
allValues.push(propertyValue);
} else {
allValues = allValues.concat(propertyValue);
}
this.urlComponents.oDataQueryParams[propertyName] += allValues.join(",");
}
/**
* @private
* Builds the full url from the URLComponents to make a request
* @returns The URL string that is qualified to make a request to graph endpoint
*/
private buildFullUrl(): string {
const url = urlJoin([this.urlComponents.host, this.urlComponents.version, this.urlComponents.path]) + this.createQueryString();
if (this.config.debugLogging) {
console.log(url);
}
return url;
}
/**
* @private
* Builds the query string from the URLComponents
* @returns The Constructed query string
*/
private createQueryString(): string {
// Combining query params from oDataQueryParams and otherURLQueryParams
const urlComponents = this.urlComponents;
const query: string[] = [];
if (Object.keys(urlComponents.oDataQueryParams).length !== 0) {
for (const property in urlComponents.oDataQueryParams) {
if (Object.prototype.hasOwnProperty.call(urlComponents.oDataQueryParams, property)) {
query.push(property + "=" + urlComponents.oDataQueryParams[property]);
}
}
}
if (Object.keys(urlComponents.otherURLQueryParams).length !== 0) {
for (const property in urlComponents.otherURLQueryParams) {
if (Object.prototype.hasOwnProperty.call(urlComponents.otherURLQueryParams, property)) {
query.push(property + "=" + urlComponents.otherURLQueryParams[property]);
}
}
}
if (urlComponents.otherURLQueryOptions.length !== 0) {
for (const str of urlComponents.otherURLQueryOptions) {
query.push(str);
}
}
return query.length > 0 ? "?" + query.join("&") : "";
}
/**
* @private
* Parses the query parameters to set the urlComponents property of the GraphRequest object
* @param {string|KeyValuePairObjectStringNumber} queryDictionaryOrString - The query parameter
* @returns The same GraphRequest instance that is being called with
*/
private parseQueryParameter(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest {
if (typeof queryDictionaryOrString === "string") {
if (queryDictionaryOrString.charAt(0) === "?") {
queryDictionaryOrString = queryDictionaryOrString.substring(1);
}
if (queryDictionaryOrString.indexOf("&") !== -1) {
const queryParams = queryDictionaryOrString.split("&");
for (const str of queryParams) {
this.parseQueryParamenterString(str);
}
} else {
this.parseQueryParamenterString(queryDictionaryOrString);
}
} else if (queryDictionaryOrString.constructor === Object) {
for (const key in queryDictionaryOrString) {
if (Object.prototype.hasOwnProperty.call(queryDictionaryOrString, key)) {
this.setURLComponentsQueryParamater(key, queryDictionaryOrString[key]);
}
}
}
return this;
}
/**
* @private
* Parses the query parameter of string type to set the urlComponents property of the GraphRequest object
* @param {string} queryParameter - the query parameters
* returns nothing
*/
private parseQueryParamenterString(queryParameter: string): void {
/* The query key-value pair must be split on the first equals sign to avoid errors in parsing nested query parameters.
Example-> "/me?$expand=home($select=city)" */
if (this.isValidQueryKeyValuePair(queryParameter)) {
const indexOfFirstEquals = queryParameter.indexOf("=");
const paramKey = queryParameter.substring(0, indexOfFirstEquals);
const paramValue = queryParameter.substring(indexOfFirstEquals + 1);
this.setURLComponentsQueryParamater(paramKey, paramValue);
} else {
/* Push values which are not of key-value structure.
Example-> Handle an invalid input->.query(test), .query($select($select=name)) and let the Graph API respond with the error in the URL*/
this.urlComponents.otherURLQueryOptions.push(queryParameter);
}
}
/**
* @private
* Sets values into the urlComponents property of GraphRequest object.
* @param {string} paramKey - the query parameter key
* @param {string} paramValue - the query paramter value
* @returns nothing
*/
private setURLComponentsQueryParamater(paramKey: string, paramValue: string | number): void {
if (oDataQueryNames.indexOf(paramKey) !== -1) {
const currentValue = this.urlComponents.oDataQueryParams[paramKey];
const isValueAppendable = currentValue && (paramKey === "$expand" || paramKey === "$select" || paramKey === "$orderby");
this.urlComponents.oDataQueryParams[paramKey] = isValueAppendable ? currentValue + "," + paramValue : paramValue;
} else {
this.urlComponents.otherURLQueryParams[paramKey] = paramValue;
}
}
/**
* @private
* Check if the query parameter string has a valid key-value structure
* @param {string} queryString - the query parameter string. Example -> "name=value"
* #returns true if the query string has a valid key-value structure else false
*/
private isValidQueryKeyValuePair(queryString: string): boolean {
const indexofFirstEquals = queryString.indexOf("=");
if (indexofFirstEquals === -1) {
return false;
}
const indexofOpeningParanthesis = queryString.indexOf("(");
if (indexofOpeningParanthesis !== -1 && queryString.indexOf("(") < indexofFirstEquals) {
// Example -> .query($select($expand=true));
return false;
}
return true;
}
/**
* @private
* Updates the custom headers and options for a request
* @param {FetchOptions} options - The request options object
* @returns Nothing
*/
private updateRequestOptions(options: FetchOptions): void {
const optionsHeaders: HeadersInit = { ...options.headers };
if (this.config.fetchOptions !== undefined) {
const fetchOptions: FetchOptions = { ...this.config.fetchOptions };
Object.assign(options, fetchOptions);
if (typeof this.config.fetchOptions.headers !== undefined) {
options.headers = { ...this.config.fetchOptions.headers };
}
}
Object.assign(options, this._options);
if (options.headers !== undefined) {
Object.assign(optionsHeaders, options.headers);
}
Object.assign(optionsHeaders, this._headers);
options.headers = optionsHeaders;
}
/**
* @private
* @async
* Adds the custom headers and options to the request and makes the HTTPClient send request call
* @param {RequestInfo} request - The request url string or the Request object value
* @param {FetchOptions} options - The options to make a request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the response content
*/
private async send(request: RequestInfo, options: FetchOptions, callback?: GraphRequestCallback): Promise<any> {
let rawResponse: Response;
const middlewareControl = new MiddlewareControl(this._middlewareOptions);
this.updateRequestOptions(options);
const customHosts = this.config?.customHosts;
try {
const context: Context = await this.httpClient.sendRequest({
request,
options,
middlewareControl,
customHosts,
});
rawResponse = context.response;
const response: any = await GraphResponseHandler.getResponse(rawResponse, this._responseType, callback);
return response;
} catch (error) {
if (error instanceof GraphClientError) {
throw error;
}
let statusCode: number;
if (rawResponse) {
statusCode = rawResponse.status;
}
const gError: GraphError = await GraphErrorHandler.getError(error, statusCode, callback, rawResponse);
throw gError;
}
}
/**
* @private
* Checks if the content-type is present in the _headers property. If not present, defaults the content-type to application/json
* @param none
* @returns nothing
*/
private setHeaderContentType(): void {
if (!this._headers) {
this.header("Content-Type", "application/json");
return;
}
const headerKeys = Object.keys(this._headers);
for (const headerKey of headerKeys) {
if (headerKey.toLowerCase() === "content-type") {
return;
}
}
// Default the content-type to application/json in case the content-type is not present in the header
this.header("Content-Type", "application/json");
}
/**
* @public
* Sets the custom header for a request
* @param {string} headerKey - A header key
* @param {string} headerValue - A header value
* @returns The same GraphRequest instance that is being called with
*/
public header(headerKey: string, headerValue: string): GraphRequest {
this._headers[headerKey] = headerValue;
return this;
}
/**
* @public
* Sets the custom headers for a request
* @param {KeyValuePairObjectStringNumber | HeadersInit} headers - The request headers
* @returns The same GraphRequest instance that is being called with
*/
public headers(headers: KeyValuePairObjectStringNumber | HeadersInit): GraphRequest {
for (const key in headers) {
if (Object.prototype.hasOwnProperty.call(headers, key)) {
this._headers[key] = headers[key] as string;
}
}
return this;
}
/**
* @public
* Sets the option for making a request
* @param {string} key - The key value
* @param {any} value - The value
* @returns The same GraphRequest instance that is being called with
*/
public option(key: string, value: any): GraphRequest {
this._options[key] = value;
return this;
}
/**
* @public
* Sets the options for making a request
* @param {{ [key: string]: any }} options - The options key value pair
* @returns The same GraphRequest instance that is being called with
*/
public options(options: { [key: string]: any }): GraphRequest {
for (const key in options) {
if (Object.prototype.hasOwnProperty.call(options, key)) {
this._options[key] = options[key];
}
}
return this;
}
/**
* @public
* Sets the middleware options for a request
* @param {MiddlewareOptions[]} options - The array of middleware options
* @returns The same GraphRequest instance that is being called with
*/
public middlewareOptions(options: MiddlewareOptions[]): GraphRequest {
this._middlewareOptions = options;
return this;
}
/**
* @public
* Sets the api endpoint version for a request
* @param {string} version - The version value
* @returns The same GraphRequest instance that is being called with
*/
public version(version: string): GraphRequest {
this.urlComponents.version = version;
return this;
}
/**
* @public
* Sets the api endpoint version for a request
* @param {ResponseType} responseType - The response type value
* @returns The same GraphRequest instance that is being called with
*/
public responseType(responseType: ResponseType): GraphRequest {
this._responseType = responseType;
return this;
}
/**
* @public
* To add properties for select OData Query param
* @param {string|string[]} properties - The Properties value
* @returns The same GraphRequest instance that is being called with, after adding the properties for $select query
*/
/*
* Accepts .select("displayName,birthday")
* and .select(["displayName", "birthday"])
* and .select("displayName", "birthday")
*
*/
public select(properties: string | string[]): GraphRequest {
this.addCsvQueryParameter("$select", properties, arguments);
return this;
}
/**
* @public
* To add properties for expand OData Query param
* @param {string|string[]} properties - The Properties value
* @returns The same GraphRequest instance that is being called with, after adding the properties for $expand query
*/
public expand(properties: string | string[]): GraphRequest {
this.addCsvQueryParameter("$expand", properties, arguments);
return this;
}
/**
* @public
* To add properties for orderby OData Query param
* @param {string|string[]} properties - The Properties value
* @returns The same GraphRequest instance that is being called with, after adding the properties for $orderby query
*/
public orderby(properties: string | string[]): GraphRequest {
this.addCsvQueryParameter("$orderby", properties, arguments);
return this;
}
/**
* @public
* To add query string for filter OData Query param. The request URL accepts only one $filter Odata Query option and its value is set to the most recently passed filter query string.
* @param {string} filterStr - The filter query string
* @returns The same GraphRequest instance that is being called with, after adding the $filter query
*/
public filter(filterStr: string): GraphRequest {
this.urlComponents.oDataQueryParams.$filter = filterStr;
return this;
}
/**
* @public
* To add criterion for search OData Query param. The request URL accepts only one $search Odata Query option and its value is set to the most recently passed search criterion string.
* @param {string} searchStr - The search criterion string
* @returns The same GraphRequest instance that is being called with, after adding the $search query criteria
*/
public search(searchStr: string): GraphRequest {
this.urlComponents.oDataQueryParams.$search = searchStr;
return this;
}
/**
* @public
* To add number for top OData Query param. The request URL accepts only one $top Odata Query option and its value is set to the most recently passed number value.
* @param {number} n - The number value
* @returns The same GraphRequest instance that is being called with, after adding the number for $top query
*/
public top(n: number): GraphRequest {
this.urlComponents.oDataQueryParams.$top = n;
return this;
}
/**
* @public
* To add number for skip OData Query param. The request URL accepts only one $skip Odata Query option and its value is set to the most recently passed number value.
* @param {number} n - The number value
* @returns The same GraphRequest instance that is being called with, after adding the number for the $skip query
*/
public skip(n: number): GraphRequest {
this.urlComponents.oDataQueryParams.$skip = n;
return this;
}
/**
* @public
* To add token string for skipToken OData Query param. The request URL accepts only one $skipToken Odata Query option and its value is set to the most recently passed token value.
* @param {string} token - The token value
* @returns The same GraphRequest instance that is being called with, after adding the token string for $skipToken query option
*/
public skipToken(token: string): GraphRequest {
this.urlComponents.oDataQueryParams.$skipToken = token;
return this;
}
/**
* @public
* To add boolean for count OData Query param. The URL accepts only one $count Odata Query option and its value is set to the most recently passed boolean value.
* @param {boolean} isCount - The count boolean
* @returns The same GraphRequest instance that is being called with, after adding the boolean value for the $count query option
*/
public count(isCount = true): GraphRequest {
this.urlComponents.oDataQueryParams.$count = isCount.toString();
return this;
}
/**
* @public
* Appends query string to the urlComponent
* @param {string|KeyValuePairObjectStringNumber} queryDictionaryOrString - The query value
* @returns The same GraphRequest instance that is being called with, after appending the query string to the url component
*/
/*
* Accepts .query("displayName=xyz")
* and .select({ name: "value" })
*/
public query(queryDictionaryOrString: string | KeyValuePairObjectStringNumber): GraphRequest {
return this.parseQueryParameter(queryDictionaryOrString);
}
/**
* @public
* @async
* Makes a http request with GET method
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the get response
*/
public async get(callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
const options: FetchOptions = {
method: RequestMethod.GET,
};
const response = await this.send(url, options, callback);
return response;
}
/**
* @public
* @async
* Makes a http request with POST method
* @param {any} content - The content that needs to be sent with the request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the post response
*/
public async post(content: any, callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
const options: FetchOptions = {
method: RequestMethod.POST,
body: serializeContent(content),
};
const className: string = content && content.constructor && content.constructor.name;
if (className === "FormData") {
// Content-Type headers should not be specified in case the of FormData type content
options.headers = {};
} else {
this.setHeaderContentType();
options.headers = this._headers;
}
return await this.send(url, options, callback);
}
/**
* @public
* @async
* Alias for Post request call
* @param {any} content - The content that needs to be sent with the request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the post response
*/
public async create(content: any, callback?: GraphRequestCallback): Promise<any> {
return await this.post(content, callback);
}
/**
* @public
* @async
* Makes http request with PUT method
* @param {any} content - The content that needs to be sent with the request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the put response
*/
public async put(content: any, callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
this.setHeaderContentType();
const options: FetchOptions = {
method: RequestMethod.PUT,
body: serializeContent(content),
};
return await this.send(url, options, callback);
}
/**
* @public
* @async
* Makes http request with PATCH method
* @param {any} content - The content that needs to be sent with the request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the patch response
*/
public async patch(content: any, callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
this.setHeaderContentType();
const options: FetchOptions = {
method: RequestMethod.PATCH,
body: serializeContent(content),
};
return await this.send(url, options, callback);
}
/**
* @public
* @async
* Alias for PATCH request
* @param {any} content - The content that needs to be sent with the request
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the patch response
*/
public async update(content: any, callback?: GraphRequestCallback): Promise<any> {
return await this.patch(content, callback);
}
/**
* @public
* @async
* Makes http request with DELETE method
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the delete response
*/
public async delete(callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
const options: FetchOptions = {
method: RequestMethod.DELETE,
};
return await this.send(url, options, callback);
}
/**
* @public
* @async
* Alias for delete request call
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the delete response
*/
public async del(callback?: GraphRequestCallback): Promise<any> {
return await this.delete(callback);
}
/**
* @public
* @async
* Makes a http request with GET method to read response as a stream.
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the getStream response
*/
public async getStream(callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
const options = {
method: RequestMethod.GET,
};
this.responseType(ResponseType.STREAM);
return await this.send(url, options, callback);
}
/**
* @public
* @async
* Makes a http request with GET method to read response as a stream.
* @param {any} stream - The stream instance
* @param {GraphRequestCallback} [callback] - The callback function to be called in response with async call
* @returns A promise that resolves to the putStream response
*/
public async putStream(stream: any, callback?: GraphRequestCallback): Promise<any> {
const url = this.buildFullUrl();
const options = {
method: RequestMethod.PUT,
headers: {
"Content-Type": "application/octet-stream",
},
body: stream,
};
return await this.send(url, options, callback);
}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphRequestUtil
*/
import { GRAPH_URLS } from "./Constants";
import { GraphClientError } from "./GraphClientError";
/**
* To hold list of OData query params
*/
export const oDataQueryNames = ["$select", "$expand", "$orderby", "$filter", "$top", "$skip", "$skipToken", "$count"];
/**
* To construct the URL by appending the segments with "/"
* @param {string[]} urlSegments - The array of strings
* @returns The constructed URL string
*/
export const urlJoin = (urlSegments: string[]): string => {
const removePostSlash = (s) => s.replace(/\/+$/, "");
const removePreSlash = (s) => s.replace(/^\/+/, "");
const joiner = (pre, cur) => [removePostSlash(pre), removePreSlash(cur)].join("/");
const parts = Array.prototype.slice.call(urlSegments);
return parts.reduce(joiner);
};
/**
* Serializes the content
* @param {any} content - The content value that needs to be serialized
* @returns The serialized content
*
* Note:
* This conversion is required due to the following reasons:
* Body parameter of Request method of isomorphic-fetch only accepts Blob, ArrayBuffer, FormData, TypedArrays string.
* Node.js platform does not support Blob, FormData. Javascript File object inherits from Blob so it is also not supported in node. Therefore content of type Blob, File, FormData will only come from browsers.
* Parallel to ArrayBuffer in javascript, node provides Buffer interface. Node's Buffer is able to send the arbitrary binary data to the server successfully for both Browser and Node platform. Whereas sending binary data via ArrayBuffer or TypedArrays was only possible using Browser. To support both Node and Browser, `serializeContent` converts TypedArrays or ArrayBuffer to `Node Buffer`.
* If the data received is in JSON format, `serializeContent` converts the JSON to string.
*/
export const serializeContent = (content: any): any => {
const className: string = content && content.constructor && content.constructor.name;
if (className === "Buffer" || className === "Blob" || className === "File" || className === "FormData" || typeof content === "string") {
return content;
}
if (className === "ArrayBuffer") {
content = Buffer.from(content);
} else if (className === "Int8Array" || className === "Int16Array" || className === "Int32Array" || className === "Uint8Array" || className === "Uint16Array" || className === "Uint32Array" || className === "Uint8ClampedArray" || className === "Float32Array" || className === "Float64Array" || className === "DataView") {
content = Buffer.from(content.buffer);
} else {
try {
content = JSON.stringify(content);
} catch (error) {
throw new Error("Unable to stringify the content");
}
}
return content;
};
/**
* Checks if the url is one of the service root endpoints for Microsoft Graph and Graph Explorer.
* @param {string} url - The url to be verified
* @returns {boolean} - Returns true if the url is a Graph URL
*/
export const isGraphURL = (url: string): boolean => {
return isValidEndpoint(url);
};
/**
* Checks if the url is for one of the custom hosts provided during client initialization
* @param {string} url - The url to be verified
* @param {Set} customHosts - The url to be verified
* @returns {boolean} - Returns true if the url is a for a custom host
*/
export const isCustomHost = (url: string, customHosts: Set<string>): boolean => {
customHosts.forEach((host) => isCustomHostValid(host));
return isValidEndpoint(url, customHosts);
};
/**
* Checks if the url is for one of the provided hosts.
* @param {string} url - The url to be verified
* @param {Set<string>} allowedHosts - A set of hosts.
* @returns {boolean} - Returns true is for one of the provided endpoints.
*/
const isValidEndpoint = (url: string, allowedHosts: Set<string> = GRAPH_URLS): boolean => {
// Valid Graph URL pattern - https://graph.microsoft.com/{version}/{resource}?{query-parameters}
// Valid Graph URL example - https://graph.microsoft.com/v1.0/
url = url.toLowerCase();
if (url.indexOf("https://") !== -1) {
url = url.replace("https://", "");
// Find where the host ends
const startofPortNoPos = url.indexOf(":");
const endOfHostStrPos = url.indexOf("/");
let hostName = "";
if (endOfHostStrPos !== -1) {
if (startofPortNoPos !== -1 && startofPortNoPos < endOfHostStrPos) {
hostName = url.substring(0, startofPortNoPos);
return allowedHosts.has(hostName);
}
// Parse out the host
hostName = url.substring(0, endOfHostStrPos);
return allowedHosts.has(hostName);
}
}
return false;
};
/**
* Throws error if the string is not a valid host/hostname and contains other url parts.
* @param {string} host - The host to be verified
*/
const isCustomHostValid = (host: string) => {
if (host.indexOf("/") !== -1) {
throw new GraphClientError("Please add only hosts or hostnames to the CustomHosts config. If the url is `http://example.com:3000/`, host is `example:3000`");
}
};
@@ -0,0 +1,182 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module GraphResponseHandler
* References - https://fetch.spec.whatwg.org/#responses
*/
import { GraphRequestCallback } from "./IGraphRequestCallback";
import { ResponseType } from "./ResponseType";
/**
* @enum
* Enum for document types
* @property {string} TEXT_HTML - The text/html content type
* @property {string} TEXT_XML - The text/xml content type
* @property {string} APPLICATION_XML - The application/xml content type
* @property {string} APPLICATION_XHTML - The application/xhml+xml content type
*/
export enum DocumentType {
TEXT_HTML = "text/html",
TEXT_XML = "text/xml",
APPLICATION_XML = "application/xml",
APPLICATION_XHTML = "application/xhtml+xml",
}
/**
* @enum
* Enum for Content types
* @property {string} TEXT_PLAIN - The text/plain content type
* @property {string} APPLICATION_JSON - The application/json content type
*/
enum ContentType {
TEXT_PLAIN = "text/plain",
APPLICATION_JSON = "application/json",
}
/**
* @enum
* Enum for Content type regex
* @property {string} DOCUMENT - The regex to match document content types
* @property {string} IMAGE - The regex to match image content types
*/
enum ContentTypeRegexStr {
DOCUMENT = "^(text\\/(html|xml))|(application\\/(xml|xhtml\\+xml))$",
IMAGE = "^image\\/.+",
}
/**
* @class
* Class for GraphResponseHandler
*/
export class GraphResponseHandler {
/**
* @private
* @static
* To parse Document response
* @param {Response} rawResponse - The response object
* @param {DocumentType} type - The type to which the document needs to be parsed
* @returns A promise that resolves to a document content
*/
private static parseDocumentResponse(rawResponse: Response, type: DocumentType): Promise<any> {
if (typeof DOMParser !== "undefined") {
return new Promise((resolve, reject) => {
rawResponse.text().then((xmlString) => {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, type);
resolve(xmlDoc);
} catch (error) {
reject(error);
}
});
});
} else {
return Promise.resolve(rawResponse.body);
}
}
/**
* @private
* @static
* @async
* To convert the native Response to response content
* @param {Response} rawResponse - The response object
* @param {ResponseType} [responseType] - The response type value
* @returns A promise that resolves to the converted response content
*/
private static async convertResponse(rawResponse: Response, responseType?: ResponseType): Promise<any> {
if (rawResponse.status === 204) {
// NO CONTENT
return Promise.resolve();
}
let responseValue: any;
const contentType = rawResponse.headers.get("Content-type");
switch (responseType) {
case ResponseType.ARRAYBUFFER:
responseValue = await rawResponse.arrayBuffer();
break;
case ResponseType.BLOB:
responseValue = await rawResponse.blob();
break;
case ResponseType.DOCUMENT:
responseValue = await GraphResponseHandler.parseDocumentResponse(rawResponse, DocumentType.TEXT_XML);
break;
case ResponseType.JSON:
responseValue = await rawResponse.json();
break;
case ResponseType.STREAM:
responseValue = await Promise.resolve(rawResponse.body);
break;
case ResponseType.TEXT:
responseValue = await rawResponse.text();
break;
default:
if (contentType !== null) {
const mimeType = contentType.split(";")[0];
if (new RegExp(ContentTypeRegexStr.DOCUMENT).test(mimeType)) {
responseValue = await GraphResponseHandler.parseDocumentResponse(rawResponse, mimeType as DocumentType);
} else if (new RegExp(ContentTypeRegexStr.IMAGE).test(mimeType)) {
responseValue = rawResponse.blob();
} else if (mimeType === ContentType.TEXT_PLAIN) {
responseValue = await rawResponse.text();
} else if (mimeType === ContentType.APPLICATION_JSON) {
responseValue = await rawResponse.json();
} else {
responseValue = Promise.resolve(rawResponse.body);
}
} else {
/**
* RFC specification {@link https://tools.ietf.org/html/rfc7231#section-3.1.1.5} says:
* A sender that generates a message containing a payload body SHOULD
* generate a Content-Type header field in that message unless the
* intended media type of the enclosed representation is unknown to the
* sender. If a Content-Type header field is not present, the recipient
* MAY either assume a media type of "application/octet-stream"
* ([RFC2046], Section 4.5.1) or examine the data to determine its type.
*
* So assuming it as a stream type so returning the body.
*/
responseValue = Promise.resolve(rawResponse.body);
}
break;
}
return responseValue;
}
/**
* @public
* @static
* @async
* To get the parsed response
* @param {Response} rawResponse - The response object
* @param {ResponseType} [responseType] - The response type value
* @param {GraphRequestCallback} [callback] - The graph request callback function
* @returns The parsed response
*/
public static async getResponse(rawResponse: Response, responseType?: ResponseType, callback?: GraphRequestCallback): Promise<any> {
if (responseType === ResponseType.RAW) {
return Promise.resolve(rawResponse);
} else {
const response = await GraphResponseHandler.convertResponse(rawResponse, responseType);
if (rawResponse.ok) {
// Status Code 2XX
if (typeof callback === "function") {
callback(null, response);
} else {
return response;
}
} else {
// NOT OK Response
throw response;
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module HTTPClient
*/
import { Context } from "./IContext";
import { Middleware } from "./middleware/IMiddleware";
/**
* @class
* Class representing HTTPClient
*/
export class HTTPClient {
/**
* @private
* A member holding first middleware of the middleware chain
*/
private middleware: Middleware;
/**
* @public
* @constructor
* Creates an instance of a HTTPClient
* @param {...Middleware} middleware - The first middleware of the middleware chain or a sequence of all the Middleware handlers
*/
public constructor(...middleware: Middleware[]) {
if (!middleware || !middleware.length) {
const error = new Error();
error.name = "InvalidMiddlewareChain";
error.message = "Please provide a default middleware chain or custom middleware chain";
throw error;
}
this.setMiddleware(...middleware);
}
/**
* @private
* Processes the middleware parameter passed to set this.middleware property
* The calling function should validate if middleware is not undefined or not empty.
* @param {...Middleware} middleware - The middleware passed
* @returns Nothing
*/
private setMiddleware(...middleware: Middleware[]): void {
if (middleware.length > 1) {
this.parseMiddleWareArray(middleware);
} else {
this.middleware = middleware[0];
}
}
/**
* @private
* Processes the middleware array to construct the chain
* and sets this.middleware property to the first middleware handler of the array
* The calling function should validate if middleware is not undefined or not empty
* @param {Middleware[]} middlewareArray - The array of middleware handlers
* @returns Nothing
*/
private parseMiddleWareArray(middlewareArray: Middleware[]) {
middlewareArray.forEach((element, index) => {
if (index < middlewareArray.length - 1) {
element.setNext(middlewareArray[index + 1]);
}
});
this.middleware = middlewareArray[0];
}
/**
* @public
* @async
* To send the request through the middleware chain
* @param {Context} context - The context of a request
* @returns A promise that resolves to the Context
*/
public async sendRequest(context: Context): Promise<Context> {
if (typeof context.request === "string" && context.options === undefined) {
const error = new Error();
error.name = "InvalidRequestOptions";
error.message = "Unable to execute the middleware, Please provide valid options for a request";
throw error;
}
await this.middleware.execute(context);
return context;
}
}
@@ -0,0 +1,79 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module HTTPClientFactory
*/
import { HTTPClient } from "./HTTPClient";
import { AuthenticationProvider } from "./IAuthenticationProvider";
import { AuthenticationHandler } from "./middleware/AuthenticationHandler";
import { HTTPMessageHandler } from "./middleware/HTTPMessageHandler";
import { Middleware } from "./middleware/IMiddleware";
import { RedirectHandlerOptions } from "./middleware/options/RedirectHandlerOptions";
import { RetryHandlerOptions } from "./middleware/options/RetryHandlerOptions";
import { RedirectHandler } from "./middleware/RedirectHandler";
import { RetryHandler } from "./middleware/RetryHandler";
import { TelemetryHandler } from "./middleware/TelemetryHandler";
/**
* @private
* To check whether the environment is node or not
* @returns A boolean representing the environment is node or not
*/
const isNodeEnvironment = (): boolean => {
return typeof process === "object" && typeof require === "function";
};
/**
* @class
* Class representing HTTPClientFactory
*/
export class HTTPClientFactory {
/**
* @public
* @static
* Creates HTTPClient with default middleware chain
* @param {AuthenticationProvider} authProvider - The authentication provider instance
* @returns A HTTPClient instance
*
* NOTE: These are the things that we need to remember while doing modifications in the below default pipeline.
* * HTTPMessageHandler should be the last one in the middleware pipeline, because this makes the actual network call of the request
* * TelemetryHandler should be the one prior to the last middleware in the chain, because this is the one which actually collects and appends the usage flag and placing this handler * before making the actual network call ensures that the usage of all features are recorded in the flag.
* * The best place for AuthenticationHandler is in the starting of the pipeline, because every other handler might have to work for multiple times for a request but the auth token for
* them will remain same. For example, Retry and Redirect handlers might be working multiple times for a request based on the response but their auth token would remain same.
*/
public static createWithAuthenticationProvider(authProvider: AuthenticationProvider): HTTPClient {
const authenticationHandler = new AuthenticationHandler(authProvider);
const retryHandler = new RetryHandler(new RetryHandlerOptions());
const telemetryHandler = new TelemetryHandler();
const httpMessageHandler = new HTTPMessageHandler();
authenticationHandler.setNext(retryHandler);
if (isNodeEnvironment()) {
const redirectHandler = new RedirectHandler(new RedirectHandlerOptions());
retryHandler.setNext(redirectHandler);
redirectHandler.setNext(telemetryHandler);
} else {
retryHandler.setNext(telemetryHandler);
}
telemetryHandler.setNext(httpMessageHandler);
return HTTPClientFactory.createWithMiddleware(authenticationHandler);
}
/**
* @public
* @static
* Creates a middleware chain with the given one
* @property {...Middleware} middleware - The first middleware of the middleware chain or a sequence of all the Middleware handlers
* @returns A HTTPClient instance
*/
public static createWithMiddleware(...middleware: Middleware[]): HTTPClient {
// Middleware should not empty or undefined. This is check is present in the HTTPClient constructor.
return new HTTPClient(...middleware);
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { AuthProviderCallback } from "./IAuthProviderCallback";
/**
* @interface
* Signature that holds authProvider
* @callback - The anonymous callback function which takes a single param
*/
export type AuthProvider = (done: AuthProviderCallback) => void;
@@ -0,0 +1,13 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @interface
* Signature that defines callback for an authentication provider
* @callback - The anonymous callback function which takes two params
*/
export type AuthProviderCallback = (error: any, accessToken: string | null) => void;
@@ -0,0 +1,22 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { AuthenticationProviderOptions } from "./IAuthenticationProviderOptions";
/**
* @interface
* A signature representing Authentication provider
* @property {Function} getAccessToken - The function to get the access token from the authentication provider
*/
export interface AuthenticationProvider {
/**
* To get access token from the authentication provider
* @param {AuthenticationProviderOptions} [authenticationProviderOptions] - The authentication provider options instance
* @returns A promise that resolves to an access token
*/
getAccessToken: (authenticationProviderOptions?: AuthenticationProviderOptions) => Promise<string>;
}
@@ -0,0 +1,15 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @interface
* A signature represents the Authentication provider options
* @property {string[]} [scopes] - The array of scopes
*/
export interface AuthenticationProviderOptions {
scopes?: string[];
}
+35
View File
@@ -0,0 +1,35 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { AuthenticationProvider } from "./IAuthenticationProvider";
import { FetchOptions } from "./IFetchOptions";
import { Middleware } from "./middleware/IMiddleware";
/**
* @interface
* Options for initializing the Graph Client
* @property {Function} [authProvider] - The authentication provider instance
* @property {string} [baseUrl] - Base url that needs to be appended to every request
* @property {boolean} [debugLogging] - The boolean to enable/disable debug logging
* @property {string} [defaultVersion] - The default version that needs to be used while making graph api request
* @property {FetchOptions} [fetchOptions] - The options for fetch request
* @property {Middleware| Middleware[]} [middleware] - The first middleware of the middleware chain or an array of the Middleware handlers
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
*/
export interface ClientOptions {
authProvider?: AuthenticationProvider;
baseUrl?: string;
debugLogging?: boolean;
defaultVersion?: string;
fetchOptions?: FetchOptions;
middleware?: Middleware | Middleware[];
/**
* Example - If URL is "https://test_host/v1.0", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
*/
customHosts?: Set<string>;
}
+30
View File
@@ -0,0 +1,30 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { FetchOptions } from "./IFetchOptions";
import { MiddlewareControl } from "./middleware/MiddlewareControl";
/**
* @interface
* @property {RequestInfo} request - The request url string or the Request instance
* @property {FetchOptions} [options] - The options for the request
* @property {Response} [response] - The response content
* @property {MiddlewareControl} [middlewareControl] - The options for the middleware chain
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
*
*/
export interface Context {
request: RequestInfo;
options?: FetchOptions;
response?: Response;
middlewareControl?: MiddlewareControl;
/**
* Example - If URL is "https://test_host", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
*/
customHosts?: Set<string>;
}
+33
View File
@@ -0,0 +1,33 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @interface {@link https://github.com/bitinn/node-fetch/#options}
* Signature to define the fetch request options for node environment
* @property {number} [follow] - node-fetch option: maximum redirect count. 0 to not follow redirect
* @property {number} [compress] - node-fetch option: support gzip/deflate content encoding. false to disable
* @property {number} [size] - node-fetch option: maximum response body size in bytes. 0 to disable
* @property {any} [agent] - node-fetch option: HTTP(S).Agent instance, allows custom proxy, certificate, lookup, family etc.
* @property {number} [highWaterMark] - node-fetch option: maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource.
* @property {boolean} [insecureHTTPParser] - node-fetch option: use an insecure HTTP parser that accepts invalid HTTP headers when `true`.
*/
export interface NodeFetchInit {
follow?: number;
compress?: boolean;
size?: number;
agent?: any;
highWaterMark?: number;
insecureHTTPParser?: boolean;
}
/**
* @interface
* Signature to define the fetch api options which includes both fetch standard options and also the extended node fetch options
* @extends RequestInit @see {@link https://fetch.spec.whatwg.org/#requestinit}
* @extends NodeFetchInit
*/
export interface FetchOptions extends RequestInit, NodeFetchInit {}
@@ -0,0 +1,14 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { GraphError } from "./GraphError";
/**
* @interface
* Signature to define the GraphRequest callback
* @callback - The anonymous callback function
*/
export type GraphRequestCallback = (error: GraphError, response: any, rawResponse?: any) => void;
+31
View File
@@ -0,0 +1,31 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { AuthProvider } from "./IAuthProvider";
import { FetchOptions } from "./IFetchOptions";
/**
* @interface
* Options for initializing the Graph Client
* @property {AuthProvider} authProvider - The function to get the authentication token
* @property {string} [baseUrl] - Base url that needs to be appended to every request
* @property {boolean} [debugLogging] - The boolean to enable/disable debug logging
* @property {string} [defaultVersion] - The default version that needs to be used while making graph api request
* @property {FetchOptions} [fetchOptions] - The options for fetch request
* @property {Set<string>}[customHosts] - A set of custom host names. Should contain hostnames only.
*/
export interface Options {
authProvider: AuthProvider;
baseUrl?: string;
debugLogging?: boolean;
defaultVersion?: string;
fetchOptions?: FetchOptions;
/**
* Example - If URL is "https://test_host/v1.0", then set property "customHosts" as "customHosts: Set<string>(["test_host"])"
*/
customHosts?: Set<string>;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @enum
* Enum for RequestMethods
* @property {string} GET - The get request type
* @property {string} PATCH - The patch request type
* @property {string} POST - The post request type
* @property {string} PUT - The put request type
* @property {string} DELETE - The delete request type
*/
export enum RequestMethod {
GET = "GET",
PATCH = "PATCH",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
}
+27
View File
@@ -0,0 +1,27 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @enum
* Enum for ResponseType values
* @property {string} ARRAYBUFFER - To download response content as an [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer}
* @property {string} BLOB - To download content as a [binary/blob] {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob}
* @property {string} DOCUMENT - This downloads content as a document or stream
* @property {string} JSON - To download response content as a json
* @property {string} STREAM - To download response as a [stream]{@link https://nodejs.org/api/stream.html}
* @property {string} TEXT - For downloading response as a text
*/
export enum ResponseType {
ARRAYBUFFER = "arraybuffer",
BLOB = "blob",
DOCUMENT = "document",
JSON = "json",
RAW = "raw",
STREAM = "stream",
TEXT = "text",
}
@@ -0,0 +1,30 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @constant
* @function
* Validates availability of Promise and fetch in global context
* @returns The true in case the Promise and fetch available, otherwise throws error
*/
export const validatePolyFilling = (): boolean => {
if (typeof Promise === "undefined" && typeof fetch === "undefined") {
const error = new Error("Library cannot function without Promise and fetch. So, please provide polyfill for them.");
error.name = "PolyFillNotAvailable";
throw error;
} else if (typeof Promise === "undefined") {
const error = new Error("Library cannot function without Promise. So, please provide polyfill for it.");
error.name = "PolyFillNotAvailable";
throw error;
} else if (typeof fetch === "undefined") {
const error = new Error("Library cannot function without fetch. So, please provide polyfill for it.");
error.name = "PolyFillNotAvailable";
throw error;
}
return true;
};
+15
View File
@@ -0,0 +1,15 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
// THIS FILE IS AUTO GENERATED
// ANY CHANGES WILL BE LOST DURING BUILD
/**
* @module Version
*/
export const PACKAGE_VERSION = "3.0.7";
@@ -0,0 +1,18 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { GetTokenOptions } from "@azure/identity";
import { AuthenticationProviderOptions } from "../../IAuthenticationProviderOptions";
/**
* @interface
* A signature represents the Authentication provider options for Token Credentials
* @property {getTokenOptions} [GetTokenOptions] - Defines options for TokenCredential.getToken.
*/
export interface TokenCredentialAuthenticationProviderOptions extends AuthenticationProviderOptions {
getTokenOptions?: GetTokenOptions;
}
@@ -0,0 +1,81 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { TokenCredential } from "@azure/identity";
import { GraphClientError } from "../../GraphClientError";
import { AuthenticationProvider } from "../../IAuthenticationProvider";
import { TokenCredentialAuthenticationProviderOptions } from "./ITokenCredentialAuthenticationProviderOptions";
/**
* @module TokenCredentialAuthenticationProvider
*/
/**
* @class
* Class representing TokenCredentialAuthenticationProvider
* This feature is introduced in Version 3.0.0
* @extends AuthenticationProvider
* Reference - https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/identity/identity/README.md
*/
export class TokenCredentialAuthenticationProvider implements AuthenticationProvider {
/**
* @private
* A member holding an instance of @azure/identity TokenCredential
*/
private tokenCredential: TokenCredential;
/**
* @private
* A member holding an instance of TokenCredentialAuthenticationProviderOptions
*/
private authenticationProviderOptions: TokenCredentialAuthenticationProviderOptions;
/**
* @public
* @constructor
* Creates an instance of TokenCredentialAuthenticationProvider
* @param {TokenCredential} tokenCredential - An instance of @azure/identity TokenCredential
* @param {TokenCredentialAuthenticationProviderOptions} authenticationProviderOptions - An instance of TokenCredentialAuthenticationProviderOptions
* @returns An instance of TokenCredentialAuthenticationProvider
*/
public constructor(tokenCredential: TokenCredential, authenticationProviderOptions: TokenCredentialAuthenticationProviderOptions) {
if (!tokenCredential) {
throw new GraphClientError("Please pass a token credential object to the TokenCredentialAuthenticationProvider class constructor");
}
if (!authenticationProviderOptions) {
throw new GraphClientError("Please pass the TokenCredentialAuthenticationProviderOptions with scopes to the TokenCredentialAuthenticationProvider class constructor");
}
this.authenticationProviderOptions = authenticationProviderOptions;
this.tokenCredential = tokenCredential;
}
/**
* @public
* @async
* To get the access token
* @param {TokenCredentialAuthenticationProviderOptions} authenticationProviderOptions - The authentication provider options object
* @returns The promise that resolves to an access token
*/
public async getAccessToken(): Promise<string> {
const scopes = this.authenticationProviderOptions.scopes;
const error = new GraphClientError();
if (!scopes || scopes.length === 0) {
error.name = "Empty Scopes";
error.message = "Scopes cannot be empty, Please provide scopes";
throw error;
}
const response = await this.tokenCredential.getToken(scopes, this.authenticationProviderOptions.getTokenOptions);
if (response) {
return response.token;
}
error.message = "Cannot retrieve accessToken from the Token Credential object";
error.name = "Access token is undefined";
throw error;
}
}
@@ -0,0 +1,8 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
export { TokenCredentialAuthenticationProviderOptions } from "./ITokenCredentialAuthenticationProviderOptions";
export { TokenCredentialAuthenticationProvider } from "./TokenCredentialAuthenticationProvider";
@@ -0,0 +1,78 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module AuthCodeMSALBrowserAuthenticationProvider
*/
import { AuthenticationResult, InteractionRequiredAuthError, InteractionType, PublicClientApplication } from "@azure/msal-browser";
import { GraphClientError } from "../../GraphClientError";
import { AuthenticationProvider } from "../../IAuthenticationProvider";
import { AuthCodeMSALBrowserAuthenticationProviderOptions } from "../msalOptions/MSALAuthenticationProviderOptions";
/**
* an AuthenticationProvider implementation supporting msal-browser library.
* This feature is introduced in Version 3.0.0
* @class
* @extends AuthenticationProvider
*/
export class AuthCodeMSALBrowserAuthenticationProvider implements AuthenticationProvider {
/**
* @public
* @constructor
* Creates an instance of ImplicitMSALAuthenticationProvider
* @param {PublicClientApplication} msalApplication - An instance of MSAL PublicClientApplication
* @param {AuthCodeMSALBrowserAuthenticationProviderOptions} options - An instance of MSALAuthenticationProviderOptions
* @returns An instance of ImplicitMSALAuthenticationProvider
*/
public constructor(private publicClientApplication: PublicClientApplication, private options: AuthCodeMSALBrowserAuthenticationProviderOptions) {
if (!options || !publicClientApplication) {
throw new GraphClientError("Please pass valid PublicClientApplication instance and AuthCodeMSALBrowserAuthenticationProviderOptions instance to instantiate MSALBrowserAuthenticationProvider");
}
}
/**
* @public
* @async
* To get the access token for the request
* @returns The promise that resolves to an access token
*/
public async getAccessToken(): Promise<string> {
const scopes = this.options && this.options.scopes;
const account = this.options && this.options.account;
const error = new GraphClientError();
if (!scopes || scopes.length === 0) {
error.name = "Empty Scopes";
error.message = "Scopes cannot be empty, Please provide scopes";
throw error;
}
try {
const response: AuthenticationResult = await this.publicClientApplication.acquireTokenSilent({
scopes,
account,
});
if (!response || !response.accessToken) {
error.name = "Access token is undefined";
error.message = "Received empty access token from PublicClientApplication";
throw error;
}
return response.accessToken;
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
if (this.options.interactionType === InteractionType.Redirect) {
this.publicClientApplication.acquireTokenRedirect({ scopes });
} else if (this.options.interactionType === InteractionType.Popup) {
const response: AuthenticationResult = await this.publicClientApplication.acquireTokenPopup({ scopes });
return response.accessToken;
}
} else {
throw error;
}
}
}
}
@@ -0,0 +1,7 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
export { AuthCodeMSALBrowserAuthenticationProvider } from "./AuthCodeMSALBrowserAuthenticationProvider";
@@ -0,0 +1,20 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module MSALAuthenticationProviderOptions
*/
import { AccountInfo, InteractionType } from "@azure/msal-browser";
import { AuthenticationProviderOptions } from "../../IAuthenticationProviderOptions";
export interface AuthCodeMSALBrowserAuthenticationProviderOptions extends AuthenticationProviderOptions {
scopes: string[];
account: AccountInfo;
interactionType: InteractionType;
}
+53
View File
@@ -0,0 +1,53 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path= "../../shims.d.ts" />
export { BatchRequestStep, BatchRequestData, BatchRequestContent, RequestData, BatchRequestBody } from "../content/BatchRequestContent";
export { BatchResponseBody, BatchResponseContent } from "../content/BatchResponseContent";
export { AuthenticationHandler } from "../middleware/AuthenticationHandler";
export { HTTPMessageHandler } from "../middleware/HTTPMessageHandler";
export { Middleware } from "../middleware/IMiddleware";
export { RetryHandler } from "../middleware/RetryHandler";
export { RedirectHandler } from "../middleware/RedirectHandler";
export { TelemetryHandler } from "../middleware/TelemetryHandler";
export { MiddlewareFactory } from "../middleware/MiddlewareFactory";
export { AuthenticationHandlerOptions } from "../middleware/options/AuthenticationHandlerOptions";
export { MiddlewareOptions } from "../middleware/options/IMiddlewareOptions";
export { ShouldRetry, RetryHandlerOptions } from "../middleware/options/RetryHandlerOptions";
export { ShouldRedirect, RedirectHandlerOptions } from "../middleware/options/RedirectHandlerOptions";
export { FeatureUsageFlag, TelemetryHandlerOptions } from "../middleware/options/TelemetryHandlerOptions";
export { ChaosHandlerOptions } from "../middleware/options/ChaosHandlerOptions";
export { ChaosStrategy } from "../middleware/options/ChaosStrategy";
export { ChaosHandler } from "../middleware/ChaosHandler";
export { SliceType, LargeFileUploadTaskOptions, LargeFileUploadTask, LargeFileUploadSession, FileObject } from "../tasks/LargeFileUploadTask";
export { OneDriveLargeFileUploadTask, OneDriveLargeFileUploadOptions } from "../tasks/OneDriveLargeFileUploadTask";
export { getValidRangeSize } from "../tasks/OneDriveLargeFileUploadTaskUtil";
export { StreamUpload } from "../tasks/FileUploadTask/FileObjectClasses/StreamUpload";
export { FileUpload } from "../tasks/FileUploadTask/FileObjectClasses/FileUpload";
export { UploadResult } from "../tasks/FileUploadTask/UploadResult";
export { UploadEventHandlers } from "../tasks/FileUploadTask/Interfaces/IUploadEventHandlers";
export { Range } from "../tasks/FileUploadTask/Range";
export { PageIteratorCallback, PageIterator, PageCollection, GraphRequestOptions } from "../tasks/PageIterator";
export { Client } from "../Client";
export { CustomAuthenticationProvider } from "../CustomAuthenticationProvider";
export { GraphError } from "../GraphError";
export { GraphClientError } from "../GraphClientError";
export { GraphRequest } from "../GraphRequest";
export { AuthProvider } from "../IAuthProvider";
export { AuthenticationProvider } from "../IAuthenticationProvider";
export { AuthenticationProviderOptions } from "../IAuthenticationProviderOptions";
export { AuthProviderCallback } from "../IAuthProviderCallback";
export { ClientOptions } from "../IClientOptions";
export { Context } from "../IContext";
export { NodeFetchInit, FetchOptions } from "../IFetchOptions";
export { GraphRequestCallback } from "../IGraphRequestCallback";
export { Options } from "../IOptions";
export { ResponseType } from "../ResponseType";
@@ -0,0 +1,486 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module BatchRequestContent
*/
import { RequestMethod } from "../RequestMethod";
/**
* @interface
* Signature to represent the buffer request body parsing method
* @property {Function} buffer - Returns a promise that resolves to a buffer of the request body
*/
interface NodeBody {
buffer(): Promise<Buffer>;
}
/**
* @interface
* Signature to represent the Request for both Node and browser environments
* @extends Request
* @extends NodeBody
*/
interface IsomorphicRequest extends Request, NodeBody {}
/**
* @interface
* Signature representing BatchRequestStep data
* @property {string} id - Unique identity for the request, Should not be an empty string
* @property {string[]} [dependsOn] - Array of dependencies
* @property {Request} request - The Request object
*/
export interface BatchRequestStep {
id: string;
dependsOn?: string[];
request: Request;
}
/**
* @interface
* Signature representing single request in a Batching
* @extends RequestInit
* @see {@link https://github.com/Microsoft/TypeScript/blob/master/lib/lib.dom.d.ts#L1337} and {@link https://fetch.spec.whatwg.org/#requestinit}
*
* @property {string} url - The url value of the request
*/
export interface RequestData extends RequestInit {
url: string;
}
/**
* @interface
* Signature representing batch request data
* @property {string} id - Unique identity for the request, Should not be an empty string
* @property {string[]} [dependsOn] - Array of dependencies
*/
export interface BatchRequestData extends RequestData {
id: string;
dependsOn?: string[];
}
/**
* @interface
* Signature representing batch request body
* @property {BatchRequestData[]} requests - Array of request data, a json representation of requests for batch
*/
export interface BatchRequestBody {
requests: BatchRequestData[];
}
/**
* @class
* Class for handling BatchRequestContent
*/
export class BatchRequestContent {
/**
* @private
* @static
* Limit for number of requests {@link - https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
*/
private static requestLimit = 20;
/**
* @public
* To keep track of requests, key will be id of the request and value will be the request json
*/
public requests: Map<string, BatchRequestStep>;
/**
* @private
* @static
* Validates the dependency chain of the requests
*
* Note:
* Individual requests can depend on other individual requests. Currently, requests can only depend on a single other request, and must follow one of these three patterns:
* 1. Parallel - no individual request states a dependency in the dependsOn property.
* 2. Serial - all individual requests depend on the previous individual request.
* 3. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.
* As JSON batching matures, these limitations will be removed.
* @see {@link https://developer.microsoft.com/en-us/graph/docs/concepts/known_issues#json-batching}
*
* @param {Map<string, BatchRequestStep>} requests - The map of requests.
* @returns The boolean indicating the validation status
*/
private static validateDependencies(requests: Map<string, BatchRequestStep>): boolean {
const isParallel = (reqs: Map<string, BatchRequestStep>): boolean => {
const iterator = reqs.entries();
let cur = iterator.next();
while (!cur.done) {
const curReq = cur.value[1];
if (curReq.dependsOn !== undefined && curReq.dependsOn.length > 0) {
return false;
}
cur = iterator.next();
}
return true;
};
const isSerial = (reqs: Map<string, BatchRequestStep>): boolean => {
const iterator = reqs.entries();
let cur = iterator.next();
const firstRequest: BatchRequestStep = cur.value[1];
if (firstRequest.dependsOn !== undefined && firstRequest.dependsOn.length > 0) {
return false;
}
let prev = cur;
cur = iterator.next();
while (!cur.done) {
const curReq: BatchRequestStep = cur.value[1];
if (curReq.dependsOn === undefined || curReq.dependsOn.length !== 1 || curReq.dependsOn[0] !== prev.value[1].id) {
return false;
}
prev = cur;
cur = iterator.next();
}
return true;
};
const isSame = (reqs: Map<string, BatchRequestStep>): boolean => {
const iterator = reqs.entries();
let cur = iterator.next();
const firstRequest: BatchRequestStep = cur.value[1];
let dependencyId: string;
if (firstRequest.dependsOn === undefined || firstRequest.dependsOn.length === 0) {
dependencyId = firstRequest.id;
} else {
if (firstRequest.dependsOn.length === 1) {
const fDependencyId = firstRequest.dependsOn[0];
if (fDependencyId !== firstRequest.id && reqs.has(fDependencyId)) {
dependencyId = fDependencyId;
} else {
return false;
}
} else {
return false;
}
}
cur = iterator.next();
while (!cur.done) {
const curReq = cur.value[1];
if ((curReq.dependsOn === undefined || curReq.dependsOn.length === 0) && dependencyId !== curReq.id) {
return false;
}
if (curReq.dependsOn !== undefined && curReq.dependsOn.length !== 0) {
if (curReq.dependsOn.length === 1 && (curReq.id === dependencyId || curReq.dependsOn[0] !== dependencyId)) {
return false;
}
if (curReq.dependsOn.length > 1) {
return false;
}
}
cur = iterator.next();
}
return true;
};
if (requests.size === 0) {
const error = new Error("Empty requests map, Please provide at least one request.");
error.name = "Empty Requests Error";
throw error;
}
return isParallel(requests) || isSerial(requests) || isSame(requests);
}
/**
* @private
* @static
* @async
* Converts Request Object instance to a JSON
* @param {IsomorphicRequest} request - The IsomorphicRequest Object instance
* @returns A promise that resolves to JSON representation of a request
*/
private static async getRequestData(request: IsomorphicRequest): Promise<RequestData> {
const requestData: RequestData = {
url: "",
};
const hasHttpRegex = new RegExp("^https?://");
// Stripping off hostname, port and url scheme
requestData.url = hasHttpRegex.test(request.url) ? "/" + request.url.split(/.*?\/\/.*?\//)[1] : request.url;
requestData.method = request.method;
const headers = {};
request.headers.forEach((value, key) => {
headers[key] = value;
});
if (Object.keys(headers).length) {
requestData.headers = headers;
}
if (request.method === RequestMethod.PATCH || request.method === RequestMethod.POST || request.method === RequestMethod.PUT) {
requestData.body = await BatchRequestContent.getRequestBody(request);
}
/**
* TODO: Check any other property needs to be used from the Request object and add them
*/
return requestData;
}
/**
* @private
* @static
* @async
* Gets the body of a Request object instance
* @param {IsomorphicRequest} request - The IsomorphicRequest object instance
* @returns The Promise that resolves to a body value of a Request
*/
private static async getRequestBody(request: IsomorphicRequest): Promise<any> {
let bodyParsed = false;
let body;
try {
const cloneReq = request.clone();
body = await cloneReq.json();
bodyParsed = true;
} catch (e) {
//TODO- Handle empty catches
}
if (!bodyParsed) {
try {
if (typeof Blob !== "undefined") {
const blob = await request.blob();
const reader = new FileReader();
body = await new Promise((resolve) => {
reader.addEventListener(
"load",
() => {
const dataURL = reader.result as string;
/**
* Some valid dataURL schemes:
* 1. data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh
* 2. data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678
* 3. data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
* 4. data:image/png,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
* 5. data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
* @see Syntax {@link https://en.wikipedia.org/wiki/Data_URI_scheme} for more
*/
const regex = new RegExp("^s*data:(.+?/.+?(;.+?=.+?)*)?(;base64)?,(.*)s*$");
const segments = regex.exec(dataURL);
resolve(segments[4]);
},
false,
);
reader.readAsDataURL(blob);
});
} else if (typeof Buffer !== "undefined") {
const buffer = await request.buffer();
body = buffer.toString("base64");
}
bodyParsed = true;
} catch (e) {
// TODO-Handle empty catches
}
}
return body;
}
/**
* @public
* @constructor
* Constructs a BatchRequestContent instance
* @param {BatchRequestStep[]} [requests] - Array of requests value
* @returns An instance of a BatchRequestContent
*/
public constructor(requests?: BatchRequestStep[]) {
this.requests = new Map();
if (typeof requests !== "undefined") {
const limit = BatchRequestContent.requestLimit;
if (requests.length > limit) {
const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
error.name = "Limit Exceeded Error";
throw error;
}
for (const req of requests) {
this.addRequest(req);
}
}
}
/**
* @public
* Adds a request to the batch request content
* @param {BatchRequestStep} request - The request value
* @returns The id of the added request
*/
public addRequest(request: BatchRequestStep): string {
const limit = BatchRequestContent.requestLimit;
if (request.id === "") {
const error = new Error(`Id for a request is empty, Please provide an unique id`);
error.name = "Empty Id For Request";
throw error;
}
if (this.requests.size === limit) {
const error = new Error(`Maximum requests limit exceeded, Max allowed number of requests are ${limit}`);
error.name = "Limit Exceeded Error";
throw error;
}
if (this.requests.has(request.id)) {
const error = new Error(`Adding request with duplicate id ${request.id}, Make the id of the requests unique`);
error.name = "Duplicate RequestId Error";
throw error;
}
this.requests.set(request.id, request);
return request.id;
}
/**
* @public
* Removes request from the batch payload and its dependencies from all dependents
* @param {string} requestId - The id of a request that needs to be removed
* @returns The boolean indicating removed status
*/
public removeRequest(requestId: string): boolean {
const deleteStatus = this.requests.delete(requestId);
const iterator = this.requests.entries();
let cur = iterator.next();
/**
* Removing dependencies where this request is present as a dependency
*/
while (!cur.done) {
const dependencies = cur.value[1].dependsOn;
if (typeof dependencies !== "undefined") {
const index = dependencies.indexOf(requestId);
if (index !== -1) {
dependencies.splice(index, 1);
}
if (dependencies.length === 0) {
delete cur.value[1].dependsOn;
}
}
cur = iterator.next();
}
return deleteStatus;
}
/**
* @public
* @async
* Serialize content from BatchRequestContent instance
* @returns The body content to make batch request
*/
public async getContent(): Promise<BatchRequestBody> {
const requests: BatchRequestData[] = [];
const requestBody: BatchRequestBody = {
requests,
};
const iterator = this.requests.entries();
let cur = iterator.next();
if (cur.done) {
const error = new Error("No requests added yet, Please add at least one request.");
error.name = "Empty Payload";
throw error;
}
if (!BatchRequestContent.validateDependencies(this.requests)) {
const error = new Error(`Invalid dependency found, Dependency should be:
1. Parallel - no individual request states a dependency in the dependsOn property.
2. Serial - all individual requests depend on the previous individual request.
3. Same - all individual requests that state a dependency in the dependsOn property, state the same dependency.`);
error.name = "Invalid Dependency";
throw error;
}
while (!cur.done) {
const requestStep: BatchRequestStep = cur.value[1];
const batchRequestData: BatchRequestData = (await BatchRequestContent.getRequestData(requestStep.request as IsomorphicRequest)) as BatchRequestData;
/**
* @see{@https://tools.ietf.org/html/rfc7578#section-4.4}
* TODO- Setting/Defaulting of content-type header to the correct value
* @see {@link https://developer.microsoft.com/en-us/graph/docs/concepts/json_batching#request-format}
*/
if (batchRequestData.body !== undefined && (batchRequestData.headers === undefined || batchRequestData.headers["content-type"] === undefined)) {
const error = new Error(`Content-type header is not mentioned for request #${requestStep.id}, For request having body, Content-type header should be mentioned`);
error.name = "Invalid Content-type header";
throw error;
}
batchRequestData.id = requestStep.id;
if (requestStep.dependsOn !== undefined && requestStep.dependsOn.length > 0) {
batchRequestData.dependsOn = requestStep.dependsOn;
}
requests.push(batchRequestData);
cur = iterator.next();
}
requestBody.requests = requests;
return requestBody;
}
/**
* @public
* Adds a dependency for a given dependent request
* @param {string} dependentId - The id of the dependent request
* @param {string} [dependencyId] - The id of the dependency request, if not specified the preceding request will be considered as a dependency
* @returns Nothing
*/
public addDependency(dependentId: string, dependencyId?: string): void {
if (!this.requests.has(dependentId)) {
const error = new Error(`Dependent ${dependentId} does not exists, Please check the id`);
error.name = "Invalid Dependent";
throw error;
}
if (typeof dependencyId !== "undefined" && !this.requests.has(dependencyId)) {
const error = new Error(`Dependency ${dependencyId} does not exists, Please check the id`);
error.name = "Invalid Dependency";
throw error;
}
if (typeof dependencyId !== "undefined") {
const dependent = this.requests.get(dependentId);
if (dependent.dependsOn === undefined) {
dependent.dependsOn = [];
}
if (dependent.dependsOn.indexOf(dependencyId) !== -1) {
const error = new Error(`Dependency ${dependencyId} is already added for the request ${dependentId}`);
error.name = "Duplicate Dependency";
throw error;
}
dependent.dependsOn.push(dependencyId);
} else {
const iterator = this.requests.entries();
let prev;
let cur = iterator.next();
while (!cur.done && cur.value[1].id !== dependentId) {
prev = cur;
cur = iterator.next();
}
if (typeof prev !== "undefined") {
const dId = prev.value[0];
if (cur.value[1].dependsOn === undefined) {
cur.value[1].dependsOn = [];
}
if (cur.value[1].dependsOn.indexOf(dId) !== -1) {
const error = new Error(`Dependency ${dId} is already added for the request ${dependentId}`);
error.name = "Duplicate Dependency";
throw error;
}
cur.value[1].dependsOn.push(dId);
} else {
const error = new Error(`Can't add dependency ${dependencyId}, There is only a dependent request in the batch`);
error.name = "Invalid Dependency Addition";
throw error;
}
}
}
/**
* @public
* Removes a dependency for a given dependent request id
* @param {string} dependentId - The id of the dependent request
* @param {string} [dependencyId] - The id of the dependency request, if not specified will remove all the dependencies of that request
* @returns The boolean indicating removed status
*/
public removeDependency(dependentId: string, dependencyId?: string): boolean {
const request = this.requests.get(dependentId);
if (typeof request === "undefined" || request.dependsOn === undefined || request.dependsOn.length === 0) {
return false;
}
if (typeof dependencyId !== "undefined") {
const index = request.dependsOn.indexOf(dependencyId);
if (index === -1) {
return false;
}
request.dependsOn.splice(index, 1);
return true;
} else {
delete request.dependsOn;
return true;
}
}
}
@@ -0,0 +1,127 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module BatchResponseContent
*/
/**
* @interface
* Signature represents key value pair object
*/
interface KeyValuePairObject {
[key: string]: any;
}
/**
* @interface
* Signature representing Batch response body
* @property {KeyValuePairObject[]} responses - An array of key value pair representing response object for every request
* @property {string} [@odata.nextLink] - The nextLink value to get next set of responses in case of asynchronous batch requests
*/
export interface BatchResponseBody {
responses: KeyValuePairObject[];
"@odata.nextLink"?: string;
}
/**
* @class
* Class that handles BatchResponseContent
*/
export class BatchResponseContent {
/**
* To hold the responses
*/
private responses: Map<string, Response>;
/**
* Holds the next link url
*/
private nextLink: string;
/**
* @public
* @constructor
* Creates the BatchResponseContent instance
* @param {BatchResponseBody} response - The response body returned for batch request from server
* @returns An instance of a BatchResponseContent
*/
public constructor(response: BatchResponseBody) {
this.responses = new Map();
this.update(response);
}
/**
* @private
* Creates native Response object from the json representation of it.
* @param {KeyValuePairObject} responseJSON - The response json value
* @returns The Response Object instance
*/
private createResponseObject(responseJSON: KeyValuePairObject): Response {
const body = responseJSON.body;
const options: KeyValuePairObject = {};
options.status = responseJSON.status;
if (responseJSON.statusText !== undefined) {
options.statusText = responseJSON.statusText;
}
options.headers = responseJSON.headers;
if (options.headers !== undefined && options.headers["Content-Type"] !== undefined) {
if (options.headers["Content-Type"].split(";")[0] === "application/json") {
const bodyString = JSON.stringify(body);
return new Response(bodyString, options);
}
}
return new Response(body, options);
}
/**
* @public
* Updates the Batch response content instance with given responses.
* @param {BatchResponseBody} response - The response json representing batch response message
* @returns Nothing
*/
public update(response: BatchResponseBody): void {
this.nextLink = response["@odata.nextLink"];
const responses = response.responses;
for (let i = 0, l = responses.length; i < l; i++) {
this.responses.set(responses[i].id, this.createResponseObject(responses[i]));
}
}
/**
* @public
* To get the response of a request for a given request id
* @param {string} requestId - The request id value
* @returns The Response object instance for the particular request
*/
public getResponseById(requestId: string): Response {
return this.responses.get(requestId);
}
/**
* @public
* To get all the responses of the batch request
* @returns The Map of id and Response objects
*/
public getResponses(): Map<string, Response> {
return this.responses;
}
/**
* @public
* To get the iterator for the responses
* @returns The Iterable generator for the response objects
*/
public *getResponsesIterator(): IterableIterator<[string, Response]> {
const iterator = this.responses.entries();
let cur = iterator.next();
while (!cur.done) {
yield cur.value;
cur = iterator.next();
}
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path= "./../shims.d.ts" />
export { BatchRequestBody, RequestData, BatchRequestContent, BatchRequestData, BatchRequestStep } from "./content/BatchRequestContent";
export { BatchResponseBody, BatchResponseContent } from "./content/BatchResponseContent";
export { AuthenticationHandler } from "./middleware/AuthenticationHandler";
export { HTTPMessageHandler } from "./middleware/HTTPMessageHandler";
export { Middleware } from "./middleware/IMiddleware";
export { RetryHandler } from "./middleware/RetryHandler";
export { RedirectHandler } from "./middleware/RedirectHandler";
export { TelemetryHandler } from "./middleware/TelemetryHandler";
export { MiddlewareFactory } from "./middleware/MiddlewareFactory";
export { AuthenticationHandlerOptions } from "./middleware/options/AuthenticationHandlerOptions";
export { MiddlewareOptions } from "./middleware/options/IMiddlewareOptions";
export { RetryHandlerOptions, ShouldRetry } from "./middleware/options/RetryHandlerOptions";
export { RedirectHandlerOptions, ShouldRedirect } from "./middleware/options/RedirectHandlerOptions";
export { FeatureUsageFlag, TelemetryHandlerOptions } from "./middleware/options/TelemetryHandlerOptions";
export { ChaosHandlerOptions } from "./middleware/options/ChaosHandlerOptions";
export { ChaosStrategy } from "./middleware/options/ChaosStrategy";
export { ChaosHandler } from "./middleware/ChaosHandler";
export { FileObject, LargeFileUploadSession, LargeFileUploadTask, LargeFileUploadTaskOptions, SliceType } from "./tasks/LargeFileUploadTask";
export { OneDriveLargeFileUploadOptions, OneDriveLargeFileUploadTask } from "./tasks/OneDriveLargeFileUploadTask";
export { getValidRangeSize } from "./tasks/OneDriveLargeFileUploadTaskUtil";
export { StreamUpload } from "./tasks/FileUploadTask/FileObjectClasses/StreamUpload";
export { FileUpload } from "./tasks/FileUploadTask/FileObjectClasses/FileUpload";
export { UploadResult } from "./tasks/FileUploadTask/UploadResult";
export { UploadEventHandlers } from "./tasks/FileUploadTask/Interfaces/IUploadEventHandlers";
export { Range } from "./tasks/FileUploadTask/Range";
export { GraphRequestOptions, PageCollection, PageIterator, PageIteratorCallback } from "./tasks/PageIterator";
export { Client } from "./Client";
export { CustomAuthenticationProvider } from "./CustomAuthenticationProvider";
export { GraphError } from "./GraphError";
export { GraphClientError } from "./GraphClientError";
export { GraphRequest, URLComponents } from "./GraphRequest";
export { AuthProvider } from "./IAuthProvider";
export { AuthenticationProvider } from "./IAuthenticationProvider";
export { AuthenticationProviderOptions } from "./IAuthenticationProviderOptions";
export { AuthProviderCallback } from "./IAuthProviderCallback";
export { ClientOptions } from "./IClientOptions";
export { Context } from "./IContext";
export { FetchOptions, NodeFetchInit } from "./IFetchOptions";
export { GraphRequestCallback } from "./IGraphRequestCallback";
export { Options } from "./IOptions";
export { ResponseType } from "./ResponseType";
@@ -0,0 +1,100 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module AuthenticationHandler
*/
import { isCustomHost, isGraphURL } from "../GraphRequestUtil";
import { AuthenticationProvider } from "../IAuthenticationProvider";
import { AuthenticationProviderOptions } from "../IAuthenticationProviderOptions";
import { Context } from "../IContext";
import { Middleware } from "./IMiddleware";
import { MiddlewareControl } from "./MiddlewareControl";
import { appendRequestHeader } from "./MiddlewareUtil";
import { AuthenticationHandlerOptions } from "./options/AuthenticationHandlerOptions";
import { FeatureUsageFlag, TelemetryHandlerOptions } from "./options/TelemetryHandlerOptions";
/**
* @class
* @implements Middleware
* Class representing AuthenticationHandler
*/
export class AuthenticationHandler implements Middleware {
/**
* @private
* A member representing the authorization header name
*/
private static AUTHORIZATION_HEADER = "Authorization";
/**
* @private
* A member to hold an AuthenticationProvider instance
*/
private authenticationProvider: AuthenticationProvider;
/**
* @private
* A member to hold next middleware in the middleware chain
*/
private nextMiddleware: Middleware;
/**
* @public
* @constructor
* Creates an instance of AuthenticationHandler
* @param {AuthenticationProvider} authenticationProvider - The authentication provider for the authentication handler
*/
public constructor(authenticationProvider: AuthenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
/**
* @public
* @async
* To execute the current middleware
* @param {Context} context - The context object of the request
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
const url = typeof context.request === "string" ? context.request : context.request.url;
if (isGraphURL(url) || (context.customHosts && isCustomHost(url, context.customHosts))) {
let options: AuthenticationHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(AuthenticationHandlerOptions) as AuthenticationHandlerOptions;
}
let authenticationProvider: AuthenticationProvider;
let authenticationProviderOptions: AuthenticationProviderOptions;
if (options) {
authenticationProvider = options.authenticationProvider;
authenticationProviderOptions = options.authenticationProviderOptions;
}
if (!authenticationProvider) {
authenticationProvider = this.authenticationProvider;
}
const token: string = await authenticationProvider.getAccessToken(authenticationProviderOptions);
const bearerKey = `Bearer ${token}`;
appendRequestHeader(context.request, context.options, AuthenticationHandler.AUTHORIZATION_HEADER, bearerKey);
TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.AUTHENTICATION_HANDLER_ENABLED);
} else {
if (context.options.headers) {
delete context.options.headers[AuthenticationHandler.AUTHORIZATION_HEADER];
}
}
return await this.nextMiddleware.execute(context);
}
/**
* @public
* To set the next middleware in the chain
* @param {Middleware} next - The middleware instance
* @returns Nothing
*/
public setNext(next: Middleware): void {
this.nextMiddleware = next;
}
}
@@ -0,0 +1,256 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module ChaosHandler
*/
import { Context } from "../IContext";
import { RequestMethod } from "../RequestMethod";
import { Middleware } from "./IMiddleware";
import { MiddlewareControl } from "./MiddlewareControl";
import { generateUUID } from "./MiddlewareUtil";
import { httpStatusCode, methodStatusCode } from "./options/ChaosHandlerData";
import { ChaosHandlerOptions } from "./options/ChaosHandlerOptions";
import { ChaosStrategy } from "./options/ChaosStrategy";
/**
* Class representing ChaosHandler
* @class
* Class
* @implements Middleware
*/
export class ChaosHandler implements Middleware {
/**
* A member holding options to customize the handler behavior
*
* @private
*/
private options: ChaosHandlerOptions;
/**
* container for the manual map that has been written by the client
*
* @private
*/
private manualMap: Map<string, Map<string, number>>;
/**
* @private
* A member to hold next middleware in the middleware chain
*/
private nextMiddleware: Middleware;
/**
* @public
* @constructor
* To create an instance of Testing Handler
* @param {ChaosHandlerOptions} [options = new ChaosHandlerOptions()] - The testing handler options instance
* @param manualMap - The Map passed by user containing url-statusCode info
* @returns An instance of Testing Handler
*/
public constructor(options: ChaosHandlerOptions = new ChaosHandlerOptions(), manualMap?: Map<string, Map<string, number>>) {
this.options = options;
this.manualMap = manualMap;
}
/**
* Generates responseHeader
* @private
* @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
* @param {string} requestID - request id
* @param {string} requestDate - date of the request
* @returns response Header
*/
private createResponseHeaders(chaosHandlerOptions: ChaosHandlerOptions, requestID: string, requestDate: string) {
const responseHeader: Headers = chaosHandlerOptions.headers ? new Headers(chaosHandlerOptions.headers) : new Headers();
responseHeader.append("Cache-Control", "no-store");
responseHeader.append("request-id", requestID);
responseHeader.append("client-request-id", requestID);
responseHeader.append("x-ms-ags-diagnostic", "");
responseHeader.append("Date", requestDate);
responseHeader.append("Strict-Transport-Security", "");
if (chaosHandlerOptions.statusCode === 429) {
// throttling case has to have a timeout scenario
responseHeader.append("retry-after", "3");
}
return responseHeader;
}
/**
* Generates responseBody
* @private
* @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
* @param {string} requestID - request id
* @param {string} requestDate - date of the request
* * @returns response body
*/
private createResponseBody(chaosHandlerOptions: ChaosHandlerOptions, requestID: string, requestDate: string) {
if (chaosHandlerOptions.responseBody) {
return chaosHandlerOptions.responseBody;
}
let body: any;
if (chaosHandlerOptions.statusCode >= 400) {
const codeMessage: string = httpStatusCode[chaosHandlerOptions.statusCode];
const errMessage: string = chaosHandlerOptions.statusMessage;
body = {
error: {
code: codeMessage,
message: errMessage,
innerError: {
"request-id": requestID,
date: requestDate,
},
},
};
} else {
body = {};
}
return body;
}
/**
* creates a response
* @private
* @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
* @param {Context} context - Contains the context of the request
*/
private createResponse(chaosHandlerOptions: ChaosHandlerOptions, context: Context) {
const requestURL = context.request as string;
const requestID = generateUUID();
const requestDate = new Date();
const responseHeader = this.createResponseHeaders(chaosHandlerOptions, requestID, requestDate.toString());
const responseBody = this.createResponseBody(chaosHandlerOptions, requestID, requestDate.toString());
const init: any = { url: requestURL, status: chaosHandlerOptions.statusCode, statusText: chaosHandlerOptions.statusMessage, headers: responseHeader };
context.response = new Response(typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody), init);
}
/**
* Decides whether to send the request to the graph or not
* @private
* @param {ChaosHandlerOptions} chaosHandlerOptions - A ChaosHandlerOptions object
* @param {Context} context - Contains the context of the request
* @returns nothing
*/
private async sendRequest(chaosHandlerOptions: ChaosHandlerOptions, context: Context): Promise<void> {
this.setStatusCode(chaosHandlerOptions, context.request as string, context.options.method as RequestMethod);
if ((chaosHandlerOptions.chaosStrategy === ChaosStrategy.MANUAL && !this.nextMiddleware) || Math.floor(Math.random() * 100) < chaosHandlerOptions.chaosPercentage) {
this.createResponse(chaosHandlerOptions, context);
} else if (this.nextMiddleware) {
await this.nextMiddleware.execute(context);
}
}
/**
* Fetches a random status code for the RANDOM mode from the predefined array
* @private
* @param {string} requestMethod - the API method for the request
* @returns a random status code from a given set of status codes
*/
private getRandomStatusCode(requestMethod: RequestMethod): number {
const statusCodeArray: number[] = methodStatusCode[requestMethod] as number[];
return statusCodeArray[Math.floor(Math.random() * statusCodeArray.length)];
}
/**
* To fetch the relative URL out of the complete URL using a predefined regex pattern
* @private
* @param {string} urlMethod - the complete URL
* @returns the string as relative URL
*/
private getRelativeURL(urlMethod: string): string {
const pattern = /https?:\/\/graph\.microsoft\.com\/[^/]+(.+?)(\?|$)/;
let relativeURL: string;
if (pattern.exec(urlMethod) !== null) {
relativeURL = pattern.exec(urlMethod)[1];
}
return relativeURL;
}
/**
* To fetch the status code from the map(if needed), then returns response by calling createResponse
* @private
* @param {ChaosHandlerOptions} chaosHandlerOptions - The ChaosHandlerOptions object
* @param {string} requestURL - the URL for the request
* @param {string} requestMethod - the API method for the request
*/
private setStatusCode(chaosHandlerOptions: ChaosHandlerOptions, requestURL: string, requestMethod: RequestMethod) {
if (chaosHandlerOptions.chaosStrategy === ChaosStrategy.MANUAL) {
if (chaosHandlerOptions.statusCode === undefined) {
// manual mode with no status code, can be a global level or request level without statusCode
const relativeURL: string = this.getRelativeURL(requestURL);
if (this.manualMap.get(relativeURL) !== undefined) {
// checking Manual Map for exact match
if (this.manualMap.get(relativeURL).get(requestMethod) !== undefined) {
chaosHandlerOptions.statusCode = this.manualMap.get(relativeURL).get(requestMethod);
}
// else statusCode would be undefined
} else {
// checking for regex match if exact match doesn't work
this.manualMap.forEach((value: Map<string, number>, key: string) => {
const regexURL = new RegExp(key + "$");
if (regexURL.test(relativeURL)) {
if (this.manualMap.get(key).get(requestMethod) !== undefined) {
chaosHandlerOptions.statusCode = this.manualMap.get(key).get(requestMethod);
}
// else statusCode would be undefined
}
});
}
// Case of redirection or request url not in map ---> statusCode would be undefined
}
} else {
// Handling the case of Random here
chaosHandlerOptions.statusCode = this.getRandomStatusCode(requestMethod);
// else statusCode would be undefined
}
}
/**
* To get the options for execution of the middleware
* @private
* @param {Context} context - The context object
* @returns options for middleware execution
*/
private getOptions(context: Context): ChaosHandlerOptions {
let options: ChaosHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(ChaosHandlerOptions) as ChaosHandlerOptions;
}
if (typeof options === "undefined") {
options = Object.assign(new ChaosHandlerOptions(), this.options);
}
return options;
}
/**
* To execute the current middleware
* @public
* @async
* @param {Context} context - The context object of the request
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
const chaosHandlerOptions: ChaosHandlerOptions = this.getOptions(context);
return await this.sendRequest(chaosHandlerOptions, context);
}
/**
* @public
* To set the next middleware in the chain
* @param {Middleware} next - The middleware instance
* @returns Nothing
*/
public setNext(next: Middleware): void {
this.nextMiddleware = next;
}
}
@@ -0,0 +1,31 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module HTTPMessageHandler
*/
import { Context } from "../IContext";
import { Middleware } from "./IMiddleware";
/**
* @class
* @implements Middleware
* Class for HTTPMessageHandler
*/
export class HTTPMessageHandler implements Middleware {
/**
* @public
* @async
* To execute the current middleware
* @param {Context} context - The request context object
* @returns A promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
context.response = await fetch(context.request, context.options);
}
}
@@ -0,0 +1,18 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { Context } from "../IContext";
/**
* @interface
* @property {Function} execute - The method to execute the middleware
* @property {Function} [setNext] - A method to set the next middleware in the chain
*/
export interface Middleware {
execute: (context: Context) => Promise<void>;
setNext?: (middleware: Middleware) => void;
}
@@ -0,0 +1,64 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module MiddlewareControl
*/
import { MiddlewareOptions } from "./options/IMiddlewareOptions";
/**
* @class
* Class representing MiddlewareControl
*/
export class MiddlewareControl {
/**
* @private
* A member holding map of MiddlewareOptions
*/
private middlewareOptions: Map<Function, MiddlewareOptions>;
/**
* @public
* @constructor
* Creates an instance of MiddlewareControl
* @param {MiddlewareOptions[]} [middlewareOptions = []] - The array of middlewareOptions
* @returns The instance of MiddlewareControl
*/
public constructor(middlewareOptions: MiddlewareOptions[] = []) {
this.middlewareOptions = new Map<Function, MiddlewareOptions>();
for (const option of middlewareOptions) {
const fn = option.constructor;
this.middlewareOptions.set(fn, option);
}
}
/**
* @public
* To get the middleware option using the class of the option
* @param {Function} fn - The class of the strongly typed option class
* @returns The middleware option
* @example
* // if you wanted to return the middleware option associated with this class (MiddlewareControl)
* // call this function like this:
* getMiddlewareOptions(MiddlewareControl)
*/
public getMiddlewareOptions(fn: Function): MiddlewareOptions {
return this.middlewareOptions.get(fn);
}
/**
* @public
* To set the middleware options using the class of the option
* @param {Function} fn - The class of the strongly typed option class
* @param {MiddlewareOptions} option - The strongly typed middleware option
* @returns nothing
*/
public setMiddlewareOptions(fn: Function, option: MiddlewareOptions): void {
this.middlewareOptions.set(fn, option);
}
}
@@ -0,0 +1,61 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module MiddlewareFactory
*/
import { AuthenticationProvider } from "../IAuthenticationProvider";
import { AuthenticationHandler } from "./AuthenticationHandler";
import { HTTPMessageHandler } from "./HTTPMessageHandler";
import { Middleware } from "./IMiddleware";
import { RedirectHandlerOptions } from "./options/RedirectHandlerOptions";
import { RetryHandlerOptions } from "./options/RetryHandlerOptions";
import { RedirectHandler } from "./RedirectHandler";
import { RetryHandler } from "./RetryHandler";
import { TelemetryHandler } from "./TelemetryHandler";
/**
* @private
* To check whether the environment is node or not
* @returns A boolean representing the environment is node or not
*/
const isNodeEnvironment = (): boolean => {
return typeof process === "object" && typeof require === "function";
};
/**
* @class
* Class containing function(s) related to the middleware pipelines.
*/
export class MiddlewareFactory {
/**
* @public
* @static
* Returns the default middleware chain an array with the middleware handlers
* @param {AuthenticationProvider} authProvider - The authentication provider instance
* @returns an array of the middleware handlers of the default middleware chain
*/
public static getDefaultMiddlewareChain(authProvider: AuthenticationProvider): Middleware[] {
const middleware: Middleware[] = [];
const authenticationHandler = new AuthenticationHandler(authProvider);
const retryHandler = new RetryHandler(new RetryHandlerOptions());
const telemetryHandler = new TelemetryHandler();
const httpMessageHandler = new HTTPMessageHandler();
middleware.push(authenticationHandler);
middleware.push(retryHandler);
if (isNodeEnvironment()) {
const redirectHandler = new RedirectHandler(new RedirectHandlerOptions());
middleware.push(redirectHandler);
}
middleware.push(telemetryHandler);
middleware.push(httpMessageHandler);
return middleware;
}
}
@@ -0,0 +1,144 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module MiddlewareUtil
*/
import { FetchOptions } from "../IFetchOptions";
/**
* @constant
* To generate the UUID
* @returns The UUID string
*/
export const generateUUID = (): string => {
let uuid = "";
for (let j = 0; j < 32; j++) {
if (j === 8 || j === 12 || j === 16 || j === 20) {
uuid += "-";
}
uuid += Math.floor(Math.random() * 16).toString(16);
}
return uuid;
};
/**
* @constant
* To get the request header from the request
* @param {RequestInfo} request - The request object or the url string
* @param {FetchOptions|undefined} options - The request options object
* @param {string} key - The header key string
* @returns A header value for the given key from the request
*/
export const getRequestHeader = (request: RequestInfo, options: FetchOptions | undefined, key: string): string | null => {
let value: string = null;
if (typeof Request !== "undefined" && request instanceof Request) {
value = (request as Request).headers.get(key);
} else if (typeof options !== "undefined" && options.headers !== undefined) {
if (typeof Headers !== "undefined" && options.headers instanceof Headers) {
value = (options.headers as Headers).get(key);
} else if (options.headers instanceof Array) {
const headers = options.headers as string[][];
for (let i = 0, l = headers.length; i < l; i++) {
if (headers[i][0] === key) {
value = headers[i][1];
break;
}
}
} else if (options.headers[key] !== undefined) {
value = options.headers[key];
}
}
return value;
};
/**
* @constant
* To set the header value to the given request
* @param {RequestInfo} request - The request object or the url string
* @param {FetchOptions|undefined} options - The request options object
* @param {string} key - The header key string
* @param {string } value - The header value string
* @returns Nothing
*/
export const setRequestHeader = (request: RequestInfo, options: FetchOptions | undefined, key: string, value: string): void => {
if (typeof Request !== "undefined" && request instanceof Request) {
(request as Request).headers.set(key, value);
} else if (typeof options !== "undefined") {
if (options.headers === undefined) {
options.headers = new Headers({
[key]: value,
});
} else {
if (typeof Headers !== "undefined" && options.headers instanceof Headers) {
(options.headers as Headers).set(key, value);
} else if (options.headers instanceof Array) {
let i = 0;
const l = options.headers.length;
for (; i < l; i++) {
const header = options.headers[i];
if (header[0] === key) {
header[1] = value;
break;
}
}
if (i === l) {
(options.headers as string[][]).push([key, value]);
}
} else {
Object.assign(options.headers, { [key]: value });
}
}
}
};
/**
* @constant
* To append the header value to the given request
* @param {RequestInfo} request - The request object or the url string
* @param {FetchOptions|undefined} options - The request options object
* @param {string} key - The header key string
* @param {string } value - The header value string
* @returns Nothing
*/
export const appendRequestHeader = (request: RequestInfo, options: FetchOptions | undefined, key: string, value: string): void => {
if (typeof Request !== "undefined" && request instanceof Request) {
(request as Request).headers.append(key, value);
} else if (typeof options !== "undefined") {
if (options.headers === undefined) {
options.headers = new Headers({
[key]: value,
});
} else {
if (typeof Headers !== "undefined" && options.headers instanceof Headers) {
(options.headers as Headers).append(key, value);
} else if (options.headers instanceof Array) {
(options.headers as string[][]).push([key, value]);
} else if (options.headers === undefined) {
options.headers = { [key]: value };
} else if (options.headers[key] === undefined) {
options.headers[key] = value;
} else {
options.headers[key] += `, ${value}`;
}
}
}
};
/**
* @constant
* To clone the request with the new url
* @param {string} url - The new url string
* @param {Request} request - The request object
* @returns A promise that resolves to request object
*/
export const cloneRequestWithNewUrl = async (newUrl: string, request: Request): Promise<Request> => {
const body = request.headers.get("Content-Type") ? await request.blob() : await Promise.resolve(undefined);
const { method, headers, referrer, referrerPolicy, mode, credentials, cache, redirect, integrity, keepalive, signal } = request;
return new Request(newUrl, { method, headers, body, referrer, referrerPolicy, mode, credentials, cache, redirect, integrity, keepalive, signal });
};
@@ -0,0 +1,237 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module RedirectHandler
*/
import { Context } from "../IContext";
import { RequestMethod } from "../RequestMethod";
import { Middleware } from "./IMiddleware";
import { MiddlewareControl } from "./MiddlewareControl";
import { cloneRequestWithNewUrl } from "./MiddlewareUtil";
import { RedirectHandlerOptions } from "./options/RedirectHandlerOptions";
import { FeatureUsageFlag, TelemetryHandlerOptions } from "./options/TelemetryHandlerOptions";
/**
* @class
* Class
* @implements Middleware
* Class representing RedirectHandler
*/
export class RedirectHandler implements Middleware {
/**
* @private
* @static
* A member holding the array of redirect status codes
*/
private static REDIRECT_STATUS_CODES: number[] = [
301, // Moved Permanently
302, // Found
303, // See Other
307, // Temporary Permanently
308, // Moved Permanently
];
/**
* @private
* @static
* A member holding SeeOther status code
*/
private static STATUS_CODE_SEE_OTHER = 303;
/**
* @private
* @static
* A member holding the name of the location header
*/
private static LOCATION_HEADER = "Location";
/**
* @private
* @static
* A member representing the authorization header name
*/
private static AUTHORIZATION_HEADER = "Authorization";
/**
* @private
* @static
* A member holding the manual redirect value
*/
private static MANUAL_REDIRECT: RequestRedirect = "manual";
/**
* @private
* A member holding options to customize the handler behavior
*/
private options: RedirectHandlerOptions;
/**
* @private
* A member to hold next middleware in the middleware chain
*/
private nextMiddleware: Middleware;
/**
* @public
* @constructor
* To create an instance of RedirectHandler
* @param {RedirectHandlerOptions} [options = new RedirectHandlerOptions()] - The redirect handler options instance
* @returns An instance of RedirectHandler
*/
public constructor(options: RedirectHandlerOptions = new RedirectHandlerOptions()) {
this.options = options;
}
/**
* @private
* To check whether the response has the redirect status code or not
* @param {Response} response - The response object
* @returns A boolean representing whether the response contains the redirect status code or not
*/
private isRedirect(response: Response): boolean {
return RedirectHandler.REDIRECT_STATUS_CODES.indexOf(response.status) !== -1;
}
/**
* @private
* To check whether the response has location header or not
* @param {Response} response - The response object
* @returns A boolean representing the whether the response has location header or not
*/
private hasLocationHeader(response: Response): boolean {
return response.headers.has(RedirectHandler.LOCATION_HEADER);
}
/**
* @private
* To get the redirect url from location header in response object
* @param {Response} response - The response object
* @returns A redirect url from location header
*/
private getLocationHeader(response: Response): string {
return response.headers.get(RedirectHandler.LOCATION_HEADER);
}
/**
* @private
* To check whether the given url is a relative url or not
* @param {string} url - The url string value
* @returns A boolean representing whether the given url is a relative url or not
*/
private isRelativeURL(url: string): boolean {
return url.indexOf("://") === -1;
}
/**
* @private
* To check whether the authorization header in the request should be dropped for consequent redirected requests
* @param {string} requestUrl - The request url value
* @param {string} redirectUrl - The redirect url value
* @returns A boolean representing whether the authorization header in the request should be dropped for consequent redirected requests
*/
private shouldDropAuthorizationHeader(requestUrl: string, redirectUrl: string): boolean {
const schemeHostRegex = /^[A-Za-z].+?:\/\/.+?(?=\/|$)/;
const requestMatches: string[] = schemeHostRegex.exec(requestUrl);
let requestAuthority: string;
let redirectAuthority: string;
if (requestMatches !== null) {
requestAuthority = requestMatches[0];
}
const redirectMatches: string[] = schemeHostRegex.exec(redirectUrl);
if (redirectMatches !== null) {
redirectAuthority = redirectMatches[0];
}
return typeof requestAuthority !== "undefined" && typeof redirectAuthority !== "undefined" && requestAuthority !== redirectAuthority;
}
/**
* @private
* @async
* To update a request url with the redirect url
* @param {string} redirectUrl - The redirect url value
* @param {Context} context - The context object value
* @returns Nothing
*/
private async updateRequestUrl(redirectUrl: string, context: Context): Promise<void> {
context.request = typeof context.request === "string" ? redirectUrl : await cloneRequestWithNewUrl(redirectUrl, context.request as Request);
}
/**
* @private
* To get the options for execution of the middleware
* @param {Context} context - The context object
* @returns A options for middleware execution
*/
private getOptions(context: Context): RedirectHandlerOptions {
let options: RedirectHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(RedirectHandlerOptions) as RedirectHandlerOptions;
}
if (typeof options === "undefined") {
options = Object.assign(new RedirectHandlerOptions(), this.options);
}
return options;
}
/**
* @private
* @async
* To execute the next middleware and to handle in case of redirect response returned by the server
* @param {Context} context - The context object
* @param {number} redirectCount - The redirect count value
* @param {RedirectHandlerOptions} options - The redirect handler options instance
* @returns A promise that resolves to nothing
*/
private async executeWithRedirect(context: Context, redirectCount: number, options: RedirectHandlerOptions): Promise<void> {
await this.nextMiddleware.execute(context);
const response = context.response;
if (redirectCount < options.maxRedirects && this.isRedirect(response) && this.hasLocationHeader(response) && options.shouldRedirect(response)) {
++redirectCount;
if (response.status === RedirectHandler.STATUS_CODE_SEE_OTHER) {
context.options.method = RequestMethod.GET;
delete context.options.body;
} else {
const redirectUrl: string = this.getLocationHeader(response);
if (!this.isRelativeURL(redirectUrl) && this.shouldDropAuthorizationHeader(response.url, redirectUrl)) {
delete context.options.headers[RedirectHandler.AUTHORIZATION_HEADER];
}
await this.updateRequestUrl(redirectUrl, context);
}
await this.executeWithRedirect(context, redirectCount, options);
} else {
return;
}
}
/**
* @public
* @async
* To execute the current middleware
* @param {Context} context - The context object of the request
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
const redirectCount = 0;
const options = this.getOptions(context);
context.options.redirect = RedirectHandler.MANUAL_REDIRECT;
TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.REDIRECT_HANDLER_ENABLED);
return await this.executeWithRedirect(context, redirectCount, options);
}
/**
* @public
* To set the next middleware in the chain
* @param {Middleware} next - The middleware instance
* @returns Nothing
*/
public setNext(next: Middleware): void {
this.nextMiddleware = next;
}
}
@@ -0,0 +1,208 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module RetryHandler
*/
import { Context } from "../IContext";
import { FetchOptions } from "../IFetchOptions";
import { RequestMethod } from "../RequestMethod";
import { Middleware } from "./IMiddleware";
import { MiddlewareControl } from "./MiddlewareControl";
import { getRequestHeader, setRequestHeader } from "./MiddlewareUtil";
import { RetryHandlerOptions } from "./options/RetryHandlerOptions";
import { FeatureUsageFlag, TelemetryHandlerOptions } from "./options/TelemetryHandlerOptions";
/**
* @class
* @implements Middleware
* Class for RetryHandler
*/
export class RetryHandler implements Middleware {
/**
* @private
* @static
* A list of status codes that needs to be retried
*/
private static RETRY_STATUS_CODES: number[] = [
429, // Too many requests
503, // Service unavailable
504, // Gateway timeout
];
/**
* @private
* @static
* A member holding the name of retry attempt header
*/
private static RETRY_ATTEMPT_HEADER = "Retry-Attempt";
/**
* @private
* @static
* A member holding the name of retry after header
*/
private static RETRY_AFTER_HEADER = "Retry-After";
/**
* @private
* A member to hold next middleware in the middleware chain
*/
private nextMiddleware: Middleware;
/**
* @private
* A member holding the retry handler options
*/
private options: RetryHandlerOptions;
/**
* @public
* @constructor
* To create an instance of RetryHandler
* @param {RetryHandlerOptions} [options = new RetryHandlerOptions()] - The retry handler options value
* @returns An instance of RetryHandler
*/
public constructor(options: RetryHandlerOptions = new RetryHandlerOptions()) {
this.options = options;
}
/**
*
* @private
* To check whether the response has the retry status code
* @param {Response} response - The response object
* @returns Whether the response has retry status code or not
*/
private isRetry(response: Response): boolean {
return RetryHandler.RETRY_STATUS_CODES.indexOf(response.status) !== -1;
}
/**
* @private
* To check whether the payload is buffered or not
* @param {RequestInfo} request - The url string or the request object value
* @param {FetchOptions} options - The options of a request
* @returns Whether the payload is buffered or not
*/
private isBuffered(request: RequestInfo, options: FetchOptions | undefined): boolean {
const method = typeof request === "string" ? options.method : (request as Request).method;
const isPutPatchOrPost: boolean = method === RequestMethod.PUT || method === RequestMethod.PATCH || method === RequestMethod.POST;
if (isPutPatchOrPost) {
const isStream = getRequestHeader(request, options, "Content-Type") === "application/octet-stream";
if (isStream) {
return false;
}
}
return true;
}
/**
* @private
* To get the delay for a retry
* @param {Response} response - The response object
* @param {number} retryAttempts - The current attempt count
* @param {number} delay - The delay value in seconds
* @returns A delay for a retry
*/
private getDelay(response: Response, retryAttempts: number, delay: number): number {
const getRandomness = () => Number(Math.random().toFixed(3));
const retryAfter = response.headers !== undefined ? response.headers.get(RetryHandler.RETRY_AFTER_HEADER) : null;
let newDelay: number;
if (retryAfter !== null) {
if (Number.isNaN(Number(retryAfter))) {
newDelay = Math.round((new Date(retryAfter).getTime() - Date.now()) / 1000);
} else {
newDelay = Number(retryAfter);
}
} else {
// Adding randomness to avoid retrying at a same
newDelay = retryAttempts >= 2 ? this.getExponentialBackOffTime(retryAttempts) + delay + getRandomness() : delay + getRandomness();
}
return Math.min(newDelay, this.options.getMaxDelay() + getRandomness());
}
/**
* @private
* To get an exponential back off value
* @param {number} attempts - The current attempt count
* @returns An exponential back off value
*/
private getExponentialBackOffTime(attempts: number): number {
return Math.round((1 / 2) * (2 ** attempts - 1));
}
/**
* @private
* @async
* To add delay for the execution
* @param {number} delaySeconds - The delay value in seconds
* @returns Nothing
*/
private async sleep(delaySeconds: number): Promise<void> {
const delayMilliseconds = delaySeconds * 1000;
return new Promise((resolve) => setTimeout(resolve, delayMilliseconds));
}
private getOptions(context: Context): RetryHandlerOptions {
let options: RetryHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(this.options.constructor) as RetryHandlerOptions;
}
if (typeof options === "undefined") {
options = Object.assign(new RetryHandlerOptions(), this.options);
}
return options;
}
/**
* @private
* @async
* To execute the middleware with retries
* @param {Context} context - The context object
* @param {number} retryAttempts - The current attempt count
* @param {RetryHandlerOptions} options - The retry middleware options instance
* @returns A Promise that resolves to nothing
*/
private async executeWithRetry(context: Context, retryAttempts: number, options: RetryHandlerOptions): Promise<void> {
await this.nextMiddleware.execute(context);
if (retryAttempts < options.maxRetries && this.isRetry(context.response) && this.isBuffered(context.request, context.options) && options.shouldRetry(options.delay, retryAttempts, context.request, context.options, context.response)) {
++retryAttempts;
setRequestHeader(context.request, context.options, RetryHandler.RETRY_ATTEMPT_HEADER, retryAttempts.toString());
const delay = this.getDelay(context.response, retryAttempts, options.delay);
await this.sleep(delay);
return await this.executeWithRetry(context, retryAttempts, options);
} else {
return;
}
}
/**
* @public
* @async
* To execute the current middleware
* @param {Context} context - The context object of the request
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
const retryAttempts = 0;
const options: RetryHandlerOptions = this.getOptions(context);
TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.RETRY_HANDLER_ENABLED);
return await this.executeWithRetry(context, retryAttempts, options);
}
/**
* @public
* To set the next middleware in the chain
* @param {Middleware} next - The middleware instance
* @returns Nothing
*/
public setNext(next: Middleware): void {
this.nextMiddleware = next;
}
}
@@ -0,0 +1,103 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module TelemetryHandler
*/
import { isCustomHost, isGraphURL } from "../GraphRequestUtil";
import { Context } from "../IContext";
import { PACKAGE_VERSION } from "../Version";
import { Middleware } from "./IMiddleware";
import { MiddlewareControl } from "./MiddlewareControl";
import { appendRequestHeader, generateUUID, getRequestHeader, setRequestHeader } from "./MiddlewareUtil";
import { TelemetryHandlerOptions } from "./options/TelemetryHandlerOptions";
/**
* @class
* @implements Middleware
* Class for TelemetryHandler
*/
export class TelemetryHandler implements Middleware {
/**
* @private
* @static
* A member holding the name of the client request id header
*/
private static CLIENT_REQUEST_ID_HEADER = "client-request-id";
/**
* @private
* @static
* A member holding the name of the sdk version header
*/
private static SDK_VERSION_HEADER = "SdkVersion";
/**
* @private
* @static
* A member holding the language prefix for the sdk version header value
*/
private static PRODUCT_NAME = "graph-js";
/**
* @private
* @static
* A member holding the key for the feature usage metrics
*/
private static FEATURE_USAGE_STRING = "featureUsage";
/**
* @private
* A member to hold next middleware in the middleware chain
*/
private nextMiddleware: Middleware;
/**
* @public
* @async
* To execute the current middleware
* @param {Context} context - The context object of the request
* @returns A Promise that resolves to nothing
*/
public async execute(context: Context): Promise<void> {
const url = typeof context.request === "string" ? context.request : context.request.url;
if (isGraphURL(url) || (context.customHosts && isCustomHost(url, context.customHosts))) {
// Add telemetry only if the request url is a Graph URL.
// Errors are reported as in issue #265 if headers are present when redirecting to a non Graph URL
let clientRequestId: string = getRequestHeader(context.request, context.options, TelemetryHandler.CLIENT_REQUEST_ID_HEADER);
if (!clientRequestId) {
clientRequestId = generateUUID();
setRequestHeader(context.request, context.options, TelemetryHandler.CLIENT_REQUEST_ID_HEADER, clientRequestId);
}
let sdkVersionValue = `${TelemetryHandler.PRODUCT_NAME}/${PACKAGE_VERSION}`;
let options: TelemetryHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(TelemetryHandlerOptions) as TelemetryHandlerOptions;
}
if (options) {
const featureUsage: string = options.getFeatureUsage();
sdkVersionValue += ` (${TelemetryHandler.FEATURE_USAGE_STRING}=${featureUsage})`;
}
appendRequestHeader(context.request, context.options, TelemetryHandler.SDK_VERSION_HEADER, sdkVersionValue);
} else {
// Remove telemetry headers if present during redirection.
delete context.options.headers[TelemetryHandler.CLIENT_REQUEST_ID_HEADER];
delete context.options.headers[TelemetryHandler.SDK_VERSION_HEADER];
}
return await this.nextMiddleware.execute(context);
}
/**
* @public
* To set the next middleware in the chain
* @param {Middleware} next - The middleware instance
* @returns Nothing
*/
public setNext(next: Middleware): void {
this.nextMiddleware = next;
}
}
@@ -0,0 +1,46 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module AuthenticationHandlerOptions
*/
import { AuthenticationProvider } from "../../IAuthenticationProvider";
import { AuthenticationProviderOptions } from "../../IAuthenticationProviderOptions";
import { MiddlewareOptions } from "./IMiddlewareOptions";
/**
* @class
* @implements MiddlewareOptions
* Class representing AuthenticationHandlerOptions
*/
export class AuthenticationHandlerOptions implements MiddlewareOptions {
/**
* @public
* A member holding an instance of an authentication provider
*/
public authenticationProvider: AuthenticationProvider;
/**
* @public
* A member holding an instance of authentication provider options
*/
public authenticationProviderOptions: AuthenticationProviderOptions;
/**
* @public
* @constructor
* To create an instance of AuthenticationHandlerOptions
* @param {AuthenticationProvider} [authenticationProvider] - The authentication provider instance
* @param {AuthenticationProviderOptions} [authenticationProviderOptions] - The authentication provider options instance
* @returns An instance of AuthenticationHandlerOptions
*/
public constructor(authenticationProvider?: AuthenticationProvider, authenticationProviderOptions?: AuthenticationProviderOptions) {
this.authenticationProvider = authenticationProvider;
this.authenticationProviderOptions = authenticationProviderOptions;
}
}
@@ -0,0 +1,88 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module ChaosHandlerData
*/
/**
* Contains RequestMethod to corresponding array of possible status codes, used for Random mode
*/
export const methodStatusCode: { [key: string]: number[] } = {
GET: [429, 500, 502, 503, 504],
POST: [429, 500, 502, 503, 504, 507],
PUT: [429, 500, 502, 503, 504, 507],
PATCH: [429, 500, 502, 503, 504],
DELETE: [429, 500, 502, 503, 504, 507],
};
/**
* Contains statusCode to statusMessage map
*/
export const httpStatusCode: { [key: number]: string } = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
};
@@ -0,0 +1,87 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module ChaosHandlerOptions
*/
import { ChaosStrategy } from "./ChaosStrategy";
import { MiddlewareOptions } from "./IMiddlewareOptions";
/**
* Class representing ChaosHandlerOptions
* @class
* Class
* @implements MiddlewareOptions
*/
export class ChaosHandlerOptions implements MiddlewareOptions {
/**
* Specifies the startegy used for the Testing Handler -> RANDOM/MANUAL
*
* @public
*/
public chaosStrategy: ChaosStrategy;
/**
* Status code to be returned in the response
*
* @public
*/
public statusCode: number;
/**
* The Message to be returned in the response
*
* @public
*/
public statusMessage: string;
/**
* The percentage of randomness/chaos in the handler
*
* Setting the default value as 10%
* @public
*/
public chaosPercentage: number;
/**
* The response body to be returned in the response
*
* @public
*/
public responseBody: any;
/**
* The response headers to be returned in the response
*
* @public
*/
public headers: Headers;
/**
* @public
* @constructor
* To create an instance of Testing Handler Options
* @param {ChaosStrategy} chaosStrategy - Specifies the startegy used for the Testing Handler -> RAMDOM/MANUAL
* @param {string} statusMessage - The Message to be returned in the response
* @param {number?} statusCode - The statusCode to be returned in the response
* @param {number?} chaosPercentage - The percentage of randomness/chaos in the handler
* @param {any?} responseBody - The response body to be returned in the response
* @returns An instance of ChaosHandlerOptions
*/
public constructor(chaosStrategy: ChaosStrategy = ChaosStrategy.RANDOM, statusMessage = "Some error Happened", statusCode?: number, chaosPercentage?: number, responseBody?: any, headers?: Headers) {
this.chaosStrategy = chaosStrategy;
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.chaosPercentage = chaosPercentage !== undefined ? chaosPercentage : 10;
this.responseBody = responseBody;
this.headers = headers;
if (this.chaosPercentage > 100) {
throw new Error("Error Pecentage can not be more than 100");
}
}
}
@@ -0,0 +1,19 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module ChaosStrategy
*/
/**
* Strategy used for Testing Handler
* @enum
*/
export enum ChaosStrategy {
MANUAL,
RANDOM,
}
@@ -0,0 +1,14 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @interface
* Signature representing the middleware options
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MiddlewareOptions {}
@@ -0,0 +1,80 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module RedirectHandlerOptions
*/
import { MiddlewareOptions } from "./IMiddlewareOptions";
/**
* @type
* A type declaration for shouldRetry callback
*/
export type ShouldRedirect = (response: Response) => boolean;
/**
* @class
* @implements MiddlewareOptions
* A class representing RedirectHandlerOptions
*/
export class RedirectHandlerOptions implements MiddlewareOptions {
/**
* @private
* @static
* A member holding default max redirects value
*/
private static DEFAULT_MAX_REDIRECTS = 5;
/**
* @private
* @static
* A member holding maximum max redirects value
*/
private static MAX_MAX_REDIRECTS = 20;
/**
* @public
* A member holding max redirects value
*/
public maxRedirects: number;
/**
* @public
* A member holding shouldRedirect callback
*/
public shouldRedirect: ShouldRedirect;
/**
* @private
* A member holding default shouldRedirect callback
*/
private static defaultShouldRedirect: ShouldRedirect = () => true;
/**
* @public
* @constructor
* To create an instance of RedirectHandlerOptions
* @param {number} [maxRedirects = RedirectHandlerOptions.DEFAULT_MAX_REDIRECTS] - The max redirects value
* @param {ShouldRedirect} [shouldRedirect = RedirectHandlerOptions.DEFAULT_SHOULD_RETRY] - The should redirect callback
* @returns An instance of RedirectHandlerOptions
*/
public constructor(maxRedirects: number = RedirectHandlerOptions.DEFAULT_MAX_REDIRECTS, shouldRedirect: ShouldRedirect = RedirectHandlerOptions.defaultShouldRedirect) {
if (maxRedirects > RedirectHandlerOptions.MAX_MAX_REDIRECTS) {
const error = new Error(`MaxRedirects should not be more than ${RedirectHandlerOptions.MAX_MAX_REDIRECTS}`);
error.name = "MaxLimitExceeded";
throw error;
}
if (maxRedirects < 0) {
const error = new Error(`MaxRedirects should not be negative`);
error.name = "MinExpectationNotMet";
throw error;
}
this.maxRedirects = maxRedirects;
this.shouldRedirect = shouldRedirect;
}
}
@@ -0,0 +1,128 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module RetryHandlerOptions
*/
import { FetchOptions } from "../../IFetchOptions";
import { MiddlewareOptions } from "./IMiddlewareOptions";
/**
* @type
* A type declaration for shouldRetry callback
*/
export type ShouldRetry = (delay: number, attempt: number, request: RequestInfo, options: FetchOptions | undefined, response: Response) => boolean;
/**
* @class
* @implements MiddlewareOptions
* Class for RetryHandlerOptions
*/
export class RetryHandlerOptions implements MiddlewareOptions {
/**
* @private
* @static
* A member holding default delay value in seconds
*/
private static DEFAULT_DELAY = 3;
/**
* @private
* @static
* A member holding default maxRetries value
*/
private static DEFAULT_MAX_RETRIES = 3;
/**
* @private
* @static
* A member holding maximum delay value in seconds
*/
private static MAX_DELAY = 180;
/**
* @private
* @static
* A member holding maximum maxRetries value
*/
private static MAX_MAX_RETRIES = 10;
/**
* @public
* A member holding delay value in seconds
*/
public delay: number;
/**
* @public
* A member holding maxRetries value
*/
public maxRetries: number;
/**
* @public
* A member holding shouldRetry callback
*/
public shouldRetry: ShouldRetry;
/**
* @private
* A member holding default shouldRetry callback
*/
private static defaultShouldRetry: ShouldRetry = () => true;
/**
* @public
* @constructor
* To create an instance of RetryHandlerOptions
* @param {number} [delay = RetryHandlerOptions.DEFAULT_DELAY] - The delay value in seconds
* @param {number} [maxRetries = RetryHandlerOptions.DEFAULT_MAX_RETRIES] - The maxRetries value
* @param {ShouldRetry} [shouldRetry = RetryHandlerOptions.DEFAULT_SHOULD_RETRY] - The shouldRetry callback function
* @returns An instance of RetryHandlerOptions
*/
public constructor(delay: number = RetryHandlerOptions.DEFAULT_DELAY, maxRetries: number = RetryHandlerOptions.DEFAULT_MAX_RETRIES, shouldRetry: ShouldRetry = RetryHandlerOptions.defaultShouldRetry) {
if (delay > RetryHandlerOptions.MAX_DELAY && maxRetries > RetryHandlerOptions.MAX_MAX_RETRIES) {
const error = new Error(`Delay and MaxRetries should not be more than ${RetryHandlerOptions.MAX_DELAY} and ${RetryHandlerOptions.MAX_MAX_RETRIES}`);
error.name = "MaxLimitExceeded";
throw error;
} else if (delay > RetryHandlerOptions.MAX_DELAY) {
const error = new Error(`Delay should not be more than ${RetryHandlerOptions.MAX_DELAY}`);
error.name = "MaxLimitExceeded";
throw error;
} else if (maxRetries > RetryHandlerOptions.MAX_MAX_RETRIES) {
const error = new Error(`MaxRetries should not be more than ${RetryHandlerOptions.MAX_MAX_RETRIES}`);
error.name = "MaxLimitExceeded";
throw error;
} else if (delay < 0 && maxRetries < 0) {
const error = new Error(`Delay and MaxRetries should not be negative`);
error.name = "MinExpectationNotMet";
throw error;
} else if (delay < 0) {
const error = new Error(`Delay should not be negative`);
error.name = "MinExpectationNotMet";
throw error;
} else if (maxRetries < 0) {
const error = new Error(`MaxRetries should not be negative`);
error.name = "MinExpectationNotMet";
throw error;
}
this.delay = Math.min(delay, RetryHandlerOptions.MAX_DELAY);
this.maxRetries = Math.min(maxRetries, RetryHandlerOptions.MAX_MAX_RETRIES);
this.shouldRetry = shouldRetry;
}
/**
* @public
* To get the maximum delay
* @returns A maximum delay
*/
public getMaxDelay(): number {
return RetryHandlerOptions.MAX_DELAY;
}
}
@@ -0,0 +1,86 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module TelemetryHandlerOptions
*/
import { Context } from "../../IContext";
import { MiddlewareControl } from "../MiddlewareControl";
import { MiddlewareOptions } from "./IMiddlewareOptions";
/**
* @enum
* @property {number} NONE - The hexadecimal flag value for nothing enabled
* @property {number} REDIRECT_HANDLER_ENABLED - The hexadecimal flag value for redirect handler enabled
* @property {number} RETRY_HANDLER_ENABLED - The hexadecimal flag value for retry handler enabled
* @property {number} AUTHENTICATION_HANDLER_ENABLED - The hexadecimal flag value for the authentication handler enabled
*/
export enum FeatureUsageFlag {
/* eslint-disable @typescript-eslint/naming-convention */
NONE = 0x0,
REDIRECT_HANDLER_ENABLED = 0x1,
RETRY_HANDLER_ENABLED = 0x2,
AUTHENTICATION_HANDLER_ENABLED = 0x4,
/* eslint-enable @typescript-eslint/naming-convention */
}
/**
* @class
* @implements MiddlewareOptions
* Class for TelemetryHandlerOptions
*/
export class TelemetryHandlerOptions implements MiddlewareOptions {
/**
* @private
* A member to hold the OR of feature usage flags
*/
private featureUsage: FeatureUsageFlag = FeatureUsageFlag.NONE;
/**
* @public
* @static
* To update the feature usage in the context object
* @param {Context} context - The request context object containing middleware options
* @param {FeatureUsageFlag} flag - The flag value
* @returns nothing
*/
public static updateFeatureUsageFlag(context: Context, flag: FeatureUsageFlag): void {
let options: TelemetryHandlerOptions;
if (context.middlewareControl instanceof MiddlewareControl) {
options = context.middlewareControl.getMiddlewareOptions(TelemetryHandlerOptions) as TelemetryHandlerOptions;
} else {
context.middlewareControl = new MiddlewareControl();
}
if (typeof options === "undefined") {
options = new TelemetryHandlerOptions();
context.middlewareControl.setMiddlewareOptions(TelemetryHandlerOptions, options);
}
options.setFeatureUsage(flag);
}
/**
* @private
* To set the feature usage flag
* @param {FeatureUsageFlag} flag - The flag value
* @returns nothing
*/
private setFeatureUsage(flag: FeatureUsageFlag): void {
this.featureUsage = this.featureUsage | flag;
}
/**
* @public
* To get the feature usage
* @returns A feature usage flag as hexadecimal string
*/
public getFeatureUsage(): string {
return this.featureUsage.toString(16);
}
}
@@ -0,0 +1,41 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { GraphClientError } from "../../../GraphClientError";
import { FileObject, SliceType } from "../../LargeFileUploadTask";
import { Range } from "../Range";
/**
* @class
* Class used for creating LargeFileUploadTask fileobject.
* This class accepts files of type ArrayBuffer, Blob, Uint8Array.
*/
export class FileUpload implements FileObject<SliceType> {
/**
* @public
* @constructor
* @param {ArrayBuffer | Blob | Uint8Array} content - The file to be uploaded
* @param {string} name - The name of the file to be uploaded
* @param {number} size - The total size of the file to be uploaded
* @returns An instance of the FileUpload class
*/
public constructor(public content: ArrayBuffer | Blob | Uint8Array, public name: string, public size: number) {
if (!content || !name || !size) {
throw new GraphClientError("Please provide the upload content, name of the file and size of the file");
}
}
/**
* @public
* Slices the file content to the given range
* @param {Range} range - The range value
* @returns The sliced file part
*/
public sliceFile(range: Range): ArrayBuffer | Blob | Uint8Array {
return this.content.slice(range.minValue, range.maxValue + 1);
}
}
@@ -0,0 +1,146 @@
import { GraphClientError } from "../../../GraphClientError";
import { FileObject, SliceType } from "../../LargeFileUploadTask";
import { Range } from "../Range";
/**
* @interface
* Interface to store slice of a stream and range of the slice.
* @property {Buffer} fileSlice - The slice of the stream
* @property {Range} range - The range of the slice
*/
interface SliceRecord {
fileSlice: Buffer;
range: Range;
}
/**
* @class
* FileObject class for Readable Stream upload
*/
export class StreamUpload implements FileObject<NodeStream> {
/**
* @private
* Represents a cache of the last attempted upload slice.
* This can be used when resuming a previously failed slice upload.
*/
private previousSlice: SliceRecord;
public constructor(public content: NodeStream, public name: string, public size: number) {
if (!content || !name || !size) {
throw new GraphClientError("Please provide the Readable Stream content, name of the file and size of the file");
}
}
/**
* @public
* Slices the file content to the given range
* @param {Range} range - The range value
* @returns The sliced file part
*/
public async sliceFile(range: Range): Promise<SliceType> {
let rangeSize = range.maxValue - range.minValue + 1;
/* readable.readable Is true if it is safe to call readable.read(),
* which means the stream has not been destroyed or emitted 'error' or 'end'
*/
const bufs = [];
/**
* The sliceFile reads the first `rangeSize` number of bytes from the stream.
* The previousSlice property is used to seek the range of bytes in the previous slice.
* Suppose, the sliceFile reads bytes from `10 - 20` from the stream but the upload of this slice fails.
* When the user resumes, the stream will have bytes from position 21.
* The previousSlice.Range is used to compare if the requested range is cached in the previousSlice property or present in the Readable Stream.
*/
if (this.previousSlice) {
if (range.minValue < this.previousSlice.range.minValue) {
throw new GraphClientError("An error occurred while uploading the stream. Please restart the stream upload from the first byte of the file.");
}
if (range.minValue < this.previousSlice.range.maxValue) {
const previousRangeMin = this.previousSlice.range.minValue;
const previousRangeMax = this.previousSlice.range.maxValue;
// Check if the requested range is same as previously sliced range
if (range.minValue === previousRangeMin && range.maxValue === previousRangeMax) {
return this.previousSlice.fileSlice;
}
/**
* The following check considers a possibility
* of an upload failing after some of the bytes of the previous slice
* were successfully uploaded.
* Example - Previous slice range - `10 - 20`. Current requested range is `15 - 20`.
*/
if (range.maxValue === previousRangeMax) {
return this.previousSlice.fileSlice.slice(range.minValue, range.maxValue + 1);
}
/**
* If an upload fails after some of the bytes of the previous slice
* were successfully uploaded and the new Range.Maximum is greater than the previous Range.Maximum
* Example - Previous slice range - `10 - 20`. Current requested range is `15 - 25`,
* then read the bytes from position 15 to 20 from previousSlice.fileSlice and read bytes from position 21 to 25 from the Readable Stream
*/
bufs.push(this.previousSlice.fileSlice.slice(range.minValue, previousRangeMax + 1));
rangeSize = range.maxValue - previousRangeMax;
}
}
if (this.content && this.content.readable) {
if (this.content.readableLength >= rangeSize) {
bufs.push(this.content.read(rangeSize));
} else {
bufs.push(await this.readNBytesFromStream(rangeSize));
}
} else {
throw new GraphClientError("Stream is not readable.");
}
const slicedChunk = Buffer.concat(bufs);
this.previousSlice = { fileSlice: slicedChunk, range };
return slicedChunk;
}
/**
* @private
* Reads the specified byte size from the stream
* @param {number} size - The size of bytes to be read
* @returns Buffer with the given length of data.
*/
private readNBytesFromStream(size: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks = [];
let remainder = size;
let length = 0;
this.content.on("end", () => {
if (remainder > 0) {
return reject(new GraphClientError("Stream ended before reading required range size"));
}
});
this.content.on("readable", () => {
/**
* (chunk = this.content.read(size)) can return null if size of stream is less than 'size' parameter.
* Read the remainder number of bytes from the stream iteratively as they are available.
*/
let chunk;
while (length < size && (chunk = this.content.read(remainder)) !== null) {
length += chunk.length;
chunks.push(chunk);
if (remainder > 0) {
remainder = size - length;
}
}
if (length === size) {
return resolve(Buffer.concat(chunks));
}
if (!this.content || !this.content.readable) {
return reject(new GraphClientError("Error encountered while reading the stream during the upload"));
}
});
});
}
}
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/type-annotation-spacing */
import { Range } from "../Range";
/**
* Interface enabling progress handling with callbacks.
*/
export interface UploadEventHandlers {
/**
* Parameters that are passed into the progress, completed, failure callback options.
*/
extraCallbackParam?: unknown;
/**
* Callback function called on each slice upload during the LargeFileUploadTask.upload() process
*/
progress?: (range?: Range, extraCallbackParam?: unknown) => void;
}
@@ -0,0 +1,41 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module Range
*/
/**
* @class
* Class representing Range
*/
export class Range {
/**
* @public
* The minimum value of the range
*/
public minValue: number;
/**
* @public
* The maximum value of the range
*/
public maxValue: number;
/**
* @public
* @constructor
* Creates a range for given min and max values
* @param {number} [minVal = -1] - The minimum value.
* @param {number} [maxVal = -1] - The maximum value.
* @returns An instance of a Range
*/
public constructor(minVal = -1, maxVal = -1) {
this.minValue = minVal;
this.maxValue = maxVal;
}
}
@@ -0,0 +1,77 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* Class representing a successful file upload result
*/
export class UploadResult {
/**
* @private
* Location value looked up in the response header
*/
private _location: string;
/**
* @private
* Response body of the final raw response
*/
private _responseBody: unknown;
/**
* @public
* Get of the location value.
* Location value is looked up in the response header
*/
public get location(): string {
return this._location;
}
/**
* @public
* Set the location value
* Location value is looked up in the response header
*/
public set location(location: string) {
this._location = location;
}
/**
* @public
* Get The response body from the completed upload response
*/
public get responseBody() {
return this._responseBody;
}
/**
* @public
* Set the response body from the completed upload response
*/
public set responseBody(responseBody: unknown) {
this._responseBody = responseBody;
}
/**
* @public
* @param {responseBody} responsebody - The response body from the completed upload response
* @param {location} location - The location value from the headers from the completed upload response
*/
public constructor(responseBody: unknown, location: string) {
// Response body or the location parameter can be undefined.
this._location = location;
this._responseBody = responseBody;
}
/**
* @public
* @param {responseBody} responseBody - The response body from the completed upload response
* @param {responseHeaders} responseHeaders - The headers from the completed upload response
*/
public static CreateUploadResult(responseBody?: unknown, responseHeaders?: Headers) {
return new UploadResult(responseBody, responseHeaders.get("location"));
}
}
@@ -0,0 +1,378 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module LargeFileUploadTask
*/
import { GraphClientError } from "../GraphClientError";
import { GraphResponseHandler } from "../GraphResponseHandler";
import { Client } from "../index";
import { ResponseType } from "../ResponseType";
import { UploadEventHandlers } from "./FileUploadTask/Interfaces/IUploadEventHandlers";
import { Range } from "./FileUploadTask/Range";
import { UploadResult } from "./FileUploadTask/UploadResult";
/**
* @interface
* Signature to representing key value pairs
* @property {[key: string] : string | number} - The Key value pair
*/
interface KeyValuePairObjectStringNumber {
[key: string]: string | number;
}
/**
* @interface
* Signature to represent the resulting response in the status enquiry request
* @property {string} expirationDateTime - The expiration time of the upload session
* @property {string[]} nextExpectedRanges - The ranges expected in next consecutive request in the upload
*/
interface UploadStatusResponse {
expirationDateTime: string;
nextExpectedRanges: string[];
}
/**
* @interface
* Signature to define options for upload task
* @property {number} [rangeSize = LargeFileUploadTask.DEFAULT_FILE_SIZE] - Specifies the range chunk size
* @property {UploadEventHandlers} uploadEventHandlers - UploadEventHandlers attached to an upload task
*/
export interface LargeFileUploadTaskOptions {
rangeSize?: number;
uploadEventHandlers?: UploadEventHandlers;
}
/**
* @interface
* Signature to represent upload session resulting from the session creation in the server
* @property {string} url - The URL to which the file upload is made
* @property {Date} expiry - The expiration of the time of the upload session
*/
export interface LargeFileUploadSession {
url: string;
expiry: Date;
isCancelled?: boolean;
}
/**
* @type
* Representing the return type of the sliceFile function that is type of the slice of a given range.
*/
export type SliceType = ArrayBuffer | Blob | Uint8Array;
/**
* @interface
* Signature to define the properties and content of the file in upload task
* @property {ArrayBuffer | File} content - The actual file content
* @property {string} name - Specifies the file name with extension
* @property {number} size - Specifies size of the file
*/
export interface FileObject<T> {
content: T;
name: string;
size: number;
sliceFile(range: Range): SliceType | Promise<SliceType>;
}
/**
* @class
* Class representing LargeFileUploadTask
*/
export class LargeFileUploadTask<T> {
/**
* @private
* Default value for the rangeSize
*/
private DEFAULT_FILE_SIZE: number = 5 * 1024 * 1024;
/**
* @protected
* The GraphClient instance
*/
protected client: Client;
/**
* @protected
* The object holding file details
*/
protected file: FileObject<T>;
/**
* @protected
* The object holding options for the task
*/
protected options: LargeFileUploadTaskOptions;
/**
* @protected
* The object for upload session
*/
protected uploadSession: LargeFileUploadSession;
/**
* @protected
* The next range needs to be uploaded
*/
protected nextRange: Range;
/**
* @public
* @static
* @async
* Makes request to the server to create an upload session
* @param {Client} client - The GraphClient instance
* @param {string} requestUrl - The URL to create the upload session
* @param {any} payload - The payload that needs to be sent
* @param {KeyValuePairObjectStringNumber} headers - The headers that needs to be sent
* @returns The promise that resolves to LargeFileUploadSession
*/
public static async createUploadSession(client: Client, requestUrl: string, payload: any, headers: KeyValuePairObjectStringNumber = {}): Promise<LargeFileUploadSession> {
const session = await client
.api(requestUrl)
.headers(headers)
.post(payload);
const largeFileUploadSession: LargeFileUploadSession = {
url: session.uploadUrl,
expiry: new Date(session.expirationDateTime),
isCancelled: false,
};
return largeFileUploadSession;
}
/**
* @public
* @constructor
* Constructs a LargeFileUploadTask
* @param {Client} client - The GraphClient instance
* @param {FileObject} file - The FileObject holding details of a file that needs to be uploaded
* @param {LargeFileUploadSession} uploadSession - The upload session to which the upload has to be done
* @param {LargeFileUploadTaskOptions} options - The upload task options
* @returns An instance of LargeFileUploadTask
*/
public constructor(client: Client, file: FileObject<T>, uploadSession: LargeFileUploadSession, options: LargeFileUploadTaskOptions = {}) {
this.client = client;
if (!file.sliceFile) {
throw new GraphClientError("Please pass the FileUpload object, StreamUpload object or any custom implementation of the FileObject interface");
} else {
this.file = file;
}
this.file = file;
if (!options.rangeSize) {
options.rangeSize = this.DEFAULT_FILE_SIZE;
}
this.options = options;
this.uploadSession = uploadSession;
this.nextRange = new Range(0, this.options.rangeSize - 1);
}
/**
* @private
* Parses given range string to the Range instance
* @param {string[]} ranges - The ranges value
* @returns The range instance
*/
private parseRange(ranges: string[]): Range {
const rangeStr = ranges[0];
if (typeof rangeStr === "undefined" || rangeStr === "") {
return new Range();
}
const firstRange = rangeStr.split("-");
const minVal = parseInt(firstRange[0], 10);
let maxVal = parseInt(firstRange[1], 10);
if (Number.isNaN(maxVal)) {
maxVal = this.file.size - 1;
}
return new Range(minVal, maxVal);
}
/**
* @private
* Updates the expiration date and the next range
* @param {UploadStatusResponse} response - The response of the upload status
* @returns Nothing
*/
private updateTaskStatus(response: UploadStatusResponse): void {
this.uploadSession.expiry = new Date(response.expirationDateTime);
this.nextRange = this.parseRange(response.nextExpectedRanges);
}
/**
* @public
* Gets next range that needs to be uploaded
* @returns The range instance
*/
public getNextRange(): Range {
if (this.nextRange.minValue === -1) {
return this.nextRange;
}
const minVal = this.nextRange.minValue;
let maxValue = minVal + this.options.rangeSize - 1;
if (maxValue >= this.file.size) {
maxValue = this.file.size - 1;
}
return new Range(minVal, maxValue);
}
/**
* @deprecated This function has been moved into FileObject interface.
* @public
* Slices the file content to the given range
* @param {Range} range - The range value
* @returns The sliced ArrayBuffer or Blob
*/
public sliceFile(range: Range): ArrayBuffer | Blob {
console.warn("The LargeFileUploadTask.sliceFile() function has been deprecated and moved into the FileObject interface.");
if (this.file.content instanceof ArrayBuffer || this.file.content instanceof Blob || this.file.content instanceof Uint8Array) {
return this.file.content.slice(range.minValue, range.maxValue + 1);
}
throw new GraphClientError("The LargeFileUploadTask.sliceFile() function expects only Blob, ArrayBuffer or Uint8Array file content. Please note that the sliceFile() function is deprecated.");
}
/**
* @public
* @async
* Uploads file to the server in a sequential order by slicing the file
* @returns The promise resolves to uploaded response
*/
public async upload(): Promise<UploadResult> {
const uploadEventHandlers = this.options && this.options.uploadEventHandlers;
while (!this.uploadSession.isCancelled) {
const nextRange = this.getNextRange();
if (nextRange.maxValue === -1) {
const err = new Error("Task with which you are trying to upload is already completed, Please check for your uploaded file");
err.name = "Invalid Session";
throw err;
}
const fileSlice = await this.file.sliceFile(nextRange);
const rawResponse = await this.uploadSliceGetRawResponse(fileSlice, nextRange, this.file.size);
if (!rawResponse) {
throw new GraphClientError("Something went wrong! Large file upload slice response is null.");
}
const responseBody = await GraphResponseHandler.getResponse(rawResponse);
/**
* (rawResponse.status === 201) -> This condition is applicable for OneDrive, PrintDocument and Outlook APIs.
* (rawResponse.status === 200 && responseBody.id) -> This additional condition is applicable only for OneDrive API.
*/
if (rawResponse.status === 201 || (rawResponse.status === 200 && responseBody.id)) {
this.reportProgress(uploadEventHandlers, nextRange);
return UploadResult.CreateUploadResult(responseBody, rawResponse.headers);
}
/* Handling the API issue where the case of Outlook upload response property -'nextExpectedRanges' is not uniform.
* https://github.com/microsoftgraph/msgraph-sdk-serviceissues/issues/39
*/
const res: UploadStatusResponse = {
expirationDateTime: responseBody.expirationDateTime || responseBody.ExpirationDateTime,
nextExpectedRanges: responseBody.NextExpectedRanges || responseBody.nextExpectedRanges,
};
this.updateTaskStatus(res);
this.reportProgress(uploadEventHandlers, nextRange);
}
}
private reportProgress(uploadEventHandlers: UploadEventHandlers, nextRange: Range) {
if (uploadEventHandlers && uploadEventHandlers.progress) {
uploadEventHandlers.progress(nextRange, uploadEventHandlers.extraCallbackParam);
}
}
/**
* @public
* @async
* Uploads given slice to the server
* @param {ArrayBuffer | Blob | File} fileSlice - The file slice
* @param {Range} range - The range value
* @param {number} totalSize - The total size of a complete file
* @returns The response body of the upload slice result
*/
public async uploadSlice(fileSlice: ArrayBuffer | Blob | File, range: Range, totalSize: number): Promise<unknown> {
return await this.client
.api(this.uploadSession.url)
.headers({
"Content-Length": `${range.maxValue - range.minValue + 1}`,
"Content-Range": `bytes ${range.minValue}-${range.maxValue}/${totalSize}`,
"Content-Type": "application/octet-stream",
})
.put(fileSlice);
}
/**
* @public
* @async
* Uploads given slice to the server
* @param {unknown} fileSlice - The file slice
* @param {Range} range - The range value
* @param {number} totalSize - The total size of a complete file
* @returns The raw response of the upload slice result
*/
public async uploadSliceGetRawResponse(fileSlice: unknown, range: Range, totalSize: number): Promise<Response> {
return await this.client
.api(this.uploadSession.url)
.headers({
"Content-Length": `${range.maxValue - range.minValue + 1}`,
"Content-Range": `bytes ${range.minValue}-${range.maxValue}/${totalSize}`,
"Content-Type": "application/octet-stream",
})
.responseType(ResponseType.RAW)
.put(fileSlice);
}
/**
* @public
* @async
* Deletes upload session in the server
* @returns The promise resolves to cancelled response
*/
public async cancel(): Promise<unknown> {
const cancelResponse = await this.client
.api(this.uploadSession.url)
.responseType(ResponseType.RAW)
.delete();
if (cancelResponse.status === 204) {
this.uploadSession.isCancelled = true;
}
return cancelResponse;
}
/**
* @public
* @async
* Gets status for the upload session
* @returns The promise resolves to the status enquiry response
*/
public async getStatus(): Promise<unknown> {
const response = await this.client.api(this.uploadSession.url).get();
this.updateTaskStatus(response);
return response;
}
/**
* @public
* @async
* Resumes upload session and continue uploading the file from the last sent range
* @returns The promise resolves to the uploaded response
*/
public async resume(): Promise<unknown> {
await this.getStatus();
return await this.upload();
}
/**
* @public
* @async
* Get the upload session information
* @returns The large file upload session
*/
public getUploadSession(): LargeFileUploadSession {
return this.uploadSession;
}
}
@@ -0,0 +1,233 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module OneDriveLargeFileUploadTask
*/
import { GraphClientError } from "../GraphClientError";
import { Client } from "../index";
import { FileUpload } from "./FileUploadTask/FileObjectClasses/FileUpload";
import { UploadEventHandlers } from "./FileUploadTask/Interfaces/IUploadEventHandlers";
import { FileObject, LargeFileUploadSession, LargeFileUploadTask, LargeFileUploadTaskOptions } from "./LargeFileUploadTask";
import { getValidRangeSize } from "./OneDriveLargeFileUploadTaskUtil";
/**
* @interface
* Signature to define options when creating an upload task
* @property {string} fileName - Specifies the name of a file to be uploaded (with extension)
* @property {string} [fileDescription] - Specifies the description of the file to be uploaded
* @property {string} [path] - The path to which the file needs to be uploaded
* @property {number} [rangeSize] - Specifies the range chunk size
* @property {string} [conflictBehavior] - Conflict behaviour option
* @property {UploadEventHandlers} [uploadEventHandlers] - UploadEventHandlers attached to an upload task
*/
export interface OneDriveLargeFileUploadOptions {
fileName: string;
fileDescription?: string;
path?: string;
rangeSize?: number;
conflictBehavior?: string;
uploadEventHandlers?: UploadEventHandlers;
/// <summary>
/// Default upload session url is : "/me/drive/root:/{file-path}:/createUploadSession"
/// Set this property to override the default upload session url. Example: "/me/drive/special/{name}"
/// </summary>
uploadSessionURL?: string;
}
/**
* @interface
* Signature to define options when creating an upload task
* @property {string} fileName - Specifies the name of a file to be uploaded (with extension)
* @property {string} [fileDescription] - Specifies the description of the file to be uploaded
* @property {string} [conflictBehavior] - Conflict behaviour option
*/
interface OneDriveFileUploadSessionPayLoad {
fileName: string;
fileDescription?: string;
conflictBehavior?: string;
}
/**
* @interface
* Signature to define the file information when processing an upload task
* @property {File | Uint8Array} content - The file content
* @property {number} size - The size of file
*/
interface FileInfo {
content: File | Uint8Array;
size: number;
}
/**
* @class
* Class representing OneDriveLargeFileUploadTask
*/
export class OneDriveLargeFileUploadTask<T> extends LargeFileUploadTask<T> {
/**
* @private
* @static
* Default path for the file being uploaded
*/
private static DEFAULT_UPLOAD_PATH = "/";
/**
* @private
* @static
* Constructs the create session url for Onedrive
* @param {string} fileName - The name of the file
* @param {path} [path = OneDriveLargeFileUploadTask.DEFAULT_UPLOAD_PATH] - The path for the upload
* @returns The constructed create session url
*/
private static constructCreateSessionUrl(fileName: string, path: string = OneDriveLargeFileUploadTask.DEFAULT_UPLOAD_PATH): string {
fileName = fileName.trim();
path = path.trim();
if (path === "") {
path = "/";
}
if (path[0] !== "/") {
path = `/${path}`;
}
if (path[path.length - 1] !== "/") {
path = `${path}/`;
}
// we choose to encode each component of the file path separately because when encoding full URI
// with encodeURI, special characters like # or % in the file name doesn't get encoded as desired
return `/me/drive/root:${path
.split("/")
.map((p) => encodeURIComponent(p))
.join("/")}${encodeURIComponent(fileName)}:/createUploadSession`;
}
/**
* @private
* @static
* Get file information
* @param {Blob | Uint8Array | File} file - The file entity
* @param {string} fileName - The file name
* @returns {FileInfo} The file information
*/
private static getFileInfo(file: Blob | Uint8Array | File, fileName: string): FileInfo {
let content;
let size;
if (typeof Blob !== "undefined" && file instanceof Blob) {
content = new File([file as Blob], fileName);
size = content.size;
} else if (typeof File !== "undefined" && file instanceof File) {
content = file as File;
size = content.size;
} else if (typeof Uint8Array !== "undefined" && file instanceof Uint8Array) {
const b = file as Uint8Array;
size = b.byteLength;
content = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
}
return {
content,
size,
};
}
/**
* @public
* @static
* @async
* Creates a OneDriveLargeFileUploadTask
* @param {Client} client - The GraphClient instance
* @param {Blob | Uint8Array | File} file - File represented as Blob, Uint8Array or File
* @param {OneDriveLargeFileUploadOptions} options - The options for upload task
* @returns The promise that will be resolves to OneDriveLargeFileUploadTask instance
*/
public static async create(client: Client, file: Blob | Uint8Array | File, options: OneDriveLargeFileUploadOptions): Promise<OneDriveLargeFileUploadTask<Blob | ArrayBuffer | Uint8Array>> {
if (!client || !file || !options) {
throw new GraphClientError("Please provide the Graph client instance, file object and OneDriveLargeFileUploadOptions value");
}
const fileName = options.fileName;
const fileInfo = OneDriveLargeFileUploadTask.getFileInfo(file, fileName);
const fileObj = new FileUpload(fileInfo.content, fileName, fileInfo.size);
return this.createTaskWithFileObject<Blob | ArrayBuffer | Uint8Array>(client, fileObj, options);
}
/**
* @public
* @static
* @async
* Creates a OneDriveLargeFileUploadTask
* @param {Client} client - The GraphClient instance
* @param {FileObject} fileObject - FileObject instance
* @param {OneDriveLargeFileUploadOptions} options - The options for upload task
* @returns The promise that will be resolves to OneDriveLargeFileUploadTask instance
*/
public static async createTaskWithFileObject<T>(client: Client, fileObject: FileObject<T>, options: OneDriveLargeFileUploadOptions): Promise<OneDriveLargeFileUploadTask<T>> {
if (!client || !fileObject || !options) {
throw new GraphClientError("Please provide the Graph client instance, FileObject interface implementation and OneDriveLargeFileUploadOptions value");
}
const requestUrl = options.uploadSessionURL ? options.uploadSessionURL: OneDriveLargeFileUploadTask.constructCreateSessionUrl(options.fileName, options.path);
const uploadSessionPayload: OneDriveFileUploadSessionPayLoad = {
fileName: options.fileName,
fileDescription: options.fileDescription,
conflictBehavior: options.conflictBehavior,
};
const session = await OneDriveLargeFileUploadTask.createUploadSession(client, requestUrl, uploadSessionPayload);
const rangeSize = getValidRangeSize(options.rangeSize);
return new OneDriveLargeFileUploadTask(client, fileObject, session, {
rangeSize,
uploadEventHandlers: options.uploadEventHandlers,
});
}
/**
* @public
* @static
* @async
* Makes request to the server to create an upload session
* @param {Client} client - The GraphClient instance
* @param {string} requestUrl - The URL to create the upload session
* @param {string} payloadOptions - The payload option. Default conflictBehavior is 'rename'
* @returns The promise that resolves to LargeFileUploadSession
*/
public static async createUploadSession(client: Client, requestUrl: string, payloadOptions: OneDriveFileUploadSessionPayLoad): Promise<LargeFileUploadSession> {
const payload = {
item: {
"@microsoft.graph.conflictBehavior": payloadOptions?.conflictBehavior || "rename",
name: payloadOptions?.fileName,
description: payloadOptions?.fileDescription,
},
};
return super.createUploadSession(client, requestUrl, payload);
}
/**
* @public
* @constructor
* Constructs a OneDriveLargeFileUploadTask
* @param {Client} client - The GraphClient instance
* @param {FileObject} file - The FileObject holding details of a file that needs to be uploaded
* @param {LargeFileUploadSession} uploadSession - The upload session to which the upload has to be done
* @param {LargeFileUploadTaskOptions} options - The upload task options
* @returns An instance of OneDriveLargeFileUploadTask
*/
public constructor(client: Client, file: FileObject<T>, uploadSession: LargeFileUploadSession, options: LargeFileUploadTaskOptions) {
super(client, file, uploadSession, options);
}
/**
* @public
* Commits upload session to end uploading
* @param {string} requestUrl - The URL to commit the upload session
* @param {string} conflictBehavior - Conflict behaviour option. Default is 'rename'
* @returns The promise resolves to committed response
*/
public async commit(requestUrl: string, conflictBehavior = "rename"): Promise<unknown> {
const payload = {
name: this.file.name,
"@microsoft.graph.conflictBehavior": conflictBehavior,
"@microsoft.graph.sourceUrl": this.uploadSession.url,
};
return await this.client.api(requestUrl).put(payload);
}
}
@@ -0,0 +1,46 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module OneDriveLargeFileUploadTaskUtil
*/
/**
* @constant
* Default value for the rangeSize
* Recommended size is between 5 - 10 MB {@link https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession#best-practices}
*/
const DEFAULT_FILE_SIZE: number = 5 * 1024 * 1024;
/**
* @constant
* Rounds off the given value to a multiple of 320 KB
* @param {number} value - The value
* @returns The rounded off value
*/
const roundTo320KB = (value: number): number => {
if (value > 320 * 1024) {
value = Math.floor(value / (320 * 1024)) * 320 * 1024;
}
return value;
};
/**
* @constant
* Get the valid rangeSize for a file slicing (validity is based on the constrains mentioned in here
* {@link https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/driveitem_createuploadsession#upload-bytes-to-the-upload-session})
*
* @param {number} [rangeSize = DEFAULT_FILE_SIZE] - The rangeSize value.
* @returns The valid rangeSize
*/
export const getValidRangeSize = (rangeSize: number = DEFAULT_FILE_SIZE): number => {
const sixtyMB = 60 * 1024 * 1024;
if (rangeSize > sixtyMB) {
rangeSize = sixtyMB;
}
return roundTo320KB(rangeSize);
};
@@ -0,0 +1,219 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
/**
* @module PageIterator
*/
import { FetchOptions } from "../IFetchOptions";
import { Client } from "../index";
import { MiddlewareOptions } from "../middleware/options/IMiddlewareOptions";
/**
* Signature representing PageCollection
* @property {any[]} value - The collection value
* @property {string} [@odata.nextLink] - The nextLink value
* @property {string} [@odata.deltaLink] - The deltaLink value
* @property {any} Additional - Any number of additional properties (This is to accept the any additional data returned by in the response to the nextLink request)
*/
export interface PageCollection {
value: any[];
"@odata.nextLink"?: string;
"@odata.deltaLink"?: string;
[Key: string]: any;
}
/**
* Signature to define the request options to be sent during request.
* The values of the GraphRequestOptions properties are passed to the Graph Request object.
* @property {HeadersInit} headers - the header options for the request
* @property {MiddlewareOptions[]} middlewareoptions - The middleware options for the request
* @property {FetchOptions} options - The fetch options for the request
*/
export interface GraphRequestOptions {
headers?: HeadersInit;
middlewareOptions?: MiddlewareOptions[];
options?: FetchOptions;
}
/**
* Signature representing callback for page iterator
* @property {Function} callback - The callback function which should return boolean to continue the continue/stop the iteration.
*/
export type PageIteratorCallback = (data: any) => boolean;
/**
* @class
* Class for PageIterator
*/
export class PageIterator {
/**
* @private
* Member holding the GraphClient instance
*/
private client: Client;
/**
* @private
* Member holding the page collection
*/
private collection: any[];
/**
* @private
* Member variable referring to nextLink of the page collection
*/
private nextLink: string | undefined;
/**
* @private
* Member variable referring to deltaLink of the request
*/
private deltaLink: string | undefined;
/**
* @private
* Holding callback for Iteration.
*/
private callback: PageIteratorCallback;
/**
* @private
* Member holding a complete/incomplete status of an iterator
*/
private complete: boolean;
/**
* @private
* Information to be added to the request
*/
private requestOptions: GraphRequestOptions;
/**
* @private
* Member holding the current position on the collection
*/
private cursor: number;
/**
* @public
* @constructor
* Creates new instance for PageIterator
* @param {Client} client - The graph client instance
* @param {PageCollection} pageCollection - The page collection object
* @param {PageIteratorCallback} callBack - The callback function
* @param {GraphRequestOptions} requestOptions - The request options
* @returns An instance of a PageIterator
*/
public constructor(client: Client, pageCollection: PageCollection, callback: PageIteratorCallback, requestOptions?: GraphRequestOptions) {
this.client = client;
this.collection = pageCollection.value;
this.nextLink = pageCollection["@odata.nextLink"];
this.deltaLink = pageCollection["@odata.deltaLink"];
this.callback = callback;
this.cursor = 0;
this.complete = false;
this.requestOptions = requestOptions;
}
/**
* @private
* Iterates over a collection by enqueuing entries one by one and kicking the callback with the enqueued entry
* @returns A boolean indicating the continue flag to process next page
*/
private iterationHelper(): boolean {
if (this.collection === undefined) {
return false;
}
let advance = true;
while (advance && this.cursor < this.collection.length) {
const item = this.collection[this.cursor];
advance = this.callback(item);
this.cursor++;
}
return advance;
}
/**
* @private
* @async
* Helper to make a get request to fetch next page with nextLink url and update the page iterator instance with the returned response
* @returns A promise that resolves to a response data with next page collection
*/
private async fetchAndUpdateNextPageData(): Promise<any> {
let graphRequest = this.client.api(this.nextLink);
if (this.requestOptions) {
if (this.requestOptions.headers) {
graphRequest = graphRequest.headers(this.requestOptions.headers);
}
if (this.requestOptions.middlewareOptions) {
graphRequest = graphRequest.middlewareOptions(this.requestOptions.middlewareOptions);
}
if (this.requestOptions.options) {
graphRequest = graphRequest.options(this.requestOptions.options);
}
}
const response: PageCollection = await graphRequest.get();
this.collection = response.value;
this.cursor = 0;
this.nextLink = response["@odata.nextLink"];
this.deltaLink = response["@odata.deltaLink"];
}
/**
* @public
* Getter to get the deltaLink in the current response
* @returns A deltaLink which is being used to make delta requests in future
*/
public getDeltaLink(): string | undefined {
return this.deltaLink;
}
/**
* @public
* @async
* Iterates over the collection and kicks callback for each item on iteration. Fetches next set of data through nextLink and iterates over again
* This happens until the nextLink is drained out or the user responds with a red flag to continue from callback
* @returns A Promise that resolves to nothing on completion and throws error incase of any discrepancy.
*/
public async iterate(): Promise<any> {
let advance = this.iterationHelper();
while (advance) {
if (this.nextLink !== undefined) {
await this.fetchAndUpdateNextPageData();
advance = this.iterationHelper();
} else {
advance = false;
}
}
if (this.nextLink === undefined && this.cursor >= this.collection.length) {
this.complete = true;
}
}
/**
* @public
* @async
* To resume the iteration
* Note: This internally calls the iterate method, It's just for more readability.
* @returns A Promise that resolves to nothing on completion and throws error incase of any discrepancy
*/
public async resume(): Promise<any> {
return this.iterate();
}
/**
* @public
* To get the completeness status of the iterator
* @returns Boolean indicating the completeness
*/
public isComplete(): boolean {
return this.complete;
}
}