Initial commit
This commit is contained in:
parent
fa0c6059d0
commit
a08f327fe4
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": true,
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"whichFolder": "subdir"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
release
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.13.0-beta.17",
|
||||
"libraryName": "react-graph-auto-batching",
|
||||
"libraryId": "03631505-3899-4de0-bebe-f59283d991ba",
|
||||
"environment": "spo",
|
||||
"packageManager": "npm",
|
||||
"isCreatingSolution": true,
|
||||
"plusBeta": true,
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
# react-graph-auto-batching
|
||||
|
||||
## Summary
|
||||
|
||||
This same shows how to abstract batching graph requests. The idea behind this sample is quite simple. To render simple user card we need three calls to MS GraphAPI.
|
||||
First one to get user information, second to get the image and third to get the presence. As with rendering multiple user card we may hit graph throttling,
|
||||
it is a good idea to batch those requests. This might create quite a repetition across our solution.
|
||||
To avoid such situation, we can implement composition pattern to abstract that batch generation. This design pattern will also help us with unit testing.
|
||||
|
||||
This is the second idea behind this sample. It provides few unit tests of quite a complex implementation detail, which auto batching is.
|
||||
You can also find how one can test react component in SPFx stack in isolation, by providing mock to all data access layer implementation.
|
||||
|
||||
Finally, let me know if You would like to see this auto batcher in some library You can import to Your solution.
|
||||
|
||||
You may ask why I used AadHttpClient instead of MSGraphClient. AadHttpClient shares more similarity with a http client based on fetch api, this means it will be easier to adopt this solution to an implementation outside of SPFx.
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![version](https://img.shields.io/badge/version-1.13-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
- [SharePoint Framework](https://aka.ms/spfx)
|
||||
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
|
||||
> Get your own free development tenant by subscribing to
|
||||
|
||||
## Prerequisites
|
||||
|
||||
None
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-graph-auto-batching | Marcin Wojciechowski [@mgwojciech](https://twitter.com/mgwojciech)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|February 3, 2022|Initial release
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- **npm install**
|
||||
- **gulp serve**
|
||||
- Add Graph Auto Batching
|
||||
|
||||
To run tests
|
||||
Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- **npm install**
|
||||
- **npx jest**
|
||||
|
||||
## Features
|
||||
|
||||
Description of the extension that expands upon high-level summary above.
|
||||
|
||||
This extension illustrates the following concepts:
|
||||
|
||||
- User Card using auto batching requests to MS Graph Client
|
||||
- Unit testing batching client using mocked http client
|
||||
- Unit testing React component in SPFx
|
||||
- In total isolation
|
||||
- Awaiting the **useEffect(()=>{},[])** operation
|
||||
|
||||
## References
|
||||
|
||||
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
|
||||
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
||||
- You can find more on unit testing on my [Blog](https://mgwdevcom.wordpress.com)
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"graph-auto-batching-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/graphAutoBatching/GraphAutoBatchingWebPart.js",
|
||||
"manifest": "./src/webparts/graphAutoBatching/GraphAutoBatchingWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"GraphAutoBatchingWebPartStrings": "lib/webparts/graphAutoBatching/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "./release/assets/"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./release/assets/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-graph-auto-batching",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-graph-auto-batching-client-side-solution",
|
||||
"id": "03631505-3899-4de0-bebe-f59283d991ba",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": ""
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-graph-auto-batching.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
var getTasks = build.rig.getTasks;
|
||||
build.rig.getTasks = function () {
|
||||
var result = getTasks.call(build.rig);
|
||||
|
||||
result.set('serve', result.get('serve-deprecated'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
build.initialize(require('gulp'));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "react-graph-auto-batching",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "16.9.0",
|
||||
"react-dom": "16.9.0",
|
||||
"office-ui-fabric-react": "7.156.0",
|
||||
"@microsoft/sp-core-library": "1.13.0-beta.17",
|
||||
"@microsoft/sp-property-pane": "1.13.0-beta.17",
|
||||
"@microsoft/sp-webpart-base": "1.13.0-beta.17",
|
||||
"@microsoft/sp-lodash-subset": "1.13.0-beta.17",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.13.0-beta.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/rush-stack-compiler-3.7": "0.2.3",
|
||||
"@microsoft/sp-build-web": "1.13.0-beta.17",
|
||||
"@microsoft/sp-module-interfaces": "1.13.0-beta.17",
|
||||
"@microsoft/sp-tslint-rules": "1.13.0-beta.17",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/react": "16.9.36",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"ajv": "~5.2.2",
|
||||
"chai": "^4.3.6",
|
||||
"gulp": "~4.0.2",
|
||||
"jest": "^27.4.7",
|
||||
"jest-environment-jsdom": "^27.4.6",
|
||||
"jsdom": "^19.0.0",
|
||||
"ts-jest": "^27.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"testEnvironment": "jsdom"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { ArrayUtilities } from "../../utils/ArrayUtilities";
|
||||
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
|
||||
import { BatchHandler } from "./BatchHandler";
|
||||
|
||||
export class BatchGraphClient implements IHttpClient {
|
||||
private batch: {
|
||||
id: string;
|
||||
url: string;
|
||||
method: "GET";
|
||||
headers: any;
|
||||
}[] = [];
|
||||
private registeredPromises: Map<string, { resolve, error }[]> = new Map<string, { resolve, error }[]>();
|
||||
constructor(protected baseClient: IHttpClient, public batchWaitTime = 500, public batchSplitThreshold = 15) {
|
||||
}
|
||||
public get(url: string, options?): Promise<IHttpClientResponse> {
|
||||
return new Promise<IHttpClientResponse>((resolve, error) => {
|
||||
this.createGetBatchRequest(url, { resolve, error });
|
||||
});
|
||||
}
|
||||
public post(url: string, options?): Promise<IHttpClientResponse> {
|
||||
return this.baseClient.post(url, options);
|
||||
}
|
||||
public patch(url: string, options?): Promise<IHttpClientResponse> {
|
||||
return this.baseClient.patch(url, options);
|
||||
}
|
||||
public put(url: string, options?): Promise<IHttpClientResponse> {
|
||||
return this.baseClient.put(url, options);
|
||||
}
|
||||
public delete(url: string): Promise<IHttpClientResponse> {
|
||||
return this.baseClient.delete(url);
|
||||
}
|
||||
protected generateBatch = async () => {
|
||||
const requestBatch = [...this.batch];
|
||||
this.batch = [];
|
||||
const requestPromises = new Map<string, { resolve, error }[]>(this.registeredPromises as any);
|
||||
|
||||
this.registeredPromises.clear();
|
||||
|
||||
//As there is an limit to max batch size (15) let's split our request to sub batches we will run sequentially
|
||||
let batches = ArrayUtilities.splitToMaxLength(requestBatch, this.batchSplitThreshold);
|
||||
for (const batch of batches) {
|
||||
let promisesToBeResolvedByCurrentBatch = ArrayUtilities.getSubMap(requestPromises, batch.map(b => b.id));
|
||||
const batchHandler = new BatchHandler(this.baseClient, promisesToBeResolvedByCurrentBatch, batch, BatchHandler.maxRetries);
|
||||
await batchHandler.executeBatch();
|
||||
}
|
||||
}
|
||||
public createGetBatchRequest = (url: string, requestPromise: { resolve, error }) => {
|
||||
if (this.batch.length === 0) {
|
||||
setTimeout(this.generateBatch, this.batchWaitTime);
|
||||
}
|
||||
let promiseId = encodeURIComponent(url);
|
||||
if (this.batch.filter(req => req.id === promiseId)[0]) {
|
||||
this.registeredPromises.get(url).push(requestPromise);
|
||||
}
|
||||
else {
|
||||
this.batch.push({
|
||||
url,
|
||||
id: promiseId,
|
||||
method: "GET",
|
||||
headers:{
|
||||
"ConsistencyLevel":"eventual"
|
||||
}
|
||||
});
|
||||
this.registeredPromises.set(promiseId, [requestPromise]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { IHttpClient } from "./IHttpClient";
|
||||
|
||||
export class BatchHandler {
|
||||
public static readonly maxRetries = 5;
|
||||
constructor(protected baseClient: IHttpClient, protected registeredPromises: Map<string, { resolve, error }[]>, protected batch: { id: string; url: string; method: "GET" }[], protected retries: number = 0) { }
|
||||
|
||||
public async executeBatch() {
|
||||
let betaRequests = this.batch.filter(req => req.url.indexOf("v1.0/") < 0);
|
||||
let v1Requests = this.batch.filter(req => req.url.indexOf("v1.0/") >= 0);
|
||||
let batchBody = JSON.stringify({ requests: betaRequests });
|
||||
let batchBodyV1 = JSON.stringify({
|
||||
requests: v1Requests.map(req => ({
|
||||
...req,
|
||||
url: req.url.replace("v1.0/", "")
|
||||
}))
|
||||
});
|
||||
let responses = [];
|
||||
if (betaRequests.length > 0) {
|
||||
let betaResponse = await this.requestBatch(batchBody);
|
||||
responses.push(...betaResponse.responses);
|
||||
}
|
||||
if (v1Requests.length > 0) {
|
||||
let v1Response = await this.requestBatch(batchBodyV1, "v1.0");
|
||||
responses.push(...v1Response.responses);
|
||||
}
|
||||
this.processBatchResponse(responses);
|
||||
if (this.batch.length > 0 && this.retries > 0) {
|
||||
this.retries--;
|
||||
this.executeBatch();
|
||||
}
|
||||
}
|
||||
|
||||
private processBatchResponse(responses) {
|
||||
const retryBatch = [];
|
||||
const retryRegisteredPromises: Map<string, { resolve, error }[]> = new Map<string, { resolve, error }[]>();
|
||||
|
||||
this.registeredPromises.forEach((promises: { resolve; error; }[], url: string) => {
|
||||
let promiseResponse = responses.filter(resp => resp.id === url)[0];
|
||||
if (promiseResponse && promiseResponse.status === 429 && this.retries > 0) {
|
||||
retryBatch.push({
|
||||
url,
|
||||
id: url,
|
||||
method: "GET"
|
||||
});
|
||||
retryRegisteredPromises.set(url, promises);
|
||||
} else {
|
||||
this.handleSingleResponse(promiseResponse, promises);
|
||||
}
|
||||
});
|
||||
|
||||
if (retryBatch.length > 0) {
|
||||
this.registeredPromises = retryRegisteredPromises;
|
||||
this.batch = retryBatch;
|
||||
} else {
|
||||
this.batch = [];
|
||||
this.registeredPromises.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private handleSingleResponse(promiseResponse: any, promises: { resolve: any; error: any; }[]) {
|
||||
if (promiseResponse) {
|
||||
promises.forEach(promise => {
|
||||
promise.resolve({
|
||||
json: () => Promise.resolve(promiseResponse.body),
|
||||
ok: promiseResponse.status === 200,
|
||||
text: () => Promise.resolve(JSON.stringify(promiseResponse.body))
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
promises.forEach(promise => {
|
||||
promise.error({
|
||||
json: () => Promise.resolve(promiseResponse),
|
||||
ok: false,
|
||||
text: () => Promise.resolve(JSON.stringify(promiseResponse))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected requestBatch = async (batchBody: string, version: string = "beta") => {
|
||||
const options = {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
ConsistencyLevel: "eventual",
|
||||
"Content-Type": "application/json"
|
||||
}, body: batchBody
|
||||
};
|
||||
const response = await this.baseClient.post(`https://graph.microsoft.com/${version}/$batch`, options);
|
||||
if (response.ok) {
|
||||
let batchResponse = await response.json();
|
||||
return batchResponse;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export interface IHttpClientResponse {
|
||||
json: () => Promise<any>;
|
||||
text: () => Promise<string>;
|
||||
blob: () => Promise<Blob>;
|
||||
ok: boolean;
|
||||
status: number;
|
||||
statusText?: string;
|
||||
}
|
||||
|
||||
export interface IHttpClient {
|
||||
get(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||
post(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||
patch(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||
put(url: string, options?: RequestInit): Promise<IHttpClientResponse>;
|
||||
delete(url: string): Promise<IHttpClientResponse>;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { IHttpClient, IHttpClientResponse } from "./IHttpClient";
|
||||
import { AadHttpClient } from "@microsoft/sp-http";
|
||||
|
||||
export class SPFxHttpClient implements IHttpClient {
|
||||
constructor(protected httpClient: AadHttpClient) {
|
||||
|
||||
}
|
||||
public get(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||
return this.httpClient.get(url, AadHttpClient.configurations.v1, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
Accept: "application/json",
|
||||
ConsistencyLevel: "eventual",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
public post(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||
return this.httpClient.post(url, AadHttpClient.configurations.v1, options);
|
||||
}
|
||||
public patch(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
Accept: "application/json",
|
||||
ConsistencyLevel: "eventual",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PATCH"
|
||||
});
|
||||
}
|
||||
public put(url: string, options?: RequestInit): Promise<IHttpClientResponse> {
|
||||
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
Accept: "application/json",
|
||||
ConsistencyLevel: "eventual",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PUT"
|
||||
});
|
||||
}
|
||||
public delete(url: string): Promise<IHttpClientResponse> {
|
||||
return this.httpClient.fetch(url, AadHttpClient.configurations.v1, {
|
||||
method: "DELETE"
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,16 @@
|
|||
export class ArrayUtilities {
|
||||
public static splitToMaxLength<T>(arr: T[], length: number): T[][] {
|
||||
let result = [];
|
||||
let startIndex = 0;
|
||||
while (startIndex < arr.length) {
|
||||
result.push(arr.slice(startIndex, startIndex + length));
|
||||
startIndex += length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public static getSubMap<T, U>(map: Map<T, U>, keys: T[]): Map<T, U> {
|
||||
let result = new Map<T, U>();
|
||||
keys.forEach(key => result.set(key, map.get(key)));
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { PersonaPresence } from "office-ui-fabric-react";
|
||||
|
||||
export class StringUtilities {
|
||||
public static getInitials(fullName: string) {
|
||||
if (!fullName) {
|
||||
return "";
|
||||
}
|
||||
let initials = "";
|
||||
let names = fullName.split(" ");
|
||||
for (let i = 0; i < names.length && i < 2; i++) {
|
||||
let name = names[i];
|
||||
if (name) {
|
||||
initials += name.charAt(0);
|
||||
}
|
||||
}
|
||||
return initials.toUpperCase();
|
||||
}
|
||||
public static getPresence(presenceString): PersonaPresence{
|
||||
switch (presenceString) {
|
||||
case "":
|
||||
return PersonaPresence.none;
|
||||
case "Available":
|
||||
return PersonaPresence.online;
|
||||
case "Busy":
|
||||
return PersonaPresence.busy;
|
||||
default:
|
||||
return PersonaPresence.offline;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "4e7a2f9d-90f1-4896-bc61-e844f0dde102",
|
||||
"alias": "GraphAutoBatchingWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Graph Auto Batching" },
|
||||
"description": { "default": "Graph Auto Batching description" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "Graph Auto Batching"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'GraphAutoBatchingWebPartStrings';
|
||||
import GraphAutoBatching from './components/GraphAutoBatching';
|
||||
import { IGraphAutoBatchingProps } from './components/IGraphAutoBatchingProps';
|
||||
import { IHttpClient } from '../../dal/http/IHttpClient';
|
||||
import { SPFxHttpClient } from '../../dal/http/SPFxHttpClient';
|
||||
import { BatchGraphClient } from '../../dal/http/BatchGraphClient';
|
||||
|
||||
export interface IGraphAutoBatchingWebPartProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default class GraphAutoBatchingWebPart extends BaseClientSideWebPart<IGraphAutoBatchingWebPartProps> {
|
||||
protected httpClient: IHttpClient;
|
||||
|
||||
protected async onInit(): Promise<void> {
|
||||
let client = await this.context.aadHttpClientFactory.getClient('https://graph.microsoft.com');
|
||||
this.httpClient = new BatchGraphClient(new SPFxHttpClient(client));
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IGraphAutoBatchingProps> = React.createElement(
|
||||
GraphAutoBatching,
|
||||
{
|
||||
graphClient: this.httpClient
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('description', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.graphAutoBatching {
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 0px auto;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
@include ms-fontColor-white;
|
||||
background-color: $ms-color-themeDark;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.column {
|
||||
@include ms-Grid-col;
|
||||
@include ms-lg10;
|
||||
@include ms-xl8;
|
||||
@include ms-xlPush2;
|
||||
@include ms-lgPush1;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include ms-font-xl;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
@include ms-font-l;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include ms-font-l;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.button {
|
||||
// Our button
|
||||
text-decoration: none;
|
||||
height: 32px;
|
||||
|
||||
// Primary Button
|
||||
min-width: 80px;
|
||||
background-color: $ms-color-themePrimary;
|
||||
border-color: $ms-color-themePrimary;
|
||||
color: $ms-color-white;
|
||||
|
||||
// Basic Button
|
||||
outline: transparent;
|
||||
position: relative;
|
||||
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: $ms-font-size-m;
|
||||
font-weight: $ms-font-weight-regular;
|
||||
border-width: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
|
||||
.label {
|
||||
font-weight: $ms-font-weight-semibold;
|
||||
font-size: $ms-font-size-m;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin: 0 4px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import styles from './GraphAutoBatching.module.scss';
|
||||
import { IGraphAutoBatchingProps } from './IGraphAutoBatchingProps';
|
||||
import { UserCard } from './UserCard';
|
||||
|
||||
export default class GraphAutoBatching extends React.Component<IGraphAutoBatchingProps, {}> {
|
||||
public render(): React.ReactElement<IGraphAutoBatchingProps> {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<UserCard graphClient={this.props.graphClient} userQuery={"/me"} />
|
||||
</div>
|
||||
<div>
|
||||
<UserCard graphClient={this.props.graphClient} userQuery={"/me/manager"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IHttpClient } from "../../../dal/http/IHttpClient";
|
||||
|
||||
export interface IGraphAutoBatchingProps {
|
||||
graphClient: IHttpClient;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Spinner } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IHttpClient } from "../../../dal/http/IHttpClient";
|
||||
import { IPersonaSharedProps, Persona, PersonaSize, PersonaPresence } from "office-ui-fabric-react";
|
||||
import { StringUtilities } from "../../../utils/StringUtilities";
|
||||
|
||||
export interface IUserCardProps{
|
||||
graphClient: IHttpClient;
|
||||
userQuery: string;
|
||||
}
|
||||
|
||||
export function UserCard(props: IUserCardProps){
|
||||
const [user, setUser] = React.useState<any>({});
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const getUserInfo = async ()=>{
|
||||
const [userInfoRequest, userPhotoRequest, presenceInfo] = await Promise.all([props.graphClient.get(props.userQuery),
|
||||
props.graphClient.get(props.userQuery + "/photo/$value"),
|
||||
props.graphClient.get(props.userQuery + "/presence")]);
|
||||
const [userResult, photo, presence] = await Promise.all([userInfoRequest.json(), userPhotoRequest.text(),presenceInfo.json()]);
|
||||
setUser({
|
||||
...userResult,
|
||||
presence: presence.availability,
|
||||
photo: photo.replace("\"","").replace("\"","")
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
getUserInfo();
|
||||
}, [props.userQuery]);
|
||||
|
||||
if(loading){
|
||||
return <Spinner />;
|
||||
}
|
||||
return (
|
||||
<Persona
|
||||
imageUrl={user.photo}
|
||||
imageInitials={StringUtilities.getInitials(user.displayName)}
|
||||
text={user.displayName}
|
||||
secondaryText={user.jobTitle}
|
||||
tertiaryText={user.presence}
|
||||
presence={StringUtilities.getPresence(user.presence)}
|
||||
size={PersonaSize.size100}
|
||||
imageAlt="Annie Lindqvist, status is blocked"
|
||||
/>
|
||||
);
|
||||
}
|
7
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/en-us.js
vendored
Normal file
7
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
10
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-graph-auto-batching/src/webparts/graphAutoBatching/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IGraphAutoBatchingWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'GraphAutoBatchingWebPartStrings' {
|
||||
const strings: IGraphAutoBatchingWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:babb825e8471dcc8357aba5203a4100cff18002fb8cb6881975d53d062a62ebe
|
||||
size 1229
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e8fc3fc81764e86c379835c47bb4cb4ddf55d135a47d36fdb6b1c9cf5da60158
|
||||
size 383
|
|
@ -0,0 +1,5 @@
|
|||
export class TestingUtilities {
|
||||
public static sleep = (ms) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,749 @@
|
|||
///<reference types="jest" />
|
||||
import { BatchGraphClient } from "../../../src/dal/http/BatchGraphClient";
|
||||
import { assert } from "chai";
|
||||
import { TestingUtilities } from "../../TestingUtilities";
|
||||
import { BatchHandler } from "../../../src/dal/http/BatchHandler";
|
||||
|
||||
describe("BatchGraphClient", ()=>{
|
||||
test("should batch get requests", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test("should request two batches", async () => {
|
||||
let postMethod = jest.fn()
|
||||
postMethod.mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
})).mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User second batch"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id second batch"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
}));
|
||||
let baseClient = {
|
||||
post: postMethod
|
||||
};
|
||||
|
||||
let batchClient = new BatchGraphClient(baseClient as any, 10);
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
|
||||
await TestingUtilities.sleep(11);
|
||||
|
||||
let secondMePromise = batchClient.get("/me");
|
||||
let secondGroupPromise = batchClient.get("/me/groups");
|
||||
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
|
||||
let secondMe = await secondMePromise;
|
||||
let secondGroup = await secondGroupPromise;
|
||||
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
assert.deepEqual((await secondMe.json()), {
|
||||
displayName: "Test User second batch"
|
||||
});
|
||||
assert.deepEqual((await secondGroup.json()), {
|
||||
value: [{
|
||||
id: "Test group id second batch"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test("should batch get requests", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 404,
|
||||
body: {
|
||||
error: {
|
||||
id: "group-error"
|
||||
}
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
try {
|
||||
await groupPromise;
|
||||
}
|
||||
catch (err) {
|
||||
assert.deepEqual((await err.json()), {
|
||||
error: {
|
||||
id: "group-error"
|
||||
}
|
||||
});
|
||||
}
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
});
|
||||
test("should batch v1 request separately", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
if (url.indexOf("/v1.0/") >= 0) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User v1"
|
||||
},
|
||||
id: encodeURIComponent("/v1.0/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
else {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User Beta"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
let mePromise = batchClient.get("/me");
|
||||
let meV1Promise = batchClient.get("/v1.0/me");
|
||||
Promise.all([mePromise, meV1Promise]);
|
||||
let betaMe = await (await mePromise).json();
|
||||
let meV1 = await (await meV1Promise).json();
|
||||
|
||||
assert.equal(meV1.displayName, "Test User v1");
|
||||
assert.equal(betaMe.displayName, "Test User Beta");
|
||||
});
|
||||
test("should batch v1 request separately (different resources)", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
if (url.indexOf("/v1.0/") >= 0) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User v1"
|
||||
},
|
||||
id: encodeURIComponent("/v1.0/some-other-resource")
|
||||
}
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
else {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User Beta"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
let mePromise = batchClient.get("/me");
|
||||
let meV1Promise = batchClient.get("/v1.0/some-other-resource");
|
||||
Promise.all([mePromise, meV1Promise]);
|
||||
let betaMe = await (await mePromise).json();
|
||||
let meV1 = await (await meV1Promise).json();
|
||||
|
||||
assert.equal(meV1.displayName, "Test User v1");
|
||||
assert.equal(betaMe.displayName, "Test User Beta");
|
||||
});
|
||||
test("should retry single request", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
}));
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test("should retry single request to v1", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
}));
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/v1.0/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/v1.0/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
|
||||
let mePromise = batchClient.get("/v1.0/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test("should retry 5 times and error request", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const throttlePromise = Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
})).mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
const batchHandlerSpy = jest.spyOn(BatchHandler.prototype, "executeBatch")
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
|
||||
expect(batchHandlerSpy).toBeCalledTimes(6);
|
||||
assert.isFalse(me.ok);
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
|
||||
batchHandlerSpy.mockClear();
|
||||
});
|
||||
test("should retry 5 times and error request (next request should work)", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const throttlePromise = Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
})).mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(throttlePromise)
|
||||
.mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
status: 200,
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
},
|
||||
],
|
||||
})
|
||||
}));
|
||||
const batchHandlerSpy = jest.spyOn(BatchHandler.prototype, "executeBatch")
|
||||
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
|
||||
expect(batchHandlerSpy).toBeCalledTimes(6);
|
||||
assert.isFalse(me.ok);
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
mePromise = batchClient.get("/me");
|
||||
groupPromise = batchClient.get("/me/groups");
|
||||
me = await mePromise;
|
||||
group = await groupPromise;
|
||||
assert.isTrue(me.ok);
|
||||
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
});
|
||||
test("should retry and concurrent request should be called", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const throttlePromise = Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 429,
|
||||
body: {
|
||||
error: "Throttle"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
});
|
||||
//So...the first request should be throttled
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(throttlePromise);
|
||||
//The first retry will occur before we call concurrent request, to make sure retry batch will not be overridden
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
//The concurrent request should be resolved with this response
|
||||
jest.spyOn(baseClient, 'post').mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User (concurrent)"
|
||||
},
|
||||
id: encodeURIComponent("/me-concurrent")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
let batchClient = new BatchGraphClient(baseClient as any, 10);
|
||||
//request throttled endpoint
|
||||
let throttledPromise = batchClient.get("/me");
|
||||
//wait 12 milliseconds to be sure retry batch is constructed
|
||||
await TestingUtilities.sleep(12);
|
||||
//then request another
|
||||
let concurrentRequest = batchClient.get("/me-concurrent");
|
||||
|
||||
let meResponse = await throttledPromise;
|
||||
let concurrentMeResponse = await concurrentRequest;
|
||||
|
||||
assert.equal((await meResponse.json()).displayName, "Test User");
|
||||
assert.equal((await concurrentMeResponse.json()).displayName, "Test User (concurrent)");
|
||||
});
|
||||
test("should split bigger batch to subsequent requests", async () => {
|
||||
let baseClient = {
|
||||
get: (url, options) => {
|
||||
throw "Don't call get!"
|
||||
},
|
||||
post: (url, options) => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(baseClient, "post").mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User"
|
||||
},
|
||||
id: encodeURIComponent("/me")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
jest.spyOn(baseClient, "post").mockReturnValueOnce(Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
responses: [
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
displayName: "Test User 2"
|
||||
},
|
||||
id: encodeURIComponent("/me2")
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {
|
||||
value: [{
|
||||
id: "Test group id 2"
|
||||
}]
|
||||
},
|
||||
id: encodeURIComponent("/me/groups2")
|
||||
}
|
||||
],
|
||||
})
|
||||
}));
|
||||
let batchClient = new BatchGraphClient(baseClient as any);
|
||||
batchClient.batchSplitThreshold = 2;
|
||||
let mePromise = batchClient.get("/me");
|
||||
let groupPromise = batchClient.get("/me/groups");
|
||||
let mePromise2 = batchClient.get("/me2");
|
||||
let groupPromise2 = batchClient.get("/me/groups2");
|
||||
let me = await mePromise;
|
||||
let group = await groupPromise;
|
||||
let me2 = await mePromise2;
|
||||
let group2 = await groupPromise2;
|
||||
assert.deepEqual((await me.json()), {
|
||||
displayName: "Test User"
|
||||
});
|
||||
assert.deepEqual((await group.json()), {
|
||||
value: [{
|
||||
id: "Test group id"
|
||||
}]
|
||||
});
|
||||
assert.deepEqual((await me2.json()), {
|
||||
displayName: "Test User 2"
|
||||
});
|
||||
assert.deepEqual((await group2.json()), {
|
||||
value: [{
|
||||
id: "Test group id 2"
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
///<reference types="jest" />
|
||||
import * as React from "react";
|
||||
import { RenderResult, render, act } from "@testing-library/react";
|
||||
import { assert } from "chai";
|
||||
import { UserCard } from "../../../src/webparts/graphAutoBatching/components/UserCard";
|
||||
|
||||
describe("<UserCard />", () => {
|
||||
test("should render user with presence", async ()=>{
|
||||
let graphClient = {
|
||||
get: async (url: string)=>Promise.resolve({})
|
||||
};
|
||||
|
||||
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: ()=>Promise.resolve({
|
||||
displayName: "Marcin Wojciechowski",
|
||||
jobTitle: "Senior Software Engineer",
|
||||
})
|
||||
}));
|
||||
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||
status: 200,
|
||||
ok: true,
|
||||
text: ()=>Promise.resolve(`"someEncodedContent"`)
|
||||
}));
|
||||
jest.spyOn(graphClient, "get").mockReturnValueOnce(Promise.resolve({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: ()=>Promise.resolve({
|
||||
availability: "Available"
|
||||
})
|
||||
}))
|
||||
|
||||
let userCard: RenderResult;
|
||||
await act(async ()=>{
|
||||
userCard = render(<UserCard graphClient={graphClient as any} userQuery={"/me"} />);
|
||||
});
|
||||
const displayNameDiv = userCard.getByText("Marcin Wojciechowski");
|
||||
assert.isNotNull(displayNameDiv);
|
||||
|
||||
const presenceDiv = userCard.getByText("Available");
|
||||
assert.isNotNull(presenceDiv);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.7/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.promise"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue