diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..cf913c5fe --- /dev/null +++ b/404.html @@ -0,0 +1,2577 @@ + + + +
+ + + + + + + + + + + + + +This library provides a thin wrapper around the @azure/identity library to make it easy to integrate Azure Identity authentication in your solution.
+You will first need to install the package:
+npm install @pnp/azidjsclient --save
The following example shows how to configure the SPFI or GraphFI object using this behavior.
+import { DefaultAzureCredential } from "@azure/identity";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/sp";
+import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { AzureIdentity } from "@pnp/azidjsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/me";
+
+const credential = new DefaultAzureCredential();
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(
+ SPDefault(),
+ AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)
+);
+
+const graph = graphfi().using(
+ GraphDefault(),
+ AzureIdentity(credential, ["https://graph.microsoft.com/.default"], null)
+);
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+Please see more scenarios in the authentication article.
+ + + + + + +The client-side pages API included in this library is an implementation that was reverse engineered from the first-party API's and is unsupported by Microsoft. Given how flexible pages are we've done our best to give you the endpoints that will provide the functionality you need but that said, implementing these APIs is one of the more complicated tasks you can do.
+It's especially important to understand the product team is constantly changing the features of pages and often that will also end up changing how the APIs that we've leveraged behave and because they are not offical third-party APIs this can cause our implementation to break. In order to fix those breaks we need to go back to the beginning and re-validate how the endpoints work searching for what has changed and then implementing those changes in our code. This is by no means simple. If you are reporting an issue with the pages API be aware that it may take significant time for us to unearth what is happening and fix it. Any research that you can provide when sharing your issue will go a long way in expediating that process, or better yet, if you can track it down and submit a PR with a fix we would be most greatful.
+This section is to offer you methods to be able to reverse engineer some of the first party web parts to help figure out how to add them to the page using the addControl
method.
Your first step needs to be creating a test page that you can inspect.
+Fetch/XHR
and then type SavePage
to filter for the specific network calls.SavePageAsDraft
call and you can then look at the Payload
of that call
+ CanvasContent1
property and copy that value. You can then paste it into a temporary file with the .json extension in your code editor so you can inspect the payload. The value is an array of objects, and each object (except the last) is the definition of the web part.Below is an example (as of the last change date of this document) of what the QuickLinks web part looks like. One key takeaway from this file is the webPartId
property which can be used when filtering for the right web part definition after getting a collection from sp.web.getClientsideWebParts();
.
++Note that it could change at any time so please do not rely on this data, please use it as an example only.
+
{
+ "position": {
+ "layoutIndex": 1,
+ "zoneIndex": 1,
+ "sectionIndex": 1,
+ "sectionFactor": 12,
+ "controlIndex": 1
+ },
+ "controlType": 3,
+ "id": "00000000-58fd-448c-9e40-6691ce30e3e4",
+ "webPartId": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
+ "addedFromPersistedData": true,
+ "reservedHeight": 141,
+ "reservedWidth": 909,
+ "webPartData": {
+ "id": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
+ "instanceId": "00000000-58fd-448c-9e40-6691ce30e3e4",
+ "title": "Quick links",
+ "description": "Show a collection of links to content such as documents, images, videos, and more in a variety of layouts with options for icons, images, and audience targeting.",
+ "audiences": [],
+ "serverProcessedContent": {
+ "htmlStrings": {},
+ "searchablePlainTexts": {
+ "items[0].title": "PnPjs Title"
+ },
+ "imageSources": {},
+ "links": {
+ "baseUrl": "https://contoso.sharepoint.com/sites/PnPJS",
+ "items[0].sourceItem.url": "/sites/PnPJS/SitePages/pnpjsTestV2.aspx"
+ },
+ "componentDependencies": {
+ "layoutComponentId": "706e33c8-af37-4e7b-9d22-6e5694d92a6f"
+ }
+ },
+ "dataVersion": "2.2",
+ "properties": {
+ "items": [
+ {
+ "sourceItem": {
+ "guids": {
+ "siteId": "00000000-4657-40d2-843d-3d6c72e647ff",
+ "webId": "00000000-e714-4de6-88db-b0ac40d17850",
+ "listId": "{00000000-8ED8-4E43-82BD-56794D9AB290}",
+ "uniqueId": "00000000-6779-4979-adad-c120a39fe311"
+ },
+ "itemType": 0,
+ "fileExtension": ".ASPX",
+ "progId": null
+ },
+ "thumbnailType": 2,
+ "id": 1,
+ "description": "",
+ "fabricReactIcon": {
+ "iconName": "heartfill"
+ },
+ "altText": "",
+ "rawPreviewImageMinCanvasWidth": 32767
+ }
+ ],
+ "isMigrated": true,
+ "layoutId": "CompactCard",
+ "shouldShowThumbnail": true,
+ "imageWidth": 100,
+ "buttonLayoutOptions": {
+ "showDescription": false,
+ "buttonTreatment": 2,
+ "iconPositionType": 2,
+ "textAlignmentVertical": 2,
+ "textAlignmentHorizontal": 2,
+ "linesOfText": 2
+ },
+ "listLayoutOptions": {
+ "showDescription": false,
+ "showIcon": true
+ },
+ "waffleLayoutOptions": {
+ "iconSize": 1,
+ "onlyShowThumbnail": false
+ },
+ "hideWebPartWhenEmpty": true,
+ "dataProviderId": "QuickLinks",
+ "webId": "00000000-e714-4de6-88db-b0ac40d17850",
+ "siteId": "00000000-4657-40d2-843d-3d6c72e647ff"
+ }
+ }
+}
+
+At this point the only aspect of the above JSON payload you're going to be paying attention to is the webPartData
. We have exposed title
, description
, and dataVersion
as default properties of the ClientsideWebpart
class. In addition we provide a getProperties
, setProperties
, getServerProcessedContent
, setServerProcessedContent
methods. The difference in this case in these set base methods is that it will merge the object you pass into those methods with the values already on the object.
The code below gives a incomplete but demonstrative example of how you would extend the ClientsideWebpart class to provide an interface to build a custom class for the QuickLinks web part illustrated in our JSON payload above. This code assumes you have already added the control to a section. For more information about that step see the documentation for Add Controls
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+
+//Define interface based on JSON object above
+interface IQLItem {
+ sourceItem: {
+ guids: {
+ siteId: string;
+ webId: string;
+ listId: string;
+ uniqueId: string;
+ },
+ itemType: number;
+ fileExtension: string;
+ progId: string;
+ }
+ thumbnailType: number;
+ id: number;
+ description: string;
+ fabricReactIcon: { iconName: string; };
+ altText: string;
+ rawPreviewImageMinCanvasWidth: number;
+}
+
+// we create a class to wrap our functionality in a reusable way
+class QuickLinksWebpart extends ClientsideWebpart {
+
+ constructor(control: ClientsideWebpart) {
+ super((<any>control).json);
+ }
+
+ // add property getter/setter for what we need, in this case items array within properties
+ public get items(): IQLItem[] {
+ return this.json.webPartData?.properties?.items || [];
+ }
+
+ public set items(value: IQLItem[]) {
+ this.json.webPartData.properties?.items = value;
+ }
+}
+
+// now we load our page
+const page = await sp.web.loadClientsidePage("/sites/PnPJS/SitePages/QuickLinks-Web-Part-Test.aspx");
+
+// get our part and pass it to the constructor of our wrapper class.
+const part = new QuickLinksWebpart(page.sections[0].columns[0].getControl(0));
+
+//Need to set all the properties
+part.items = [{IQLItem_properties}];
+
+await page.save();
+
+
+
+
+
+
+
+ We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.
+npm install @pnp/msaljsclient --save
At this time we're using version 1.x of the msal
library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.
Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
+import { Configuration, AuthenticationParameters } from "msal";
+
+const configuration: Configuration = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{tenant Id}/",
+ clientId: "{AAD Application Id/Client Id}"
+ }
+};
+
+const authParams: AuthenticationParameters = {
+ scopes: ["https://graph.microsoft.com/.default"]
+};
+
+import { spfi, SPBrowser } from "@pnp/sp";
+import { graphfi, GraphBrowser } from "@pnp/graph";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/users";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams));
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(GraphBrowser(), MSAL(configuration, authParams));
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+
+
+
+
+
+
+ We support MSAL for both browser and nodejs and Azure Identity for nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible configurations, but any parameters supplied are passed through to the underlying implementation.
+Depending on which package you want to use you will need to install an additional package from the library because of the large dependencies.
+We support MSAL through the msal-node library which is included by the @pnp/nodejs package.
+For the Azure Identity package:
+npm install @pnp/azidjsclient --save
We support Azure Identity through the @azure/identity library which simplifies the authentication process and makes it easy to integrate Azure Identity authentication in your solution.
+The SPDefault and GraphDefault exported by the nodejs library include MSAL and takes the parameters directly.
+The following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
+import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/graph";
+import { Configuration, AuthenticationParameters } from "msal";
+import "@pnp/graph/users";
+import "@pnp/sp/webs";
+
+const configuration: Configuration = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{tenant Id}/",
+ clientId: "{AAD Application Id/Client Id}"
+ }
+};
+
+const sp = spfi("{site url}").using(SPDefault({
+ msal: {
+ config: configuration,
+ scopes: ["https://{tenant}.sharepoint.com/.default"],
+ },
+}));
+
+const graph = graphfi().using(GraphDefault({
+ msal: {
+ config: configuration,
+ scopes: ["https://graph.microsoft.com/.default"],
+ },
+}));
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+It is also possible to use the MSAL behavior directly if you are composing your own strategies.
+import { SPDefault, GraphDefault, MSAL } from "@pnp/nodejs";
+
+const sp = spfi("{site url}").using(SPDefault(), MSAL({
+ config: configuration,
+ scopes: ["https://{tenant}.sharepoint.com/.default"],
+}));
+
+const graph = graphfi().using(GraphDefault(), MSAL({
+ config: configuration,
+ scopes: ["https://graph.microsoft.com/.default"],
+}));
+
+
+The following sample shows how to pass the credential object to the AzureIdentity behavior including scopes.
+import { DefaultAzureCredential } from "@azure/identity";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/sp";
+import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { AzureIdentity } from "@pnp/azidjsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/users";
+
+// We're using DefaultAzureCredential but the credential can be any valid `Credential Type`
+const credential = new DefaultAzureCredential();
+
+const sp = spfi("https://{tenant}.sharepoint.com/sites/dev").using(
+ SPDefault(),
+ AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)
+);
+
+const graph = graphfi().using(
+ GraphDefault(),
+ AzureIdentity(credential, ["https://graph.microsoft.com/.default"], null)
+);
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+
+
+
+
+
+
+ When building in SharePoint Framework you only need to provide the context to either sp or graph to ensure proper authentication. This will use the default SharePoint AAD application to manage scopes. If you would prefer to use a different AAD application please see the MSAL section below.
+import { SPFx, spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const sp = spfi().using(SPFx(this.context));
+
+const webData = await sp.web();
+
+import { SPFx, graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(SPFx(this.context));
+
+const meData = await graph.me();
+
+When using the SPFx behavior, authentication is handled by a cookie stored on the users client. In very specific instances some of the SharePoint methods will require a token. We have added a custom behavior to support that called SPFxToken
. This will require that you add the appropriate application role to the SharePoint Framework's package-solution.json
-> webApiPermissionRequests section where you will define the resource and scope for the request.
Here's an example of how you would build an instance of the SPFI that would include an Bearer Token in the header. Be advised if you use this instance to make calls to SharePoint endpoints that you have not specifically authorized they will fail.
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.
+npm install @pnp/msaljsclient --save
At this time we're using version 1.x of the msal
library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.
Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
+import { SPFx as graphSPFx, graphfi } from "@pnp/graph";
+import { SPFx as spSPFx, spfi } from "@pnp/sp";
+import { MSAL } from "@pnp/msaljsclient";
+import { Configuration, AuthenticationParameters } from "msal";
+import "@pnp/graph/users";
+import "@pnp/sp/webs";
+
+const configuration: Configuration = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{tenant Id}/",
+ clientId: "{AAD Application Id/Client Id}"
+ }
+};
+
+const authParams: AuthenticationParameters = {
+ scopes: ["https://graph.microsoft.com/.default"]
+};
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(graphSPFx(this.context), MSAL(configuration, authParams));
+const sp = spfi().using(spSPFx(this.context), MSAL(configuration, authParams));
+
+const meData = await graph.me();
+const webData = await sp.web();
+
+
+
+
+
+
+
+ One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods.
+We provide multiple ways to authenticate based on the scenario you're developing for, see one of these more detailed guides:
+If you have more specific authentication requirements you can always build your own by using the new queryable pattern which exposes a dedicated auth moment. That moment expects observers with the signature:
+async function(url, init) {
+
+ // logic to apply authentication to the request
+
+ return [url, init];
+}
+
+You can follow this example as a general pattern to build your own custom authentication model. You can then wrap your authentication in a behavior for easy reuse.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using({behaviors});
+const web = sp.web;
+
+// we will use custom auth on this web
+web.on.auth(async function(url, init) {
+
+ // some code to get a token
+ const token = getToken();
+
+ // set the Authorization header in the init (this init is what is passed directly to the fetch call)
+ init.headers["Authorization"] = `Bearer ${token}`;
+
+ return [url, init];
+});
+
+
+
+
+
+
+
+ When optimizing for performance you can combine batching and caching to reduce the overall number of requests. On the first request any cachable data is stored as expected once the request completes. On subsequent requests if data is found in the cache it is returned immediately and that request is not added to the batch, in fact the batch will never register the request. This can work across many requests such that some returned cached data and others do not - the non-cached requests will be added to and processed by the batch as expected.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+batchedSP.using(Caching());
+
+batchedSP.web().then(console.log);
+
+batchedSP.web.lists().then(console.log);
+
+// execute the first set of batched requests, no information is currently cached
+await execute();
+
+// create a new batch
+const [batchedSP2, execute2] = await sp.batched();
+batchedSP2.using(Caching());
+
+// add the same requests - this simulates the user navigating away from or reloading the page
+batchedSP2.web().then(console.log);
+batchedSP2.web.lists().then(console.log);
+
+// executing the batch will return the cached values from the initial requests
+await execute2();
+
+In this second example we include an update to the web's title. Because non-get requests are never cached the update code will always run, but the results from the two get requests will resolve from the cache prior to being added to the batch.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+batchedSP.using(Caching());
+
+batchedSP.web().then(console.log);
+
+batchedSP.web.lists().then(console.log);
+
+// this will never be cached
+batchedSP.web.update({
+ Title: "dev web 1",
+});
+
+// execute the first set of batched requests, no information is currently cached
+await execute();
+
+// create a new batch
+const [batchedSP2, execute2] = await sp.batched();
+batchedSP2.using(Caching());
+
+// add the same requests - this simulates the user navigating away from or reloading the page
+batchedSP2.web().then(console.log);
+batchedSP2.web.lists().then(console.log);
+
+// this will never be cached
+batchedSP2.web.update({
+ Title: "dev web 2",
+});
+
+// executing the batch will return the cached values from the initial requests
+await execute2();
+
+
+
+
+
+
+
+ Where possible batching can significantly increase application performance by combining multiple requests to the server into one. This is especially useful when first establishing state, but applies for any scenario where you need to make multiple requests before loading or based on a user action. Batching is supported within the sp and graph libraries as shown below.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+let res = [];
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedSP.web().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedSP.web.lists.select("Title")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+ ///Do something with the results
+}
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedWeb, execute] = sp.web.batched();
+
+let res = [];
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedWeb().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedWeb.lists.select("Title")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+ ///Do something with the results
+}
+
+++Batches must be for the same web, you cannot combine requests from multiple webs into a batch.
+
import { graphfi } from "@pnp/graph";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/batching";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+const [batchedGraph, execute] = graph.batched();
+
+let res = [];
+
+// Pushes the results of these calls to an array
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedGraph.users().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedGraph.groups.select("Id")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i=0; i<res.length; i++){
+ // Do something with the results
+}
+
+For most cases the above usage should be sufficient, however you may be in a situation where you do not have convenient access to either an spfi instance or a web. Let's say for example you want to add a lot of items to a list and have an IList. You can in these cases use the createBatch function directly. We recommend as much as possible using the sp or web or graph batched method, but also provide this additional flexibility if you need it.
+import { createBatch } from "@pnp/sp/batching";
+import { SPDefault } from "@pnp/nodejs";
+import { IList } from "@pnp/sp/lists";
+import "@pnp/sp/items/list";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPDefault({ /* ... */ }));
+
+// in one part of your application you setup a list instance
+const list: IList = sp.web.lists.getByTitle("MyList");
+
+
+// in another part of your application you want to batch requests, but do not have the sp instance available, just the IList
+
+// note here the first part of the tuple is NOT the object, rather the behavior that enables batching. You must still register it with `using`.
+const [batchedListBehavior, execute] = createBatch(list);
+// this list is now batching all its requests
+list.using(batchedListBehavior);
+
+// these will all occur within a single batch
+list.items.add({ Title: `1: ${getRandomString(4)}` });
+list.items.add({ Title: `2: ${getRandomString(4)}` });
+list.items.add({ Title: `3: ${getRandomString(4)}` });
+list.items.add({ Title: `4: ${getRandomString(4)}` });
+
+await execute();
+
+This is of course also possible with the graph library as shown below.
+import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+const users = graph.users;
+
+const [batchedBehavior, execute] = createBatch(users);
+users.using(batchedBehavior);
+
+users();
+// we can only place the 'users' instance into the batch once
+graph.users.using(batchedBehavior)();
+graph.users.using(batchedBehavior)();
+graph.users.using(batchedBehavior)();
+
+await execute();
+
+
+It shouldn't come up often, but you can not make multiple requests using the same instance of a queryable in a batch. Let's consider the incorrect example below:
+++The error message will be "This instance is already part of a batch. Please review the docs at https://pnp.github.io/pnpjs/concepts/batching#reuse."
+
import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+// gain a batched instance of the graph
+const [batchedGraph, execute] = graph.batched();
+
+// we take a reference to the value returned from .users
+const users = batchedGraph.users;
+
+// we invoke it, adding it to the batch (this is a request to /users), it will succeed
+users();
+
+// we invoke it again, because this instance has already been added to the batch, this request will throw an error
+users();
+
+// we execute the batch, this promise will resolve
+await execute();
+
+To overcome this you can either start a new fluent chain or use the factory method. Starting a new fluent chain at any point will create a new instance. Please review the corrected sample below.
+import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import { Users } from "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+// gain a batched instance of the graph
+const [batchedGraph, execute] = graph.batched();
+
+// we invoke a new instance of users from the batchedGraph
+batchedGraph.users();
+
+// we again invoke a new instance of users from the batchedGraph, this is fine
+batchedGraph.users();
+
+const users = batchedGraph.users;
+// we can do this once
+users();
+
+// by creating a new users instance using the Users factory we can keep adding things to the batch
+// users2 will be part of the same batch
+const users2 = Users(users);
+users2();
+
+// we execute the batch, this promise will resolve
+await execute();
+
+++In addition you cannot continue using a batch after execute. Once execute has resolved the batch is done. You should create a new batch using one of the described methods to conduct another batched call.
+
In the following example, the results of adding items to the list is an object with a type of IItemAddResult which is {data: any, item: IItem}
. Since version v1 the expectation is that the item
object is immediately usable to make additional queries. When this object is the result of a batched call, this was not the case so we have added additional code to reset the observers using the original base from witch the batch was created, mimicing the behavior had the IItem been created from that base withyout a batch involved. We use CopyFrom to ensure that we maintain the references to the InternalResolve and InternalReject events through the end of this timelines lifecycle.
import { createBatch } from "@pnp/sp/batching";
+import { SPDefault } from "@pnp/nodejs";
+import { IList } from "@pnp/sp/lists";
+import "@pnp/sp/items/list";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPDefault({ /* ... */ }));
+
+// in one part of your application you setup a list instance
+const list: IList = sp.web.lists.getByTitle("MyList");
+
+const [batchedListBehavior, execute] = createBatch(list);
+// this list is now batching all its requests
+list.using(batchedListBehavior);
+
+let res: IItemAddResult[] = [];
+
+// these will all occur within a single batch
+list.items.add({ Title: `1: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `2: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `3: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `4: ${getRandomString(4)}` }).then(r => res.push(r));
+
+await execute();
+
+let newItems: IItem[] = [];
+
+for(let i=0; i<res.length; i++){
+ //This line will correctly resolve
+ const newItem = await res[i].item.select("Title")<{Title: string}>();
+ newItems.push(newItem);
+}
+
+
+
+
+
+
+
+ If you find that there are endpoints that have not yet been implemented, or have changed in such a way that there are issues using the implemented endpoint, you can still make those calls and take advantage of the plumbing provided by the library.
+To issue calls against the SharePoint REST endpoints you would use one of the existing operations:
+To construct a call you will need to pass, to the operation call an SPQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.
+Below are a couple of examples to get you started.
+Let's pretend that the getById method didn't exist on a lists items. The example below shows two methods for constructing our SPQueryable method.
+The first is the easiest to use because, as the queryable documentation tells us, this will maintain all the registered observers on the original queryable instance. We would start with the queryable object closest to the endpoint we want to use, in this case list
. We do this because we need to construct the full URL that will be called. Using list
in this instance gives us the first part of the URL (e.g. https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')
) and then we can construct the remainder of the call by passing in a string.
The second method essentially starts from scratch where the user constructs the entire url and then registers observers on the SPQuerable instance. Then uses spGet to execute the call. There are many other variations to arrive at the same outcome, all are dependent on your requirements.
+import { spfi } from "@pnp/sp";
+import { AssignFrom } from "@pnp/core";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { spGet, SPQueryable, SPFx } from "@pnp/sp";
+
+// Establish SPFI instance passing in the appropriate behavior to register the initial observers.
+const sp = spfi(...);
+
+// create an instance of the items queryable
+
+const list = sp.web.lists.getByTitle("My List");
+
+// get the item with an id of 1, easiest method
+const item: any = await spGet(SPQueryable(list, "items(1)"));
+
+// get the item with an id of 1, constructing a new queryable and registering behaviors
+const spQueryable = SPQueryable("https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)").using(SPFx(this.context));
+
+// ***or***
+
+// For v3 the full url is require for SPQuerable when providing just a string
+const spQueryable = SPQueryable("https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)").using(AssignFrom(sp.web));
+
+// and then use spQueryable to make the request
+const item: any = await spGet(spQueryable);
+
+The resulting call will be to the endpoint:
+https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)
Let's now pretend that we need to get the changes on a list and want to call the getchanges
method off list.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { IChangeQuery, spPost, SPQueryable } from "@pnp/sp";
+import { body } from "@pnp/queryable";
+
+// Establish SPFI instance passing in the appropriate behavior to register the initial observers.
+const sp = spfi(...);
+
+
+// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore
+const query: IChangeQuery = {
+ Add: true,
+ ChangeTokenEnd: null,
+ ChangeTokenStart: null,
+ DeleteObject: true,
+ Rename: true,
+ Restore: true,
+};
+
+// create an instance of the items queryable
+const list = sp.web.lists.getByTitle("My List");
+
+// get the item with an id of 1
+const changes: any = await spPost(SPQueryable(list, "getchanges"), body({query}));
+
+
+The resulting call will be to the endpoint:
+https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/getchanges
To issue calls against the Microsoft Graph REST endpoints you would use one of the existing operations:
+To construct a call you will need to pass, to the operation call an GraphQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.
+Below are a couple of examples to get you started.
+Here's an example for getting the chats for a particular user. This uses the simplest method for constructing the graphQueryable which is to start with a instance of a queryable that is close to the endpoint we want to call, in this case user
and then adding the additional path as a string. For a more advanced example see spGet
above.
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import { GraphQueryable, graphGet } from "@pnp/graph";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+// create an instance of the user queryable
+const user = graph.users.getById('jane@contoso.com');
+
+// get the chats for the user
+const chat: any = await graphGet(GraphQueryable(user, "chats"));
+
+The results call will be to the endpoint:
+https://graph.microsoft.com/v1.0/users/jane@contoso.com/chats
This is an example of adding an event to a calendar.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/calendars";
+import { GraphQueryable, graphPost } from "@pnp/graph";
+import { body, InjectHeaders } from "@pnp/queryable";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+// create an instance of the user queryable
+const calendar = graph.users.getById('jane@contoso.com').calendar;
+
+const props = {
+ "subject": "Let's go for lunch",
+ "body": {
+ "contentType": "HTML",
+ "content": "Does noon work for you?"
+ },
+ "start": {
+ "dateTime": "2017-04-15T12:00:00",
+ "timeZone": "Pacific Standard Time"
+ },
+ "end": {
+ "dateTime": "2017-04-15T14:00:00",
+ "timeZone": "Pacific Standard Time"
+ },
+ "location":{
+ "displayName":"Harry's Bar"
+ },
+ "attendees": [
+ {
+ "emailAddress": {
+ "address":"samanthab@contoso.onmicrosoft.com",
+ "name": "Samantha Booth"
+ },
+ "type": "required"
+ }
+ ],
+ "allowNewTimeProposals": true,
+ "transactionId":"7E163156-7762-4BEB-A1C6-729EA81755A7"
+};
+
+// custom request init to add timezone header.
+const graphQueryable = GraphQueryable(calendar, "events").using(InjectHeaders({
+ "Prefer": 'outlook.timezone="Pacific Standard Time"',
+}));
+
+// adds a new event to the user's calendar
+const event: any = await graphPost(graphQueryable, body(props));
+
+The results call will be to the endpoint:
+https://graph.microsoft.com/v1.0/users/jane@contoso.com/calendar/events
If you find you need to create an instance of Queryable (for either graph or SharePoint) that would hang off the root of the url you can use the AssignFrom
or CopyFrom
behaviors.
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import { GraphQueryable, graphPost } from "@pnp/graph";
+import { body, InjectHeaders } from "@pnp/queryable";
+import { AssignFrom } from "@pnp/core";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+const chatsQueryable = GraphQueryable("chats").using(AssignFrom(graph.me));
+
+const chat: any = await graphPost(chatsQueryable, body(chatBody));
+
+The results call will be to the endpoint:
+https://graph.microsoft.com/v1.0/chats
With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles.
+Scenarios could include:
+You can see/clone a sample project of this example here.
+ + + + + + +This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.
+++For 429, 503, and 504 errors we include retry logic within the library
+
All errors resulting from executed web requests will be returned as an HttpRequestError
object which extends the base Error
. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.
Property Name | +Description | +
---|---|
name | +Standard Error.name property. Always 'Error' | +
message | +Normalized string containing the status, status text, and the full response text | +
stack | +The callstack producing the error | +
isHttpRequestError | +Always true, allows you to reliably determine if you have an HttpRequestError instance | +
response | +Unread copy of the Response object resulting in the thrown error | +
status | +The Response.status value (such as 404) | +
statusText | +The Response.statusText value (such as 'Not Found') | +
For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen 😉. The most basic type of error handling involves a simple try-catch when using the async/await promises pattern.
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+try {
+
+ // get a list that doesn't exist
+ const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+ console.error(e);
+}
+
+This will produce output like:
+Error making HttpClient request in queryable [404] Not Found ::> {"odata.error":{"code":"-1, System.ArgumentException","message":{"lang":"en-US","value":"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'."}}} Data: {"response":{"size":0,"timeout":0},"status":404,"statusText":"Not Found","isHttpRequestError":true}
+
+This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.
+In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { HttpRequestError } from "@pnp/queryable";
+
+try {
+
+ // get a list that doesn't exist
+ const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+ // are we dealing with an HttpRequestError?
+ if (e?.isHttpRequestError) {
+
+ // we can read the json from the response
+ const json = await (<HttpRequestError>e).response.json();
+
+ // if we have a value property we can show it
+ console.log(typeof json["odata.error"] === "object" ? json["odata.error"].message.value : e.message);
+
+ // add of course you have access to the other properties and can make choices on how to act
+ if ((<HttpRequestError>e).status === 404) {
+ console.error((<HttpRequestError>e).statusText);
+ // maybe create the resource, or redirect, or fallback to a secondary data source
+ // just ideas, handle any of the status codes uniquely as needed
+ }
+
+ } else {
+ // not an HttpRequestError so we just log message
+ console.log(e.message);
+ }
+}
+
+Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.
+import { Logger } from "@pnp/logging";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+try {
+ // get a list that doesn't exist
+ const w = await sp.web.lists.getByTitle("no")();
+} catch (e) {
+
+ Logger.error(e);
+}
+
+You may want to read the response and customize the message as described above:
+import { Logger } from "@pnp/logging";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { HttpRequestError } from "@pnp/queryable";
+
+try {
+ // get a list that doesn't exist
+ const w = await sp.web.lists.getByTitle("no")();
+} catch (e) {
+
+ if (e?.isHttpRequestError) {
+
+ // we can read the json from the response
+ const data = await (<HttpRequestError>e).response.json();
+
+ // parse this however you want
+ const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
+
+ // we use the status to determine a custom logging level
+ const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
+
+ // create a custom log entry
+ Logger.log({
+ data,
+ level,
+ message,
+ });
+
+ } else {
+ // not an HttpRequestError so we just log message
+ Logger.error(e);
+ }
+}
+
+After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.
+import { Logger } from "@pnp/logging";
+import { HttpRequestError } from "@pnp/queryable";
+import { hOP } from "@pnp/core";
+
+export async function handleError(e: Error | HttpRequestError): Promise<void> {
+
+ if (hOP(e, "isHttpRequestError")) {
+
+ // we can read the json from the response
+ const data = await (<HttpRequestError>e).response.json();
+
+ // parse this however you want
+ const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
+
+ // we use the status to determine a custom logging level
+ const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
+
+ // create a custom log entry
+ Logger.log({
+ data,
+ level,
+ message,
+ });
+
+ } else {
+ // not an HttpRequestError so we just log message
+ Logger.error(e);
+ }
+}
+
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { handleError } from "./errorhandler";
+
+try {
+
+ const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+ await handleError(e);
+}
+
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { handleError } from "./errorhandler";
+
+try {
+
+ const w = await sp.web.lists();
+
+} catch (e) {
+
+ await handleError(e);
+}
+
+In Version 3 the library introduced the concept of a Timeline object and moments. One of the broadcast moments is error. To create your own custom error handler you can define a special handler for the error moment something like the following:
+
+//Custom Error Behavior
+export function CustomError(): TimelinePipe<Queryable> {
+
+ return (instance: Queryable) => {
+
+ instance.on.error((err) => {
+ if (logging) {
+ console.log(`🛑 PnPjs Testing Error - ${err.toString()}`);
+ }
+ });
+
+ return instance;
+ };
+}
+
+//Adding our CustomError behavior to our timline
+
+const sp = spfi().using(SPDefault(this.context)).using(CustomError());
+
+
+
+
+
+
+
+ For people who have been using the library since the early days you are familiar with the need to use the ()
method to invoke a method chain: Starting with v3 this is no longer possible, you must invoke the object directly to execute the default action for that class:
const lists = await sp.web.lists();
+
+
+
+
+
+
+
+ Starting with version 3 we support nightly builds, which are built from the version-3 branch each evening and include all the changes merged ahead of a particular build. These are a great way to try out new features before a release, or get a fix or enhancement without waiting for the monthly builds.
+You can install the nightly builds using the below examples. While we only show examples for sp
and graph
nightly builds are available for all packages.
npm install @pnp/sp@v3nightly --save
+
+npm install @pnp/graph@v3nightly --save
+
+++ + + + + + +Nightly builds are NOT monthly releases and aren't tested as deeply. We never intend to release broken code, but nightly builds may contain some code that is not entirely final or fully reviewed. As always if you encounter an issue please let us know, especially for nightly builds so we can be sure to address it before the next monthly release.
+
Due to the introduction of selective imports it can be somewhat frustrating to import all of the needed dependencies every time you need them across many files. Instead the preferred approach, especially for SPFx, is to create a project config file or establish a service to manage your PnPjs interfaces. Doing so centralizes the imports, configuration, and optionally extensions to PnPjs in a single place.
+++If you have multiple projects that share dependencies on PnPjs you can benefit from creating a custom bundle and using them across your projects.
+
These steps reference an SPFx solution, but apply to any solution.
+Within the src directory create a new file named pnpjs-config.ts
and copy in the below content.
import { WebPartContext } from "@microsoft/sp-webpart-base";
+
+// import pnp, pnp logging system, and any other selective imports needed
+import { spfi, SPFI, SPFx } from "@pnp/sp";
+import { LogLevel, PnPLogging } from "@pnp/logging";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching";
+
+var _sp: SPFI = null;
+
+export const getSP = (context?: WebPartContext): SPFI => {
+ if (context != null) {
+ //You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
+ // The LogLevel set's at what level a message will be written to the console
+ _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
+ }
+ return _sp;
+};
+
+To initialize the configuration, from the onInit
function (or whatever function runs first in your code) make a call to getSP passing in the SPFx context object (or whatever configuration you would require for your setup).
protected async onInit(): Promise<void> {
+ this._environmentMessage = this._getEnvironmentMessage();
+
+ super.onInit();
+
+ //Initialize our _sp object that we can then use in other packages without having to pass around the context.
+ // Check out pnpjsConfig.ts for an example of a project setup file.
+ getSP(this.context);
+}
+
+Now you can consume your configured _sp
object from anywhere else in your code by simply referencing the pnpjs-presets.ts
file via an import statement and then getting a local instance of the _sp
object using the getSP()
method without passing any context.
import { getSP } from './pnpjs-config.ts';
+...
+export default class PnPjsExample extends React.Component<IPnPjsExampleProps, IIPnPjsExampleState> {
+
+ private _sp: SPFI;
+
+ constructor(props: IPnPjsExampleProps) {
+ super(props);
+ // set initial state
+ this.state = {
+ items: [],
+ errors: []
+ };
+ this._sp = getSP();
+ }
+
+ ...
+
+}
+
+Because you do not have full access to the context object within a service you need to setup things a little differently.
+import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
+import { PageContext } from "@microsoft/sp-page-context";
+import { AadTokenProviderFactory } from "@microsoft/sp-http";
+import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
+import { graphfi, GraphFI, SPFx as gSPFx } from "@pnp/graph";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+export interface ISampleService {
+ getLists(): Promise<any[]>;
+}
+
+export class SampleService {
+
+ public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);
+ private _sp: SPFI;
+ private _graph: GraphFI;
+
+ constructor(serviceScope: ServiceScope) {
+
+ serviceScope.whenFinished(() => {
+
+ const pageContext = serviceScope.consume(PageContext.serviceKey);
+ const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);
+
+ //SharePoint
+ this._sp = spfi().using(spSPFx({ pageContext }));
+
+ //Graph
+ this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));
+ }
+
+ public getLists(): Promise<any[]> {
+ return this._sp.web.lists();
+ }
+}
+
+Depending on the architecture of your solution you can also opt to export the service as a global. If you choose this route you would need to modify the service to create an Init function where you would pass the service scope instead of doing so in the constructor. You would then export a constant that creates a global instance of the service.
+export const mySampleService = new SampleService();
+
+For a full sample, please see our PnPjs Version 3 Sample Project
+ + + + + + +As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking.
+This concept works well with custom bundling to create a shared package tailored exactly to your needs.
+If you would prefer to not worry about selective imports please see the section on presets.
+++A quick note on how TypeScript handles type only imports. If you have a line like
+import { IWeb } from "@pnp/sp/webs"
everything will transpile correctly but you will get runtime errors because TS will see that line as a type only import and drop it. You need to include bothimport { IWeb } from "@pnp/sp/webs"
andimport "@pnp/sp/webs"
to ensure the webs functionality is correctly included. You can see this in the last example below.
// the sp var now has almost nothing attached at import time and relies on
+
+// we need to import each of the pieces we need to "attach" them for chaining
+// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
+
+Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific
+// the sp var now has almost nothing attached at import time and relies on
+
+// we need to import each of the pieces we need to "attach" them for chaining
+// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
+
+The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example:
+// this import statement will attach content-type functionality to list, web, and item
+import "@pnp/sp/content-types";
+
+// this import statement will only attach content-type functionality to web
+import "@pnp/sp/content-types/web";
+
+If you only need to access content types on the web object you can reduce size by only importing that piece.
+The below example shows the need to import types and module augmentation separately.
+// this will fail
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists";
+
+// do this instead
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { IList } from "@pnp/sp/lists";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const lists = await sp.web.lists();
+
+Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. Both libraries supply an "all" preset that will attach all of the available library functionality.
+++While the presets provided may be useful, we encourage you to look at making your own project presets or custom bundles as a preferred solution. Use of the presets in client-side solutions is not recommended.
+
import "@pnp/sp/presets/all";
+
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+// sp.* will have all of the library functionality bound to it, tree shaking will not work
+const lists = await sp.web.lists();
+
+The graph library contains a single preset, "all" mimicking the v1 structure.
+import "@pnp/graph/presets/all";
+import { graphfi } from "@pnp/graph";
+
+// placeholder for fully configuring the sp interface
+const graph = graphfi();
+
+// graph.* will have all of the library functionality bound to it, tree shaking will not work
+const me = await graph.me();
+
+
+
+
+
+
+
+ Whenever you make a request of the library for data from an object and utilize the select
method to reduce the size of the objects in the payload its preferable in TypeScript to be able to type that returned object. The library provides you a method to do so by using TypeScript's Generics declaration.
By defining the objects type in the <> after the closure of the select method the resulting object is typed.
+ .select("Title")<{Title: string}>()
+
+Below are some examples of typing the return payload:
+ const _sp = spfi().using(SPFx(this.context));
+
+ //Typing the Title property of a field
+ const field = await _sp.site.rootWeb.fields.getById(titleFieldId).select("Title")<{ Title: string }>();
+
+ //Typing the ParentWebUrl property of the selected list.
+ const testList = await _sp.web.lists.getByTitle('MyList').select("ParentWebUrl")<{ ParentWebUrl: string }>();
+
+++ + + + + + +There have been discussions in the past around auto-typing based on select and the expected properties of the return object. We haven't done so for a few reasons: there is no even mildly complex way to account for all the possibilities expand introduces to selects, and if we "ignore" expand it effectively makes the select typings back to "any". Looking at template types etc, we haven't yet seen a way to do this that makes it worth the effort and doesn't introduce some other limitation or confusion.
+
With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.
+We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts:
+import { getRandomString } from "@pnp/core";
+import { testSettings } from "../main";
+import { expect } from "chai";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+import { IList } from "@pnp/sp/lists";
+
+describe("Items", () => {
+
+ // any tests that make a web request should be withing a block checking if web tests are enabled
+ if (testSettings.enableWebTests) {
+
+ // a block scoped var we will use across our tests
+ let list: IList = null;
+
+ // we use the before block to setup
+ // executed before all the tests in this block, see the mocha docs for more details
+ // mocha prefers using function vs arrow functions and this is recommended
+ before(async function () {
+
+ // execute a request to ensure we have a list
+ const ler = await sp.web.lists.ensure("ItemTestList", "Used to test item operations");
+ list = ler.list;
+
+ // in this case we want to have some items in the list for testing so we add those
+ // only if the list was just created
+ if (ler.created) {
+
+ // add a few items to get started
+ const batch = sp.web.createBatch();
+ list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+ list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+ list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+ list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+ list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+ await batch.execute();
+ }
+ });
+
+ // this test has a label "get items" and is run via an async function
+ it("get items", async function () {
+
+ // make a request for the list's items
+ const items = await list.items();
+
+ // report that we expect that result to be an array with more than 0 items
+ expect(items.length).to.be.gt(0);
+ });
+
+ // ... remainder of code removed
+ }
+}
+
+Now that you've written tests to cover your changes you'll need to update the docs.
+ + + + + + +Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on.
+Before proceeding be sure you have reviewed how to setup for local configuration and debugging.
+The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.
+You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules.
+All of the setup for the node client is handled within sp.ts using the settings from the local configuration.
+Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit.
+All of the setup for the node client is handled within graph.ts using the settings from the local configuration.
+If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git.
+Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content:
+// note we can use the actual package names for our imports (ex: @pnp/logging)
+import { Logger, LogLevel, ConsoleListener } from "@pnp/logging";
+// using the all preset for simplicity in the example, selective imports work as expected
+import { sp, ListEnsureResult } from "@pnp/sp/presets/all";
+
+declare var process: { exit(code?: number): void };
+
+export async function MyDebug() {
+
+ // configure your options
+ // you can have different configs in different modules as needed for your testing/dev work
+ sp.setup({
+ sp: {
+ fetchClientFactory: () => {
+ return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret);
+ },
+ },
+ });
+
+ // run some debugging
+ const list = await sp.web.lists.ensure("MyFirstList");
+
+ Logger.log({
+ data: list.created,
+ level: LogLevel.Info,
+ message: "Was list created?",
+ });
+
+ if (list.created) {
+
+ Logger.log({
+ data: list.data,
+ level: LogLevel.Info,
+ message: "Raw data from list creation.",
+ });
+
+ } else {
+
+ Logger.log({
+ data: null,
+ level: LogLevel.Info,
+ message: "List already existed!",
+ });
+ }
+
+ process.exit(0);
+}
+
+First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this:
+// ...
+
+// comment out the example
+// import { Example } from "./example";
+// Example();
+
+import { MyDebug } from "./mydebug"
+MyDebug();
+
+// ...
+
+++Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)
+
Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.
+Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.
+You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js
, allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.
This will serve a package with ./debug/serve/main.ts as the entry.
+npm run serve
Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
+<script src="https://localhost:8080/assets/pnp.js"></script>
+<div id="pnp-test"></div>
+
+You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.
+Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.
+You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser.
+Now you can learn about extending the library.
+ + + + + + +Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.
+Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.
+Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.
+When executing the pip module on Windows you can prefix it with python -m. +For example:
+python -m pip install mkdocs-material
mkdocs serve
http://127.0.0.1:8000/
++Please see the official mkdocs site for more details on working with mkdocs
+
After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request!
+ + + + + + +++This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property.
+
At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the "webs" property is added to the web class.
+// TypeScript property, returning an interface
+public get webs(): IWebs {
+ // using the Webs factory function and providing "this" as the first parameter
+ return Webs(this);
+}
+
+PnPjs v3 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form.
+// create a constant which is a function of type ISPInvokableFactory having the name Webs
+// this is bound by the generic type param to return an IWebs instance
+// and it will use the _Webs concrete class to form the internal type of the invocable
+export const Webs = spInvokableFactory<IWebs>(_Webs);
+
+The ISPInvokableFactory type looks like:
+export type ISPInvokableFactory<R = any> = (baseUrl: string | ISharePointQueryable, path?: string) => R;
+
+And the matching graph type:
+<R>(f: any): (baseUrl: string | IGraphQueryable, path?: string) => R
+
+The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples.
+import { SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+// create a web from an absolute url
+const web = Web("https://tenant.sharepoint.com").using(SPFx(this.context));
+
+// as an example, create a new web using the first as a base
+// targets: https://tenant.sharepoint.com/sites/dev
+const web2 = Web(web, "sites/dev");
+
+// or you can add any path components you want, here as an example we access the current user property
+const cu = Web(web, "currentuser");
+const currentUserInfo = cu();
+
+Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their "type". It is an important concept when working with the library to always remember we are just building strings.
+Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method
+/*
+The concrete class implementation. This is never exported or shown directly
+to consumers of the library. It is wrapped by the Proxy we do expose.
+
+It extends the _SharePointQueryableInstance class for which there is a matching
+_SharePointQueryableCollection. The generic parameter defines the return type
+of a get operation and the invoked result.
+
+Classes can have methods and properties as normal. This one has a single property as a simple example
+*/
+export class _HubSite extends _SharePointQueryableInstance<IHubSiteInfo> {
+
+ /**
+ * Gets the ISite instance associated with this hub site
+ */
+ // the tag decorator is used to provide some additional telemetry on what methods are
+ // being called.
+ @tag("hs.getSite")
+ public async getSite(): Promise<ISite> {
+
+ // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result
+ const d = await this.select("SiteUrl")();
+
+ // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl
+ return Site(d.SiteUrl);
+ }
+}
+
+/*
+This defines the interface we export and expose to consumers.
+In most cases this extends the concrete object but may add or remove some methods/properties
+in special cases
+*/
+export interface IHubSite extends _HubSite { }
+
+/*
+This defines the HubSite factory function as discussed above
+binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite.
+
+This is understood to mean that HubSite is a factory function that returns a types of IHubSite
+which the spInvokableFactory will create using _HubSite as the concrete underlying type.
+*/
+export const HubSite = spInvokableFactory<IHubSite>(_HubSite);
+
+In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class.
+export class _View extends _SharePointQueryableInstance<IViewInfo> {
+
+ // ... other code removed
+
+ // add the property, and provide a return type
+ // return types should be interfaces
+ public get fields(): IViewFields {
+ // we use the ViewFields factory function supplying "this" as the first parameter
+ // this will create a url like ".../fields/viewfields" due to the defaultPath decorator
+ // on the _ViewFields class. This is equivalent to: ViewFields(this, "viewfields")
+ return ViewFields(this);
+ }
+
+ // ... other code removed
+}
+
+++There are many examples throughout the library that follow this pattern.
+
Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method:
+@defaultPath("items")
+export class _Items extends _SharePointQueryableCollection {
+
+ /**
+ * Gets an Item by id
+ *
+ * @param id The integer id of the item to retrieve
+ */
+ // we declare a method and set the return type to an interface
+ public getById(id: number): IItem {
+ // here we use the tag helper to add some telemetry to our request
+ // we create a new IItem using the factory and appending the id value to the end
+ // this gives us a valid url path to a single item .../items/getById(2)
+ // we can then use the returned IItem to extend our chain or execute a request
+ return tag.configure(Item(this).concat(`(${id})`), "is.getById");
+ }
+
+ // ... other code removed
+}
+
+A second example is a method that performs a request. Here we use the _Item recycle method as an example:
+/**
+ * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item.
+ */
+// we use the tag decorator to add telemetry
+@tag("i.recycle")
+// we return a promise
+public recycle(): Promise<string> {
+ // we use the spPost method to post the request created by cloning our current instance IItem using
+ // the Item factory and adding the path "recycle" to the end. Url will look like .../items/getById(2)/recycle
+ return spPost<string>(Item(this, "recycle"));
+}
+
+To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available.
+// import the addProp helper
+import { addProp } from "@pnp/queryable";
+// import the _List concrete class from the types module (not the index!)
+import { _List } from "../lists/types";
+// import the interface and factory we are going to add to the List
+import { Items, IItems } from "./types";
+
+// This module declaration fixes up the types, allowing .items to appear in intellisense
+// when you import "@pnp/sp/items/list";
+declare module "../lists/types" {
+ // we need to extend the concrete type
+ interface _List {
+ readonly items: IItems;
+ }
+ // we need to extend the interface
+ // this may not be strictly necessary as the IList interface extends _List so it
+ // should pick up the same additions, but we have seen in some cases this does seem
+ // to be required. So we include it for safety as it will all be removed during
+ // transpilation we don't need to care about the extra code
+ interface IList {
+ readonly items: IItems;
+ }
+}
+
+// finally we add the property to the _List class
+// this method call says add a property to _List named "items" and that property returns a result using the Items factory
+// The factory will be called with "this" when the property is accessed. If needed there is a fourth parameter to append additional path
+// information to the property url
+addProp(_List, "items", Items);
+
+Now that you have extended the library you need to write a test to cover it!
+ + + + + + +Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality.
+Section | +Description | +
---|---|
NPM Scripts | +Explains the npm scripts and their uses | +
Setup Dev Machine | +Covers setting up your machine to ensure you are ready to debug the solution | +
Local Debug Configuration | +Discusses the steps required to establish local configuration used for debugging and running tests | +
Debugging | +Describes how to debug PnPjs locally | +
Extending the library | +Basic examples on how to extend the library such as adding a method or property | +
Writing Tests | +How to write and debug tests | +
Update Documentation | +Describes the steps required to edit and locally view the documentation | +
Submit a Pull Request | +Outlines guidance for submitting a pull request | +
The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.
+Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.
+To learn more and register for an upcoming session, please visit the Sharing is Caring website.
+ + + + + + +This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).
+Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js.
+For more information the settings file please see Settings
You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag.
+The following configuration file allows you to run all the tests that do not contact services.
+ var sets = {
+ testing: {
+ enableWebTests: false,
+ }
+ }
+
+module.exports = sets;
+
+If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.
+ + + + + + +As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies.
+This article outlines the current scripts we've implemented and how to use them, with available options and examples.
+Executes the serve
command
npm start
+
+Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node.
+npm run serve
+
+Runs the tests and coverage for the library.
+More details on setting up MSAL for node.
+There are several options you can provide to the test command. All of these need to be separated using a "--" double hyphen so they are passed to the spawned sub-commands.
++++
--package
or-p
This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory.
+# run only sp tests
+npm test -- -p sp
+
+# run only logging tests
+npm test -- -package logging
+
++++
--single
or--s
You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags.
+# run only sp web tests
+npm test -- -p sp -s web
+
+# run only graph groups tests
+npm test -- -package graph -single groups
+
++++
--site
By default every time you run the tests a new sub-site is created below the site specified in your settings file. You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option.
+This option can be used with any or none of the other testing options.
+# run only sp web tests with a certain site
+npm test -- -p sp -s web --site https://some.site.com/sites/dev
+
++++
--cleanup
If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site
as it will be deleted.
# clean up our testing site
+npm test -- --cleanup
+
++++
--logging
If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output.
+# enable logging during testing
+npm test -- --logging
+
+You can also optionally set a log level of error, warning, info, or verbose:
+# enable logging during testing in verbose (lots of info)
+npm test -- --logging verbose
+
+# enable logging during testing in error
+npm test -- --logging error
+
++++
--spverbose
This flag will enable "verbose" OData mode for SharePoint tests. This flag is compatible with other flags.
+npm test -- --spverbose
+
+Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed.
+npm run build
+
+Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published.
+npm run package
+
+Runs the linter.
+npm run lint
+
+Removes any generated folders from the working directory.
+npm run clean
+
+
+
+
+
+
+
+ Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release.
+npm test
npm run lint
npm run package
++If you need to target a PR for version 1, please target the "version-1" branch
+
The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.
+Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.
+To learn more and register for an upcoming session, please visit the Sharing is Caring website.
+Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted.
+Thank you for helping PnPjs grow and improve!!
+ + + + + + +This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally.
+The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root.
+The settings file is configured with MSAL authentication for both SharePoint and Graph. For more information coinfiguring MSAL please review the section in the authentication section for node.
+MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always "https://{tenant}.sharepoint.com/.default" or "https://graph.microsoft.com/.default" depending on what you are calling.
+++If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated.
+
You will need to create testing certs for the sample settings file below. Using the following code you end up with three files, "cert.pem", "key.pem", and "keytmp.pem". The "cert.pem" file is uploaded to your AAD application registration. The "key.pem" is read as the private key for the configuration. Copy the contents of the "key.pem" file and paste it in the privateKey
variable below. The gitignore
file in this repository will ignore the settings.js file.
++Replace
+HereIsMySuperPass
with your own password
mkdir \temp
+cd \temp
+openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'
+openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass
+
+const privateKey = `-----BEGIN RSA PRIVATE KEY-----
+your private key, read from a file or included here
+-----END RSA PRIVATE KEY-----
+`;
+
+var msalInit = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{tenant id}",
+ clientCertificate: {
+ thumbprint: "{certificate thumbnail}",
+ privateKey: privateKey,
+ },
+ clientId: "{AAD App registration id}",
+ }
+}
+
+export const settings = {
+ testing: {
+ enableWebTests: true,
+ testUser: "i:0#.f|membership|user@consto.com",
+ testGroupId:"{ Microsoft 365 Group ID }",
+ sp: {
+ url: "{required for MSAL - absolute url of test site}",
+ notificationUrl: "{ optional: notification url }",
+ msal: {
+ init: msalInit,
+ scopes: ["https://{tenant}.sharepoint.com/.default"]
+ },
+ },
+ graph: {
+ msal: {
+ init: msalInit,
+ scopes: ["https://graph.microsoft.com/.default"]
+ },
+ },
+ },
+}
+
+
+The settings object has a single sub-object testing
which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below.
+ | + |
---|---|
enableWebTests | +Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. | +
testUser | +AAD login account to be used when running tests. | +
testGroupId | +Group ID of Microsoft 365 Group to be used when running test cases. | +
sp | +Settings used to configure SharePoint (sp library) debugging and tests | +
graph | +Settings used to configure Microsoft Graph (graph library) debugging and tests | +
name | +description | +
---|---|
url | +The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. | +
notificationUrl | +Url used when registering test subscriptions | +
msal | +Information about MSAL authentication setup | +
The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.
+name | +description | +
---|---|
msal | +Information about MSAL authentication setup | +
++ + + + + + +If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.
+
If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging.
+These steps will help you get your environment setup for contributing to the core library.
+Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like.
+Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).
+++This library requires node >= 10.18.0
+
On Windows: Install Python
+[Optional] Install the tslint extension in VS Code:
+All of our contributions come via pull requests and you'll need to fork the repository
+Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.
+Once you have the code locally, navigate to the root of the project in your console. Type the following command:
+npm install
Follow the guidance to complete the one-time local configuration required to debug and run tests.
+Then you can follow the guidance in the debugging article.
+This article contains example recipes for building your own behaviors. We don't want to include every possible behavior within the library, but do want folks to have easy ways to solve the problems they encounter. If have ideas for a missing recipe, please let us know in the issues list OR submit them to this page as a PR! We want to see what types of behaviors folks build and will evaluate options to either include them in the main libraries, leave them here as a reference resource, or possibly release a community behaviors package.
+++Alternatively we encourage you to publish your own behaviors as npm packages to share with others!
+
At times you might need to introduce a proxy for requests for debugging or other networking needs. You can easily do so using your proxy of choice in Nodejs. This example uses "https-proxy-agent" but would work similarly for any implementation.
+proxy.ts
+import { TimelinePipe } from "@pnp/core";
+import { Queryable } from "@pnp/queryable";
+import { HttpsProxyAgent } from "https-proxy-agent";
+
+export function Proxy(proxyInit: string): TimelinePipe<Queryable>;
+// eslint-disable-next-line no-redeclare
+export function Proxy(proxyInit: any): TimelinePipe<Queryable>;
+// eslint-disable-next-line no-redeclare
+export function Proxy(proxyInit: any): TimelinePipe<Queryable> {
+
+ const proxy = typeof proxyInit === "string" ? new HttpsProxyAgent(proxyInit) : proxyInit;
+
+ return (instance: Queryable) => {
+
+ instance.on.pre(async (url, init, result) => {
+
+ // we add the proxy to the request
+ (<any>init).agent = proxy;
+
+ return [url, init, result];
+ });
+
+ return instance;
+ };
+}
+
+usage
+import { Proxy } from "./proxy.ts";
+
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+// would work with graph library in the same manner
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault({
+ msal: {
+ config: { config },
+ scopes: {scopes },
+ },
+}), Proxy("http://127.0.0.1:8888"));
+
+const webInfo = await sp.webs();
+
+In some instances users express a desire to append something to the querystring to avoid getting cached responses back for requests. This pattern is an example of doing that in v3.
+query-cache-param.ts
+export function CacheBust(): TimelinePipe<Queryable> {
+
+ return (instance: Queryable) => {
+
+ instance.on.pre(async (url, init, result) => {
+
+ url += url.indexOf("?") > -1 ? "&" : "?";
+
+ url += "nonce=" + encodeURIComponent(new Date().toISOString());
+
+ return [url, init, result];
+ });
+
+ return instance;
+ };
+}
+
+usage
+import { CacheBust } from "./query-cache-param.ts";
+
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+// would work with graph library in the same manner
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault({
+ msal: {
+ config: { config },
+ scopes: { scopes },
+ },
+}), CacheBust());
+
+const webInfo = await sp.webs();
+
+Starting with v3 we no longer provide support for ACS authentication within the library. However you may have a need (legacy applications, on-premises) to use ACS authentication while wanting to migrate to v3. Below you can find an example implementation of an Authentication observer for ACS. This is not a 100% full implementation, for example the tokens are not cached.
+++Whenever possible we encourage you to use AAD authentication and move away from ACS for securing your server-side applications.
+
export function ACS(clientId: string, clientSecret: string, authUrl = "https://accounts.accesscontrol.windows.net"): (instance: Queryable) => Queryable {
+
+ const SharePointServicePrincipal = "00000003-0000-0ff1-ce00-000000000000";
+
+ async function getRealm(siteUrl: string): Promise<string> {
+
+ const url = combine(siteUrl, "_vti_bin/client.svc");
+
+ const r = await nodeFetch(url, {
+ "headers": {
+ "Authorization": "Bearer ",
+ },
+ "method": "POST",
+ });
+
+ const data: string = r.headers.get("www-authenticate") || "";
+ const index = data.indexOf("Bearer realm=\"");
+ return data.substring(index + 14, index + 50);
+ }
+
+ function getFormattedPrincipal(principalName: string, hostName: string, realm: string): string {
+ let resource = principalName;
+ if (hostName !== null && hostName !== "") {
+ resource += "/" + hostName;
+ }
+ resource += "@" + realm;
+ return resource;
+ }
+
+ async function getFullAuthUrl(realm: string): Promise<string> {
+
+ const url = combine(authUrl, `/metadata/json/1?realm=${realm}`);
+
+ const r = await nodeFetch(url, { method: "GET" });
+ const json: { endpoints: { protocol: string; location: string }[] } = await r.json();
+
+ const eps = json.endpoints.filter(ep => ep.protocol === "OAuth2");
+ if (eps.length > 0) {
+ return eps[0].location;
+ }
+
+ throw Error("Auth URL Endpoint could not be determined from data.");
+ }
+
+ return (instance: Queryable) => {
+
+ instance.on.auth.replace(async (url: URL, init: RequestInit) => {
+
+ const realm = await getRealm(url.toString());
+ const fullAuthUrl = await getFullAuthUrl(realm);
+
+ const resource = getFormattedPrincipal(SharePointServicePrincipal, url.host, realm);
+ const formattedClientId = getFormattedPrincipal(clientId, "", realm);
+
+ const body: string[] = [];
+ body.push("grant_type=client_credentials");
+ body.push(`client_id=${formattedClientId}`);
+ body.push(`client_secret=${encodeURIComponent(clientSecret)}`);
+ body.push(`resource=${resource}`);
+
+ const r = await nodeFetch(fullAuthUrl, {
+ body: body.join("&"),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ method: "POST",
+ });
+
+ const accessToken: { access_token: string } = await r.json();
+
+ init.headers = { ...init.headers, Authorization: `Bearer ${accessToken.access_token}` };
+
+ return [url, init];
+ });
+
+ return instance;
+ };
+}
+
+usage
+import { CacheBust } from "./acs-auth-behavior.ts";
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault(), ACS("{client id}", "{client secret}"));
+
+// you can optionally provide the authentication url, here using the one for China's sovereign cloud or an local url if working on-premises
+// const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault(), ACS("{client id}", "{client secret}", "https://accounts.accesscontrol.chinacloudapi.cn"));
+
+const webInfo = await sp.webs();
+
+
+
+
+
+
+
+ While you can always register observers to any Timeline's moments using the .on.moment
syntax, to make things easier we have included the ability to create behaviors. Behaviors define one or more observer registrations abstracted into a single registration. To differentiate behaviors are applied with the .using
method. The power of behaviors is they are composable so a behavior can apply other behaviors.
Let's create a behavior that will register two observers to a Timeline. We'll use error and log since they exist on all Timelines. In this example let's imagine we need to include some special secret into every lifecycle for logging to work. And we also want a company wide method to track errors. So we roll our own behavior.
+import { Timeline, TimelinePipe } from "@pnp/core";
+import { MySpecialLoggingFunction } from "../mylogging.js";
+
+// top level function allows binding of values within the closure
+export function MyBehavior(specialSecret: string): TimelinePipe {
+
+ // returns the actual behavior function that is applied to the instance
+ return (instance: Timeline<any>) => {
+
+ // register as many observers as needed
+ instance.on.log(function (message: string, severity: number) {
+
+ MySpecialLoggingFunction(message, severity, specialSecret);
+ });
+
+ instance.on.error(function (err: string | Error) {
+
+ MySpecialLoggingFunction(typeof err === "string" ? err : err.toString(), severity, specialSecret);
+ });
+
+ return instance;
+ };
+}
+
+// apply the behavior to a Timeline/Queryable
+obj.using(MyBehavior("HereIsMySuperSecretValue"));
+
+We encourage you to use our defaults, or create your own default behavior appropriate to your needs. You can see all of the behaviors available in @pnp/nodejs, @pnp/queryable, @pnp/sp, and @pnp/graph.
+As an example, let's create our own behavior for a nodejs project. We want to call the graph, default to the beta endpoint, setup MSAL, and include a custom header we need for our environment. To do so we create a composed behavior consisting of graph's DefaultInit, graph's DefaultHeaders, nodejs's MSAL, nodejs's NodeFetchWithRetry, and queryable's DefaultParse & InjectHeaders. Then we can import this behavior into all our projects to configure them.
+company-default.ts
+import { TimelinePipe } from "@pnp/core";
+import { DefaultParse, Queryable, InjectHeaders } from "@pnp/queryable";
+import { DefaultHeaders, DefaultInit } from "@pnp/graph";
+import { NodeFetchWithRetry, MSAL } from "@pnp/nodejs";
+
+export function CompanyDefault(): TimelinePipe<Queryable> {
+
+ return (instance: Queryable) => {
+
+ instance.using(
+ // use the default headers
+ DefaultHeaders(),
+ // use the default init, but change the base url to beta
+ DefaultInit("https://graph.microsoft.com/beta"),
+ // use node-fetch with retry
+ NodeFetchWithRetry(),
+ // use the default parsing
+ DefaultParse(),
+ // inject our special header to all requests
+ InjectHeaders({
+ "X-SomeSpecialToken": "{THE SPECIAL TOKEN VALUE}",
+ }),
+ // setup node's MSAL with configuration from the environment (or any source)
+ MSAL(process.env.MSAL_CONFIG));
+
+ return instance;
+ };
+}
+
+index.ts
+import { CompanyDefault } from "./company-default.ts";
+import { graphfi } from "@pnp/graph";
+
+// we can consistently and easily setup our graph instance using a single behavior
+const graph = graphfi().using(CompanyDefault());
+
+++ +You can easily share your composed behaviors across your projects using library components in SPFx, a company CDN, or an npm package.
+
This section describes two behaviors provided by the @pnp/core
library, AssignFrom and CopyFrom. Likely you won't often need them directly - they are used in some places internally - but they are made available should they prove useful.
This behavior creates a ref to the supplied Timeline implementation's observers and resets the inheriting flag. This means that changes to the parent, here being the supplied Timeline, will begin affecting the target to which this behavior is applied.
+import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { AssignFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will now hold a reference to the observers contained in source
+// changes to the subscribed observers in source will apply to target
+// anything that was added by "MyCustomeBehavior" will no longer be present
+target.using(AssignFrom(source.web));
+
+// you can always apply additional behaviors or register directly on the events
+// but once you modify target it will not longer ref source and changes to source will no longer apply
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+Similar to AssignFrom, this method creates a copy of all the observers on the source and applies them to the target. This can be done either as a replace
or append
operation using the second parameter. The default is "append".
on
operation.on
operation++By design CopyFrom does NOT include moments defined by symbol keys.
+
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { CopyFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are maintained as the default behavior is to append
+target.using(CopyFrom(source.web));
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are removed
+target.using(CopyFrom(source.web, "replace"));
+
+// you can always apply additional behaviors or register directly on the events
+// with CopyFrom no reference to source is maintained
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+As well CopyFrom
supports a filter parameter if you only want to copy the observers from a subset of moments. This filter is a predicate function taking a single string key and returning true if the observers from that moment should be copied to the target.
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { CopyFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are maintained as the default behavior is to append
+target.using(CopyFrom(source.web));
+
+// target will have the observers `auth` and `send` copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are removed
+target.using(CopyFrom(source.web, "replace", (k) => /(auth|send)/i.test(k)));
+
+// you can always apply additional behaviors or register directly on the events
+// with CopyFrom no reference to source is maintained
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+
+
+
+
+
+
+ Moments are the name we use to describe the steps executed during a timeline lifecycle. They are defined on a plain object by a series of functions with the general form:
+// the first argument is the set of observers subscribed to the given moment
+// the rest of the args vary by an interaction between moment and observer types and represent the args passed when emit is called for a given moment
+function (observers: any[], ...args: any[]): any;
+
+Let's have a look at one of the included moment factory functions, which define how the moment interacts with its registered observers, and use it to understand a bit more on how things work. In this example we'll look at the broadcast moment, used to mimic a classic event where no return value is tracked, we just want to emit an event to all the subscribed observers.
+// the broadcast factory function, returning the actual moment implementation function
+// The type T is used by the typings of Timeline to described the arguments passed in emit
+export function broadcast<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {
+
+ // this is the actual moment implementation, called each time a given moment occurs in the timeline
+ return function (observers: T[], ...args: any[]): void {
+
+ // we make a local ref of the observers
+ const obs = [...observers];
+
+ // we loop through sending the args to each observer
+ for (let i = 0; i < obs.length; i++) {
+
+ // note that within every moment and observer "this" will be the current timeline object
+ Reflect.apply(obs[i], this, args);
+ }
+ };
+}
+
+Let's use broadcast
in a couple examples to show how it works. You can also review the timeline article for a fuller example.
// our first type determines the type of the observers that will be regsitered to the moment "first"
+type Broadcast1ObserverType = (this: Timeline<any>, message: string) => void;
+
+// our second type determines the type of the observers that will be regsitered to the moment "second"
+type Broadcast2ObserverType = (this: Timeline<any>, value: number, value2: number) => void;
+
+const moments = {
+ first: broadcast<Broadcast1ObserverType>(),
+ second: broadcast<Broadcast2ObserverType>(),
+} as const;
+
+Now that we have defined two moments we can update our Timeline implementing class to emit each as we desire, as covered in the timeline article. Let's focus on the relationship between the moment definition and the typings inherited by on
and emit
in Timeline.
Because we want observers of a given moment to understand what arguments they will get the typings of Timeline are setup to use the type defining the moment's observer across all operations. For example, using our moment "first" from above. Each moment can be subscribed by zero or more observers.
+// our observer function matches the type of Broadcast1ObserverType and the intellisense will reflect that.
+// If you want to change the signature you need only do so in the type Broadcast1ObserverType and the change will update the on and emit typings as well
+// here we want to reference "this" inside our observer function (preferred)
+obj.on.first(function (this: Timeline<any>, message: string) {
+ // we use "this", which will be the current timeline and the default log method to emit a logging event
+ this.log(message, 0);
+});
+
+// we don't need to reference "this" so we use arrow notation
+obj.on.first((message: string) => {
+ console.log(message);
+});
+
+Similarily for second
our observers would match Broadcast2Observer.
obj.on.second(function (this: Timeline<any>, value: number, value2: number) {
+ // we use "this", which will be the current timeline and the default log method to emit a logging event
+ this.log(`got value1: ${value} value2: ${value2}`, 0);
+});
+
+obj.on.second((value: number, value2: number) => {
+ console.log(`got value1: ${value} value2: ${value2}`);
+});
+
+You a already familiar with broadcast
which passes the emited args to all subscribed observers, this section lists the existing built in moment factories:
Creates a moment that passes the emited args to all subscribed observers. Takes a single type parameter defining the observer signature and always returns void. Is not async.
+import { broadcast } from "@pnp/core";
+
+// can have any method signature you want that returns void, "this" will always be set
+type BroadcastObserver = (this: Timeline<any>, message: string) => void;
+
+const moments = {
+ example: broadcast<BroadcastObserver>(),
+} as const;
+
+obj.on.example(function (this: Timeline<any>, message: string) {
+ this.log(message, 0);
+});
+
+obj.emit.example("Hello");
+
+Creates a moment that executes each observer asynchronously, awaiting the result and passes the returned arguments as the arguments to the next observer. This is very much like the redux pattern taking the arguments as the state which each observer may modify then returning a new state.
+import { asyncReduce } from "@pnp/core";
+
+// can have any method signature you want, so long as it is async and returns a tuple matching in order the arguments, "this" will always be set
+type AsyncReduceObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<[string, number]>;
+
+const moments = {
+ example: asyncReduce<AsyncReduceObserver>(),
+} as const;
+
+obj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {
+
+ this.log(message, 0);
+
+ // we can manipulate the values
+ arg2++;
+
+ // always return a tuple of the passed arguments, possibly modified
+ return [arg1, arg2];
+});
+
+obj.emit.example("Hello", 42);
+
+Creates a moment where the first registered observer is used to asynchronously execute a request, returning a single result. If no result is returned (undefined) no further action is taken and the result will be undefined (i.e. additional observers are not used).
+This is used by us to execute web requets, but would also serve to represent any async request such as a database read, file read, or provisioning step.
+import { request } from "@pnp/core";
+
+// can have any method signature you want, "this" will always be set
+type RequestObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<string>;
+
+const moments = {
+ example: request<RequestObserver>(),
+} as const;
+
+obj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {
+
+ this.log(`Sending request: ${arg1}`, 0);
+
+ // request expects a single value result
+ return `result value ${arg2}`;
+});
+
+obj.emit.example("Hello", 42);
+
+Perhaps you have a situation where you would like to wait until all of the subscribed observers for a given moment complete, but they can run async in parallel.
+export function waitall<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<void> {
+
+ // this is the actual moment implementation, called each time a given moment occurs in the timeline
+ return function (observers: T[], ...args: any[]): void {
+
+ // we make a local ref of the observers
+ const obs = [...observers];
+
+ const promises = [];
+
+ // we loop through sending the args to each observer
+ for (let i = 0; i < obs.length; i++) {
+
+ // note that within every moment and observer "this" will be the current timeline object
+ promises.push(Reflect.apply(obs[i], this, args));
+ }
+
+ return Promise.all(promises).then(() => void(0));
+ };
+}
+
+Perhaps you would instead like to only get the result of the first observer to return.
+export function first<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<any> {
+
+ // this is the actual moment implementation, called each time a given moment occurs in the timeline
+ return function (observers: T[], ...args: any[]): void {
+
+ // we make a local ref of the observers
+ const obs = [...observers];
+
+ const promises = [];
+
+ // we loop through sending the args to each observer
+ for (let i = 0; i < obs.length; i++) {
+
+ // note that within every moment and observer "this" will be the current timeline object
+ promises.push(Reflect.apply(obs[i], this, args));
+ }
+
+ return Promise.race(promises);
+ };
+}
+
+
+
+
+
+
+
+ Observers are used to implement all of the functionality within a Timeline's moments. Each moment defines the signature of observers you can register, and calling the observers is orchestrated by the implementation of the moment. A few facts about observers:
+error
moment.++For details on implementing observers for Queryable, please see this article.
+
Timelines created from other timelines (i.e. how sp and graph libraries work) inherit all of the observers from the parent. Observers added to the parent will apply for all children.
+When you make a change to the set of observers through any of the subscription methods outlined below that inheritance is broken. Meaning changes to the parent will no longer apply to that child, and changes to a child never affect a parent. This applies to ALL moments on change of ANY moment, there is no per-moment inheritance concept.
+const sp = new spfi().using(...lots of behaviors);
+
+// web is current inheriting all observers from "sp"
+const web = sp.web;
+
+// at this point web no longer inherits from "sp" and has its own observers
+// but still includes everything that was registered in sp before this call
+web.on.log(...);
+
+// web2 inherits from sp as each invocation of .web creates a fresh IWeb instance
+const web2 = sp.web;
+
+// list inherits from web's observers and will contain the extra `log` observer added above
+const list = web.lists.getById("");
+
+// this new behavior will apply to web2 and any subsequent objects created from sp
+sp.using(AnotherBehavior());
+
+// web will again inherit from sp through web2, the extra log handler is gone
+// list now ALSO is reinheriting from sp as it was pointing to web
+web.using(AssignFrom(web2));
+// see below for more information on AssignFrom
+
+All timeline moments are exposed through the on
property with three options for subscription.
This is the default, and adds your observer to the end of the array of subscribed observers.
+obj.on.log(function(this: Queryable, message: string, level: number) {
+ if (level > 1) {
+ console.log(message);
+ }
+});
+
+Using prepend will place your observer as the first item in the array of subscribed observers. There is no gaurantee it will always remain first, other code can also use prepend.
+obj.on.log.prepend(function(this: Queryable, message: string, level: number) {
+ if (level > 1) {
+ console.log(message);
+ }
+});
+
+Replace will remove all other subscribed observers from a moment and add the supplied observer as the only one in the array of subscribed observers.
+obj.on.log.replace(function(this: Queryable, message: string, level: number) {
+ if (level > 1) {
+ console.log(message);
+ }
+});
+
+The ToArray method creates a cloned copy of the array of registered observers for a given moment. Note that because it is a clone changes to the returned array do not affect the registered observers.
+const arr = obj.on.log.toArray();
+
+This clears ALL observers for a given moment, returning true if any observers were removed, and false if no changes were made.
+const didChange = obj.on.log.clear();
+
+The core library includes two special behaviors used to help manage observer inheritance. The best case is to manage inheritance using the methods described above, but these provide quick shorthand to help in certain scenarios. These are AssignFrom and CopyFrom.
+ + + + + + +This module provides a thin wrapper over the browser local and session storage. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.
+The main export of this module, contains properties representing local and session storage.
+import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+const myvalue = storage.local.get("mykey");
+
+Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.
+import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// get a value from storage
+const value = storage.local.get("mykey");
+
+// put a value into storage
+storage.local.put("mykey2", "my value");
+
+// put a value into storage with an expiration
+storage.local.put("mykey2", "my value", new Date());
+
+// put a simple object into storage
+// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects
+storage.local.put("mykey3", {
+ key: "value",
+ key2: "value2",
+});
+
+// remove a value from storage
+storage.local.delete("mykey3");
+
+// get an item or add it if it does not exist
+// returns a promise in case you need time to get the value for storage
+// optionally takes a third parameter specifying the expiration
+storage.local.getOrPut("mykey4", () => {
+ return Promise.resolve("value");
+});
+
+// delete expired items
+storage.local.deleteExpired();
+
+The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished by explicitly calling the deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.
+import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// session storage
+storage.session.deleteExpired();
+
+// local storage
+storage.local.deleteExpired();
+
+// this returns a promise, so you can perform some activity after the expired items are removed:
+storage.local.deleteExpired().then(_ => {
+ // init my application
+});
+
+In previous versions we included code to automatically remove expired items. Due to a lack of necessity we removed that, but you can recreate the concept as shown below:
+function expirer(timeout = 3000) {
+
+ // session storage
+ storage.session.deleteExpired();
+
+ // local storage
+ storage.local.deleteExpired();
+
+ setTimeout(() => expirer(timeout), timeout);
+}
+
+
+
+
+
+
+
+ Timeline provides base functionality for ochestrating async operations. A timeline defines a set of moments to which observers can be registered. Observers are functions that can act independently or together during a moment in the timeline. The model is event-like but each moment's implementation can be unique in how it interacts with the registered observers. Keep reading under Define Moments to understand more about what a moment is and how to create one.
+ +The easiest way to understand Timeline is to walk through implementing a simple one below. You also review Queryable to see how we use Timeline internally to the library.
+Implementing a timeline involves several steps, each explained below.
+A timeline is made up of a set of moments which are themselves defined by a plain object with one or more properties, each of which is a function. You can use predefined moments, or create your own to meet your exact requirements. Below we define two moments within the MyMoments
object, first and second. These names are entirely your choice and the order moments are defined in the plain object carries no meaning.
The first
moment uses a pre-defined moment implementation asyncReduce
. This moment allows you to define a state based on the arguments of the observer function, in this case FirstObserver
. asyncReduce
takes those arguments, does some processing, and returns a promise resolving an array matching the input arguments in order and type with optionally changed values. Those values become the arguments to the next observer registered to that moment.
import { asyncReduce, ObserverAction, Timeline } from "@pnp/core";
+
+// the first observer is a function taking a number and async returning a number in an array
+// all asyncReduce observers must follow this pattern of returning async a tuple matching the args
+export type FirstObserver = (this: any, counter: number) => Promise<[number]>;
+
+// the second observer is a function taking a number and returning void
+export type SecondObserver = (this: any, result: number) => void;
+
+// this is a custom moment definition as an example.
+export function report<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {
+
+ return function (observers: T[], ...args: any[]): void {
+
+ const obs = [...observers];
+
+ // for this
+ if (obs.length > 0) {
+ Reflect.apply(obs[0], this, args);
+ }
+ };
+}
+
+// this plain object defines the moments which will be available in our timeline
+// the property name "first" and "second" will be the moment names, used when we make calls such as instance.on.first and instance.on.second
+const TestingMoments = {
+ first: asyncReduce<FirstObserver>(),
+ second: report<SecondObserver>(),
+} as const;
+// note as well the use of as const, this allows TypeScript to properly resolve all the complex typings and not treat the plain object as "any"
+
+After defining our moments we need to subclass Timeline to define how those moments emit through the lifecycle of the Timeline. Timeline has a single abstract method "execute" you must implement. You will also need to provide a way for callers to trigger the protected "start" method.
+// our implementation of timeline, note we use `typeof TestingMoments` and ALSO pass the testing moments object to super() in the constructor
+class TestTimeline extends Timeline<typeof TestingMoments> {
+
+ // we create two unique refs for our implementation we will use
+ // to resolve the execute promise
+ private InternalResolveEvent = Symbol.for("Resolve");
+ private InternalRejectEvent = Symbol.for("Reject");
+
+ constructor() {
+ // we need to pass the moments to the base Timeline
+ super(TestingMoments);
+ }
+
+ // we implement the execute the method to define when, in what order, and how our moments are called. This give you full control within the Timeline framework
+ // to determine your implementation's behavior
+ protected async execute(init?: any): Promise<any> {
+
+ // we can always emit log to any subscribers
+ this.log("Starting", 0);
+
+ // set our timeline to start in the next tick
+ setTimeout(async () => {
+
+ try {
+
+ // we emit our "first" event
+ let [value] = await this.emit.first(init);
+
+ // we emit our "second" event
+ [value] = await this.emit.second(value);
+
+ // we reolve the execute promise with the final value
+ this.emit[this.InternalResolveEvent](value);
+
+ } catch (e) {
+
+ // we emit our reject event
+ this.emit[this.InternalRejectEvent](e);
+ // we emit error to any subscribed observers
+ this.error(e);
+ }
+ }, 0);
+
+ // return a promise which we will resolve/reject during the timeline lifecycle
+ return new Promise((resolve, reject) => {
+ this.on[this.InternalResolveEvent].replace(resolve);
+ this.on[this.InternalRejectEvent].replace(reject);
+ });
+ }
+
+ // provide a method to trigger our timeline, this could be protected or called directly by the user, your choice
+ public go(startValue = 0): Promise<number> {
+
+ // here we take a starting number
+ return this.start(startValue);
+ }
+}
+
+import { TestTimeline } from "./file.js";
+
+const tl = new TestTimeline();
+
+// register observer
+tl.on.first(async (n) => [++n]);
+
+// register observer
+tl.on.second(async (n) => [++n]);
+
+// h === 2
+const h = await tl.go(0);
+
+// h === 7
+const h2 = await tl.go(5);
+
+Now that you implemented a simple timeline let's take a minute to understand the lifecycle of a timeline execution. There are four moments always defined for every timeline: init, dispose, log, and error. Of these init and dispose are used within the lifecycle, while log and error are used as you need.
+As well the moments log and error exist on every Timeline derived class and can occur at any point during the lifecycle.
+Let's say that you want to contruct a system whereby you can create Timeline based instances from other Timeline based instances - which is what Queryable does. Imagine we have a class with a pseudo-signature like:
+class ExampleTimeline extends Timeline<typeof SomeMoments> {
+
+ // we create two unique refs for our implementation we will use
+ // to resolve the execute promise
+ private InternalResolveEvent = Symbol.for("Resolve");
+ private InternalRejectEvent = Symbol.for("Reject");
+
+ constructor(base: ATimeline) {
+
+ // we need to pass the moments to the base Timeline
+ super(TestingMoments, base.observers);
+ }
+
+ //...
+}
+
+We can then use it like:
+const tl1 = new ExampleTimeline();
+tl1.on.first(async (n) => [++n]);
+tl1.on.second(async (n) => [++n]);
+
+// at this point tl2's observer collection is a pointer to the same collection as tl1
+const tl2 = new ExampleTimeline(tl1);
+
+// we add a second observer to first, it is applied to BOTH tl1 and tl2
+tl1.on.first(async (n) => [++n]);
+
+// BUT when we modify tl2's observers, either by adding or clearing a moment it begins to track its own collection
+tl2.on.first(async (n) => [++n]);
+
+
+
+
+
+
+
+ This module contains utility methods that you can import individually from the core library.
+Combines any number of paths, normalizing the slashes as required
+import { combine } from "@pnp/core";
+
+// "https://microsoft.com/something/more"
+const paths = combine("https://microsoft.com", "something", "more");
+
+// "also/works/with/relative"
+const paths2 = combine("/also/", "/works", "with/", "/relative\\");
+
+Manipulates a date, please see the Stack Overflow discussion from which this method was taken.
+import { dateAdd } from "@pnp/core";
+
+const now = new Date();
+
+const newData = dateAdd(now, "minute", 10);
+
+Creates a random guid, please see the Stack Overflow discussion from which this method was taken.
+import { getGUID } from "@pnp/core";
+
+const newGUID = getGUID();
+
+Gets a random string containing the number of characters specified.
+import { getRandomString } from "@pnp/core";
+
+const randomString = getRandomString(10);
+
+Shortcut for Object.hasOwnProperty. Determines if an object has a specified property.
+import { HttpRequestError } from "@pnp/queryable";
+import { hOP } from "@pnp/core";
+
+export async function handleError(e: Error | HttpRequestError): Promise<void> {
+
+ //Checks to see if the error object has a property called isHttpRequestError. Returns a bool.
+ if (hOP(e, "isHttpRequestError")) {
+ // Handle this type or error
+ } else {
+ // not an HttpRequestError so we do something else
+
+ }
+}
+
+Shorthand for JSON.stringify
+import { jsS } from "@pnp/core";
+
+const s: string = jsS({ hello: "world" });
+
+Determines if a supplied variable represents an array.
+import { isArray } from "@pnp/core";
+
+const x = [1, 2, 3];
+
+if (isArray(x)){
+ console.log("I am an array");
+} else {
+ console.log("I am not an array");
+}
+
+Determines if a supplied variable represents a function.
+import { isFunc } from "@pnp/core";
+
+public testFunction() {
+ console.log("test function");
+ return
+}
+
+if (isFunc(testFunction)){
+ console.log("this is a function");
+ testFunction();
+}
+
+Determines if a supplied url is absolute, returning true; otherwise returns false.
+import { isUrlAbsolute } from "@pnp/core";
+
+const webPath = 'https://{tenant}.sharepoint.com/sites/dev/';
+
+if (isUrlAbsolute(webPath)){
+ console.log("URL is absolute");
+}else{
+ console.log("URL is not absolute");
+}
+
+Determines if an object is defined and not null.
+import { objectDefinedNotNull } from "@pnp/core";
+
+const obj = {
+ prop: 1
+};
+
+if (objectDefinedNotNull(obj)){
+ console.log("Not null");
+} else {
+ console.log("Null");
+}
+
+Determines if a supplied string is null or empty.
+import { stringIsNullOrEmpty } from "@pnp/core";
+
+const x: string = "hello";
+
+if (stringIsNullOrEmpty(x)){
+ console.log("Null or empty");
+} else {
+ console.log("Not null or empty");
+}
+
+Gets a (mostly) unique hashcode for a specified string.
+++Taken from: https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
+
import { getHashCode } from "@pnp/core";
+
+const x: string = "hello";
+
+const hash = getHashCode(x);
+
+Provides an awaitable delay specified in milliseconds.
+import { delay } from "@pnp/core";
+
+// wait 1 second
+await delay(1000);
+
+// wait 10 second
+await delay(10000);
+
+
+
+
+
+
+
+ This library is geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.
+If you need to support older browsers, SharePoint on-premisis servers, or older versions of the SharePoint Framework, please revert to version 2 of the library and see related documentation on polyfills for required functionality.
+- NodeJs: >= 14
+- TypeScript: 4.x
+- Node Modules Supported: ESM Only
+
+First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. @pnp/sp
to access the SharePoint REST API and @pnp/graph
to access some of the Microsoft Graph API. This step applies to any environment or project.
npm install @pnp/sp @pnp/graph --save
Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples.
+import { getRandomString } from "@pnp/core";
+
+(function() {
+
+ // get and log a random string
+ console.log(getRandomString(20));
+
+})()
+
+The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises you will need to use version 2 of the library. If you are targeting SharePoint online you will need to take the additional steps outlined below based on the version of the SharePoint Framework you are targeting.
+We've created two Getting Started samples. The first uses the more traditional React Component classes and can be found in the react-pnp-js-sample project, utilizing SPFx 1.15.2 and PnPjs V3, it showcases some of the more dramatic changes to the library. There is also a companion video series on YouTube if you prefer to see things done through that medium here's a link to the playlist for the 5 part series:
+Getting started with PnPjs 3.0: 5-part series
+In addition, we have converted the sample project from React Component to React Hooks. This version can be found in react-pnp-js-hooks. This sample will help those struggling to establish context correctly while using the hooks conventions.
+The SharePoint Framework supports different versions of TypeScript natively and as of 1.14 release still doesn't natively support TypeScript 4.x. Sadly, this means that to use Version 3 of PnPjs you will need to take a few additional configuration steps to get them to work together.
+No additional steps required
+Update the rush stack compiler to 4.2. This is covered in this great article by Elio, but the steps are listed below.
+npm uninstall @microsoft/rush-stack-compiler-3.?
npm i @microsoft/rush-stack-compiler-4.2
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json"
Replace the contents of the gulpfile.js with: + >Note: The only change is the addition of the line to disable tslint.
+```js +'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;
+
+};
+// * ADDED * +// disable tslint +build.tslintCmd.enabled = false; +// * ADDED *
+build.initialize(require('gulp')); +```
+At this time there is no documented method to use version 3.x with SPFx versions earlier than 1.12.1. We recommend that you fall back to using version 2 of the library or update your SPFx version.
+Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. To establish context within the library you will need to use the SharePoint or Graph Factory Interface depending on which set of APIs you want to utilize. For SharePoint you will use the spfi
interface and for the Microsoft Graph you will use the graphfi
interface whic are both in the main export of the corresponding package. Examples of both methods are shown below.
Depending on how you architect your solution establishing context is done where you want to make calls to the API. The examples demonstrate doing so in the onInit method as a local variable but this could also be done to a private variable or passed into a service.
+++Note if you are going to use both the @pnp/sp and @pnp/graph packages in SPFx you will need to alias the SPFx behavior import, please see the section below for more details.
+
spfi
factory interface in SPFx¶import { spfi, SPFx } from "@pnp/sp";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+ await super.onInit();
+ const sp = spfi().using(SPFx(this.context));
+
+}
+
+// ...
+
+
+graphfi
factory interface in SPFx¶import { graphfi, SPFx } from "@pnp/graph";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+ await super.onInit();
+ const graph = graphfi().using(SPFx(this.context));
+
+}
+
+// ...
+
+
+
+import { spfi, SPFx as spSPFx } from "@pnp/sp";
+import { graphfi, SPFx as graphSPFx} from "@pnp/graph";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+ await super.onInit();
+ const sp = spfi().using(spSPFx(this.context));
+ const graph = graphfi().using(graphSPFx(this.context));
+
+}
+
+// ...
+
+
+Please see the documentation on setting up a config file or a services for more information about establishing and instance of the spfi or graphfi interfaces that can be reused. It is a common mistake with users of V3 that they try and create the interface in event handlers which causes issues.
+++Due to the way in which Node resolves ESM modules when you use selective imports in node you must include the
+index.js
part of the path. Meaning an import likeimport "@pnp/sp/webs"
in examples must beimport "@pnp/sp/webs/index.js"
. Root level imports such asimport { spfi } from "@pnp/sp"
remain correct. The samples in this section demonstrate this for their selective imports.
++Note that the NodeJS integration relies on code in the module
+@pnp/nodejs
. It is therefore required that you import this near the beginning of your program, using simply+
js +import "@pnp/nodejs";
To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below.
+mkdir \temp
+cd \temp
+openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'
+openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass
+
+++Using the above code you end up with three files, "cert.pem", "key.pem", and "keytmp.pem". The "cert.pem" file is uploaded to your AAD application registration. The "key.pem" is read as the private key for the configuration.
+
spfi
factory interface in NodeJS¶++Version 3 of this library only supports ESModules. If you still require commonjs modules please check out version 2.
+
The first step is to install the packages that will be needed. You can read more about what each package does starting on the packages page.
+npm i @pnp/sp @pnp/nodejs
+
+Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:
+
+import { SPDefault } from "@pnp/nodejs";
+import "@pnp/sp/webs/index.js";
+import { readFileSync } from 'fs';
+import { Configuration } from "@azure/msal-node";
+
+function() {
+ // configure your node options (only once in your application)
+ const buffer = readFileSync("c:/temp/key.pem");
+
+ const config: Configuration = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{tenant id or common}/",
+ clientId: "{application (client) i}",
+ clientCertificate: {
+ thumbprint: "{certificate thumbprint, displayed in AAD}",
+ privateKey: buffer.toString(),
+ },
+ },
+ };
+
+ const sp = spfi().using(SPDefault({
+ baseUrl: 'https://{my tenant}.sharepoint.com/sites/dev/',
+ msal: {
+ config: config,
+ scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]
+ }
+ }));
+
+ // make a call to SharePoint and log it in the console
+ const w = await sp.web.select("Title", "Description")();
+ console.log(JSON.stringify(w, null, 4));
+}();
+
+graphfi
factory interface in NodeJS¶Similar to the above you can also make calls to the Microsoft Graph API from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example.
+npm i @pnp/graph @pnp/nodejs
+
+Now we need to import what we'll need to call graph
+import { graphfi } from "@pnp/graph";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users/index.js";
+
+function() {
+ const graph = graphfi().using(GraphDefault({
+ baseUrl: 'https://graph.microsoft.com',
+ msal: {
+ config: config,
+ scopes: [ 'https://graph.microsoft.com/.default' ]
+ }
+ }));
+ // make a call to Graph and get all the groups
+ const userInfo = await graph.users.top(1)();
+ console.log(JSON.stringify(userInfo, null, 4));
+}();
+
+For TypeScript projects which output commonjs but need to import esm modules you will need to take a few additional steps to use the pnp esm modules. This is true of any esm module with a project structured in this way, not specific to PnP's implementation. It is very possible there are other configurations that make this work, but these steps worked in our testing. We have also provided a basic sample showing this setup.
+You must install TypeScript @next or you will get errors using node12 module resolution. This may change but is the current behavior when we did our testing.
+npm install -D typescript@next
The tsconfig file for your project should have the "module": "CommonJS"
and "moduleResolution": "node12",
settings in addition to whatever else you need.
tsconfig.json
+{
+ "compilerOptions": {
+ "module": "CommonJS",
+ "moduleResolution": "node12"
+}
+
+You must then import the esm dependencies using the async import pattern. This works as expected with our selective imports, and vscode will pick up the intellisense as expected.
+index.ts
+import { settings } from "./settings.js";
+
+// this is a simple example as async await is not supported with commonjs output
+// at the root.
+setTimeout(async () => {
+
+ const { spfi } = await import("@pnp/sp");
+ const { SPDefault } = await import("@pnp/nodejs");
+ await import("@pnp/sp/webs/index.js");
+
+ const sp = spfi().using(SPDefault({
+ baseUrl: settings.testing.sp.url,
+ msal: {
+ config: settings.testing.sp.msal.init,
+ scopes: settings.testing.sp.msal.scopes
+ }
+ }));
+
+ // make a call to SharePoint and log it in the console
+ const w = await sp.web.select("Title", "Description")();
+ console.log(JSON.stringify(w, null, 4));
+
+}, 0);
+
+Finally, when launching node you need to include the `` flag with a setting of 'node'.
+node --experimental-specifier-resolution=node dist/index.js
++Read more in the releated TypeScript Issue, TS pull request Adding the functionality, and the TS Docs.
+
In some cases you may be working in a client-side application that doesn't have context to the SharePoint site. In that case you will need to utilize the MSAL Client, you can get the details on creating that connection in this article.
+This library has a lot of functionality and you may not need all of it. For that reason, we support selective imports which allow you to only import the parts of the sp or graph library you need, which reduces your overall solution bundle size - and enables treeshaking.
+You can read more about selective imports.
+This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.
+Because of the way the fluent library is designed by definition it's extendible. That means that if you want to build your own custom functions that extend the features of the library this can be done fairly simply. To get more information about creating your own custom extensions check out extending the library article.
+The new factory function allows you to create a connection to a different web maintaining the same setup as your existing interface. You have two options, either to 'AssignFrom' or 'CopyFrom' the base timeline's observers. The below example utilizes 'AssignFrom' but the method would be the same regadless of which route you choose. For more information on these behaviors see Core/Behaviors.
+import { spfi, SPFx } from "@pnp/sp";
+import { AssignFrom } from "@pnp/core";
+import "@pnp/sp/webs";
+
+//Connection to the current context's Web
+const sp = spfi(...);
+
+// Option 1: Create a new instance of Queryable
+const spWebB = spfi({Other Web URL}).using(SPFx(this.context));
+
+// Option 2: Copy/Assign a new instance of Queryable using the existing
+const spWebB = spfi({Other Web URL}).using(AssignFrom(sp.web));
+
+// Option 3: Create a new instance of Queryable using other credentials?
+const spWebB = spfi({Other Web URL}).using(SPFx(this.context));
+
+// Option 4: Create new Web instance by using copying SPQuerable and new pointing to new web url (e.g. https://contoso.sharepoint.com/sites/Web2)
+const web = Web([sp.web, {Other Web URL}]);
+
+For more complicated authentication scnearios please review the article describing all of the available authentication methods.
+ + + + + + +The article describes the behaviors exported by the @pnp/graph
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/nodejs.
The DefaultInit
behavior, itself a composed behavior includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit and ensures the request url is absolute.
import { graphfi, DefaultInit } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultInit());
+
+await graph.users();
+
+The DefaultHeaders
behavior uses InjectHeaders to set the Content-Type header.
import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+await graph.users();
+
+++DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.
+
Added in 3.4.0
+The Paged behavior allows you to access the information in a collection through a series of pages. While you can use it directly, you will likely use the paged
method of the collections which handles things for you.
++Note that not all entity types support
+count
and where it is unsupported it will return 0.
Basic example, read all users:
+import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allUsers = [];
+let users = await graph.users.top(300).paged();
+
+allUsers.push(...users.value);
+
+while (users.hasNext) {
+ users = await users.next();
+ allUsers.push(...users.value);
+}
+
+console.log(`All users: ${JSON.stringify(allUsers)}`);
+
+Beyond the basics other query operations are supported such as filter and select.
+import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allUsers = [];
+let users = await graph.users.top(50).select("userPrincipalName", "displayName").filter("startswith(displayName, 'A')").paged();
+
+allUsers.push(...users.value);
+
+while (users.hasNext) {
+ users = await users.next();
+ allUsers.push(...users.value);
+}
+
+console.log(`All users: ${JSON.stringify(allUsers)}`);
+
+And similarly for groups, showing the same pattern for different types of collections
+import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allGroups = [];
+let groups = await graph.groups.paged();
+
+allGroups.push(...groups.value);
+
+while (groups.hasNext) {
+ groups = await groups.next();
+ allGroups.push(...groups.value);
+}
+
+console.log(`All groups: ${JSON.stringify(allGroups)}`);
+
+This behavior is used to change the endpoint to which requests are made, either "beta" or "v1.0". This allows you to easily switch back and forth between the endpoints as needed.
+import { graphfi, Endpoint } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const beta = graphfi().using(Endpoint("beta"));
+
+const vOne = graphfi().using(Endpoint("v1.0"));
+
+await beta.users();
+
+await vOne.users();
+
+It can also be used at any point in the fluid chain to switch an isolated request to a different endpoint.
+import { graphfi, Endpoint } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// will point to v1 by default
+const graph = graphfi().using();
+
+const user = graph.users.getById("{id}");
+
+// this only applies to the "user" instance now
+const userInfoFromBeta = user.using(Endpoint("beta"))();
+
+Finally, if you always want to make your requests to the beta end point (as an example) it is more efficient to set it in the graphfi factory.
+import { graphfi } from "@pnp/graph";
+
+const beta = graphfi("https://graph.microsoft.com/beta");
+
+A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.
+The baseUrl prop can be used to configure the graph endpoint to which requests will be sent.
+++If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.
+
import { graphfi, GraphBrowser } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphBrowser());
+
+await graph.users();
+
+You can also set a baseUrl. This is equivelent to calling graphfi with an absolute url.
+import { graphfi, GraphBrowser } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphBrowser({ baseUrl: "https://graph.microsoft.com/v1.0" }));
+
+// this is the same as the above, and maybe a litter easier to read, and is more efficient
+// const graph = graphfi("https://graph.microsoft.com/v1.0").using(GraphBrowser());
+
+await graph.users();
+
+This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx
is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. It also replaces any authentication present with a method to get a token from the SPFx aadTokenProviderFactory.
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi(...).using(SPFx(this.context));
+
+await graph.users();
+
+Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.
+import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'
+import { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'
+
+const sp = spfi().using(spSPFx(this.context));
+const graph = graphfi().using(graphSPFx(this.context));
+
+If you want to use a different form of authentication you can apply that behavior after SPFx
to override it. In this case we are using the client MSAL authentication.
Added in 3.12
+Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi(...).using(SPFxToken(this.context));
+
+await graph.users();
+
+import { graphfi } from "@pnp/graph";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi().using(SPFx(this.context), MSAL({ /* proper MSAL settings */}));
+
+await graph.users();
+
+This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.
+++You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.
+
import { graphfi, Telemetry } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(Telemetry());
+
+await graph.users();
+
+Using this behavior you can set the consistency level of your requests. You likely won't need to use this directly as we include it where needed.
+Basic usage:
+import { graphfi, ConsistencyLevel } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(ConsistencyLevel());
+
+await graph.users();
+
+If in the future there is another value other than "eventual" you can supply it to the behavior. For now only "eventual" is a valid value, which is the default, so you do not need to pass it as a param.
+import { graphfi, ConsistencyLevel } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(ConsistencyLevel("{level value}"));
+
+await graph.users();
+
+
+
+
+
+
+
+ Represents the Bookings services available to a user.
+You can learn more by reading the Official Microsoft Graph Documentation.
+Get the supported currencies
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+
+const graph = graphfi(...);
+
+// Get all the currencies
+const currencies = await graph.bookingCurrencies();
+// get the details of the first currency
+const currency = await graph.bookingCurrencies.getById(currencies[0].id)();
+
+Get the bookings businesses
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+
+const graph = graphfi(...);
+
+// Get all the businesses
+const businesses = await graph.bookingBusinesses();
+// get the details of the first business
+const business = graph.bookingBusinesses.getById(businesses[0].id)();
+const businessDetails = await business();
+// get the business calendar
+const calView = await business.calendarView("2022-06-01", "2022-08-01")();
+// publish the business
+await business.publish();
+// unpublish the business
+await business.unpublish();
+
+Get the bookings business services
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingService } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business services
+const services = await business.services();
+// add a service
+const newServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};
+const newService = services.add(newServiceDesc);
+// get service by id
+const service = await business.services.getById({service id})();
+// update service
+const updateServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};
+const update = await business.services.getById({service id}).update(updateServiceDesc);
+// delete service
+await business.services.getById({service id}).delete();
+
+Get the bookings business customers
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingCustomer } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business customers
+const customers = await business.customers();
+// add a customer
+const newCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};
+const newCustomer = customers.add(newCustomerDesc);
+// get customer by id
+const customer = await business.customers.getById({customer id})();
+// update customer
+const updateCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};
+const update = await business.customers.getById({customer id}).update(updateCustomerDesc);
+// delete customer
+await business.customers.getById({customer id}).delete();
+
+Get the bookings business staffmembers
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingStaffMember } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business staff members
+const staffmembers = await business.staffMembers();
+// add a staff member
+const newStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};
+const newStaffMember = staffmembers.add(newStaffMemberDesc);
+// get staff member by id
+const staffmember = await business.staffMembers.getById({staff member id})();
+// update staff member
+const updateStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};
+const update = await business.staffMembers.getById({staff member id}).update(updateStaffMemberDesc);
+// delete staffmember
+await business.staffMembers.getById({staff member id}).delete();
+
+Get the bookings business appointments
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingAppointment } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business appointments
+const appointments = await business.appointments();
+// add a appointment
+const newAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};
+const newAppointment = appointments.add(newAppointmentDesc);
+// get appointment by id
+const appointment = await business.appointments.getById({appointment id})();
+// cancel the appointment
+await appointment.cancel();
+// update appointment
+const updateAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};
+const update = await business.appointments.getById({appointment id}).update(updateAppointmentDesc);
+// delete appointment
+await business.appointments.getById({appointment id}).delete();
+
+Get the bookings business custom questions
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingCustomQuestion } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business custom questions
+const customQuestions = await business.customQuestions();
+// add a custom question
+const newCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};
+const newCustomQuestion = customQuestions.add(newCustomQuestionDesc);
+// get custom question by id
+const customquestion = await business.customQuestions.getById({customquestion id})();
+// update custom question
+const updateCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};
+const update = await business.customQuestions.getById({custom question id}).update(updateCustomQuestionDesc);
+// delete custom question
+await business.customQuestions.getById({customquestion id}).delete();
+
+
+
+
+
+
+
+ More information can be found in the official Graph documentation:
+ +import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars();
+
+const myCalendars = await graph.me.calendars();
+
+
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
+
+const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)();
+
+const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();
+
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar();
+
+const myCalendar = await graph.me.calendar();
+
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+// You can get the default calendar events
+const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events();
+// or get all events for the user
+const events = await graph.users.getById('user@tenant.onmicrosoft.com').events();
+
+// You can get my default calendar events
+const events = await graph.me.calendar.events();
+// or get all events for me
+const events = await graph.me.events();
+
+You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar.
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA==';
+
+const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
+
+// Get events by ID
+const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID);
+
+const events = await graph.me.events.getByID(EventID);
+
+// Get an event by ID from a specific calendar
+const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID);
+
+const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);
+
+
+This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add(
+{
+ "subject": "Let's go for lunch",
+ "body": {
+ "contentType": "HTML",
+ "content": "Does late morning work for you?"
+ },
+ "start": {
+ "dateTime": "2017-04-15T12:00:00",
+ "timeZone": "Pacific Standard Time"
+ },
+ "end": {
+ "dateTime": "2017-04-15T14:00:00",
+ "timeZone": "Pacific Standard Time"
+ },
+ "location":{
+ "displayName":"Harry's Bar"
+ },
+ "attendees": [
+ {
+ "emailAddress": {
+ "address":"samanthab@contoso.onmicrosoft.com",
+ "name": "Samantha Booth"
+ },
+ "type": "required"
+ }
+ ]
+});
+
+This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
+
+await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({
+ reminderMinutesBeforeStart: 99,
+});
+
+This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
+
+await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete();
+
+await graph.me.events.getById(EVENT_ID).delete();
+
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/groups';
+
+const graph = graph.using(SPFx(this.context));
+
+const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();
+
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/groups';
+
+const graph = graphfi(...);
+
+// You can do one of
+const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events();
+// or
+const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();
+
+Gets the events in a calendar during a specified date range.
+import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+// basic request, note need to invoke the returned queryable
+const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01")();
+
+// you can use select, top, etc to filter your returned results
+const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01").select("subject").top(3)();
+
+// you can specify times along with the dates
+const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01T19:00:00-08:00", "2020-03-01T19:00:00-08:00")();
+
+const view4 = await graph.me.calendarView("2020-01-01", "2020-03-01")();
+
+Gets the emailAddress
objects that represent all the meeting rooms in the user's tenant or in a specific room list.
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+// basic request, note need to invoke the returned queryable
+const rooms1 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms()();
+// you can pass a room list to filter results
+const rooms2 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms('roomlist@tenant.onmicrosoft.com')();
+// you can use select, top, etc to filter your returned results
+const rooms3 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms().select('name').top(10)();
+
+Get the instances (occurrences) of an event for a specified time range.
+If the event is a seriesMaster
type, this returns the occurrences and exceptions of the event in the specified time range.
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+const event = graph.me.events.getById('');
+// basic request, note need to invoke the returned queryable
+const instances = await event.instances("2020-01-01", "2020-03-01")();
+// you can use select, top, etc to filter your returned results
+const instances2 = await event.instances("2020-01-01", "2020-03-01").select("subject").top(3)();
+// you can specify times along with the dates
+const instance3 = await event.instances("2020-01-01T19:00:00-08:00", "2020-03-01T19:00:00-08:00")();
+
+
+
+
+
+
+
+ The ability to retrieve information about a user's presence, including their availability and user activity.
+More information can be found in the official Graph documentation:
+ +Gets a list of all the contacts for the user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/cloud-communications";
+
+const graph = graphfi(...);
+
+const presenceMe = await graph.me.presence();
+
+const presenceThem = await graph.users.getById("99999999-9999-9999-9999-999999999999").presence();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/cloud-communications";
+
+const graph = graphfi(...);
+
+const presenceList = await graph.communications.getPresencesByUserId(["99999999-9999-9999-9999-999999999999"]);
+
+
+
+
+
+
+
+
+ More information can be found in the official Graph documentation:
+ + +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteColumns = await graph.site.getById("{site identifier}").columns();
+const listColumns = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns();
+const contentTypeColumns = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.getById("{column identifier}")();
+const listColumn = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns.getById("{column identifier}")();
+const contentTypeColumn = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns.getById("{column identifier}")();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const sampleColumn: ColumnDefinition = {
+ description: "PnPTestColumn Description",
+ enforceUniqueValues: false,
+ hidden: false,
+ indexed: false,
+ name: "PnPTestColumn",
+ displayName: "PnPTestColumn",
+ text: {
+ allowMultipleLines: false,
+ appendChangesToExistingText: false,
+ linesForEditing: 0,
+ maxLength: 255,
+ },
+};
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.add(sampleColumn);
+const listColumn = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns.add(sampleColumn);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for content types
+import "@pnp/graph/content-ypes";
+
+const graph = graphfi(...);
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.getById("{column identifier}")();
+const contentTypeColumn = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns.addRef(siteColumn);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const updatedSiteColumn = await site.columns.getById("{column identifier}").update({ displayName: "New Name" });
+const updateListColumn = await site.lists.getById("{list identifier}").columns.getById("{column identifier}").update({ displayName: "New Name" });
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const siteColumn = await site.columns.getById("{column identifier}").delete();
+const listColumn = await site.lists.getById("{list identifier}").columns.getById("{column identifier}").delete();
+const contentTypeColumn = await site.contentTypes.getById("{content type identifier}").columns.getById("{column identifier}").delete();
+
+
+
+
+
+
+
+ The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graphfi(). Through the methods described +you can add and edit both contacts and folders in a users Outlook.
+More information can be found in the official Graph documentation:
+ +To make user calls you can use getById where the id is the users email address. +Contact ID, Folder ID, and Parent Folder ID use the following format "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA="
+Gets a list of all the contacts for the user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users"
+import "@pnp/graph/contacts"
+
+const graph = graphfi(...);
+
+const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts();
+
+const contacts2 = await graph.me.contacts();
+
+
+Gets a specific contact by ID for the user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)();
+
+const contact2 = await graph.me.contacts.getById(contactID)();
+
+
+Adds a new contact for the user.
+import { graphfi } from "@pnp/graph";
+import { EmailAddress } from "@microsoft/microsoft-graph-types";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+
+Updates a specific contact by ID for teh designated user
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: "1986-05-30" });
+
+const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: "1986-05-30" });
+
+
+Delete a contact from the list of contacts for a user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete();
+
+const delContact2 = await graph.me.contacts.getById(contactID).delete();
+
+
+Get all the folders for the designated user's contacts
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders();
+
+const contactFolders2 = await graph.me.contactFolders();
+
+
+Get a contact folder by ID for the specified user
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)();
+
+const contactFolder2 = await graph.me.contactFolders.getById(folderID)();
+
+
+Add a new folder in the users contacts
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const parentFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=";
+
+const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add("New Folder", parentFolderID);
+
+const addedContactFolder2 = await graph.me.contactFolders.add("New Folder", parentFolderID);
+
+
+Update an existing folder in the users contacts
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: "Updated Folder" });
+
+const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: "Updated Folder" });
+
+
+Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete();
+
+const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();
+
+
+Get all the contacts in a folder
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts();
+
+const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();
+
+
+Get child folders from contact folder
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders();
+
+const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();
+
+
+Add a new child folder to a contact folder
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
+
+const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
+
+Get child folder by ID from user contacts
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
+
+const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)();
+
+const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();
+
+Add a new contact to a child folder
+import { graphfi } from "@pnp/graph";
+import { EmailAddress } from "./@microsoft/microsoft-graph-types";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+
+
+
+
+
+
+
+ More information can be found in the official Graph documentation:
+ + +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentTypes = await graph.site.getById("{site identifier}").contentTypes();
+const listContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}")();
+const listContentType = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getById("{content type identifier}")();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const sampleContentType: ContentType = {
+ name: "PnPTestContentType",
+ description: "PnPTestContentType Description",
+ base: {
+ name: "Item",
+ id: "0x01",
+ },
+ group: "PnPTest Content Types",
+ id: "0x0100CDB27E23CEF44850904C80BD666FA645",
+};
+
+const siteContentType = await graph.sites.getById("{site identifier}").contentTypes.add(sampleContentType);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/lists";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+//Get a list of compatible site content types for the list
+const siteContentType = await graph.site.getById("{site identifier}").getApplicableContentTypesForList("{list identifier}")();
+//Get a specific content type from the site.
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}")();
+const listContentType = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.addCopy(siteContentType);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const updatedSiteContentType = await site.contentTypes.getById("{content type identifier}").update({ description: "New Description" });
+const updateListContentType = await site.lists.getById("{list identifier}").contentTypes.getById("{content type identifier}").update({ description: "New Description" });
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").delete();
+await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getById("{content type identifier}").delete();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentTypes = await graph.site.getById("{site identifier}").contentTypes.getCompatibleHubContentTypes();
+const listContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getCompatibleHubContentTypes();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const hubSiteContentTypes = await graph.site.getById("{site identifier}").contentTypes.getCompatibleHubContentTypes();
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.addCopyFromContentTypeHub(hubSiteContentTypes[0].Id);
+
+const hubListContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getCompatibleHubContentTypes();
+const listContentType = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.addCopyFromContentTypeHub(hubListContentTypes[0].Id);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteContentType = graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}");
+const isPublished = await siteContentType.isPublished();
+await siteContentType.publish();
+await siteContentType.unpublish();;
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const hubSiteUrls: string[] = [hubSiteUrl1, hubSiteUrl2, hubSiteUrl3];
+const propagateToExistingLists = true;
+// NOTE: the site must be the content type hub
+const contentTypeHub = graph.site.getById("{content type hub site identifier}");
+const siteContentType = await contentTypeHub.contentTypes.getById("{content type identifier}").associateWithHubSites(hubSiteUrls, propagateToExistingLists);
+
+++Not fully implemented, requires Files support
+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+// Not fully implemented
+const sourceFile: ItemReference = {};
+const destinationFileName: string = "NewFileName";
+
+const site = graph.site.getById("{site identifier}");
+const siteContentType = await site.contentTypes.getById("{content type identifier}").copyToDefaultContentLocation(sourceFile, destinationFileName);
+
+
+
+
+
+
+
+ Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types.
+More information can be found in the official Graph documentation:
+ +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf();
+
+const memberOf2 = await graph.me.memberOf();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups();
+
+const memberGroups2 = await graph.me.getMemberGroups();
+
+// Returns only security enabled groups
+const memberGroups3 = await graph.me.getMemberGroups(true);
+
+const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects();
+
+const memberObjects2 = await graph.me.getMemberObjects();
+
+// Returns only security enabled groups
+const memberObjects3 = await graph.me.getMemberObjects(true);
+
+const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
+
+And returns from that list those groups of which the specified user, group, or directory object is a member
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers2 = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/directory-objects";
+
+const graph = graphfi(...);
+
+const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/directory-objects";
+
+const graph = graphfi(...);
+
+const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()
+
+
+
+
+
+
+
+
+ Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent.
+Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups.
+You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation.
+Add a new group.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import { GroupType } from '@pnp/graph/groups';
+
+const graph = graphfi(...);
+
+const groupAddResult = await graph.groups.add("GroupName", "Mail_NickName", GroupType.Office365);
+const group = await groupAddResult.group();
+
+Deletes an existing group.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").delete();
+
+Updates an existing group.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").update({ displayName: newName, propertyName: updatedValue});
+
+Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").addFavorite();
+
+Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").removeFavorite();
+
+Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").resetUnseenCount();
+
+Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").subscribeByMail();
+
+Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").unsubscribeByMail();
+
+Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const startDate = new Date("2020-04-01");
+const endDate = new Date("2020-03-01");
+
+const events = graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").getCalendarView(startDate, endDate);
+
+See Photos
+Get the members and/or owners of a group.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/members";
+
+const graph = graphfi(...);
+const members = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").members();
+const owners = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").owners();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites/group";
+
+const graph = graphfi(...);
+
+const teamSite = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").sites.root();
+const url = teamSite.webUrl
+
+
+
+
+
+
+
+ This module helps you get Insights in form of Trending, Used and Shared. The results are based on relationships calculated using advanced analytics and machine learning techniques.
+Returns documents from OneDrive and SharePoint sites trending around a user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const trending = await graph.me.insights.trending()
+
+const trending = await graph.users.getById("userId").insights.trending()
+
+Using the getById method to get a trending document by Id.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const trendingDoc = await graph.me.insights.trending.getById('Id')()
+
+const trendingDoc = await graph.users.getById("userId").insights.trending.getById('Id')()
+
+Using the resources method to get the resource from a trending document.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.trending.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.trending.getById('Id').resource()
+
+Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const used = await graph.me.insights.used()
+
+const used = await graph.users.getById("userId").insights.used()
+
+Using the getById method to get a used document by Id.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const usedDoc = await graph.me.insights.used.getById('Id')()
+
+const usedDoc = await graph.users.getById("userId").insights.used.getById('Id')()
+
+Using the resources method to get the resource from a used document.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.used.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.used.getById('Id').resource()
+
+Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const shared = await graph.me.insights.shared()
+
+const shared = await graph.users.getById("userId").insights.shared()
+
+Using the getById method to get a shared document by Id.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const sharedDoc = await graph.me.insights.shared.getById('Id')()
+
+const sharedDoc = await graph.users.getById("userId").insights.shared.getById('Id')()
+
+Using the resources method to get the resource from a shared document.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.shared.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.shared.getById('Id').resource()
+
+
+
+
+
+
+
+ The ability invite an external user via the invitation manager
+Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/invitations";
+
+const graph = graphfi(...);
+
+const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');
+
+
+
+
+
+
+
+
+ Currently, there is no module in graph to access all items directly. Please, instead, default to search by path using the following methods.
+ +import { Site } from "@pnp/graph/sites";
+
+const sites = graph.sites.getById("{site id}");
+
+const items = await Site(sites, "lists/{listid}/items")();
+
+import { Site } from "@pnp/graph/sites";
+
+const sites = graph.sites.getById("{site id}");
+
+const users = await Site(sites, "lists/{listid}/items/{item id}/versions")();
+
+import { Site } from "@pnp/graph/sites";
+import "@pnp/graph/lists";
+
+const sites = graph.sites.getById("{site id}");
+
+const listItems : IList[] = await Site(sites, "lists/{site id}/items?$expand=fields")();
+
+More information can be found in the official Graph documentation:
+ + +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteLists = await graph.site.getById("{site identifier}").lists();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const listInfo = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}")();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const sampleList: List = {
+ displayName: "PnPGraphTestList",
+ list: { "template": "genericList" },
+};
+
+const list = await graph.sites.getById("{site identifier}").lists.add(listTemplate);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const list = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").update({ displayName: "MyNewListName" });
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").delete();
+
+For more information about working please see documentation on columns
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+import "@pnp/graph/columns";
+
+const graph = graphfi(...);
+
+await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").columns();
+
+Currently, recieving list items via @pnpjs/graph API is not possible.
+This can currently be done with a call by path as documented under @pnpjs/graph/items
+ + + + + + +More information can be found in the official Graph documentation:
+ + +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/messages";
+
+const graph = graphfi(...);
+
+const currentUser = graph.me;
+const messages = await currentUser.messages();
+
+
+
+
+
+
+
+ The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive.
+Using the drive you can get the users default drive from Onedrive, or the groups or sites default document library.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const otherUserDrive = await graph.users.getById("user@tenant.onmicrosoft.com").drive();
+
+const currentUserDrive = await graph.me.drive();
+
+const groupDrive = await graph.groups.getById("{group identifier}").drive();
+
+const siteDrive = await graph.sites.getById("{site identifier}").drive();
+
+Using the drives() you can get the users available drives from Onedrive
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const otherUserDrive = await graph.users.getById("user@tenant.onmicrosoft.com").drives();
+
+const currentUserDrive = await graph.me.drives();
+
+const groupDrives = await graph.groups.getById("{group identifier}").drives();
+
+const siteDrives = await graph.sites.getById("{site identifier}").drives();
+
+
+Using the drives.getById() you can get one of the available drives in Outlook
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const drive = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}")();
+
+const drive = await graph.me.drives.getById("{drive id}")();
+
+const drive = await graph.drives.getById("{drive id}")();
+
+
+Using the list() you get the associated list information
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const list = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").list();
+
+const list = await graph.me.drives.getById("{drive id}").list();
+
+
+Using the getList(), from the lists implementation, you get the associated IList object. +Form more infomration about acting on the IList object see @pnpjs/graph/lists
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const listObject: IList = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").getList();
+
+const listOBject: IList = await graph.me.drives.getById("{drive id}").getList();
+
+const list = await listObject();
+
+Using the recent() you get the recent files
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const files = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").recent();
+
+const files = await graph.me.drives.getById("{drive id}").recent();
+
+
+Using the sharedWithMe() you get the files shared with the user
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const shared = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").sharedWithMe();
+
+const shared = await graph.me.drives.getById("{drive id}").sharedWithMe();
+
+// By default, sharedWithMe return items shared within your own tenant. To include items shared from external tenants include the options object.
+
+const options: ISharingWithMeOptions = {allowExternal: true};
+const shared = await graph.me.drives.getById("{drive id}").sharedWithMe(options);
+
+
+List the items that have been followed by the signed in user.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const files = await graph.me.drives.getById("{drive id}").following();
+
+
+Using the root() you get the root folder
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/sites";
+import "@pnp/graph/groups";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const root = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root();
+const root = await graph.users.getById("user@tenant.onmicrosoft.com").drive.root();
+
+const root = await graph.me.drives.getById("{drive id}").root();
+const root = await graph.me.drive.root();
+
+const root = await graph.sites.getById("{site id}").drives.getById("{drive id}").root();
+const root = await graph.sites.getById("{site id}").drive.root();
+
+const root = await graph.groups.getById("{site id}").drives.getById("{drive id}").root();
+const root = await graph.groups.getById("{site id}").drive.root();
+
+
+Using the children() you get the children
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const rootChildren = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children();
+
+const rootChildren = await graph.me.drives.getById("{drive id}").root.children();
+
+const itemChildren = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").children();
+
+const itemChildren = await graph.me.drives.getById("{drive id}").root.items.getById("{item id}").children();
+
+
+Using the drive.getItemsByPath() you can get the contents of a particular folder path
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getItemsByPath("MyFolder/MySubFolder")();
+
+const item = await graph.me.drives.getItemsByPath("MyFolder/MySubFolder")();
+
+
+Using the add you can add an item, for more options please user the upload method instead.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users";
+import {IDriveItemAddResult} from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const add1: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children.add("test.txt", "My File Content String");
+const add2: IDriveItemAddResult = await graph.me.drives.getById("{drive id}").root.children.add("filename.txt", "My File Content String");
+
+Using the .upload method you can add or update the content of an item.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users";
+import {IFileOptions, IDriveItemAddResult} from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// file path is only file name
+const fileOptions: IFileOptions = {
+ content: "This is some test content",
+ filePathName: "pnpTest.txt",
+ contentType: "text/plain;charset=utf-8"
+}
+
+const uDriveRoot: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drive.root.upload(fileOptions);
+
+const uFolder: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drive.getItemById("{folder id}").upload(fileOptions);
+
+const uDriveIdRoot: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.upload(fileOptions);
+
+// file path includes folders
+const fileOptions2: IFileOptions = {
+ content: "This is some test content",
+ filePathName: "folderA/pnpTest.txt",
+ contentType: "text/plain;charset=utf-8"
+}
+
+const uFileOptions: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.upload(fileOptions2);
+
+Using addFolder you can add a folder
+import { graph } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users"
+import {IDriveItemAddResult} from "@pnp/graph/ondrive";
+
+const graph = graphfi(...);
+
+const addFolder1: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children.addFolder('New Folder');
+const addFolder2: IDriveItemAddResult = await graph.me.drives.getById("{drive id}").root.children.addFolder('New Folder');
+
+
+Using the search() you can search for items, and optionally select properties
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Where searchTerm is the query text used to search for items.
+// Values may be matched across several fields including filename, metadata, and file content.
+
+const search = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.search(searchTerm)();
+
+const search = await graph.me.drives.getById("{drive id}").root.search(searchTerm)();
+
+
+Using the items.getById() you can get a specific item from the current drive
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}")();
+
+const item = await graph.me.drives.getById("{drive id}").items.getById("{item id}")();
+
+
+Using the drive.getItemByPath() you can get a specific item from the current drive
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getItemByPath("MyFolder/MySubFolder/myFile.docx")();
+
+const item = await graph.me.drives.getItemByPath("MyFolder/MySubFolder/myFile.docx")();
+
+
+Using the item.getContent() you can get the content of a file.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+private _readFileAsync(file: Blob): Promise<ArrayBuffer> {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as ArrayBuffer);
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(file);
+ });
+}
+
+// Where itemId is the id of the item
+const fileContents: Blob = await graph.me.drive.getItemById(itemId).getContent();
+const content: ArrayBuffer = await this._readFileAsync(fileContents);
+
+// This is an example of decoding plain text from the ArrayBuffer
+const decoder = new TextDecoder('utf-8');
+const decodedContent = decoder.decode(content);
+
+Using the item.convertContent() you can get a PDF version of the file. See official documentation for supported file types.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+private _readFileAsync(file: Blob): Promise<ArrayBuffer> {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ resolve(reader.result as ArrayBuffer);
+ };
+ reader.onerror = reject;
+ reader.readAsArrayBuffer(file);
+ });
+}
+
+// Where itemId is the id of the item
+const fileContents: Blob = await graph.me.drive.getItemById(itemId).convertContent("pdf");
+const content: ArrayBuffer = await this._readFileAsync(fileContents);
+
+// Further manipulation of the array buffer will be needed based on your requriements.
+
+Using the thumbnails() you get the thumbnails
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const thumbs = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").thumbnails();
+
+const thumbs = await graph.me.drives.getById("{drive id}").items.getById("{item id}").thumbnails();
+
+
+Using the delete() you delete the current item
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const thumbs = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").delete();
+
+const thumbs = await graph.me.drives.getById("{drive id}").items.getById("{item id}").delete();
+
+
+Using the update() you update the current item
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const update = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").update({name: "New Name"});
+
+const update = await graph.me.drives.getById("{drive id}").items.getById("{item id}").update({name: "New Name"});
+
+
+Using the move() you move the current item, and optionally update it
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Requires a parentReference to the destination folder location
+const moveOptions: IItemOptions = {
+ parentReference: {
+ id?: {parentLocationId};
+ driveId?: {parentLocationDriveId}};
+ };
+ name?: {newName};
+};
+
+const move = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").move(moveOptions);
+
+const move = await graph.me.drives.getById("{drive id}").items.getById("{item id}").move(moveOptions);
+
+
+Using the copy() you can copy the current item to a new location, returns the path to the new location
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Requires a parentReference to the destination folder location
+const copyOptions: IItemOptions = {
+ parentReference: {
+ id?: {parentLocationId};
+ driveId?: {parentLocationDriveId}};
+ };
+ name?: {newName};
+};
+
+const copy = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").copy(copyOptions);
+
+const copy = await graph.me.drives.getById("{drive id}").items.getById("{item id}").copy(copyOptions);
+
+
+Using the users default drive you can get special folders, including: Documents, Photos, CameraRoll, AppRoot, Music
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { SpecialFolder, IDriveItem } from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Get the special folder (App Root)
+const driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.AppRoot)();
+
+// Get the special folder (Documents)
+const driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.Documents)();
+
+// ETC
+
+This action allows you to obtain a short-lived embeddable URL for an item in order to render a temporary preview.
+If you want to obtain long-lived embeddable links, use the createLink API instead.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IPreviewOptions, IDriveItemPreviewInfo } from "@pnp/graph/onedrive";
+import { ItemPreviewInfo } from "@microsoft/microsoft-graph-types"
+
+const graph = graphfi(...);
+
+const preview: ItemPreviewInfo = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").preview();
+
+const preview: ItemPreviewInfo = await graph.me.drives.getById("{drive id}").items.getById("{item id}").preview();
+
+const previewOptions: IPreviewOptions = {
+ page: 1,
+ zoom: 90
+}
+
+const preview2: ItemPreviewInfo = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").preview(previewOptions);
+
+
+Track changes in a driveItem and its children over time.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IDeltaItems } from "@pnp/graph/ondrive";
+
+const graph = graphfi(...);
+
+// Get the changes for the drive items from inception
+const delta: IDeltaItems = await graph.me.drive.root.delta()();
+const delta: IDeltaItems = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.delta()();
+
+// Get the changes for the drive items from token
+const delta: IDeltaItems = await graph.me.drive.root.delta("{token}")();
+
+Using the analytics() you get the ItemAnalytics for a DriveItem
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IAnalyticsOptions } from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Defaults to lastSevenDays
+const analytics = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").analytics()();
+
+const analytics = await graph.me.drives.getById("{drive id}").items.getById("{item id}").analytics()();
+
+const analyticOptions: IAnalyticsOptions = {
+ timeRange: "allTime"
+};
+
+const analyticsAllTime = await graph.me.drives.getById("{drive id}").items.getById("{item id}").analytics(analyticOptions)();
+
+
+
+
+
+
+
+ Represents the Outlook services available to a user. Currently, only interacting with categories is supported.
+You can learn more by reading the Official Microsoft Graph Documentation.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories();
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+await graph.me.outlook.masterCategories.add({
+ displayName: 'Newsletters',
+ color: 'preset2'
+});
+// Application permissions
+await graph.users.getById('{user id}').outlook.masterCategories.add({
+ displayName: 'Newsletters',
+ color: 'preset2'
+});
+
+ Testing has shown that displayName
cannot be updated.
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+import { OutlookCategory } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const categoryUpdate: OutlookCategory = {
+ color: "preset5"
+}
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate);
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete();
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();
+
+
+
+
+
+
+
+ A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64.
+You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.
+This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.me.photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.me.photos.getBySize("48x48").getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photos.getBySize("120x120").getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.teams.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.me.photo.setContent(file);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.setContent(file);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.teams.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.setContent(file);
+
+
+
+
+
+
+
+ The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner.
+Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const plan = await graph.planner.plans.getById('planId')();
+
+
+Using the planner.plans.add() you can create a new Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newPlan = await graph.planner.plans.add('groupObjectId', 'title');
+
+
+Using the tasks() you can get the Tasks in a Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planTasks = await graph.planner.plans.getById('planId').tasks();
+
+
+Using the buckets() you can get the Buckets in a Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planBuckets = await graph.planner.plans.getById('planId').buckets();
+
+
+Using the details() you can get the details in a Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planDetails = await graph.planner.plans.getById('planId').details();
+
+
+Using the delete() you can get delete a Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delPlan = await graph.planner.plans.getById('planId').delete('planEtag');
+
+
+Using the update() you can get update a Plan.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});
+
+
+Using the tasks() you can get the Tasks across all plans
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planTasks = await graph.me.tasks()
+
+
+Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const task = await graph.planner.tasks.getById('taskId')();
+
+
+Using the planner.tasks.add() you can create a new Task.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newTask = await graph.planner.tasks.add('planId', 'title');
+
+
+Using the details() you can get the details in a Task.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const taskDetails = await graph.planner.tasks.getById('taskId').details();
+
+
+Using the delete() you can get delete a Task.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');
+
+
+Using the update() you can get update a Task.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});
+
+
+Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const bucket = await graph.planner.buckets.getById('bucketId')();
+
+
+Using the planner.buckets.add() you can create a new Bucket.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newBucket = await graph.planner.buckets.add('name', 'planId');
+
+
+Using the update() you can get update a Bucket.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name", eTag:'bucketEtag'});
+
+
+Using the delete() you can get delete a Bucket.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');
+
+
+Using the tasks() you can get Tasks in a Bucket.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();
+
+
+Gets all the plans for a group
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const plans = await graph.groups.getById("b179a282-9f94-4bb5-a395-2a80de5a5a78").plans();
+
+
+
+
+
+
+
+
+ The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below.
+ +This example shows calling the search API via the query
method of the root graph object.
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/search";
+
+const graph = graphfi(...);
+
+const results = await graph.query({
+ entityTypes: ["site"],
+ query: {
+ queryString: "test"
+ },
+});
+
+++ + + + + + +Note: This library allows you to pass multiple search requests to the
+query
method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.
The shares module allows you to access shared files, or any file in the tenant using encoded file urls.
+ +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const shareInfo = await graph.shares.getById("{shareId}")();
+
+If you don't have a share id but have the absolute path to a file you can encode it into a sharing link, allowing you to access it directly using the /shares endpoint.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const shareLink: string = graph.shares.encodeSharingLink("https://{tenant}.sharepoint.com/sites/dev/Shared%20Documents/new.pptx");
+
+const shareInfo = await graph.shares.getById(shareLink)();
+
+You can also access the full functionality of the driveItem via a share. Find more details on the capabilities of driveItem here.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const driveItemInfo = await graph.shares.getById("{shareId}").driveItem();
+
+
+
+
+
+
+
+ The search module allows you to access the Microsoft Graph Sites API.
+ +import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+
+const sitesInfo = await graph.sites();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+
+const siteInfo = await graph.sites.getById("{site identifier}")();
+
+Using the sites.getByUrl() you can get a site using url instead of identifier
+If you get a site with this method, the graph does not support chaining a request further than .drive. We will review and try and create a work around for this issue.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+const sharepointHostName = "contoso.sharepoint.com";
+const serverRelativeUrl = "/sites/teamsite1";
+const siteInfo = await graph.sites.getByUrl(sharepointHostName, serverRelativeUrl)();
+
+We don't currently implement all of the available options in graph for sites, rather focusing on the sp library. While we do accept PRs to add functionality, you can also make calls by path.
+ + + + + + +The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft graph. Currently, subscriptions are enabled for the following resources:
+Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const subscriptions = await graph.subscriptions();
+
+
+Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z");
+
+
+Using the subscriptions.getById() you can get one of the subscriptions
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const subscription = await graph.subscriptions.getById('subscriptionId')();
+
+
+Using the subscriptions.getById().delete() you can remove one of the Subscriptions
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const delSubscription = await graph.subscriptions.getById('subscriptionId').delete();
+
+
+Using the subscriptions.getById().update() you can update one of the Subscriptions
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" });
+
+
+
+
+
+
+
+
+ The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams();
+
+const myJoinedTeams = await graph.me.joinedTeams();
+
+
+Using the teams.getById() you can get a specific Team.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();
+
+The first way to create a new Team and corresponding Group is to first create the group and then create the team. +Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.
+Here we get the group via id and use createTeam
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({
+"memberSettings": {
+ "allowCreateUpdateChannels": true
+},
+"messagingSettings": {
+ "allowUserEditMessages": true,
+"allowUserDeleteMessages": true
+},
+"funSettings": {
+ "allowGiphy": true,
+ "giphyContentRating": "strict"
+}});
+
+The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const team = {
+ "template@odata.bind": "https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
+ "displayName": "PnPJS Test Team",
+ "description": "PnPJS Test Team’s Description",
+ "members": [
+ {
+ "@odata.type": "#microsoft.graph.aadUserConversationMember",
+ "roles": ["owner"],
+ "user@odata.bind": "https://graph.microsoft.com/v1.0/users('{owners user id}')",
+ },
+ ],
+ };
+
+const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team);
+//To check the status of the team creation, call getOperationById for the newly created team.
+const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
+'Cloned','description','apps,tabs,settings,channels,members','public');
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
+'Cloned','description','apps,tabs,settings,channels,members','public');
+const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();
+
+Using the teams.getById() you can get a specific Team.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').primaryChannel();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();
+
+
+import { graphfi } from "@pnp/graph";
+
+const graph = graphfi(...);
+
+const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const chatMessages = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import { ChatMessage } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const message = {
+ "body": {
+ "content": "Hello World"
+ }
+ }
+const chatMessage: ChatMessage = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages.add(message);
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.delete();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').update({
+ displayName: "New tab name"
+});
+
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').delete();
+
+
+Get the members and/or owners of a group.
+See Groups
+ + + + + + +Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services.
+You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const currentUser = await graph.me();
+
+++If you want to get all users you will need to use paging
+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const allUsers = await graph.users();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const matchingUser = await graph.users.getById('jane@contoso.com')();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+await graph.me.memberOf();
+await graph.me.transitiveMemberOf();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+await graph.me.update({
+ displayName: 'John Doe'
+});
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const people = await graph.me.people();
+
+// get the top 3 people
+const people = await graph.me.people.top(3)();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const manager = await graph.me.manager();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const reports = await graph.me.directReports();
+
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const currentUser = await graph.me.photo();
+const specificUser = await graph.users.getById('jane@contoso.com').photo();
+
+See Photos
+See Messages
+See OneDrive
+ + + + + + +PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.
+These articles provide general guidance for working with the libraries. If you are migrating from V2 please review the transition guide.
+ + +Animation of the library in use, note intellisense help in building your queries
+Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.
+The latest published version is .
++ | + | + |
---|---|---|
@pnp/ | ++ | + |
+ | azidjsclient | +Provides an Azure Identity wrapper suitable for use with PnPjs | +
+ | core | +Provides shared functionality across all pnp libraries | +
+ | graph | +Provides a fluent api for working with Microsoft Graph | +
+ | logging | +Light-weight, subscribable logging framework | +
+ | msaljsclient | +Provides an msal wrapper suitable for use with PnPjs | +
+ | nodejs | +Provides functionality enabling the @pnp libraries within nodejs | +
+ | queryable | +Provides shared query functionality and base classes | +
+ | sp | +Provides a fluent api for working with SharePoint REST | +
+ | sp-admin | +Provides a fluent api for working with M365 Tenant admin methods | +
We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!
+Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.
+Please review the CHANGELOG for release details on all library changes.
+This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
+Please use http://aka.ms/community/home for the latest updates around the whole Microsoft 365 and Power Platform Community(PnP) initiative.
+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.
+ + + + + + +The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.
+Install the logging module, it has no other dependencies
+npm install @pnp/logging --save
The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter.
+/**
+ * Interface that defines a log listener
+ *
+ */
+export interface ILogListener {
+ /**
+ * Any associated data that a given logging listener may choose to log or ignore
+ *
+ * @param entry The information to be logged
+ */
+ log(entry: ILogEntry): void;
+}
+
+/**
+ * Interface that defines a log entry
+ *
+ */
+export interface ILogEntry {
+ /**
+ * The main message to be logged
+ */
+ message: string;
+ /**
+ * The level of information this message represents
+ */
+ level: LogLevel;
+ /**
+ * Any associated data that a given logging listener may choose to log or ignore
+ */
+ data?: any;
+}
+
+export const enum LogLevel {
+ Verbose = 0,
+ Info = 1,
+ Warning = 2,
+ Error = 3,
+ Off = 99,
+}
+
+To write information to a logger you can use either write, writeJSON, or log.
+import {
+ Logger,
+ LogLevel
+} from "@pnp/logging";
+
+// write logs a simple string as the message value of the LogEntry
+Logger.write("This is logging a simple string");
+
+// optionally passing a level, default level is Verbose
+Logger.write("This is logging a simple string", LogLevel.Error);
+
+// this will convert the object to a string using JSON.stringify and set the message with the result
+Logger.writeJSON({ name: "value", name2: "value2"});
+
+// optionally passing a level, default level is Verbose
+Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warning);
+
+// specify the entire LogEntry interface using log
+Logger.log({
+ data: { name: "value", name2: "value2"},
+ level: LogLevel.Warning,
+ message: "This is my message"
+});
+
+There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance passed in, the level will be 'Error', and the message will be the Error instance's message property.
+const e = Error("An Error");
+
+Logger.error(e);
+
+By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.
+import {
+ Logger,
+ ConsoleListener,
+ LogLevel
+} from "@pnp/logging";
+
+// subscribe a listener
+Logger.subscribe(ConsoleListener());
+
+// set the active log level
+Logger.activeLogLevel = LogLevel.Info;
+
+There are two listeners included in the library, ConsoleListener and FunctionListener.
+This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.
+Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).
+To add a prefix to all output, supply a string in the constructor:
+import {
+ Logger,
+ ConsoleListener,
+ LogLevel
+} from "@pnp/logging";
+
+const LOG_SOURCE: string = 'MyAwesomeWebPart';
+Logger.subscribe(ConsoleListener(LOG_SOURCE));
+Logger.activeLogLevel = LogLevel.Info;
+
+With the above configuration, Logger.write("My special message");
will be output to the console as:
MyAwesomeWebPart - My special message
+
+You can also specify text color for your messages by supplying an IConsoleListenerColors
object. You can simply specify color
to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color
out and all other log levels will use the default color).
Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.):
+import {
+ Logger,
+ ConsoleListener,
+ LogLevel
+} from "@pnp/logging";
+
+const LOG_SOURCE: string = 'MyAwesomeWebPart';
+Logger.subscribe(ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'}));
+Logger.activeLogLevel = LogLevel.Info;
+
+With the above configuration:
+Logger.write("My special message");
+Logger.write("A warning!", LogLevel.Warning);
+
+Will result in messages that look like this:
+ +Color options:
+color
: Default text color for all logging levels unless they're specifiedverboseColor
: Text color to use for messages with LogLevel.VerboseinfoColor
: Text color to use for messages with LogLevel.InfowarningColor
: Text color to use for messages with LogLevel.WarningerrorColor
: Text color to use for messages with LogLevel.ErrorTo set colors without a prefix, specify either undefined
or an empty string for the first parameter:
Logger.subscribe(ConsoleListener(undefined, {color:'purple'}));
+
+The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.
+import {
+ Logger,
+ FunctionListener,
+ ILogEntry
+} from "@pnp/logging";
+
+let listener = new FunctionListener((entry: ILogEntry) => {
+
+ // pass all logging data to an existing framework
+ MyExistingCompanyLoggingFramework.log(entry.message);
+});
+
+Logger.subscribe(listener);
+
+If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface.
+import {
+ Logger,
+ ILogListener,
+ ILogEntry
+} from "@pnp/logging";
+
+class MyListener implements ILogListener {
+
+ log(entry: ILogEntry): void {
+ // here you would do something with the entry
+ }
+}
+
+Logger.subscribe(new MyListener());
+
+To allow seamless logging with v3 we have introduced the PnPLogging
behavior. It takes a single augument representing the log level of that behavior, allowing you to be very selective in what logging you want to get. As well the log level applied here ignores any global level set with activeLogLevel
on Logger.
import { LogLevel, PnPLogging, Logger, ConsoleListener } from "@pnp/logging";
+import { spfi, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+// subscribe a listener
+Logger.subscribe(ConsoleListener());
+
+// at the root we only want to log errors, which will be sent to all subscribed loggers on Logger
+const sp = spfi().using(SPFx(this.context), PnPLogging(LogLevel.Error));
+
+
+const list = sp.web.lists.getByTitle("My List");
+// use verbose logging with this particular list because you are trying to debug something
+list.using(PnPLogging(LogLevel.Verbose));
+
+const listData = await list();
+
+
+
+
+
+
+
+ This library provides a thin wrapper around the msal library to make it easy to integrate MSAL authentication in the browser.
+You will first need to install the package:
+npm install @pnp/msaljsclient --save
The configuration and authParams
+import { spfi, SPBrowser } from "@pnp/sp";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/sp/webs";
+
+const configuation = {
+ auth: {
+ authority: "https://login.microsoftonline.com/common",
+ clientId: "{client id}",
+ }
+};
+
+const authParams = {
+ scopes: ["https://{tenant}.sharepoint.com/.default"],
+};
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams));
+
+const webData = await sp.web();
+
+Please see more scenarios in the authentication article.
+ + + + + + +Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.
+This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules.
+We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community.
+Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured "roots" such as "sp" or "graph" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios.
+Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.
+In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227.
+These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November.
+ +++1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds
+
We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
+Month | +Count | +* | +Month | +Count | +
---|---|---|---|---|
January | +100,686 | +* | +July | +36,805 | +
February | +34,437 | +* | +August | +38,897 | +
March | +34,574 | +* | +September | +45,968 | +
April | +32,436 | +* | +October | +46,655 | +
May | +34,482 | +* | +November | +45,511 | +
June | +34,408 | +* | +December | +58,977 | +
+ | + | + | + | + |
+ | + | + | Grand Total | +543,836 | +
With 2020 our total all time downloads of @pnp/sp is now at: 949,638
+++Stats from https://npm-stat.com/
+
Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date.
+Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.
+With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work.
+Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean.
+We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.
+As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.
++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
+Thank You
++ + + + + + + + + + + + + + + + + +
+In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
+Wishing you the very best for 2021,
+The PnPjs Team
+ + + + + + +Welcome to our second year in review report for PnPjs. 2021 found us planning, building, testing, and documenting a whole new version of PnPjs. The goal is to deliver a much improved and flexible experience and none of that would have been possible without the support and participation of everyone in the PnP community - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.
+Because of the huge useage we've seen with the library and issues we found implementing some of the much requested enhancements, we felt we really needed to start from the ground up and rearchitect the library completely. This new design, built on the concept of a "Timeline", enabled us to build a significantly lighter weight solution that is more extensible than ever. And bonus, we were able to keep the overall development experience largly unchanged, so that makes transitioning all that much easier. In addition we took extra effort to validate our development efforts by making sure all our tests passed so that we could better ensure quality of the library. Check out our Transition Guide and ChangeLog for all the details.
+In other news, we fixed 47 reported bugs, answered 89 questions, and made 51 suggested enhancements to version 2 of the library - all driven by feedback from users and the community.
+In 2021 we transitioned from rapid growth to slower growth but maintaining a request/month rate over 11 billion, approaching 13 billion by the end of the year. These requests came from more than 25 thousand tenants including some of the largest M365 customers. Due to some data cleanup we don't have the full year's information, but the below graph shows the final 7 months of the year.
+ +We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
+Month | +Count | +* | +Month | +Count | +
---|---|---|---|---|
January | +49,446 | +* | +July | +73,491 | +
February | +56,054 | +* | +August | +74,236 | +
March | +66,113 | +* | +September | +69,179 | +
April | +58,526 | +* | +October | +77,645 | +
May | +62,747 | +* | +November | +74,966 | +
June | +69,349 | +* | +December | +61,995 | +
+ | + | + | + | + |
+ | + | + | Grand Total | +793,747 | +
For comparison our total downloads in 2020 was 543,836.
+With 2021 our total all time downloads of @pnp/sp is now at: 1,743,385
+In 2020 the all time total was 949,638.
+++Stats from https://npm-stat.com/
+
Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.
+As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.
++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
+Thank You
++ + + + + + + + + + + + + + + + + +
+In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
+Wishing you the very best for 2022,
+The PnPjs Team
+ + + + + + +Wow, what a year for PnPjs! We released our latest major version 3.0 on Valentine's Day 2022 which included significant performance improvements, a completely rewritten internal architecture, and reduced the bundled library size by two-thirds. As well we continued out monthly releases bringing enhancements and bug fixes to our users on a continual basis.
+But before we go any further we once again say Thank You!!! to everyone that has used, contributed to, and provided feedback on the library. This journey is not possible without you, and this last year you have driven us to be our best.
+Version 3 introduces a completely new design for the internals of the library, easily allowing consumers to customize any part of the request process to their needs. Centered around an extensible Timeline and extended for http requests by Queryable this new pattern reduced code duplication, interlock, and complexity significantly. It allows everything in the request flow to be controlled through behaviors, which are plain functions acting at the various stages of the request. Using this model we reimagined batching, caching, authentication, and parsing in simpler, composable ways. If you have not yet updated to version 3, we encourage you to do so. You can review the transition guide to get started.
+As one last treat, we set up nightly builds so that each day you can get a fresh version with any updates merged the previous day. This is super helpful if you're waiting for a specific fix or feature for your project. It allows for easier testing of new features through the full dev lifecycle, as well.
+In other news, we fixed 54 reported bugs, answered 123 questions, and made 54 suggested enhancements to version 3 of the library - all driven by feedback from users and the community.
+In 2022 we continued to see steady usage and growth maintaining a requst/month rate over 30 billion for much of the year. These requets came from ~29K tenants a month, including some of our largest M365 customers.
+ +We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
+Month | +Count | +* | +Month | +Count | +
---|---|---|---|---|
January | +70,863 | +* | +July | +63,844 | +
February | +76,649 | +* | +August | +75,713 | +
March | +83,902 | +* | +September | +71,447 | +
April | +70,429 | +* | +October | +84,744 | +
May | +72,406 | +* | +November | +82,459 | +
June | +71,375 | +* | +December | +65,785 | +
+ | + | + | + | + |
+ | + | + | Grand Total | +889,616 | +
For comparison our total downloads in 2021 was 793,747.
+With 2022 our total all time downloads of @pnp/sp is now at: 2,543,639
+In 2021 the all time total was 1,743,385.
+++Stats from https://npm-stat.com/
+
Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.
+As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2021! If you are interested in becoming a contributor check out our guide on ways to get started.
++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
+Thank You
+ +In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
+Wishing you the very best for 2023,
+The PnPjs Team
+ + + + + + +The article describes the behaviors exported by the @pnp/nodejs
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/graph.
This behavior, for use in nodejs, provides basic fetch support through the node-fetch
package. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing
++For fetch configuration in browsers please see @pnp/queryable behaviors.
+
import { NodeFetch } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetch());
+
+await sp.webs();
+
+import { NodeFetch } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetch({ replace: false }));
+
+await sp.webs();
+
+This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.
+import { NodeFetchWithRetry } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetchWithRetry());
+
+await sp.webs();
+
+You can also control how the behavior works through its props. The replace
value works as described above for NodeFetch. interval
specifies the initial dynamic back off value in milliseconds. This value is ignored if a "Retry-After" header exists in the response. retries
indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.
import { NodeFetchWithRetry } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetchWithRetry({
+ retries: 2,
+ interval: 400,
+ replace: true,
+}));
+
+await sp.webs();
+
+The GraphDefault
behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse, graph's DefaultHeaders, and graph's DefaultInit. It is configured using a props argument:
interface IGraphDefaultProps {
+ baseUrl?: string;
+ msal: {
+ config: Configuration;
+ scopes?: string[];
+ };
+}
+
+You can use the baseUrl property to specify either v1.0 or beta - or one of the special graph urls.
+import { GraphDefault } from "@pnp/nodejs";
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users/index.js";
+
+const graph = graphfi().using(GraphDefault({
+ // use the German national graph endpoint
+ baseUrl: "https://graph.microsoft.de/v1.0",
+ msal: {
+ config: { /* my msal config */ },
+ }
+}));
+
+await graph.me();
+
+This behavior provides a thin wrapper around the @azure/msal-node
library. The options you provide are passed directly to msal, and all options are available.
import { MSAL } from "@pnp/nodejs";
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users/index.js";
+
+const graph = graphfi().using(MSAL(config: { /* my msal config */ }, scopes: ["https://graph.microsoft.com/.default"]);
+
+await graph.me();
+
+The SPDefault
behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse,sp's DefaultHeaders, and sp's DefaultInit. It is configured using a props argument:
interface ISPDefaultProps {
+ baseUrl?: string;
+ msal: {
+ config: Configuration;
+ scopes: string[];
+ };
+}
+
+You can use the baseUrl property to specify the absolute site/web url to which queries should be set.
+import { SPDefault } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(SPDefault({
+ msal: {
+ config: { /* my msal config */ },
+ scopes: ["Scope.Value", "Scope2.Value"],
+ }
+}));
+
+await sp.web();
+
+StreamParse
is a specialized parser allowing request results to be read as a nodejs stream. The return value when using this parser will be of the shape:
{
+ body: /* The .body property of the Response object */,
+ knownLength: /* number value calculated from the Response's content-length header */
+}
+
+import { StreamParse } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(StreamParse());
+
+const streamResult = await sp.someQueryThatReturnsALargeFile();
+
+// read the stream as text
+const txt = await new Promise<string>((resolve) => {
+ let data = "";
+ streamResult.body.on("data", (chunk) => data += chunk);
+ streamResult.body.on("end", () => resolve(data));
+});
+
+
+
+
+
+
+
+ By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api.
+Allows you to read a response body as a nodejs PassThrough stream.
+// by importing the the library the node specific extensions are automatically applied
+import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi("https://something.com").using(SPDefault({
+ // config
+}));
+
+// get the stream
+const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
+
+// see if we have a known length
+console.log(streamResult.knownLength);
+
+// read the stream
+// this is a very basic example - you can do tons more with streams in node
+const txt = await new Promise<string>((resolve) => {
+ let data = "";
+ stream.body.on("data", (chunk) => data += chunk);
+ stream.body.on("end", () => resolve(data));
+});
+
+import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs/index.js";
+import "@pnp/sp/folders/web.js";
+import "@pnp/sp/folders/list.js";
+import "@pnp/sp/files/web.js";
+import "@pnp/sp/files/folder.js";
+import * as fs from "fs";
+
+const sp = spfi("https://something.com").using(SPDefault({
+ // config
+}));
+
+// NOTE: you must supply the highWaterMark to determine the block size for stream uploads
+const stream = fs.createReadStream("{file path}", { highWaterMark: 10485760 });
+const files = sp.web.defaultDocumentLibrary.rootFolder.files;
+
+// passing the chunkSize parameter has no affect when using a stream, use the highWaterMark as shown above when creating the stream
+await files.addChunked(name, stream, null, true);
+
+import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs/index.js";
+import "@pnp/sp/folders/web.js";
+import "@pnp/sp/folders/list.js";
+import "@pnp/sp/files/web.js";
+import "@pnp/sp/files/folder.js";
+import * as fs from "fs";
+
+const sp = spfi("https://something.com").using(SPDefault({
+ // config
+}));
+
+// NOTE: you must supply the highWaterMark to determine the block size for stream uploads
+const stream = fs.createReadStream("{file path}", { highWaterMark: 10485760 });
+const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName("file-name.txt");
+
+await file.setStreamContentChunked(stream);
+
+If you don't need to import anything from the library, but would like to include the extensions just import the library as shown.
+import "@pnp/nodejs";
+
+// get the stream
+const streamResult = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
+
+There are classes and interfaces included in extension modules, which you can access through a namespace, "SPNS".
+import { SPNS } from "@pnp/nodejs-commonjs";
+
+const parser = new SPNS.StreamParser();
+
+
+
+
+
+
+
+ The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope.
+The latest published version is .
+Central to everything PnPjs builds on with utility methods, Timeline, the behavior plumbing, and the extendable framework.
+npm install @pnp/core --save
This package provides a fluent SDK for calling the Microsoft Graph.
+npm install @pnp/graph --save
A light-weight, subscribable logging framework.
+npm install @pnp/logging --save
Provides an msal wrapper suitable for use with PnPjs's request structure.
+npm install @pnp/msaljsclient --save
Provides functionality enabling the @pnp libraries within nodejs, including extension methods for working with streams.
+npm install @pnp/nodejs --save
Extending Timeline this package provides the base functionality to create web requests in a fluent manner. It defines the available moments to which observers are subscribed for building the request.
+npm install @pnp/queryable --save
This package provides a fluent SDK for calling SharePoint.
+npm install @pnp/sp --save
This package provides a fluent SDK for calling SharePoint tenant admin APIs
+npm install @pnp/sp-admin --save
The article describes the behaviors exported by the @pnp/queryable
library. Please also see available behaviors in @pnp/core, @pnp/nodejs, @pnp/sp, and @pnp/graph.
Generally you won't need to use these behaviors individually when using the defaults supplied by the library, but when appropriate you can create your own composed behaviors using these as building blocks.
+Allows you to inject an existing bearer token into the request. This behavior will not replace any existing authentication behaviors, so you may want to ensure they are cleared if you are supplying your own tokens, regardless of their source. This behavior does no caching or performs any operation other than including your token in an authentication heading.
+import { BearerToken } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BearerToken("HereIsMyBearerTokenStringFromSomeSource"));
+
+// optionally clear any configured authentication as you are supplying a token so additional calls shouldn't be needed
+// but take care as other behaviors may add observers to auth
+sp.on.auth.clear();
+
+// the bearer token supplied above will be applied to all requests made from `sp`
+const webInfo = await sp.webs();
+
+This behavior, for use in web browsers, provides basic fetch support through the browser's fetch global method. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing
+++For fetch configuration in nodejs please see @pnp/nodejs behaviors.
+
import { BrowserFetch } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetch());
+
+const webInfo = await sp.webs();
+
+import { BrowserFetch } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetch({ replace: false }));
+
+const webInfo = await sp.webs();
+
+This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.
+import { BrowserFetchWithRetry } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetchWithRetry());
+
+const webInfo = await sp.webs();
+
+You can also control how the behavior works through its props. The replace
value works as described above for BrowserFetch. interval
specifies the initial dynamic back off value in milliseconds. This value is ignored if a "Retry-After" header exists in the response. retries
indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.
import { BrowserFetchWithRetry } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetchWithRetry({
+ retries: 2,
+ interval: 400,
+ replace: true,
+}));
+
+const webInfo = await sp.webs();
+
+This behavior allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map. It is a good idea to include caching in your projects to improve performance. By default items in the cache will expire after 5 minutes.
+import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(Caching());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+You can also supply custom functionality to control how keys are generated and calculate the expirations.
+The cache key factory has the form (url: string) => string
and you must ensure your keys are unique enough that you won't have collisions.
The expire date factory has the form (url: string) => Date
and should return the Date when the cached data should expire. If you know that some particular data won't expire often you can set this date far in the future, or for more frequently updated information you can set it lower. If you set the expiration too short there is no reason to use caching as any stored information will likely always be expired. Additionally, you can set the storage to use local storage which will persist across sessions.
++Note that for sp.search() requests if you want to specify a key you will need to use the CacheKey behavior below, the keyFactory value will be overwritten
+
import { getHashCode, PnPClientStorage, dateAdd, TimelinePipe } from "@pnp/core";
+import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(Caching({
+ store: "local",
+ // use a hascode for the key
+ keyFactory: (url) => getHashCode(url.toLowerCase()).toString(),
+ // cache for one minute
+ expireFunc: (url) => dateAdd(new Date(), "minute", 1),
+}));
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+As with any behavior you have the option to only apply caching to certain requests:
+import { getHashCode, dateAdd } from "@pnp/core";
+import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// caching will only apply to requests using `cachingList` as the base of the fluent chain
+const cachingList = sp.web.lists.getByTitle("{List Title}").using(Caching());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const itemsInfo = await cachingList.items();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const itemsInfo2 = await cachingList.items();
+
+Added in 3.10.0
+The bindCachingCore
method is supplied to allow all caching behaviors to share a common logic around the handling of ICachingProps. Usage of this function is not required to build your own caching method. However, it does provide consistent logic and will incoroporate any future enhancements. It can be used to create your own caching behavior. Here we show how we use the binding function within Caching
as a basic example.
The bindCachingCore
method is designed for use in a pre
observer and the first two parameters are the url and init passed to pre. The third parameter is an optional PartialbindCachingCore
. The third value is a function to which you pass a value to cache. The key and expiration are similarly calculated and held within bindCachingCore
.
import { TimelinePipe } from "@pnp/core";
+import { bindCachingCore, ICachingProps, Queryable } from "@pnp/queryable";
+
+export function Caching(props?: ICachingProps): TimelinePipe<Queryable> {
+
+ return (instance: Queryable) => {
+
+ instance.on.pre(async function (this: Queryable, url: string, init: RequestInit, result: any): Promise<[string, RequestInit, any]> {
+
+ const [shouldCache, getCachedValue, setCachedValue] = bindCachingCore(url, init, props);
+
+ // only cache get requested data or where the CacheAlways header is present (allows caching of POST requests)
+ if (shouldCache) {
+
+ const cached = getCachedValue();
+
+ // we need to ensure that result stays "undefined" unless we mean to set null as the result
+ if (cached === null) {
+
+ // if we don't have a cached result we need to get it after the request is sent. Get the raw value (un-parsed) to store into cache
+ this.on.rawData(noInherit(async function (response) {
+ setCachedValue(response);
+ }));
+
+ } else {
+ // if we find it in cache, override send request, and continue flow through timeline and parsers.
+ this.on.auth.clear();
+ this.on.send.replace(async function (this: Queryable) {
+ return new Response(cached, {});
+ });
+ }
+ }
+
+ return [url, init, result];
+ });
+
+ return instance;
+ };
+}
+
+Added in 3.5.0
+This behavior allows you to set a pre-determined cache key for a given request. It needs to be used PER request otherwise the value will be continuously overwritten.
+import { Caching, CacheKey } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+// note the application of the behavior on individual requests, if you share a CacheKey behavior across requests you'll encounter conflicts
+const webInfo = await sp.web.using(CacheKey("MyWebInfoCacheKey"))();
+
+const listsInfo = await sp.web.lists.using(CacheKey("MyListsInfoCacheKey"))();
+
+Added in 3.8.0
+This behavior allows you to force caching for a given request. This should not be used for update/create operations as the request will not execute if a result is found in the cache
+import { Caching, CacheAlways } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+const webInfo = await sp.web.using(CacheAlways())();
+
+Added in 3.10.0
+This behavior allows you to force skipping caching for a given request.
+import { Caching, CacheNever } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+const webInfo = await sp.web.using(CacheNever())();
+
+This behavior is slightly different than our default Caching behavior in that it will always return the cached value if there is one, but also asyncronously update the cached value in the background. Like the default CAchine behavior it allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map.
+If you do not provide an expiration function then the cache will be updated asyncronously on every call, if you do provide an expiration then the cached value will only be updated, although still asyncronously, only when the cache has expired.
+import { CachingPessimisticRefresh } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(CachingPessimisticRefresh());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+Again as with the default Caching behavior you can provide custom functions for key generation and expiration. Please see the Custom Key Function documentation above for more details.
+Adds any specified headers to a given request. Can be used multiple times with a timeline. The supplied headers are added to all requests, and last applied wins - meaning if two InjectHeaders are included in the pipeline which inlcude a value for the same header, the second one applied will be used.
+import { InjectHeaders } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(InjectHeaders({
+ "X-Something": "a value",
+ "MyCompanySpecialAuth": "special company token",
+}));
+
+const webInfo = await sp.webs();
+
+Parsers convert the returned fetch Response into something usable. We have included the most common parsers we think you'll need - but you can always write your own parser based on the signature of the parse moment.
+++All of these parsers when applied through using will replace any other observers on the parse moment.
+
Performs error handling and parsing of JSON responses. This is the one you'll use for most of your requests and it is included in all the defaults.
+import { DefaultParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(DefaultParse());
+
+const webInfo = await sp.webs();
+
+Checks for errors and parses the results as text with no further manipulation.
+import { TextParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(TextParse());
+
+Checks for errors and parses the results a Blob with no further manipulation.
+import { BlobParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BlobParse());
+
+Checks for errors and parses the results as JSON with no further manipulation. Meaning you will get the raw JSON response vs DefaultParse which will remove wrapping JSON.
+import { JSONParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(JSONParse());
+
+Checks for errors and parses the results a Buffer with no further manipulation.
+import { BufferParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BufferParse());
+
+Checks for errors and parses the headers of the Response as the result. This is a specialised parses which can be used in those infrequent scenarios where you need information from the headers of a response.
+import { HeaderParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(HeaderParse());
+
+Checks for errors and parses the headers of the Respnose as well as the JSON and returns an object with both values.
+import { JSONHeaderParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(JSONHeaderParse());
+
+...sp.data
+...sp.headers
+
+These two behaviors are special and should always be included when composing your own defaults. They implement the expected behavior of resolving or rejecting the promise returned when executing a timeline. They are implemented as behaviors should there be a need to do something different the logic is not locked into the core of the library.
+import { ResolveOnData, RejectOnError } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(ResolveOnData(), RejectOnError());
+
+The Timeout behavior allows you to include a timeout in requests. You can specify either a number, representing the number of milliseconds until the request should timeout or an AbortSignal.
+++In Nodejs you will need to polyfill
+AbortController
if your version (<15) does not include it when using Timeout and passing a number. If you are supplying your own AbortSignal you do not.
import { Timeout } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+// requests should timeout in 5 seconds
+const sp = spfi(...).using(Timeout(5000));
+
+import { Timeout } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const controller = new AbortController();
+
+const sp = spfi(...).using(Timeout(controller.signal));
+
+// abort requests after 6 seconds using our own controller
+const timer = setTimeout(() => {
+ controller.abort();
+}, 6000);
+
+// this request will be cancelled if it doesn't complete in 6 seconds
+const webInfo = await sp.webs();
+
+// be a good citizen and cancel unneeded timers
+clearTimeout(timer);
+
+Updated as Beta 2 in 3.5.0
+This behavior allows you to cancel requests before they are complete. It is similar to timeout however you control when and if the request is canceled. Please consider this behavior as beta while we work to stabalize the functionality.
+import { Cancelable, CancelablePromise } from "@pnp/queryable";
+import { IWebInfo } from "@pnp/sp/webs";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(Cancelable());
+
+const p: CancelablePromise<IWebInfo> = <any>sp.web();
+
+setTimeout(() => {
+
+ // you should await the cancel operation to ensure it completes
+ await p.cancel();
+}, 200);
+
+// this is awaiting the results of the request
+const webInfo: IWebInfo = await p;
+
+Some operations such as chunked uploads that take longer to complete are good candidates for canceling based on user input such as a button select.
+import { Cancelable, CancelablePromise } from "@pnp/queryable";
+import { IFileAddResult } from "@pnp/sp/files";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import { getRandomString } from "@pnp/core";
+import { createReadStream } from "fs";
+
+const sp = spfi().using(Cancelable());
+
+const file = createReadStream(join("C:/some/path", "test.mp4"));
+
+const p: CancelablePromise<IFileAddResult> = <any>sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents").files.addChunked(`te's't-${getRandomString(4)}.mp4`, <any>file);
+
+setTimeout(() => {
+
+ // you should await the cancel operation to ensure it completes
+ await p.cancel();
+}, 10000);
+
+// this is awaiting the results of the request
+await p;
+
+
+
+
+
+
+
+ Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invokable and allow you to control any behavior of the library with extensions.
+There are two types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.
+The first type is a simple function with a signature:
+(op: "apply" | "get" | "has" | "set", target: T, ...rest: any[]): void
+
+This function is passed the current operation as the first argument, currently one of "apply", "get", "has", or "set". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.
+Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.
+import { extendFactory } from "@pnp/queryable";
+import { sp, List, Lists, IWeb, ILists, List, IList, Web } from "@pnp/sp/presets/all";
+import { escapeQueryStrValue } from "@pnp/sp/utils/escapeQueryStrValue";
+
+// create a plain object with the props and methods we want to add/change
+const myExtensions = {
+ // override the lists property
+ get lists(this: IWeb): ILists {
+ // we will always order our lists by title and select just the Title for ALL calls (just as an example)
+ return Lists(this).orderBy("Title").select("Title");
+ },
+ // override the getByTitle method
+ getByTitle: function (this: ILists, title: string): IList {
+ // in our example our list has moved, so we rewrite the request on the fly
+ if (title === "List1") {
+ return List(this, `getByTitle('List2')`);
+ } else {
+ // you can't at this point call the "base" method as you will end up in loop within the proxy
+ // so you need to ensure you patch/include any original functionality you need
+ return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
+ }
+ },
+};
+
+// register all the named Extensions
+extendFactory(Web, myExtensions);
+
+// this will use our extension to ensure the lists are ordered
+const lists = await sp.web.lists();
+
+console.log(JSON.stringify(lists, null, 2));
+
+// we will get the items from List1 but within the extension it is rewritten as List2
+const items = await sp.web.lists.getByTitle("List1").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.
+import { extendFactory } from "@pnp/queryable";
+import { sp, Lists, IWeb, ILists, Web } from "@pnp/sp/presets/all";
+import { escapeQueryStrValue } from "@pnp/sp/utils/escapeSingleQuote";
+
+const myExtensions = {
+ get: (target, p: string | number | symbol, _receiver: any) => {
+ switch (p) {
+ case "getByTitle":
+ return (title: string) => {
+
+ // in our example our list has moved, so we rewrite the request on the fly
+ if (title === "LookupList") {
+ return List(target, `getByTitle('OrderByList')`);
+ } else {
+ // you can't at this point call the "base" method as you will end up in loop within the proxy
+ // so you need to ensure you patch/include any original functionality you need
+ return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);
+ }
+ };
+ }
+ },
+};
+
+extendFactory(Web, myExtensions);
+
+const lists = sp.web.lists;
+const items = await lists.getByTitle("LookupList").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+You can register Extensions on an invocable factory or on a per-object basis, and you can register a single extension or an array of Extensions.
+The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IWeb, Web } from "@pnp/sp/webs";
+import { ILists, Lists } from "@pnp/sp/lists";
+import { extendFactory } from "@pnp/queryable";
+import { sp } from "@pnp/sp";
+
+const sp = spfi().using(...);
+
+// sets up the types correctly when importing across your application
+declare module "@pnp/sp/webs/types" {
+
+ // we need to extend the interface
+ interface IWeb {
+ orderedLists: ILists;
+ }
+}
+
+// sets up the types correctly when importing across your application
+declare module "@pnp/sp/lists/types" {
+
+ // we need to extend the interface
+ interface ILists {
+ getOrderedListsQuery: (this: ILists) => ILists;
+ }
+}
+
+extendFactory(Web, {
+ // add an ordered lists property
+ get orderedLists(this: IWeb): ILists {
+ return this.lists.getOrderedListsQuery();
+ },
+});
+
+extendFactory(Lists, {
+ // add an ordered lists property
+ getOrderedListsQuery(this: ILists): ILists {
+ return this.top(10).orderBy("Title").select("Title");
+ },
+});
+
+// regardless of how we access the web and lists collections our extensions remain with all new instance based on
+const web = Web([sp.web, "https://tenant.sharepoint.com/sites/dev/"]);
+const lists1 = await web.orderedLists();
+console.log(JSON.stringify(lists1, null, 2));
+
+const lists2 = await Web([sp.web, "https://tenant.sharepoint.com/sites/dev/"]).orderedLists();
+console.log(JSON.stringify(lists2, null, 2));
+
+const lists3 = await sp.web.orderedLists();
+console.log(JSON.stringify(lists3, null, 2));
+
+You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.
+++Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.
+
Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.
+import { extendObj } from "@pnp/queryable";
+import { sp, List, ILists } from "@pnp/sp/presets/all";
+
+const myExtensions = {
+ getByTitle: function (this: ILists, title: string) {
+ // in our example our list has moved, so we rewrite the request on the fly
+ if (title === "List1") {
+ return List(this, "getByTitle('List2')");
+ } else {
+ // you can't at this point call the "base" method as you will end up in loop within the proxy
+ // so you need to ensure you patch/include any original functionality you need
+ return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
+ }
+ },
+};
+
+const lists = extendObj(sp.web.lists, myExtensions);
+const items = await lists.getByTitle("LookupList").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.
+import { enableExtensions, disableExtensions, clearGlobalExtensions } from "@pnp/queryable";
+
+// disable Extensions
+disableExtensions();
+
+// enable Extensions
+enableExtensions();
+
+
+
+
+
+
+
+ Queryable is the base class for both the sp and graph fluent interfaces and provides the structure to which observers are registered. As a background to understand more of the mechanics please see the articles on Timeline, moments, and observers. For reuse it is recommended to compose your observer registrations with behaviors.
+By design the library is meant to allow creating the next part of a url from the current part. In this way each queryable instance is built from a previous instance. As such understanding the Queryable constructor's behavior is important. The constructor takes two parameters, the first required and the second optional.
+The first parameter can be another queryable, a string, or a tuple of [Queryable, string].
+Parameter | +Behavior | +
---|---|
Queryable | +The new queryable inherits all of the supplied queryable's observers. Any supplied path (second constructor param) is appended to the supplied queryable's url becoming the url of the newly constructed queryable | +
string | +The new queryable will have NO registered observers. Any supplied path (second constructor param) is appended to the string becoming the url of the newly constructed queryable | +
[Queryable, string] | +The observers from the supplied queryable are used by the new queryable. The url is a combination of the second tuple argument (absolute url string) and any supplied path. | +
++The tuple constructor call can be used to rebase a queryable to call a different host in an otherwise identical way to another queryable. When using the tuple constructor the url provided must be absolute.
+
// represents a fully configured queryable with url and registered observers
+// url: https://something.com
+const baseQueryable;
+
+// child1 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://something.com/subpath"
+const child1 = Child(baseQueryable, "subpath");
+
+// child2 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://something.com"
+const child2 = Child(baseQueryable);
+
+// nonchild1 will:
+// - have NO registered observers or connection to baseQueryable
+// - have a url of "https://somethingelse.com"
+const nonchild1 = Child("https://somethingelse.com");
+
+// nonchild2 will:
+// - have NO registered observers or connection to baseQueryable
+// - have a url of "https://somethingelse.com/subpath"
+const nonchild2 = Child("https://somethingelse.com", "subpath");
+
+// rebased1 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://somethingelse.com"
+const rebased1 = Child([baseQueryable, "https://somethingelse.com"]);
+
+// rebased2 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://somethingelse.com/subpath"
+const rebased2 = Child([baseQueryable, "https://somethingelse.com"], "subpath");
+
+The Queryable lifecycle is:
+construct
(Added in 3.5.0)init
pre
auth
send
parse
post
data
dispose
As well log
and error
can emit at any point during the lifecycle.
If you see an error thrown with the message No observers registered for this request.
it means at the time of execution the given object has no actions to take. Because all the request logic is defined within observers, an absence of observers is likely an error condition. If the object was created by a method within the library please report an issue as it is likely a bug. If you created the object through direct use of one of the factory functions, please be sure you have registered observers with using
or on
as appropriate. More information on observers is available in this article.
If you for some reason want to execute a queryable with no registred observers, you can simply register a noop observer to any of the moments.
+This section outlines how to write observers for the Queryable lifecycle, and the expectations of each moment's observer behaviors.
+++In the below samples consider the variable
+query
to mean any valid Queryable derived object.
Anything can log to a given timeline's log using the public log
method and to intercept those message you can subscribed to the log event.
The log
observer's signature is: (this: Timeline<T>, message: string, level: number) => void
query.on.log((message, level) => {
+
+ // log only warnings or errors
+ if (level > 1) {
+ console.log(message);
+ }
+});
+
+++The level value is a number indicating the severity of the message. Internally we use the values from the LogLevel enum in @pnp/logging: Verbose = 0, Info = 1, Warning = 2, Error = 3. Be aware that nothing enforces those values other than convention and log can be called with any value for level.
+
As well we provide easy support to use PnP logging within a Timeline derived class:
+import { LogLevel, PnPLogging } from "@pnp/logging";
+
+// any messages of LogLevel Info or higher (1) will be logged to all subscribers of the logging framework
+query.using(PnPLogging(LogLevel.Info));
+
+++More details on the pnp logging framework
+
Errors can happen at anytime and for any reason. If you are using the RejectOnError
behavior, and both sp and graph include that in the defaults, the request promise will be rejected as expected and you can handle the error that way.
The error
observer's signature is: (this: Timeline<T>, err: string | Error) => void
import { spfi, DefaultInit, DefaultHeaders } from "@pnp/sp";
+import { BrowserFetchWithRetry, DefaultParse } from "@pnp/queryable";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultInit(), DefaultHeaders(), BrowserFetchWithRetry(), DefaultParse());
+
+try {
+
+ const result = await sp.web();
+
+} catch(e) {
+
+ // any errors emitted will result in the promise being rejected
+ // and ending up in the catch block as expected
+}
+
+In addition to the default behavior you can register your own observers on error
, though it is recommended you leave the default behavior in place.
query.on.error((err) => {
+
+ if (err) {
+ console.error(err);
+ // do other stuff with the error (send it to telemetry)
+ }
+});
+
+Added in 3.5.0
+This moment exists to assist behaviors that need to transfer some information from a parent to a child through the fluent chain. We added this to support cancelable scopes for the Cancelable behavior, but it may have other uses. It is invoked AFTER the new instance is fully realized via new
and supplied with the parameters used to create the new instance. As with all moments the "this" within the observer is the current (NEW) instance.
For your observers on the construct method to work correctly they must be registered before the instance is created.
+++The construct moment is NOT async and is designed to support simple operations.
+
query.on.construct(function (this: Queryable, init: QueryableInit, path?: string): void {
+ if (typeof init !== "string") {
+
+ // get a ref to the parent Queryable instance used to create this new instance
+ const parent = isArray(init) ? init[0] : init;
+
+ if (Reflect.has(parent, "SomeSpecialValueKey")) {
+
+ // copy that specail value to the new child
+ this["SomeSpecialValueKey"] = parent["SomeSpecialValueKey"];
+ }
+ }
+});
+
+query.on.pre(async function(url, init, result) {
+
+ // we have access to the copied special value throughout the lifecycle
+ this.log(this["SomeSpecialValueKey"]);
+
+ return [url, init, result];
+});
+
+query.on.dispose(() => {
+
+ // be a good citizen and clean up your behavior's values when you're done
+ delete this["SomeSpecialValueKey"];
+});
+
+Along with dispose
, init
is a special moment that occurs before any of the other lifecycle providing a first chance at doing any tasks before the rest of the lifecycle starts. It is not await aware so only sync operations are supported in init by design.
The init
observer's signature is: (this: Timeline<T>) => void
++In the case of init you manipulate the Timeline instance itself
+
query.on.init(function (this: Queryable) {
+
+ // init is a great place to register additioanl observers ahead of the lifecycle
+ this.on.pre(async function (this: Quyerable, url, init, result) {
+ // stuff happens
+ return [url, init, result];
+ });
+});
+
+Pre is used by observers to configure the request before sending. Note there is a dedicated auth moment which is prefered by convention to handle auth related tasks.
+The pre
observer's signature is: (this: IQueryable, url: string, init: RequestInit, result: any) => Promise<[string, RequestInit, any]>
++The
+pre
,auth
,parse
, andpost
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
Example of when to use pre are updates to the init, caching scenarios, or manipulation of the url (ensuring it is absolute). The init passed to pre (and auth) is the same object that will be eventually passed to fetch, meaning you can add any properties/congifuration you need. The result should always be left undefined unless you intend to end the lifecycle. If pre completes and result has any value other than undefined that value will be emitted to data
and the timeline lifecycle will end.
query.on.pre(async function(url, init, result) {
+
+ init.cache = "no-store";
+
+ return [url, init, result];
+});
+
+query.on.pre(async function(url, init, result) {
+
+ // setting result causes no moments after pre to be emitted other than data
+ // once data is emitted (resolving the request promise by default) the lifecycle ends
+ result = "My result";
+
+ return [url, init, result];
+});
+
+Auth functions very much like pre
except it does not have the option to set the result, and the url is considered immutable by convention. Url manipulation should be done in pre. Having a seperate moment for auth allows for easily changing auth specific behavior without having to so a lot of complicated parsing of pre
observers.
The auth
observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<[URL, RequestInit]>
.
++The
+pre
,auth
,parse
, andpost
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
query.on.auth(async function(url, init) {
+
+ // some code to get a token
+ const token = getToken();
+
+ init.headers["Authorization"] = `Bearer ${token}`;
+
+ return [url, init];
+});
+
+Send is implemented using the request moment which uses the first registered observer and invokes it expecting an async Response.
+The send
observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<Response>
.
query.on.send(async function(url, init) {
+
+ // this could represent reading a file, querying a database, or making a web call
+ return fetch(url.toString(), init);
+});
+
+Parse is responsible for turning the raw Response into something usable. By default we handle errors and parse JSON responses, but any logic could be injected here. Perhaps your company encrypts things and you need to decrypt them before parsing further.
+The parse
observer's signature is: (this: IQueryable, url: URL, response: Response, result: any | undefined) => Promise<[URL, Response, any]>
.
++The
+pre
,auth
,parse
, andpost
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
// you should be careful running multiple parse observers so we replace with our functionality
+// remember every registered observer is run, so if you set result and a later observer sets a
+// different value last in wins.
+query.on.parse.replace(async function(url, response, result) {
+
+ if (response.ok) {
+
+ result = await response.json();
+
+ } else {
+
+ // just an example
+ throw Error(response.statusText);
+ }
+
+ return [url, response, result];
+});
+
+Post is run after parse, meaning you should have a valid fully parsed result, and provides a final opportunity to do caching, some final checks, or whatever you might need immediately prior to the request promise resolving with the value. It is recommened to NOT manipulate the result within post though nothing prevents you from doing so.
+The post
observer's signature is: (this: IQueryable, url: URL, result: any | undefined) => Promise<[URL, any]>
.
++The
+pre
,auth
,parse
, andpost
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
query.on.post(async function(url, result) {
+
+ // here we do some caching of a result
+ const key = hash(url);
+ cache(key, result);
+
+ return [url, result];
+});
+
+Data is called with the result of the Queryable lifecycle produced by send
, understood by parse
, and passed through post
. By default the request promise will resolve with the value, but you can add any additional observers you need.
The data
observer's signature is: (this: IQueryable, result: T) => void
.
++Clearing the data moment (ie. .on.data.clear()) after the lifecycle has started will result in the request promise never resolving
+
query.on.data(function(result) {
+
+ console.log(`Our result! ${JSON.stringify(result)}`);
+});
+
+Along with init
, dispose
is a special moment that occurs after all other lifecycle moments have completed. It is not await aware so only sync operations are supported in dispose by design.
The dispose
observer's signature is: (this: Timeline<T>) => void
++In the case of dispose you manipulate the Timeline instance itself
+
query.on.dispose(function (this: Queryable) {
+
+ // maybe your queryable calls a database?
+ db.connection.close();
+});
+
+Queryable exposes some additional methods beyond the observer registration.
+Appends the supplied string to the url without mormalizing slashes.
+// url: something.com/items
+query.concat("(ID)");
+// url: something.com/items(ID)
+
+Converts the queryable's internal url parameters (url and query) into a relative or absolute url.
+const s = query.toRequestUrl();
+
+Map used to manage any query string parameters that will be included. Anything added here will be represented in toRequestUrl
's output.
query.query.add("$select", "Title");
+
+Returns the url currently represented by the Queryable, without the querystring part
+const s = query.toUrl();
+
+
+
+
+
+
+
+ PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.
These articles provide general guidance for working with the libraries. If you are migrating from V2 please review the transition guide.
Animation of the library in use, note intellisense help in building your queries
"},{"location":"#packages","title":"Packages","text":"Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.
The latest published version is .
@pnp/ azidjsclient Provides an Azure Identity wrapper suitable for use with PnPjs core Provides shared functionality across all pnp libraries graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs queryable Provides shared query functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-admin Provides a fluent api for working with M365 Tenant admin methods"},{"location":"#authentication","title":"Authentication","text":"We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!
"},{"location":"#issues-questions-ideas","title":"Issues, Questions, Ideas","text":"Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.
"},{"location":"#changelog","title":"Changelog","text":"Please review the CHANGELOG for release details on all library changes.
"},{"location":"#code-of-conduct","title":"Code of Conduct","text":"This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
"},{"location":"#sharing-is-caring","title":"\"Sharing is Caring\"","text":"Please use http://aka.ms/community/home for the latest updates around the whole Microsoft 365 and Power Platform Community(PnP) initiative.
"},{"location":"#disclaimer","title":"Disclaimer","text":"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.
"},{"location":"getting-started/","title":"Getting Started","text":"This library is geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.
If you need to support older browsers, SharePoint on-premisis servers, or older versions of the SharePoint Framework, please revert to version 2 of the library and see related documentation on polyfills for required functionality.
"},{"location":"getting-started/#minimal-requirements","title":"Minimal Requirements","text":"- NodeJs: >= 14\n- TypeScript: 4.x\n- Node Modules Supported: ESM Only\n
"},{"location":"getting-started/#install","title":"Install","text":"First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. @pnp/sp
to access the SharePoint REST API and @pnp/graph
to access some of the Microsoft Graph API. This step applies to any environment or project.
npm install @pnp/sp @pnp/graph --save
Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples.
import { getRandomString } from \"@pnp/core\";\n\n(function() {\n\n // get and log a random string\n console.log(getRandomString(20));\n\n})()\n
"},{"location":"getting-started/#getting-started-with-sharepoint-framework","title":"Getting Started with SharePoint Framework","text":"The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises you will need to use version 2 of the library. If you are targeting SharePoint online you will need to take the additional steps outlined below based on the version of the SharePoint Framework you are targeting.
We've created two Getting Started samples. The first uses the more traditional React Component classes and can be found in the react-pnp-js-sample project, utilizing SPFx 1.15.2 and PnPjs V3, it showcases some of the more dramatic changes to the library. There is also a companion video series on YouTube if you prefer to see things done through that medium here's a link to the playlist for the 5 part series:
Getting started with PnPjs 3.0: 5-part series
In addition, we have converted the sample project from React Component to React Hooks. This version can be found in react-pnp-js-hooks. This sample will help those struggling to establish context correctly while using the hooks conventions.
The SharePoint Framework supports different versions of TypeScript natively and as of 1.14 release still doesn't natively support TypeScript 4.x. Sadly, this means that to use Version 3 of PnPjs you will need to take a few additional configuration steps to get them to work together.
"},{"location":"getting-started/#spfx-version-1150-later","title":"SPFx Version 1.15.0 & later","text":"No additional steps required
"},{"location":"getting-started/#spfx-version-1121-1140","title":"SPFx Version 1.12.1 => 1.14.0","text":"Update the rush stack compiler to 4.2. This is covered in this great article by Elio, but the steps are listed below.
npm uninstall @microsoft/rush-stack-compiler-3.?
npm i @microsoft/rush-stack-compiler-4.2
\"extends\": \"./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json\"
Replace the contents of the gulpfile.js with: >Note: The only change is the addition of the line to disable tslint.
```js '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'));\n\nreturn result;\n
};
// * ADDED * // disable tslint build.tslintCmd.enabled = false; // * ADDED *
build.initialize(require('gulp')); ```
At this time there is no documented method to use version 3.x with SPFx versions earlier than 1.12.1. We recommend that you fall back to using version 2 of the library or update your SPFx version.
"},{"location":"getting-started/#imports-and-usage","title":"Imports and usage","text":"Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. To establish context within the library you will need to use the SharePoint or Graph Factory Interface depending on which set of APIs you want to utilize. For SharePoint you will use the spfi
interface and for the Microsoft Graph you will use the graphfi
interface whic are both in the main export of the corresponding package. Examples of both methods are shown below.
Depending on how you architect your solution establishing context is done where you want to make calls to the API. The examples demonstrate doing so in the onInit method as a local variable but this could also be done to a private variable or passed into a service.
Note if you are going to use both the @pnp/sp and @pnp/graph packages in SPFx you will need to alias the SPFx behavior import, please see the section below for more details.
"},{"location":"getting-started/#using-pnpsp-spfi-factory-interface-in-spfx","title":"Using @pnp/spspfi
factory interface in SPFx","text":"import { spfi, SPFx } from \"@pnp/sp\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n await super.onInit();\n const sp = spfi().using(SPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#using-pnpgraph-graphfi-factory-interface-in-spfx","title":"Using @pnp/graph graphfi
factory interface in SPFx","text":"import { graphfi, SPFx } from \"@pnp/graph\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n await super.onInit();\n const graph = graphfi().using(SPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#using-both-pnpsp-and-pnpgraph-in-spfx","title":"Using both @pnp/sp and @pnp/graph in SPFx","text":"\nimport { spfi, SPFx as spSPFx } from \"@pnp/sp\";\nimport { graphfi, SPFx as graphSPFx} from \"@pnp/graph\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n await super.onInit();\n const sp = spfi().using(spSPFx(this.context));\n const graph = graphfi().using(graphSPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#project-configservices-setup","title":"Project Config/Services Setup","text":"Please see the documentation on setting up a config file or a services for more information about establishing and instance of the spfi or graphfi interfaces that can be reused. It is a common mistake with users of V3 that they try and create the interface in event handlers which causes issues.
"},{"location":"getting-started/#getting-started-with-nodejs","title":"Getting started with NodeJS","text":"Due to the way in which Node resolves ESM modules when you use selective imports in node you must include the index.js
part of the path. Meaning an import like import \"@pnp/sp/webs\"
in examples must be import \"@pnp/sp/webs/index.js\"
. Root level imports such as import { spfi } from \"@pnp/sp\"
remain correct. The samples in this section demonstrate this for their selective imports.
Note that the NodeJS integration relies on code in the module @pnp/nodejs
. It is therefore required that you import this near the beginning of your program, using simply
js import \"@pnp/nodejs\";
To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below.
mkdir \\temp\ncd \\temp\nopenssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'\nopenssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass\n
Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration.
"},{"location":"getting-started/#using-pnpsp-spfi-factory-interface-in-nodejs","title":"Using @pnp/spspfi
factory interface in NodeJS","text":"Version 3 of this library only supports ESModules. If you still require commonjs modules please check out version 2.
The first step is to install the packages that will be needed. You can read more about what each package does starting on the packages page.
npm i @pnp/sp @pnp/nodejs\n
Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:
\nimport { SPDefault } from \"@pnp/nodejs\";\nimport \"@pnp/sp/webs/index.js\";\nimport { readFileSync } from 'fs';\nimport { Configuration } from \"@azure/msal-node\";\n\nfunction() {\n // configure your node options (only once in your application)\n const buffer = readFileSync(\"c:/temp/key.pem\");\n\n const config: Configuration = {\n auth: {\n authority: \"https://login.microsoftonline.com/{tenant id or common}/\",\n clientId: \"{application (client) i}\",\n clientCertificate: {\n thumbprint: \"{certificate thumbprint, displayed in AAD}\",\n privateKey: buffer.toString(),\n },\n },\n };\n\n const sp = spfi().using(SPDefault({\n baseUrl: 'https://{my tenant}.sharepoint.com/sites/dev/',\n msal: {\n config: config,\n scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]\n }\n }));\n\n // make a call to SharePoint and log it in the console\n const w = await sp.web.select(\"Title\", \"Description\")();\n console.log(JSON.stringify(w, null, 4));\n}();\n
"},{"location":"getting-started/#using-pnpgraph-graphfi-factory-interface-in-nodejs","title":"Using @pnp/graph graphfi
factory interface in NodeJS","text":"Similar to the above you can also make calls to the Microsoft Graph API from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example.
npm i @pnp/graph @pnp/nodejs\n
Now we need to import what we'll need to call graph
import { graphfi } from \"@pnp/graph\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users/index.js\";\n\nfunction() {\n const graph = graphfi().using(GraphDefault({\n baseUrl: 'https://graph.microsoft.com',\n msal: {\n config: config,\n scopes: [ 'https://graph.microsoft.com/.default' ]\n }\n }));\n // make a call to Graph and get all the groups\n const userInfo = await graph.users.top(1)();\n console.log(JSON.stringify(userInfo, null, 4));\n}();\n
"},{"location":"getting-started/#node-project-using-typescript-producing-commonjs-modules","title":"Node project using TypeScript producing commonjs modules","text":"For TypeScript projects which output commonjs but need to import esm modules you will need to take a few additional steps to use the pnp esm modules. This is true of any esm module with a project structured in this way, not specific to PnP's implementation. It is very possible there are other configurations that make this work, but these steps worked in our testing. We have also provided a basic sample showing this setup.
You must install TypeScript @next or you will get errors using node12 module resolution. This may change but is the current behavior when we did our testing.
npm install -D typescript@next
The tsconfig file for your project should have the \"module\": \"CommonJS\"
and \"moduleResolution\": \"node12\",
settings in addition to whatever else you need.
tsconfig.json
{\n \"compilerOptions\": {\n \"module\": \"CommonJS\",\n \"moduleResolution\": \"node12\"\n}\n
You must then import the esm dependencies using the async import pattern. This works as expected with our selective imports, and vscode will pick up the intellisense as expected.
index.ts
import { settings } from \"./settings.js\";\n\n// this is a simple example as async await is not supported with commonjs output\n// at the root.\nsetTimeout(async () => {\n\n const { spfi } = await import(\"@pnp/sp\");\n const { SPDefault } = await import(\"@pnp/nodejs\");\n await import(\"@pnp/sp/webs/index.js\");\n\n const sp = spfi().using(SPDefault({\n baseUrl: settings.testing.sp.url,\n msal: {\n config: settings.testing.sp.msal.init,\n scopes: settings.testing.sp.msal.scopes\n }\n }));\n\n // make a call to SharePoint and log it in the console\n const w = await sp.web.select(\"Title\", \"Description\")();\n console.log(JSON.stringify(w, null, 4));\n\n}, 0);\n
Finally, when launching node you need to include the `` flag with a setting of 'node'.
node --experimental-specifier-resolution=node dist/index.js
Read more in the releated TypeScript Issue, TS pull request Adding the functionality, and the TS Docs.
"},{"location":"getting-started/#single-page-application-context","title":"Single Page Application Context","text":"In some cases you may be working in a client-side application that doesn't have context to the SharePoint site. In that case you will need to utilize the MSAL Client, you can get the details on creating that connection in this article.
"},{"location":"getting-started/#selective-imports","title":"Selective Imports","text":"This library has a lot of functionality and you may not need all of it. For that reason, we support selective imports which allow you to only import the parts of the sp or graph library you need, which reduces your overall solution bundle size - and enables treeshaking.
You can read more about selective imports.
"},{"location":"getting-started/#error-handling","title":"Error Handling","text":"This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.
"},{"location":"getting-started/#extending-the-library","title":"Extending the Library","text":"Because of the way the fluent library is designed by definition it's extendible. That means that if you want to build your own custom functions that extend the features of the library this can be done fairly simply. To get more information about creating your own custom extensions check out extending the library article.
"},{"location":"getting-started/#connect-to-a-different-web","title":"Connect to a different Web","text":"The new factory function allows you to create a connection to a different web maintaining the same setup as your existing interface. You have two options, either to 'AssignFrom' or 'CopyFrom' the base timeline's observers. The below example utilizes 'AssignFrom' but the method would be the same regadless of which route you choose. For more information on these behaviors see Core/Behaviors.
import { spfi, SPFx } from \"@pnp/sp\";\nimport { AssignFrom } from \"@pnp/core\";\nimport \"@pnp/sp/webs\";\n\n//Connection to the current context's Web\nconst sp = spfi(...);\n\n// Option 1: Create a new instance of Queryable\nconst spWebB = spfi({Other Web URL}).using(SPFx(this.context));\n\n// Option 2: Copy/Assign a new instance of Queryable using the existing\nconst spWebB = spfi({Other Web URL}).using(AssignFrom(sp.web));\n\n// Option 3: Create a new instance of Queryable using other credentials?\nconst spWebB = spfi({Other Web URL}).using(SPFx(this.context));\n\n// Option 4: Create new Web instance by using copying SPQuerable and new pointing to new web url (e.g. https://contoso.sharepoint.com/sites/Web2)\nconst web = Web([sp.web, {Other Web URL}]);\n
"},{"location":"getting-started/#next-steps","title":"Next Steps","text":"For more complicated authentication scnearios please review the article describing all of the available authentication methods.
"},{"location":"packages/","title":"Packages","text":"The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope.
The latest published version is .
"},{"location":"packages/#core","title":"Core","text":"Central to everything PnPjs builds on with utility methods, Timeline, the behavior plumbing, and the extendable framework.
npm install @pnp/core --save
This package provides a fluent SDK for calling the Microsoft Graph.
npm install @pnp/graph --save
A light-weight, subscribable logging framework.
npm install @pnp/logging --save
Provides an msal wrapper suitable for use with PnPjs's request structure.
npm install @pnp/msaljsclient --save
Provides functionality enabling the @pnp libraries within nodejs, including extension methods for working with streams.
npm install @pnp/nodejs --save
Extending Timeline this package provides the base functionality to create web requests in a fluent manner. It defines the available moments to which observers are subscribed for building the request.
npm install @pnp/queryable --save
This package provides a fluent SDK for calling SharePoint.
npm install @pnp/sp --save
This package provides a fluent SDK for calling SharePoint tenant admin APIs
npm install @pnp/sp-admin --save
It is our hope that the transition from version 2.* to 3.* will be as painless as possible, however given the transition we have made from a global sp object to an instance based object some architectural and inital setup changes will need to be addressed. In the following sections we endevor to provide an overview of what changes will be required. If we missed something, please let us know in the issues list so we can update the guide. Thanks!
For a full, detailed list of what's been added, updated, and removed please see our CHANGELOG
For a full sample project, utilizing SPFx 1.14 and V3 that showcases some of the more dramatic changes to the library check out this sample.
"},{"location":"transition-guide/#benefits-and-advancements-in-v3","title":"Benefits and Advancements in V3","text":"For version 2 the core themes were selective imports, a model based on factory functions & interfaces, and improving the docs. This foundation gave us the opportunity to re-write the entire request pipeline internals with minimal external library changes - showing a bit of long-term planning \ud83d\ude42. With version 3 your required updates are likely to only affect the initial configuration of the library, a huge accomplishment when updating the entire internals.
Our request pipeline remained largely unchanged since it was first written ~5 years ago, hard to change something so central to the library. The advantage of this update it is now easy for developers to inject their own logic into the request process. As always, this work was based on feedback over the years and understanding how we can be a better library. The new observer design allows you to customize every aspect of the request, in a much clearer way than was previously possible. In addition this work greatly reduced internal boilerplate code and optimized for library size. We reduced the size of sp & graph libraries by almost 2/3. As well we embraced a fully async design built around the new Timeline. Check out the new model around authoring observers and understand how they relate to moments. We feel this new architecture will allow far greater flexibility for consumers of the library to customize the behavior to exactly meet their needs.
We also used this as an opportunity to remove duplicate methods, clean up and improve our typings & method signatures, and drop legacy methods. Be sure to review the changelog. As always we do our best to minimize breaking changes but major versions are breaking versions.
We thank you for using the library. Your continued feedback drives these improvements, and we look forward to seeing what you build!
"},{"location":"transition-guide/#global-vs-instance-architecture","title":"Global vs Instance Architecture","text":"The biggest change in version 3 of the library is the movement away from the globally defined sp and graph objects. Starting in version 2.1.0 we added the concept of Isolated Runtime
which allowed you to create a separate instance of the global object that would have a separate configuration. We found that the implementation was finicky and prone to issues, so we have rebuilt the internals of the library from the ground up to better address this need. In doing so, we decided not to offer a global object at all.
Because of this change, any architecture that relies on the sp
or graph
objects being configured during initialization and then reused throughout the solution will need to be rethought. Essentially you have three options:
spfi
/graphfi
object wherever it's required.In other words, the sp
and graph
objects have been deprecated and will need to be replaced.
For more information on getting started with these new setup methods please see the Getting Started docs for a deeper look into the Queryable interface see Queryable.
"},{"location":"transition-guide/#assignfrom-and-copyfrom","title":"AssignFrom and CopyFrom","text":"With the new Querable instance architecture we have provided a way to branch from one instance to another. To do this we provide two methods: AssignFrom and CopyFrom. These methods can be helpful when you want to establish a new instance to which you might apply other behaviors but want to reuse the configuration from a source. To learn more about them check out the Core/Behaviors documentation.
"},{"location":"transition-guide/#dropping-get","title":"Dropping \".get()\"","text":"If you are still using the queryableInstance.get()
method of queryable you must replace it with a direct invoke call queryableInstance()
.
Another benefit of the new updated internals is a significantly streamlined and simplified process for batching requests. Essentially, the interface for SP and Graph now function the same.
A new module called \"batching\" will need to be imported which then provides the batched interface which will return a tuple with a new Querable instance and an execute function. To see more details check out Batching.
"},{"location":"transition-guide/#web-spfi","title":"Web -> SPFI","text":"In V2, to connect to a different web you would use the function
const web = Web({Other Web URL});\n
In V3 you would create a new instance of queryable connecting to the web of your choice. This new method provides you significantly more flexibility by not only allowing you to easily connect to other webs in the same tenant but also to webs in other tenants.
We are seeing a significant number of people report an error when using this method:
No observers registered for this request.
which results when it hasn't been updated to use the version 3 convention. Please see the examples below to pick the one that most suits your codebase.
import { spfi, SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\nconst spWebA = spfi().using(SPFx(this.context));\n\n// Easiest transition is to use the tuple pattern and the Web constructor which will copy all the observers from the object but set the url to the one provided\nconst spWebE = Web([spWebA.web, \"{Absolute URL of Other Web}\"]);\n\n// Create a new instance of Queryable\nconst spWebB = spfi(\"{Other Web URL}\").using(SPFx(this.context));\n\n// Copy/Assign a new instance of Queryable using the existing\nconst spWebC = spfi(\"{Other Web URL}\").using(AssignFrom(sp.web));\n\n// Create a new instance of Queryable using other credentials?\nconst spWebD = spfi(\"{Other Web URL}\").using(SPFx(this.context));\n\n
Please see the documentation for more information on the updated Web constructor.
"},{"location":"transition-guide/#dropping-commonjs-packages","title":"Dropping -Commonjs Packages","text":"Starting with v3 we are dropping the commonjs versions of all packages. Previously we released these as we worked to transition to esm and the current node didn't yet support esm. With esm now a supported module type, and having done the work to ensure they work in node we feel it is a good time to drop the -commonjs variants. Please see documentation on Getting started with NodeJS Project using TypeScript producing CommonJS modules
"},{"location":"azidjsclient/","title":"@pnp/azidjsclient","text":"This library provides a thin wrapper around the @azure/identity library to make it easy to integrate Azure Identity authentication in your solution.
You will first need to install the package:
npm install @pnp/azidjsclient --save
The following example shows how to configure the SPFI or GraphFI object using this behavior.
import { DefaultAzureCredential } from \"@azure/identity\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/sp\";\nimport { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { AzureIdentity } from \"@pnp/azidjsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/me\";\n\nconst credential = new DefaultAzureCredential();\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(\n SPDefault(),\n AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)\n);\n\nconst graph = graphfi().using(\n GraphDefault(),\n AzureIdentity(credential, [\"https://graph.microsoft.com/.default\"], null)\n);\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
Please see more scenarios in the authentication article.
"},{"location":"concepts/adv-clientside-pages/","title":"Client-Side Pages","text":"The client-side pages API included in this library is an implementation that was reverse engineered from the first-party API's and is unsupported by Microsoft. Given how flexible pages are we've done our best to give you the endpoints that will provide the functionality you need but that said, implementing these APIs is one of the more complicated tasks you can do.
It's especially important to understand the product team is constantly changing the features of pages and often that will also end up changing how the APIs that we've leveraged behave and because they are not offical third-party APIs this can cause our implementation to break. In order to fix those breaks we need to go back to the beginning and re-validate how the endpoints work searching for what has changed and then implementing those changes in our code. This is by no means simple. If you are reporting an issue with the pages API be aware that it may take significant time for us to unearth what is happening and fix it. Any research that you can provide when sharing your issue will go a long way in expediating that process, or better yet, if you can track it down and submit a PR with a fix we would be most greatful.
"},{"location":"concepts/adv-clientside-pages/#tricks-to-help-you-figure-out-how-to-add-first-party-web-parts-to-your-page","title":"Tricks to help you figure out how to add first-party web parts to your page","text":"This section is to offer you methods to be able to reverse engineer some of the first party web parts to help figure out how to add them to the page using the addControl
method.
Your first step needs to be creating a test page that you can inspect.
Fetch/XHR
and then type SavePage
to filter for the specific network calls.SavePageAsDraft
call and you can then look at the Payload
of that call CanvasContent1
property and copy that value. You can then paste it into a temporary file with the .json extension in your code editor so you can inspect the payload. The value is an array of objects, and each object (except the last) is the definition of the web part.Below is an example (as of the last change date of this document) of what the QuickLinks web part looks like. One key takeaway from this file is the webPartId
property which can be used when filtering for the right web part definition after getting a collection from sp.web.getClientsideWebParts();
.
Note that it could change at any time so please do not rely on this data, please use it as an example only.
{\n \"position\": {\n \"layoutIndex\": 1,\n \"zoneIndex\": 1,\n \"sectionIndex\": 1,\n \"sectionFactor\": 12,\n \"controlIndex\": 1\n },\n \"controlType\": 3,\n \"id\": \"00000000-58fd-448c-9e40-6691ce30e3e4\",\n \"webPartId\": \"c70391ea-0b10-4ee9-b2b4-006d3fcad0cd\",\n \"addedFromPersistedData\": true,\n \"reservedHeight\": 141,\n \"reservedWidth\": 909,\n \"webPartData\": {\n \"id\": \"c70391ea-0b10-4ee9-b2b4-006d3fcad0cd\",\n \"instanceId\": \"00000000-58fd-448c-9e40-6691ce30e3e4\",\n \"title\": \"Quick links\",\n \"description\": \"Show a collection of links to content such as documents, images, videos, and more in a variety of layouts with options for icons, images, and audience targeting.\",\n \"audiences\": [],\n \"serverProcessedContent\": {\n \"htmlStrings\": {},\n \"searchablePlainTexts\": {\n \"items[0].title\": \"PnPjs Title\"\n },\n \"imageSources\": {},\n \"links\": {\n \"baseUrl\": \"https://contoso.sharepoint.com/sites/PnPJS\",\n \"items[0].sourceItem.url\": \"/sites/PnPJS/SitePages/pnpjsTestV2.aspx\"\n },\n \"componentDependencies\": {\n \"layoutComponentId\": \"706e33c8-af37-4e7b-9d22-6e5694d92a6f\"\n }\n },\n \"dataVersion\": \"2.2\",\n \"properties\": {\n \"items\": [\n {\n \"sourceItem\": {\n \"guids\": {\n \"siteId\": \"00000000-4657-40d2-843d-3d6c72e647ff\",\n \"webId\": \"00000000-e714-4de6-88db-b0ac40d17850\",\n \"listId\": \"{00000000-8ED8-4E43-82BD-56794D9AB290}\",\n \"uniqueId\": \"00000000-6779-4979-adad-c120a39fe311\"\n },\n \"itemType\": 0,\n \"fileExtension\": \".ASPX\",\n \"progId\": null\n },\n \"thumbnailType\": 2,\n \"id\": 1,\n \"description\": \"\",\n \"fabricReactIcon\": {\n \"iconName\": \"heartfill\"\n },\n \"altText\": \"\",\n \"rawPreviewImageMinCanvasWidth\": 32767\n }\n ],\n \"isMigrated\": true,\n \"layoutId\": \"CompactCard\",\n \"shouldShowThumbnail\": true,\n \"imageWidth\": 100,\n \"buttonLayoutOptions\": {\n \"showDescription\": false,\n \"buttonTreatment\": 2,\n \"iconPositionType\": 2,\n \"textAlignmentVertical\": 2,\n \"textAlignmentHorizontal\": 2,\n \"linesOfText\": 2\n },\n \"listLayoutOptions\": {\n \"showDescription\": false,\n \"showIcon\": true\n },\n \"waffleLayoutOptions\": {\n \"iconSize\": 1,\n \"onlyShowThumbnail\": false\n },\n \"hideWebPartWhenEmpty\": true,\n \"dataProviderId\": \"QuickLinks\",\n \"webId\": \"00000000-e714-4de6-88db-b0ac40d17850\",\n \"siteId\": \"00000000-4657-40d2-843d-3d6c72e647ff\"\n }\n }\n}\n
At this point the only aspect of the above JSON payload you're going to be paying attention to is the webPartData
. We have exposed title
, description
, and dataVersion
as default properties of the ClientsideWebpart
class. In addition we provide a getProperties
, setProperties
, getServerProcessedContent
, setServerProcessedContent
methods. The difference in this case in these set base methods is that it will merge the object you pass into those methods with the values already on the object.
The code below gives a incomplete but demonstrative example of how you would extend the ClientsideWebpart class to provide an interface to build a custom class for the QuickLinks web part illustrated in our JSON payload above. This code assumes you have already added the control to a section. For more information about that step see the documentation for Add Controls
import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\n\n//Define interface based on JSON object above\ninterface IQLItem {\n sourceItem: {\n guids: {\n siteId: string;\n webId: string;\n listId: string;\n uniqueId: string;\n },\n itemType: number;\n fileExtension: string;\n progId: string;\n }\n thumbnailType: number;\n id: number;\n description: string;\n fabricReactIcon: { iconName: string; };\n altText: string;\n rawPreviewImageMinCanvasWidth: number;\n}\n\n// we create a class to wrap our functionality in a reusable way\nclass QuickLinksWebpart extends ClientsideWebpart {\n\n constructor(control: ClientsideWebpart) {\n super((<any>control).json);\n }\n\n // add property getter/setter for what we need, in this case items array within properties\n public get items(): IQLItem[] {\n return this.json.webPartData?.properties?.items || [];\n }\n\n public set items(value: IQLItem[]) {\n this.json.webPartData.properties?.items = value;\n }\n}\n\n// now we load our page\nconst page = await sp.web.loadClientsidePage(\"/sites/PnPJS/SitePages/QuickLinks-Web-Part-Test.aspx\");\n\n// get our part and pass it to the constructor of our wrapper class.\nconst part = new QuickLinksWebpart(page.sections[0].columns[0].getControl(0));\n\n//Need to set all the properties\npart.items = [{IQLItem_properties}];\n\nawait page.save();\n
"},{"location":"concepts/auth-browser/","title":"Authentication in a custom browser based application","text":"We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.
npm install @pnp/msaljsclient --save
At this time we're using version 1.x of the msal
library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.
Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
import { Configuration, AuthenticationParameters } from \"msal\";\n\nconst configuration: Configuration = {\n auth: {\n authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n clientId: \"{AAD Application Id/Client Id}\"\n }\n};\n\nconst authParams: AuthenticationParameters = {\n scopes: [\"https://graph.microsoft.com/.default\"] \n};\n
"},{"location":"concepts/auth-browser/#msal-browser","title":"MSAL + Browser","text":"import { spfi, SPBrowser } from \"@pnp/sp\";\nimport { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/users\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPBrowser(), MSAL(configuration, authParams));\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(GraphBrowser(), MSAL(configuration, authParams));\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-nodejs/","title":"Authentication in NodeJS","text":"We support MSAL for both browser and nodejs and Azure Identity for nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible configurations, but any parameters supplied are passed through to the underlying implementation.
Depending on which package you want to use you will need to install an additional package from the library because of the large dependencies.
We support MSAL through the msal-node library which is included by the @pnp/nodejs package.
For the Azure Identity package:
npm install @pnp/azidjsclient --save
We support Azure Identity through the @azure/identity library which simplifies the authentication process and makes it easy to integrate Azure Identity authentication in your solution.
"},{"location":"concepts/auth-nodejs/#msal-nodejs","title":"MSAL + NodeJS","text":"The SPDefault and GraphDefault exported by the nodejs library include MSAL and takes the parameters directly.
The following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
import { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/graph\";\nimport { Configuration, AuthenticationParameters } from \"msal\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/sp/webs\";\n\nconst configuration: Configuration = {\n auth: {\n authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n clientId: \"{AAD Application Id/Client Id}\"\n }\n};\n\nconst sp = spfi(\"{site url}\").using(SPDefault({\n msal: {\n config: configuration,\n scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n },\n}));\n\nconst graph = graphfi().using(GraphDefault({\n msal: {\n config: configuration,\n scopes: [\"https://graph.microsoft.com/.default\"],\n },\n}));\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-nodejs/#use-nodejs-msal-behavior-directly","title":"Use Nodejs MSAL behavior directly","text":"It is also possible to use the MSAL behavior directly if you are composing your own strategies.
import { SPDefault, GraphDefault, MSAL } from \"@pnp/nodejs\";\n\nconst sp = spfi(\"{site url}\").using(SPDefault(), MSAL({\n config: configuration,\n scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n}));\n\nconst graph = graphfi().using(GraphDefault(), MSAL({\n config: configuration,\n scopes: [\"https://graph.microsoft.com/.default\"],\n}));\n\n
"},{"location":"concepts/auth-nodejs/#azure-identity-nodejs","title":"Azure Identity + NodeJS","text":"The following sample shows how to pass the credential object to the AzureIdentity behavior including scopes.
import { DefaultAzureCredential } from \"@azure/identity\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/sp\";\nimport { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { AzureIdentity } from \"@pnp/azidjsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/users\";\n\n// We're using DefaultAzureCredential but the credential can be any valid `Credential Type`\nconst credential = new DefaultAzureCredential();\n\nconst sp = spfi(\"https://{tenant}.sharepoint.com/sites/dev\").using(\n SPDefault(),\n AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)\n);\n\nconst graph = graphfi().using(\n GraphDefault(),\n AzureIdentity(credential, [\"https://graph.microsoft.com/.default\"], null)\n);\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-spfx/","title":"Authentication in SharePoint Framework","text":"When building in SharePoint Framework you only need to provide the context to either sp or graph to ensure proper authentication. This will use the default SharePoint AAD application to manage scopes. If you would prefer to use a different AAD application please see the MSAL section below.
"},{"location":"concepts/auth-spfx/#spfx-sharepoint","title":"SPFx + SharePoint","text":"import { SPFx, spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst sp = spfi().using(SPFx(this.context));\n\nconst webData = await sp.web();\n
"},{"location":"concepts/auth-spfx/#spfx-graph","title":"SPFx + Graph","text":"import { SPFx, graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(SPFx(this.context));\n\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-spfx/#spfx-authentication-token","title":"SPFx + Authentication Token","text":"When using the SPFx behavior, authentication is handled by a cookie stored on the users client. In very specific instances some of the SharePoint methods will require a token. We have added a custom behavior to support that called SPFxToken
. This will require that you add the appropriate application role to the SharePoint Framework's package-solution.json
-> webApiPermissionRequests section where you will define the resource and scope for the request.
Here's an example of how you would build an instance of the SPFI that would include an Bearer Token in the header. Be advised if you use this instance to make calls to SharePoint endpoints that you have not specifically authorized they will fail.
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\n\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n
"},{"location":"concepts/auth-spfx/#msal-spfx","title":"MSAL + SPFx","text":"We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.
npm install @pnp/msaljsclient --save
At this time we're using version 1.x of the msal
library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.
Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:
import { SPFx as graphSPFx, graphfi } from \"@pnp/graph\";\nimport { SPFx as spSPFx, spfi } from \"@pnp/sp\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport { Configuration, AuthenticationParameters } from \"msal\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/sp/webs\";\n\nconst configuration: Configuration = {\n auth: {\n authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n clientId: \"{AAD Application Id/Client Id}\"\n }\n};\n\nconst authParams: AuthenticationParameters = {\n scopes: [\"https://graph.microsoft.com/.default\"] \n};\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(graphSPFx(this.context), MSAL(configuration, authParams));\nconst sp = spfi().using(spSPFx(this.context), MSAL(configuration, authParams));\n\nconst meData = await graph.me();\nconst webData = await sp.web();\n
"},{"location":"concepts/authentication/","title":"Authentication","text":"One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods.
We provide multiple ways to authenticate based on the scenario you're developing for, see one of these more detailed guides:
If you have more specific authentication requirements you can always build your own by using the new queryable pattern which exposes a dedicated auth moment. That moment expects observers with the signature:
async function(url, init) {\n\n // logic to apply authentication to the request\n\n return [url, init];\n}\n
You can follow this example as a general pattern to build your own custom authentication model. You can then wrap your authentication in a behavior for easy reuse.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using({behaviors});\nconst web = sp.web;\n\n// we will use custom auth on this web\nweb.on.auth(async function(url, init) {\n\n // some code to get a token\n const token = getToken();\n\n // set the Authorization header in the init (this init is what is passed directly to the fetch call)\n init.headers[\"Authorization\"] = `Bearer ${token}`;\n\n return [url, init];\n});\n
"},{"location":"concepts/batching-caching/","title":"Batching and Caching","text":"When optimizing for performance you can combine batching and caching to reduce the overall number of requests. On the first request any cachable data is stored as expected once the request completes. On subsequent requests if data is found in the cache it is returned immediately and that request is not added to the batch, in fact the batch will never register the request. This can work across many requests such that some returned cached data and others do not - the non-cached requests will be added to and processed by the batch as expected.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nbatchedSP.using(Caching());\n\nbatchedSP.web().then(console.log);\n\nbatchedSP.web.lists().then(console.log);\n\n// execute the first set of batched requests, no information is currently cached\nawait execute();\n\n// create a new batch\nconst [batchedSP2, execute2] = await sp.batched();\nbatchedSP2.using(Caching());\n\n// add the same requests - this simulates the user navigating away from or reloading the page\nbatchedSP2.web().then(console.log);\nbatchedSP2.web.lists().then(console.log);\n\n// executing the batch will return the cached values from the initial requests\nawait execute2();\n
In this second example we include an update to the web's title. Because non-get requests are never cached the update code will always run, but the results from the two get requests will resolve from the cache prior to being added to the batch.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nbatchedSP.using(Caching());\n\nbatchedSP.web().then(console.log);\n\nbatchedSP.web.lists().then(console.log);\n\n// this will never be cached\nbatchedSP.web.update({\n Title: \"dev web 1\",\n});\n\n// execute the first set of batched requests, no information is currently cached\nawait execute();\n\n// create a new batch\nconst [batchedSP2, execute2] = await sp.batched();\nbatchedSP2.using(Caching());\n\n// add the same requests - this simulates the user navigating away from or reloading the page\nbatchedSP2.web().then(console.log);\nbatchedSP2.web.lists().then(console.log);\n\n// this will never be cached\nbatchedSP2.web.update({\n Title: \"dev web 2\",\n});\n\n// executing the batch will return the cached values from the initial requests\nawait execute2();\n
"},{"location":"concepts/batching/","title":"Batching","text":"Where possible batching can significantly increase application performance by combining multiple requests to the server into one. This is especially useful when first establishing state, but applies for any scenario where you need to make multiple requests before loading or based on a user action. Batching is supported within the sp and graph libraries as shown below.
"},{"location":"concepts/batching/#sp-example","title":"SP Example","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nlet res = [];\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedSP.web().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedSP.web.lists.select(\"Title\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n ///Do something with the results\n}\n
"},{"location":"concepts/batching/#using-a-batched-web","title":"Using a batched web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedWeb, execute] = sp.web.batched();\n\nlet res = [];\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedWeb().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedWeb.lists.select(\"Title\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n ///Do something with the results\n}\n
Batches must be for the same web, you cannot combine requests from multiple webs into a batch.
"},{"location":"concepts/batching/#graph-example","title":"Graph Example","text":"import { graphfi } from \"@pnp/graph\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/batching\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\nconst [batchedGraph, execute] = graph.batched();\n\nlet res = [];\n\n// Pushes the results of these calls to an array\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedGraph.users().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedGraph.groups.select(\"Id\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i=0; i<res.length; i++){\n // Do something with the results\n}\n
"},{"location":"concepts/batching/#advanced-batching","title":"Advanced Batching","text":"For most cases the above usage should be sufficient, however you may be in a situation where you do not have convenient access to either an spfi instance or a web. Let's say for example you want to add a lot of items to a list and have an IList. You can in these cases use the createBatch function directly. We recommend as much as possible using the sp or web or graph batched method, but also provide this additional flexibility if you need it.
import { createBatch } from \"@pnp/sp/batching\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { IList } from \"@pnp/sp/lists\";\nimport \"@pnp/sp/items/list\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPDefault({ /* ... */ }));\n\n// in one part of your application you setup a list instance\nconst list: IList = sp.web.lists.getByTitle(\"MyList\");\n\n\n// in another part of your application you want to batch requests, but do not have the sp instance available, just the IList\n\n// note here the first part of the tuple is NOT the object, rather the behavior that enables batching. You must still register it with `using`.\nconst [batchedListBehavior, execute] = createBatch(list);\n// this list is now batching all its requests\nlist.using(batchedListBehavior);\n\n// these will all occur within a single batch\nlist.items.add({ Title: `1: ${getRandomString(4)}` });\nlist.items.add({ Title: `2: ${getRandomString(4)}` });\nlist.items.add({ Title: `3: ${getRandomString(4)}` });\nlist.items.add({ Title: `4: ${getRandomString(4)}` });\n\nawait execute();\n
This is of course also possible with the graph library as shown below.
import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\nconst users = graph.users;\n\nconst [batchedBehavior, execute] = createBatch(users);\nusers.using(batchedBehavior);\n\nusers();\n// we can only place the 'users' instance into the batch once\ngraph.users.using(batchedBehavior)();\ngraph.users.using(batchedBehavior)();\ngraph.users.using(batchedBehavior)();\n\nawait execute(); \n
"},{"location":"concepts/batching/#dont-reuse-objects-in-batching","title":"Don't reuse objects in Batching","text":"It shouldn't come up often, but you can not make multiple requests using the same instance of a queryable in a batch. Let's consider the incorrect example below:
The error message will be \"This instance is already part of a batch. Please review the docs at https://pnp.github.io/pnpjs/concepts/batching#reuse.\"
import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\n// gain a batched instance of the graph\nconst [batchedGraph, execute] = graph.batched();\n\n// we take a reference to the value returned from .users\nconst users = batchedGraph.users;\n\n// we invoke it, adding it to the batch (this is a request to /users), it will succeed\nusers();\n\n// we invoke it again, because this instance has already been added to the batch, this request will throw an error\nusers();\n\n// we execute the batch, this promise will resolve\nawait execute(); \n
To overcome this you can either start a new fluent chain or use the factory method. Starting a new fluent chain at any point will create a new instance. Please review the corrected sample below.
import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport { Users } from \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\n// gain a batched instance of the graph\nconst [batchedGraph, execute] = graph.batched();\n\n// we invoke a new instance of users from the batchedGraph\nbatchedGraph.users();\n\n// we again invoke a new instance of users from the batchedGraph, this is fine\nbatchedGraph.users();\n\nconst users = batchedGraph.users;\n// we can do this once\nusers();\n\n// by creating a new users instance using the Users factory we can keep adding things to the batch\n// users2 will be part of the same batch\nconst users2 = Users(users);\nusers2();\n\n// we execute the batch, this promise will resolve\nawait execute(); \n
In addition you cannot continue using a batch after execute. Once execute has resolved the batch is done. You should create a new batch using one of the described methods to conduct another batched call.
"},{"location":"concepts/batching/#case-where-batch-result-returns-an-object-that-can-be-invoked","title":"Case where batch result returns an object that can be invoked","text":"In the following example, the results of adding items to the list is an object with a type of IItemAddResult which is {data: any, item: IItem}
. Since version v1 the expectation is that the item
object is immediately usable to make additional queries. When this object is the result of a batched call, this was not the case so we have added additional code to reset the observers using the original base from witch the batch was created, mimicing the behavior had the IItem been created from that base withyout a batch involved. We use CopyFrom to ensure that we maintain the references to the InternalResolve and InternalReject events through the end of this timelines lifecycle.
import { createBatch } from \"@pnp/sp/batching\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { IList } from \"@pnp/sp/lists\";\nimport \"@pnp/sp/items/list\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPDefault({ /* ... */ }));\n\n// in one part of your application you setup a list instance\nconst list: IList = sp.web.lists.getByTitle(\"MyList\");\n\nconst [batchedListBehavior, execute] = createBatch(list);\n// this list is now batching all its requests\nlist.using(batchedListBehavior);\n\nlet res: IItemAddResult[] = [];\n\n// these will all occur within a single batch\nlist.items.add({ Title: `1: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `2: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `3: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `4: ${getRandomString(4)}` }).then(r => res.push(r));\n\nawait execute();\n\nlet newItems: IItem[] = [];\n\nfor(let i=0; i<res.length; i++){\n //This line will correctly resolve\n const newItem = await res[i].item.select(\"Title\")<{Title: string}>();\n newItems.push(newItem);\n}\n
"},{"location":"concepts/calling-other-endpoints/","title":"Calling other endpoints not currently implemented in PnPjs library","text":"If you find that there are endpoints that have not yet been implemented, or have changed in such a way that there are issues using the implemented endpoint, you can still make those calls and take advantage of the plumbing provided by the library.
"},{"location":"concepts/calling-other-endpoints/#sharepoint","title":"SharePoint","text":"To issue calls against the SharePoint REST endpoints you would use one of the existing operations:
To construct a call you will need to pass, to the operation call an SPQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.
Below are a couple of examples to get you started.
"},{"location":"concepts/calling-other-endpoints/#example-spget","title":"Example spGet","text":"Let's pretend that the getById method didn't exist on a lists items. The example below shows two methods for constructing our SPQueryable method.
The first is the easiest to use because, as the queryable documentation tells us, this will maintain all the registered observers on the original queryable instance. We would start with the queryable object closest to the endpoint we want to use, in this case list
. We do this because we need to construct the full URL that will be called. Using list
in this instance gives us the first part of the URL (e.g. https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')
) and then we can construct the remainder of the call by passing in a string.
The second method essentially starts from scratch where the user constructs the entire url and then registers observers on the SPQuerable instance. Then uses spGet to execute the call. There are many other variations to arrive at the same outcome, all are dependent on your requirements.
import { spfi } from \"@pnp/sp\";\nimport { AssignFrom } from \"@pnp/core\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { spGet, SPQueryable, SPFx } from \"@pnp/sp\";\n\n// Establish SPFI instance passing in the appropriate behavior to register the initial observers.\nconst sp = spfi(...);\n\n// create an instance of the items queryable\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get the item with an id of 1, easiest method\nconst item: any = await spGet(SPQueryable(list, \"items(1)\"));\n\n// get the item with an id of 1, constructing a new queryable and registering behaviors\nconst spQueryable = SPQueryable(\"https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)\").using(SPFx(this.context));\n\n// ***or***\n\n// For v3 the full url is require for SPQuerable when providing just a string\nconst spQueryable = SPQueryable(\"https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)\").using(AssignFrom(sp.web));\n\n// and then use spQueryable to make the request\nconst item: any = await spGet(spQueryable);\n
The resulting call will be to the endpoint: https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)
Let's now pretend that we need to get the changes on a list and want to call the getchanges
method off list.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { IChangeQuery, spPost, SPQueryable } from \"@pnp/sp\";\nimport { body } from \"@pnp/queryable\";\n\n// Establish SPFI instance passing in the appropriate behavior to register the initial observers.\nconst sp = spfi(...);\n\n\n// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore\nconst query: IChangeQuery = {\n Add: true,\n ChangeTokenEnd: null,\n ChangeTokenStart: null,\n DeleteObject: true,\n Rename: true,\n Restore: true,\n};\n\n// create an instance of the items queryable\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get the item with an id of 1\nconst changes: any = await spPost(SPQueryable(list, \"getchanges\"), body({query}));\n\n
The resulting call will be to the endpoint: https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/getchanges
To issue calls against the Microsoft Graph REST endpoints you would use one of the existing operations:
To construct a call you will need to pass, to the operation call an GraphQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.
Below are a couple of examples to get you started.
"},{"location":"concepts/calling-other-endpoints/#example-graphget","title":"Example graphGet","text":"Here's an example for getting the chats for a particular user. This uses the simplest method for constructing the graphQueryable which is to start with a instance of a queryable that is close to the endpoint we want to call, in this case user
and then adding the additional path as a string. For a more advanced example see spGet
above.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport { GraphQueryable, graphGet } from \"@pnp/graph\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\n// create an instance of the user queryable\nconst user = graph.users.getById('jane@contoso.com');\n\n// get the chats for the user\nconst chat: any = await graphGet(GraphQueryable(user, \"chats\"));\n
The results call will be to the endpoint: https://graph.microsoft.com/v1.0/users/jane@contoso.com/chats
This is an example of adding an event to a calendar.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/calendars\";\nimport { GraphQueryable, graphPost } from \"@pnp/graph\";\nimport { body, InjectHeaders } from \"@pnp/queryable\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\n// create an instance of the user queryable\nconst calendar = graph.users.getById('jane@contoso.com').calendar;\n\nconst props = {\n \"subject\": \"Let's go for lunch\",\n \"body\": {\n \"contentType\": \"HTML\",\n \"content\": \"Does noon work for you?\"\n },\n \"start\": {\n \"dateTime\": \"2017-04-15T12:00:00\",\n \"timeZone\": \"Pacific Standard Time\"\n },\n \"end\": {\n \"dateTime\": \"2017-04-15T14:00:00\",\n \"timeZone\": \"Pacific Standard Time\"\n },\n \"location\":{\n \"displayName\":\"Harry's Bar\"\n },\n \"attendees\": [\n {\n \"emailAddress\": {\n \"address\":\"samanthab@contoso.onmicrosoft.com\",\n \"name\": \"Samantha Booth\"\n },\n \"type\": \"required\"\n }\n ],\n \"allowNewTimeProposals\": true,\n \"transactionId\":\"7E163156-7762-4BEB-A1C6-729EA81755A7\"\n};\n\n// custom request init to add timezone header.\nconst graphQueryable = GraphQueryable(calendar, \"events\").using(InjectHeaders({\n \"Prefer\": 'outlook.timezone=\"Pacific Standard Time\"',\n}));\n\n// adds a new event to the user's calendar\nconst event: any = await graphPost(graphQueryable, body(props));\n
The results call will be to the endpoint: https://graph.microsoft.com/v1.0/users/jane@contoso.com/calendar/events
If you find you need to create an instance of Queryable (for either graph or SharePoint) that would hang off the root of the url you can use the AssignFrom
or CopyFrom
behaviors.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport { GraphQueryable, graphPost } from \"@pnp/graph\";\nimport { body, InjectHeaders } from \"@pnp/queryable\";\nimport { AssignFrom } from \"@pnp/core\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\nconst chatsQueryable = GraphQueryable(\"chats\").using(AssignFrom(graph.me));\n\nconst chat: any = await graphPost(chatsQueryable, body(chatBody));\n
The results call will be to the endpoint: https://graph.microsoft.com/v1.0/chats
With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles.
Scenarios could include:
You can see/clone a sample project of this example here.
"},{"location":"concepts/error-handling/","title":"Error Handling","text":"This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.
For 429, 503, and 504 errors we include retry logic within the library
"},{"location":"concepts/error-handling/#the-httprequesterror","title":"The HttpRequestError","text":"All errors resulting from executed web requests will be returned as an HttpRequestError
object which extends the base Error
. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.
For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch when using the async/await promises pattern.
import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\ntry {\n\n // get a list that doesn't exist\n const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n console.error(e);\n}\n
This will produce output like:
Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true}\n
This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.
"},{"location":"concepts/error-handling/#reading-the-response","title":"Reading the Response","text":"In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:
import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { HttpRequestError } from \"@pnp/queryable\";\n\ntry {\n\n // get a list that doesn't exist\n const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n // are we dealing with an HttpRequestError?\n if (e?.isHttpRequestError) {\n\n // we can read the json from the response\n const json = await (<HttpRequestError>e).response.json();\n\n // if we have a value property we can show it\n console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message);\n\n // add of course you have access to the other properties and can make choices on how to act\n if ((<HttpRequestError>e).status === 404) {\n console.error((<HttpRequestError>e).statusText);\n // maybe create the resource, or redirect, or fallback to a secondary data source\n // just ideas, handle any of the status codes uniquely as needed\n }\n\n } else {\n // not an HttpRequestError so we just log message\n console.log(e.message);\n }\n}\n
"},{"location":"concepts/error-handling/#logging-errors","title":"Logging errors","text":"Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.
import { Logger } from \"@pnp/logging\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\ntry {\n // get a list that doesn't exist\n const w = await sp.web.lists.getByTitle(\"no\")(); \n} catch (e) {\n\n Logger.error(e);\n}\n
You may want to read the response and customize the message as described above:
import { Logger } from \"@pnp/logging\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { HttpRequestError } from \"@pnp/queryable\";\n\ntry {\n // get a list that doesn't exist\n const w = await sp.web.lists.getByTitle(\"no\")(); \n} catch (e) {\n\n if (e?.isHttpRequestError) {\n\n // we can read the json from the response\n const data = await (<HttpRequestError>e).response.json();\n\n // parse this however you want\n const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message;\n\n // we use the status to determine a custom logging level\n const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;\n\n // create a custom log entry\n Logger.log({\n data,\n level,\n message,\n });\n\n } else {\n // not an HttpRequestError so we just log message\n Logger.error(e);\n }\n}\n
"},{"location":"concepts/error-handling/#putting-it-all-together","title":"Putting it All Together","text":"After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.
"},{"location":"concepts/error-handling/#errorhandlerts","title":"errorhandler.ts","text":"import { Logger } from \"@pnp/logging\";\nimport { HttpRequestError } from \"@pnp/queryable\";\nimport { hOP } from \"@pnp/core\";\n\nexport async function handleError(e: Error | HttpRequestError): Promise<void> {\n\n if (hOP(e, \"isHttpRequestError\")) {\n\n // we can read the json from the response\n const data = await (<HttpRequestError>e).response.json();\n\n // parse this however you want\n const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message;\n\n // we use the status to determine a custom logging level\n const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;\n\n // create a custom log entry\n Logger.log({\n data,\n level,\n message,\n });\n\n } else {\n // not an HttpRequestError so we just log message\n Logger.error(e);\n }\n}\n
"},{"location":"concepts/error-handling/#web-requestts","title":"web-request.ts","text":"import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { handleError } from \"./errorhandler\";\n\ntry {\n\n const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n await handleError(e);\n}\n
"},{"location":"concepts/error-handling/#web-request2ts","title":"web-request2.ts","text":"import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { handleError } from \"./errorhandler\";\n\ntry {\n\n const w = await sp.web.lists();\n\n} catch (e) {\n\n await handleError(e);\n}\n
"},{"location":"concepts/error-handling/#building-a-custom-error-handler","title":"Building a Custom Error Handler","text":"In Version 3 the library introduced the concept of a Timeline object and moments. One of the broadcast moments is error. To create your own custom error handler you can define a special handler for the error moment something like the following:
\n//Custom Error Behavior\nexport function CustomError(): TimelinePipe<Queryable> {\n\n return (instance: Queryable) => {\n\n instance.on.error((err) => {\n if (logging) {\n console.log(`\ud83d\uded1 PnPjs Testing Error - ${err.toString()}`);\n }\n });\n\n return instance;\n };\n}\n\n//Adding our CustomError behavior to our timline\n\nconst sp = spfi().using(SPDefault(this.context)).using(CustomError());\n
"},{"location":"concepts/invokable/","title":"Invokables","text":"For people who have been using the library since the early days you are familiar with the need to use the ()
method to invoke a method chain: Starting with v3 this is no longer possible, you must invoke the object directly to execute the default action for that class:
const lists = await sp.web.lists();\n
"},{"location":"concepts/nightly-builds/","title":"Nightly Builds","text":"Starting with version 3 we support nightly builds, which are built from the version-3 branch each evening and include all the changes merged ahead of a particular build. These are a great way to try out new features before a release, or get a fix or enhancement without waiting for the monthly builds.
You can install the nightly builds using the below examples. While we only show examples for sp
and graph
nightly builds are available for all packages.
npm install @pnp/sp@v3nightly --save\n
"},{"location":"concepts/nightly-builds/#microsoft-graph","title":"Microsoft Graph","text":"npm install @pnp/graph@v3nightly --save\n
Nightly builds are NOT monthly releases and aren't tested as deeply. We never intend to release broken code, but nightly builds may contain some code that is not entirely final or fully reviewed. As always if you encounter an issue please let us know, especially for nightly builds so we can be sure to address it before the next monthly release.
"},{"location":"concepts/project-preset/","title":"Project Config/Services Setup","text":"Due to the introduction of selective imports it can be somewhat frustrating to import all of the needed dependencies every time you need them across many files. Instead the preferred approach, especially for SPFx, is to create a project config file or establish a service to manage your PnPjs interfaces. Doing so centralizes the imports, configuration, and optionally extensions to PnPjs in a single place.
If you have multiple projects that share dependencies on PnPjs you can benefit from creating a custom bundle and using them across your projects.
These steps reference an SPFx solution, but apply to any solution.
"},{"location":"concepts/project-preset/#using-a-config-file","title":"Using a config file","text":"Within the src directory create a new file named pnpjs-config.ts
and copy in the below content.
import { WebPartContext } from \"@microsoft/sp-webpart-base\";\n\n// import pnp, pnp logging system, and any other selective imports needed\nimport { spfi, SPFI, SPFx } from \"@pnp/sp\";\nimport { LogLevel, PnPLogging } from \"@pnp/logging\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\";\n\nvar _sp: SPFI = null;\n\nexport const getSP = (context?: WebPartContext): SPFI => {\n if (context != null) {\n //You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency\n // The LogLevel set's at what level a message will be written to the console\n _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));\n }\n return _sp;\n};\n
To initialize the configuration, from the onInit
function (or whatever function runs first in your code) make a call to getSP passing in the SPFx context object (or whatever configuration you would require for your setup).
protected async onInit(): Promise<void> {\n this._environmentMessage = this._getEnvironmentMessage();\n\n super.onInit();\n\n //Initialize our _sp object that we can then use in other packages without having to pass around the context.\n // Check out pnpjsConfig.ts for an example of a project setup file.\n getSP(this.context);\n}\n
Now you can consume your configured _sp
object from anywhere else in your code by simply referencing the pnpjs-presets.ts
file via an import statement and then getting a local instance of the _sp
object using the getSP()
method without passing any context.
import { getSP } from './pnpjs-config.ts';\n...\nexport default class PnPjsExample extends React.Component<IPnPjsExampleProps, IIPnPjsExampleState> {\n\n private _sp: SPFI;\n\n constructor(props: IPnPjsExampleProps) {\n super(props);\n // set initial state\n this.state = {\n items: [],\n errors: []\n };\n this._sp = getSP();\n }\n\n ...\n\n}\n
"},{"location":"concepts/project-preset/#use-a-service-class","title":"Use a service class","text":"Because you do not have full access to the context object within a service you need to setup things a little differently.
import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\";\nimport { PageContext } from \"@microsoft/sp-page-context\";\nimport { AadTokenProviderFactory } from \"@microsoft/sp-http\";\nimport { spfi, SPFI, SPFx as spSPFx } from \"@pnp/sp\";\nimport { graphfi, GraphFI, SPFx as gSPFx } from \"@pnp/graph\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\nexport interface ISampleService {\n getLists(): Promise<any[]>;\n}\n\nexport class SampleService {\n\n public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);\n private _sp: SPFI;\n private _graph: GraphFI;\n\n constructor(serviceScope: ServiceScope) {\n\n serviceScope.whenFinished(() => {\n\n const pageContext = serviceScope.consume(PageContext.serviceKey);\n const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);\n\n //SharePoint\n this._sp = spfi().using(spSPFx({ pageContext }));\n\n //Graph\n this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));\n }\n\n public getLists(): Promise<any[]> {\n return this._sp.web.lists();\n }\n}\n
Depending on the architecture of your solution you can also opt to export the service as a global. If you choose this route you would need to modify the service to create an Init function where you would pass the service scope instead of doing so in the constructor. You would then export a constant that creates a global instance of the service.
export const mySampleService = new SampleService();\n
For a full sample, please see our PnPjs Version 3 Sample Project
"},{"location":"concepts/selective-imports/","title":"Selective Imports","text":"As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking.
This concept works well with custom bundling to create a shared package tailored exactly to your needs.
If you would prefer to not worry about selective imports please see the section on presets.
A quick note on how TypeScript handles type only imports. If you have a line like import { IWeb } from \"@pnp/sp/webs\"
everything will transpile correctly but you will get runtime errors because TS will see that line as a type only import and drop it. You need to include both import { IWeb } from \"@pnp/sp/webs\"
and import \"@pnp/sp/webs\"
to ensure the webs functionality is correctly included. You can see this in the last example below.
// the sp var now has almost nothing attached at import time and relies on\n\n// we need to import each of the pieces we need to \"attach\" them for chaining\n// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();\n
Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific
// the sp var now has almost nothing attached at import time and relies on\n\n// we need to import each of the pieces we need to \"attach\" them for chaining\n// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();\n
The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example:
// this import statement will attach content-type functionality to list, web, and item\nimport \"@pnp/sp/content-types\";\n\n// this import statement will only attach content-type functionality to web\nimport \"@pnp/sp/content-types/web\";\n
If you only need to access content types on the web object you can reduce size by only importing that piece.
The below example shows the need to import types and module augmentation separately.
// this will fail\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists\";\n\n// do this instead\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { IList } from \"@pnp/sp/lists\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst lists = await sp.web.lists();\n
"},{"location":"concepts/selective-imports/#presets","title":"Presets","text":"Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. Both libraries supply an \"all\" preset that will attach all of the available library functionality.
While the presets provided may be useful, we encourage you to look at making your own project presets or custom bundles as a preferred solution. Use of the presets in client-side solutions is not recommended.
"},{"location":"concepts/selective-imports/#sp","title":"SP","text":"import \"@pnp/sp/presets/all\";\n\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\n// sp.* will have all of the library functionality bound to it, tree shaking will not work\nconst lists = await sp.web.lists();\n
"},{"location":"concepts/selective-imports/#graph","title":"Graph","text":"The graph library contains a single preset, \"all\" mimicking the v1 structure.
import \"@pnp/graph/presets/all\";\nimport { graphfi } from \"@pnp/graph\";\n\n// placeholder for fully configuring the sp interface\nconst graph = graphfi();\n\n// graph.* will have all of the library functionality bound to it, tree shaking will not work\nconst me = await graph.me();\n
"},{"location":"concepts/typings/","title":"Typing Return Objects","text":"Whenever you make a request of the library for data from an object and utilize the select
method to reduce the size of the objects in the payload its preferable in TypeScript to be able to type that returned object. The library provides you a method to do so by using TypeScript's Generics declaration.
By defining the objects type in the <> after the closure of the select method the resulting object is typed.
.select(\"Title\")<{Title: string}>()\n
Below are some examples of typing the return payload:
const _sp = spfi().using(SPFx(this.context));\n\n //Typing the Title property of a field\n const field = await _sp.site.rootWeb.fields.getById(titleFieldId).select(\"Title\")<{ Title: string }>();\n\n //Typing the ParentWebUrl property of the selected list.\n const testList = await _sp.web.lists.getByTitle('MyList').select(\"ParentWebUrl\")<{ ParentWebUrl: string }>();\n
There have been discussions in the past around auto-typing based on select and the expected properties of the return object. We haven't done so for a few reasons: there is no even mildly complex way to account for all the possibilities expand introduces to selects, and if we \"ignore\" expand it effectively makes the select typings back to \"any\". Looking at template types etc, we haven't yet seen a way to do this that makes it worth the effort and doesn't introduce some other limitation or confusion.
"},{"location":"contributing/","title":"Contributing to PnPjs","text":"Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality.
Section Description NPM Scripts Explains the npm scripts and their uses Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request"},{"location":"contributing/#need-help","title":"Need Help?","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.
Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.
To learn more and register for an upcoming session, please visit the Sharing is Caring website.
"},{"location":"contributing/debug-tests/","title":"Writing Tests","text":"With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.
"},{"location":"contributing/debug-tests/#how-to-write-tests","title":"How to write Tests","text":"We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts:
import { getRandomString } from \"@pnp/core\";\nimport { testSettings } from \"../main\";\nimport { expect } from \"chai\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\nimport { IList } from \"@pnp/sp/lists\";\n\ndescribe(\"Items\", () => {\n\n // any tests that make a web request should be withing a block checking if web tests are enabled\n if (testSettings.enableWebTests) {\n\n // a block scoped var we will use across our tests\n let list: IList = null;\n\n // we use the before block to setup\n // executed before all the tests in this block, see the mocha docs for more details\n // mocha prefers using function vs arrow functions and this is recommended\n before(async function () {\n\n // execute a request to ensure we have a list\n const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\");\n list = ler.list;\n\n // in this case we want to have some items in the list for testing so we add those\n // only if the list was just created\n if (ler.created) {\n\n // add a few items to get started\n const batch = sp.web.createBatch();\n list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n await batch.execute();\n }\n });\n\n // this test has a label \"get items\" and is run via an async function\n it(\"get items\", async function () {\n\n // make a request for the list's items\n const items = await list.items();\n\n // report that we expect that result to be an array with more than 0 items\n expect(items.length).to.be.gt(0);\n });\n\n // ... remainder of code removed\n }\n}\n
"},{"location":"contributing/debug-tests/#general-guidelines-for-writing-tests","title":"General Guidelines for Writing Tests","text":"Now that you've written tests to cover your changes you'll need to update the docs.
"},{"location":"contributing/debugging/","title":"Debugging","text":"Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on.
Before proceeding be sure you have reviewed how to setup for local configuration and debugging.
"},{"location":"contributing/debugging/#debugging-library-features","title":"Debugging Library Features","text":"The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.
"},{"location":"contributing/debugging/#basic-sharepoint-testing","title":"Basic SharePoint Testing","text":"You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules.
All of the setup for the node client is handled within sp.ts using the settings from the local configuration.
"},{"location":"contributing/debugging/#basic-graph-testing","title":"Basic Graph Testing","text":"Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit.
All of the setup for the node client is handled within graph.ts using the settings from the local configuration.
"},{"location":"contributing/debugging/#how-to-create-a-debug-module","title":"How to: Create a Debug Module","text":"If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git.
Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content:
// note we can use the actual package names for our imports (ex: @pnp/logging)\nimport { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\";\n// using the all preset for simplicity in the example, selective imports work as expected\nimport { sp, ListEnsureResult } from \"@pnp/sp/presets/all\";\n\ndeclare var process: { exit(code?: number): void };\n\nexport async function MyDebug() {\n\n // configure your options\n // you can have different configs in different modules as needed for your testing/dev work\n sp.setup({\n sp: {\n fetchClientFactory: () => {\n return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret);\n },\n },\n });\n\n // run some debugging\n const list = await sp.web.lists.ensure(\"MyFirstList\");\n\n Logger.log({\n data: list.created,\n level: LogLevel.Info,\n message: \"Was list created?\",\n });\n\n if (list.created) {\n\n Logger.log({\n data: list.data,\n level: LogLevel.Info,\n message: \"Raw data from list creation.\",\n });\n\n } else {\n\n Logger.log({\n data: null,\n level: LogLevel.Info,\n message: \"List already existed!\",\n });\n }\n\n process.exit(0);\n}\n
"},{"location":"contributing/debugging/#update-maints-to-launch-your-module","title":"Update main.ts to launch your module","text":"First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this:
// ...\n\n// comment out the example\n// import { Example } from \"./example\";\n// Example();\n\nimport { MyDebug } from \"./mydebug\"\nMyDebug();\n\n// ...\n
Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)
"},{"location":"contributing/debugging/#debug","title":"Debug","text":"Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.
"},{"location":"contributing/debugging/#debug-module-next-steps","title":"Debug Module Next Steps","text":"Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.
"},{"location":"contributing/debugging/#in-browser-debugging","title":"In Browser Debugging","text":"You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js
, allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.
This will serve a package with ./debug/serve/main.ts as the entry.
npm run serve
Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
<script src=\"https://localhost:8080/assets/pnp.js\"></script>\n<div id=\"pnp-test\"></div>\n
You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.
"},{"location":"contributing/debugging/#debug_1","title":"Debug","text":"Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.
"},{"location":"contributing/debugging/#next-steps","title":"Next Steps","text":"You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser.
Now you can learn about extending the library.
"},{"location":"contributing/documentation/","title":"Documentation","text":"Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.
"},{"location":"contributing/documentation/#writing-docs","title":"Writing Docs","text":"Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.
"},{"location":"contributing/documentation/#building-docs-locally","title":"Building Docs Locally","text":"Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.
When executing the pip module on Windows you can prefix it with python -m. For example:
python -m pip install mkdocs-material
mkdocs serve
http://127.0.0.1:8000/
Please see the official mkdocs site for more details on working with mkdocs
"},{"location":"contributing/documentation/#next-steps","title":"Next Steps","text":"After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request!
"},{"location":"contributing/extending-the-library/","title":"Extending PnPjs","text":"This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property.
At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class.
// TypeScript property, returning an interface\npublic get webs(): IWebs {\n // using the Webs factory function and providing \"this\" as the first parameter\n return Webs(this);\n}\n
"},{"location":"contributing/extending-the-library/#understanding-factory-functions","title":"Understanding Factory Functions","text":"PnPjs v3 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form.
// create a constant which is a function of type ISPInvokableFactory having the name Webs\n// this is bound by the generic type param to return an IWebs instance\n// and it will use the _Webs concrete class to form the internal type of the invocable\nexport const Webs = spInvokableFactory<IWebs>(_Webs);\n
The ISPInvokableFactory type looks like:
export type ISPInvokableFactory<R = any> = (baseUrl: string | ISharePointQueryable, path?: string) => R;\n
And the matching graph type:
<R>(f: any): (baseUrl: string | IGraphQueryable, path?: string) => R\n
The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples.
import { SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\n// create a web from an absolute url\nconst web = Web(\"https://tenant.sharepoint.com\").using(SPFx(this.context));\n\n// as an example, create a new web using the first as a base\n// targets: https://tenant.sharepoint.com/sites/dev\nconst web2 = Web(web, \"sites/dev\");\n\n// or you can add any path components you want, here as an example we access the current user property\nconst cu = Web(web, \"currentuser\");\nconst currentUserInfo = cu();\n
Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings.
"},{"location":"contributing/extending-the-library/#class-structure","title":"Class structure","text":"Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method
/*\nThe concrete class implementation. This is never exported or shown directly\nto consumers of the library. It is wrapped by the Proxy we do expose.\n\nIt extends the _SharePointQueryableInstance class for which there is a matching\n_SharePointQueryableCollection. The generic parameter defines the return type\nof a get operation and the invoked result.\n\nClasses can have methods and properties as normal. This one has a single property as a simple example\n*/\nexport class _HubSite extends _SharePointQueryableInstance<IHubSiteInfo> {\n\n /**\n * Gets the ISite instance associated with this hub site\n */\n // the tag decorator is used to provide some additional telemetry on what methods are\n // being called.\n @tag(\"hs.getSite\")\n public async getSite(): Promise<ISite> {\n\n // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result\n const d = await this.select(\"SiteUrl\")();\n\n // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl\n return Site(d.SiteUrl);\n }\n}\n\n/*\nThis defines the interface we export and expose to consumers.\nIn most cases this extends the concrete object but may add or remove some methods/properties\nin special cases\n*/\nexport interface IHubSite extends _HubSite { }\n\n/*\nThis defines the HubSite factory function as discussed above\nbinding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite.\n\nThis is understood to mean that HubSite is a factory function that returns a types of IHubSite\nwhich the spInvokableFactory will create using _HubSite as the concrete underlying type.\n*/\nexport const HubSite = spInvokableFactory<IHubSite>(_HubSite);\n
"},{"location":"contributing/extending-the-library/#add-a-property","title":"Add a Property","text":"In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class.
export class _View extends _SharePointQueryableInstance<IViewInfo> {\n\n // ... other code removed\n\n // add the property, and provide a return type\n // return types should be interfaces\n public get fields(): IViewFields {\n // we use the ViewFields factory function supplying \"this\" as the first parameter\n // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator\n // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\")\n return ViewFields(this);\n }\n\n // ... other code removed\n}\n
There are many examples throughout the library that follow this pattern.
"},{"location":"contributing/extending-the-library/#add-a-method","title":"Add a Method","text":"Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method:
@defaultPath(\"items\")\nexport class _Items extends _SharePointQueryableCollection {\n\n /**\n * Gets an Item by id\n *\n * @param id The integer id of the item to retrieve\n */\n // we declare a method and set the return type to an interface\n public getById(id: number): IItem {\n // here we use the tag helper to add some telemetry to our request\n // we create a new IItem using the factory and appending the id value to the end\n // this gives us a valid url path to a single item .../items/getById(2)\n // we can then use the returned IItem to extend our chain or execute a request\n return tag.configure(Item(this).concat(`(${id})`), \"is.getById\");\n }\n\n // ... other code removed\n}\n
"},{"location":"contributing/extending-the-library/#web-request-method","title":"Web Request Method","text":"A second example is a method that performs a request. Here we use the _Item recycle method as an example:
/**\n * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item.\n */\n// we use the tag decorator to add telemetry\n@tag(\"i.recycle\")\n// we return a promise\npublic recycle(): Promise<string> {\n // we use the spPost method to post the request created by cloning our current instance IItem using\n // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle\n return spPost<string>(Item(this, \"recycle\"));\n}\n
"},{"location":"contributing/extending-the-library/#augment-using-selective-imports","title":"Augment Using Selective Imports","text":"To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available.
// import the addProp helper\nimport { addProp } from \"@pnp/queryable\";\n// import the _List concrete class from the types module (not the index!)\nimport { _List } from \"../lists/types\";\n// import the interface and factory we are going to add to the List\nimport { Items, IItems } from \"./types\";\n\n// This module declaration fixes up the types, allowing .items to appear in intellisense\n// when you import \"@pnp/sp/items/list\";\ndeclare module \"../lists/types\" {\n // we need to extend the concrete type\n interface _List {\n readonly items: IItems;\n }\n // we need to extend the interface\n // this may not be strictly necessary as the IList interface extends _List so it\n // should pick up the same additions, but we have seen in some cases this does seem\n // to be required. So we include it for safety as it will all be removed during\n // transpilation we don't need to care about the extra code\n interface IList {\n readonly items: IItems;\n }\n}\n\n// finally we add the property to the _List class\n// this method call says add a property to _List named \"items\" and that property returns a result using the Items factory\n// The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path\n// information to the property url\naddProp(_List, \"items\", Items);\n
"},{"location":"contributing/extending-the-library/#general-rules-for-extending-pnpjs","title":"General Rules for Extending PnPjs","text":"Now that you have extended the library you need to write a test to cover it!
"},{"location":"contributing/local-debug-configuration/","title":"Local Debugging Configuration","text":"This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).
"},{"location":"contributing/local-debug-configuration/#create-settingsjs","title":"Create settings.js","text":"Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings
You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag.
The following configuration file allows you to run all the tests that do not contact services.
var sets = {\n testing: {\n enableWebTests: false,\n }\n }\n\nmodule.exports = sets;\n
"},{"location":"contributing/local-debug-configuration/#test-your-setup","title":"Test your setup","text":"If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.
"},{"location":"contributing/npm-scripts/","title":"Supported NPM Scripts","text":"As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies.
This article outlines the current scripts we've implemented and how to use them, with available options and examples.
"},{"location":"contributing/npm-scripts/#start","title":"Start","text":"Executes the serve
command
npm start\n
"},{"location":"contributing/npm-scripts/#serve","title":"Serve","text":"Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node.
npm run serve\n
"},{"location":"contributing/npm-scripts/#test","title":"Test","text":"Runs the tests and coverage for the library.
More details on setting up MSAL for node.
"},{"location":"contributing/npm-scripts/#options","title":"Options","text":"There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands.
"},{"location":"contributing/npm-scripts/#test-a-single-package","title":"Test a Single Package","text":"--package
or -p
This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory.
# run only sp tests\nnpm test -- -p sp\n\n# run only logging tests\nnpm test -- -package logging\n
"},{"location":"contributing/npm-scripts/#run-a-single-test-file","title":"Run a Single Test File","text":"--single
or --s
You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags.
# run only sp web tests\nnpm test -- -p sp -s web\n\n# run only graph groups tests\nnpm test -- -package graph -single groups\n
"},{"location":"contributing/npm-scripts/#specify-a-site","title":"Specify a Site","text":"--site
By default every time you run the tests a new sub-site is created below the site specified in your settings file. You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option.
This option can be used with any or none of the other testing options.
# run only sp web tests with a certain site\nnpm test -- -p sp -s web --site https://some.site.com/sites/dev\n
"},{"location":"contributing/npm-scripts/#cleanup","title":"Cleanup","text":"--cleanup
If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site
as it will be deleted.
# clean up our testing site\nnpm test -- --cleanup\n
"},{"location":"contributing/npm-scripts/#logging","title":"Logging","text":"--logging
If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output.
# enable logging during testing\nnpm test -- --logging\n
You can also optionally set a log level of error, warning, info, or verbose:
# enable logging during testing in verbose (lots of info)\nnpm test -- --logging verbose\n
# enable logging during testing in error\nnpm test -- --logging error\n
"},{"location":"contributing/npm-scripts/#spverbose","title":"spVerbose","text":"--spverbose
This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags.
npm test -- --spverbose\n
"},{"location":"contributing/npm-scripts/#build","title":"build","text":"Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed.
npm run build\n
"},{"location":"contributing/npm-scripts/#package","title":"package","text":"Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published.
npm run package\n
"},{"location":"contributing/npm-scripts/#lint","title":"lint","text":"Runs the linter.
npm run lint\n
"},{"location":"contributing/npm-scripts/#clean","title":"clean","text":"Removes any generated folders from the working directory.
npm run clean\n
"},{"location":"contributing/pull-requests/","title":"Submitting Pull Requests","text":"Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release.
npm test
npm run lint
npm run package
If you need to target a PR for version 1, please target the \"version-1\" branch
"},{"location":"contributing/pull-requests/#sharing-is-caring-pull-request-guidance","title":"Sharing is Caring - Pull Request Guidance","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.
Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.
To learn more and register for an upcoming session, please visit the Sharing is Caring website.
"},{"location":"contributing/pull-requests/#next-steps","title":"Next Steps","text":"Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted.
Thank you for helping PnPjs grow and improve!!
"},{"location":"contributing/settings/","title":"Project Settings","text":"This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally.
The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root.
"},{"location":"contributing/settings/#settings-file-format","title":"Settings File Format","text":"The settings file is configured with MSAL authentication for both SharePoint and Graph. For more information coinfiguring MSAL please review the section in the authentication section for node.
MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling.
If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated.
You will need to create testing certs for the sample settings file below. Using the following code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. Copy the contents of the \"key.pem\" file and paste it in the privateKey
variable below. The gitignore
file in this repository will ignore the settings.js file.
Replace HereIsMySuperPass
with your own password
mkdir \\temp\ncd \\temp\nopenssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'\nopenssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass\n
const privateKey = `-----BEGIN RSA PRIVATE KEY-----\nyour private key, read from a file or included here\n-----END RSA PRIVATE KEY-----\n`;\n\nvar msalInit = {\n auth: {\n authority: \"https://login.microsoftonline.com/{tenant id}\",\n clientCertificate: {\n thumbprint: \"{certificate thumbnail}\",\n privateKey: privateKey,\n },\n clientId: \"{AAD App registration id}\",\n }\n}\n\nexport const settings = {\n testing: {\n enableWebTests: true,\n testUser: \"i:0#.f|membership|user@consto.com\",\n testGroupId:\"{ Microsoft 365 Group ID }\",\n sp: {\n url: \"{required for MSAL - absolute url of test site}\",\n notificationUrl: \"{ optional: notification url }\",\n msal: {\n init: msalInit,\n scopes: [\"https://{tenant}.sharepoint.com/.default\"]\n },\n },\n graph: {\n msal: {\n init: msalInit,\n scopes: [\"https://graph.microsoft.com/.default\"]\n },\n },\n },\n}\n\n
The settings object has a single sub-object testing
which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below.
The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.
name description msal Information about MSAL authentication setup"},{"location":"contributing/settings/#create-settingsjs-file","title":"Create Settings.js file","text":"If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.
"},{"location":"contributing/setup-dev-machine/","title":"Setting up your Developer Machine","text":"If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging.
"},{"location":"contributing/setup-dev-machine/#setup-your-development-environment","title":"Setup your development environment","text":"These steps will help you get your environment setup for contributing to the core library.
Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like.
Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).
This library requires node >= 10.18.0
On Windows: Install Python
[Optional] Install the tslint extension in VS Code:
All of our contributions come via pull requests and you'll need to fork the repository
Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.
Once you have the code locally, navigate to the root of the project in your console. Type the following command:
npm install
Follow the guidance to complete the one-time local configuration required to debug and run tests.
Then you can follow the guidance in the debugging article.
This article contains example recipes for building your own behaviors. We don't want to include every possible behavior within the library, but do want folks to have easy ways to solve the problems they encounter. If have ideas for a missing recipe, please let us know in the issues list OR submit them to this page as a PR! We want to see what types of behaviors folks build and will evaluate options to either include them in the main libraries, leave them here as a reference resource, or possibly release a community behaviors package.
Alternatively we encourage you to publish your own behaviors as npm packages to share with others!
"},{"location":"core/behavior-recipes/#proxy","title":"Proxy","text":"At times you might need to introduce a proxy for requests for debugging or other networking needs. You can easily do so using your proxy of choice in Nodejs. This example uses \"https-proxy-agent\" but would work similarly for any implementation.
proxy.ts
import { TimelinePipe } from \"@pnp/core\";\nimport { Queryable } from \"@pnp/queryable\";\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\n\nexport function Proxy(proxyInit: string): TimelinePipe<Queryable>;\n// eslint-disable-next-line no-redeclare\nexport function Proxy(proxyInit: any): TimelinePipe<Queryable>;\n// eslint-disable-next-line no-redeclare\nexport function Proxy(proxyInit: any): TimelinePipe<Queryable> {\n\n const proxy = typeof proxyInit === \"string\" ? new HttpsProxyAgent(proxyInit) : proxyInit;\n\n return (instance: Queryable) => {\n\n instance.on.pre(async (url, init, result) => {\n\n // we add the proxy to the request\n (<any>init).agent = proxy;\n\n return [url, init, result];\n });\n\n return instance;\n };\n}\n
usage
import { Proxy } from \"./proxy.ts\";\n\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\n// would work with graph library in the same manner\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault({\n msal: {\n config: { config },\n scopes: {scopes },\n },\n}), Proxy(\"http://127.0.0.1:8888\"));\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behavior-recipes/#add-querystring-to-bypass-request-caching","title":"Add QueryString to bypass request caching","text":"In some instances users express a desire to append something to the querystring to avoid getting cached responses back for requests. This pattern is an example of doing that in v3.
query-cache-param.ts
export function CacheBust(): TimelinePipe<Queryable> {\n\n return (instance: Queryable) => {\n\n instance.on.pre(async (url, init, result) => {\n\n url += url.indexOf(\"?\") > -1 ? \"&\" : \"?\";\n\n url += \"nonce=\" + encodeURIComponent(new Date().toISOString());\n\n return [url, init, result];\n });\n\n return instance;\n };\n}\n
usage
import { CacheBust } from \"./query-cache-param.ts\";\n\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\n// would work with graph library in the same manner\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault({\n msal: {\n config: { config },\n scopes: { scopes },\n },\n}), CacheBust());\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behavior-recipes/#acs-authentication","title":"ACS Authentication","text":"Starting with v3 we no longer provide support for ACS authentication within the library. However you may have a need (legacy applications, on-premises) to use ACS authentication while wanting to migrate to v3. Below you can find an example implementation of an Authentication observer for ACS. This is not a 100% full implementation, for example the tokens are not cached.
Whenever possible we encourage you to use AAD authentication and move away from ACS for securing your server-side applications.
export function ACS(clientId: string, clientSecret: string, authUrl = \"https://accounts.accesscontrol.windows.net\"): (instance: Queryable) => Queryable {\n\n const SharePointServicePrincipal = \"00000003-0000-0ff1-ce00-000000000000\";\n\n async function getRealm(siteUrl: string): Promise<string> {\n\n const url = combine(siteUrl, \"_vti_bin/client.svc\");\n\n const r = await nodeFetch(url, {\n \"headers\": {\n \"Authorization\": \"Bearer \",\n },\n \"method\": \"POST\",\n });\n\n const data: string = r.headers.get(\"www-authenticate\") || \"\";\n const index = data.indexOf(\"Bearer realm=\\\"\");\n return data.substring(index + 14, index + 50);\n }\n\n function getFormattedPrincipal(principalName: string, hostName: string, realm: string): string {\n let resource = principalName;\n if (hostName !== null && hostName !== \"\") {\n resource += \"/\" + hostName;\n }\n resource += \"@\" + realm;\n return resource;\n }\n\n async function getFullAuthUrl(realm: string): Promise<string> {\n\n const url = combine(authUrl, `/metadata/json/1?realm=${realm}`);\n\n const r = await nodeFetch(url, { method: \"GET\" });\n const json: { endpoints: { protocol: string; location: string }[] } = await r.json();\n\n const eps = json.endpoints.filter(ep => ep.protocol === \"OAuth2\");\n if (eps.length > 0) {\n return eps[0].location;\n }\n\n throw Error(\"Auth URL Endpoint could not be determined from data.\");\n }\n\n return (instance: Queryable) => {\n\n instance.on.auth.replace(async (url: URL, init: RequestInit) => {\n\n const realm = await getRealm(url.toString());\n const fullAuthUrl = await getFullAuthUrl(realm);\n\n const resource = getFormattedPrincipal(SharePointServicePrincipal, url.host, realm);\n const formattedClientId = getFormattedPrincipal(clientId, \"\", realm);\n\n const body: string[] = [];\n body.push(\"grant_type=client_credentials\");\n body.push(`client_id=${formattedClientId}`);\n body.push(`client_secret=${encodeURIComponent(clientSecret)}`);\n body.push(`resource=${resource}`);\n\n const r = await nodeFetch(fullAuthUrl, {\n body: body.join(\"&\"),\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n method: \"POST\",\n });\n\n const accessToken: { access_token: string } = await r.json();\n\n init.headers = { ...init.headers, Authorization: `Bearer ${accessToken.access_token}` };\n\n return [url, init];\n });\n\n return instance;\n };\n}\n
usage
import { CacheBust } from \"./acs-auth-behavior.ts\";\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault(), ACS(\"{client id}\", \"{client secret}\"));\n\n// you can optionally provide the authentication url, here using the one for China's sovereign cloud or an local url if working on-premises\n// const sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault(), ACS(\"{client id}\", \"{client secret}\", \"https://accounts.accesscontrol.chinacloudapi.cn\"));\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behaviors/","title":"@pnp/core : behaviors","text":"While you can always register observers to any Timeline's moments using the .on.moment
syntax, to make things easier we have included the ability to create behaviors. Behaviors define one or more observer registrations abstracted into a single registration. To differentiate behaviors are applied with the .using
method. The power of behaviors is they are composable so a behavior can apply other behaviors.
Let's create a behavior that will register two observers to a Timeline. We'll use error and log since they exist on all Timelines. In this example let's imagine we need to include some special secret into every lifecycle for logging to work. And we also want a company wide method to track errors. So we roll our own behavior.
import { Timeline, TimelinePipe } from \"@pnp/core\";\nimport { MySpecialLoggingFunction } from \"../mylogging.js\";\n\n// top level function allows binding of values within the closure\nexport function MyBehavior(specialSecret: string): TimelinePipe {\n\n // returns the actual behavior function that is applied to the instance\n return (instance: Timeline<any>) => {\n\n // register as many observers as needed\n instance.on.log(function (message: string, severity: number) {\n\n MySpecialLoggingFunction(message, severity, specialSecret);\n });\n\n instance.on.error(function (err: string | Error) {\n\n MySpecialLoggingFunction(typeof err === \"string\" ? err : err.toString(), severity, specialSecret);\n });\n\n return instance;\n };\n}\n\n// apply the behavior to a Timeline/Queryable\nobj.using(MyBehavior(\"HereIsMySuperSecretValue\"));\n
"},{"location":"core/behaviors/#composing-behaviors","title":"Composing Behaviors","text":"We encourage you to use our defaults, or create your own default behavior appropriate to your needs. You can see all of the behaviors available in @pnp/nodejs, @pnp/queryable, @pnp/sp, and @pnp/graph.
As an example, let's create our own behavior for a nodejs project. We want to call the graph, default to the beta endpoint, setup MSAL, and include a custom header we need for our environment. To do so we create a composed behavior consisting of graph's DefaultInit, graph's DefaultHeaders, nodejs's MSAL, nodejs's NodeFetchWithRetry, and queryable's DefaultParse & InjectHeaders. Then we can import this behavior into all our projects to configure them.
company-default.ts
import { TimelinePipe } from \"@pnp/core\";\nimport { DefaultParse, Queryable, InjectHeaders } from \"@pnp/queryable\";\nimport { DefaultHeaders, DefaultInit } from \"@pnp/graph\";\nimport { NodeFetchWithRetry, MSAL } from \"@pnp/nodejs\";\n\nexport function CompanyDefault(): TimelinePipe<Queryable> {\n\n return (instance: Queryable) => {\n\n instance.using(\n // use the default headers\n DefaultHeaders(),\n // use the default init, but change the base url to beta\n DefaultInit(\"https://graph.microsoft.com/beta\"),\n // use node-fetch with retry\n NodeFetchWithRetry(),\n // use the default parsing\n DefaultParse(),\n // inject our special header to all requests\n InjectHeaders({\n \"X-SomeSpecialToken\": \"{THE SPECIAL TOKEN VALUE}\",\n }),\n // setup node's MSAL with configuration from the environment (or any source)\n MSAL(process.env.MSAL_CONFIG));\n\n return instance;\n };\n}\n
index.ts
import { CompanyDefault } from \"./company-default.ts\";\nimport { graphfi } from \"@pnp/graph\";\n\n// we can consistently and easily setup our graph instance using a single behavior\nconst graph = graphfi().using(CompanyDefault());\n
You can easily share your composed behaviors across your projects using library components in SPFx, a company CDN, or an npm package.
"},{"location":"core/behaviors/#core-behaviors","title":"Core Behaviors","text":"This section describes two behaviors provided by the @pnp/core
library, AssignFrom and CopyFrom. Likely you won't often need them directly - they are used in some places internally - but they are made available should they prove useful.
This behavior creates a ref to the supplied Timeline implementation's observers and resets the inheriting flag. This means that changes to the parent, here being the supplied Timeline, will begin affecting the target to which this behavior is applied.
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { AssignFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will now hold a reference to the observers contained in source\n// changes to the subscribed observers in source will apply to target\n// anything that was added by \"MyCustomeBehavior\" will no longer be present\ntarget.using(AssignFrom(source.web));\n\n// you can always apply additional behaviors or register directly on the events\n// but once you modify target it will not longer ref source and changes to source will no longer apply\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n
"},{"location":"core/behaviors/#copyfrom","title":"CopyFrom","text":"Similar to AssignFrom, this method creates a copy of all the observers on the source and applies them to the target. This can be done either as a replace
or append
operation using the second parameter. The default is \"append\".
on
operation.on
operationBy design CopyFrom does NOT include moments defined by symbol keys.
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { CopyFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are maintained as the default behavior is to append\ntarget.using(CopyFrom(source.web));\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are removed\ntarget.using(CopyFrom(source.web, \"replace\"));\n\n// you can always apply additional behaviors or register directly on the events\n// with CopyFrom no reference to source is maintained\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n
As well CopyFrom
supports a filter parameter if you only want to copy the observers from a subset of moments. This filter is a predicate function taking a single string key and returning true if the observers from that moment should be copied to the target.
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { CopyFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are maintained as the default behavior is to append\ntarget.using(CopyFrom(source.web));\n\n// target will have the observers `auth` and `send` copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are removed\ntarget.using(CopyFrom(source.web, \"replace\", (k) => /(auth|send)/i.test(k)));\n\n// you can always apply additional behaviors or register directly on the events\n// with CopyFrom no reference to source is maintained\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n
"},{"location":"core/moments/","title":"@pnp/core : moments","text":"Moments are the name we use to describe the steps executed during a timeline lifecycle. They are defined on a plain object by a series of functions with the general form:
// the first argument is the set of observers subscribed to the given moment\n// the rest of the args vary by an interaction between moment and observer types and represent the args passed when emit is called for a given moment\nfunction (observers: any[], ...args: any[]): any;\n
Let's have a look at one of the included moment factory functions, which define how the moment interacts with its registered observers, and use it to understand a bit more on how things work. In this example we'll look at the broadcast moment, used to mimic a classic event where no return value is tracked, we just want to emit an event to all the subscribed observers.
// the broadcast factory function, returning the actual moment implementation function\n// The type T is used by the typings of Timeline to described the arguments passed in emit\nexport function broadcast<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {\n\n // this is the actual moment implementation, called each time a given moment occurs in the timeline\n return function (observers: T[], ...args: any[]): void {\n\n // we make a local ref of the observers\n const obs = [...observers];\n\n // we loop through sending the args to each observer\n for (let i = 0; i < obs.length; i++) {\n\n // note that within every moment and observer \"this\" will be the current timeline object\n Reflect.apply(obs[i], this, args);\n }\n };\n}\n
Let's use broadcast
in a couple examples to show how it works. You can also review the timeline article for a fuller example.
// our first type determines the type of the observers that will be regsitered to the moment \"first\"\ntype Broadcast1ObserverType = (this: Timeline<any>, message: string) => void;\n\n// our second type determines the type of the observers that will be regsitered to the moment \"second\"\ntype Broadcast2ObserverType = (this: Timeline<any>, value: number, value2: number) => void;\n\nconst moments = {\n first: broadcast<Broadcast1ObserverType>(),\n second: broadcast<Broadcast2ObserverType>(),\n} as const;\n
Now that we have defined two moments we can update our Timeline implementing class to emit each as we desire, as covered in the timeline article. Let's focus on the relationship between the moment definition and the typings inherited by on
and emit
in Timeline.
Because we want observers of a given moment to understand what arguments they will get the typings of Timeline are setup to use the type defining the moment's observer across all operations. For example, using our moment \"first\" from above. Each moment can be subscribed by zero or more observers.
// our observer function matches the type of Broadcast1ObserverType and the intellisense will reflect that.\n// If you want to change the signature you need only do so in the type Broadcast1ObserverType and the change will update the on and emit typings as well\n// here we want to reference \"this\" inside our observer function (preferred)\nobj.on.first(function (this: Timeline<any>, message: string) {\n // we use \"this\", which will be the current timeline and the default log method to emit a logging event\n this.log(message, 0);\n});\n\n// we don't need to reference \"this\" so we use arrow notation\nobj.on.first((message: string) => {\n console.log(message);\n});\n
Similarily for second
our observers would match Broadcast2Observer.
obj.on.second(function (this: Timeline<any>, value: number, value2: number) {\n // we use \"this\", which will be the current timeline and the default log method to emit a logging event\n this.log(`got value1: ${value} value2: ${value2}`, 0);\n});\n\nobj.on.second((value: number, value2: number) => {\n console.log(`got value1: ${value} value2: ${value2}`);\n});\n
"},{"location":"core/moments/#existing-moment-factories","title":"Existing Moment Factories","text":"You a already familiar with broadcast
which passes the emited args to all subscribed observers, this section lists the existing built in moment factories:
Creates a moment that passes the emited args to all subscribed observers. Takes a single type parameter defining the observer signature and always returns void. Is not async.
import { broadcast } from \"@pnp/core\";\n\n// can have any method signature you want that returns void, \"this\" will always be set\ntype BroadcastObserver = (this: Timeline<any>, message: string) => void;\n\nconst moments = {\n example: broadcast<BroadcastObserver>(),\n} as const;\n\nobj.on.example(function (this: Timeline<any>, message: string) {\n this.log(message, 0);\n});\n\nobj.emit.example(\"Hello\");\n
"},{"location":"core/moments/#asyncreduce","title":"asyncReduce","text":"Creates a moment that executes each observer asynchronously, awaiting the result and passes the returned arguments as the arguments to the next observer. This is very much like the redux pattern taking the arguments as the state which each observer may modify then returning a new state.
import { asyncReduce } from \"@pnp/core\";\n\n// can have any method signature you want, so long as it is async and returns a tuple matching in order the arguments, \"this\" will always be set\ntype AsyncReduceObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<[string, number]>;\n\nconst moments = {\n example: asyncReduce<AsyncReduceObserver>(),\n} as const;\n\nobj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {\n\n this.log(message, 0);\n\n // we can manipulate the values\n arg2++;\n\n // always return a tuple of the passed arguments, possibly modified\n return [arg1, arg2];\n});\n\nobj.emit.example(\"Hello\", 42);\n
"},{"location":"core/moments/#request","title":"request","text":"Creates a moment where the first registered observer is used to asynchronously execute a request, returning a single result. If no result is returned (undefined) no further action is taken and the result will be undefined (i.e. additional observers are not used).
This is used by us to execute web requets, but would also serve to represent any async request such as a database read, file read, or provisioning step.
import { request } from \"@pnp/core\";\n\n// can have any method signature you want, \"this\" will always be set\ntype RequestObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<string>;\n\nconst moments = {\n example: request<RequestObserver>(),\n} as const;\n\nobj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {\n\n this.log(`Sending request: ${arg1}`, 0);\n\n // request expects a single value result\n return `result value ${arg2}`;\n});\n\nobj.emit.example(\"Hello\", 42);\n
"},{"location":"core/moments/#additional-examples","title":"Additional Examples","text":""},{"location":"core/moments/#waitall","title":"waitall","text":"Perhaps you have a situation where you would like to wait until all of the subscribed observers for a given moment complete, but they can run async in parallel.
export function waitall<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<void> {\n\n // this is the actual moment implementation, called each time a given moment occurs in the timeline\n return function (observers: T[], ...args: any[]): void {\n\n // we make a local ref of the observers\n const obs = [...observers];\n\n const promises = [];\n\n // we loop through sending the args to each observer\n for (let i = 0; i < obs.length; i++) {\n\n // note that within every moment and observer \"this\" will be the current timeline object\n promises.push(Reflect.apply(obs[i], this, args));\n }\n\n return Promise.all(promises).then(() => void(0));\n };\n}\n
"},{"location":"core/moments/#first","title":"first","text":"Perhaps you would instead like to only get the result of the first observer to return.
export function first<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<any> {\n\n // this is the actual moment implementation, called each time a given moment occurs in the timeline\n return function (observers: T[], ...args: any[]): void {\n\n // we make a local ref of the observers\n const obs = [...observers];\n\n const promises = [];\n\n // we loop through sending the args to each observer\n for (let i = 0; i < obs.length; i++) {\n\n // note that within every moment and observer \"this\" will be the current timeline object\n promises.push(Reflect.apply(obs[i], this, args));\n }\n\n return Promise.race(promises);\n };\n}\n
"},{"location":"core/observers/","title":"@pnp/core : observers","text":"Observers are used to implement all of the functionality within a Timeline's moments. Each moment defines the signature of observers you can register, and calling the observers is orchestrated by the implementation of the moment. A few facts about observers:
error
moment.For details on implementing observers for Queryable, please see this article.
"},{"location":"core/observers/#observer-inheritance","title":"Observer Inheritance","text":"Timelines created from other timelines (i.e. how sp and graph libraries work) inherit all of the observers from the parent. Observers added to the parent will apply for all children.
When you make a change to the set of observers through any of the subscription methods outlined below that inheritance is broken. Meaning changes to the parent will no longer apply to that child, and changes to a child never affect a parent. This applies to ALL moments on change of ANY moment, there is no per-moment inheritance concept.
const sp = new spfi().using(...lots of behaviors);\n\n// web is current inheriting all observers from \"sp\"\nconst web = sp.web;\n\n// at this point web no longer inherits from \"sp\" and has its own observers\n// but still includes everything that was registered in sp before this call\nweb.on.log(...);\n\n// web2 inherits from sp as each invocation of .web creates a fresh IWeb instance\nconst web2 = sp.web;\n\n// list inherits from web's observers and will contain the extra `log` observer added above\nconst list = web.lists.getById(\"\");\n\n// this new behavior will apply to web2 and any subsequent objects created from sp\nsp.using(AnotherBehavior());\n\n// web will again inherit from sp through web2, the extra log handler is gone\n// list now ALSO is reinheriting from sp as it was pointing to web\nweb.using(AssignFrom(web2));\n// see below for more information on AssignFrom\n
"},{"location":"core/observers/#obserever-subscriptions","title":"Obserever Subscriptions","text":"All timeline moments are exposed through the on
property with three options for subscription.
This is the default, and adds your observer to the end of the array of subscribed observers.
obj.on.log(function(this: Queryable, message: string, level: number) {\n if (level > 1) {\n console.log(message);\n }\n});\n
"},{"location":"core/observers/#prepend","title":"Prepend","text":"Using prepend will place your observer as the first item in the array of subscribed observers. There is no gaurantee it will always remain first, other code can also use prepend.
obj.on.log.prepend(function(this: Queryable, message: string, level: number) {\n if (level > 1) {\n console.log(message);\n }\n});\n
"},{"location":"core/observers/#replace","title":"Replace","text":"Replace will remove all other subscribed observers from a moment and add the supplied observer as the only one in the array of subscribed observers.
obj.on.log.replace(function(this: Queryable, message: string, level: number) {\n if (level > 1) {\n console.log(message);\n }\n});\n
"},{"location":"core/observers/#toarray","title":"ToArray","text":"The ToArray method creates a cloned copy of the array of registered observers for a given moment. Note that because it is a clone changes to the returned array do not affect the registered observers.
const arr = obj.on.log.toArray();\n
"},{"location":"core/observers/#clear","title":"Clear","text":"This clears ALL observers for a given moment, returning true if any observers were removed, and false if no changes were made.
const didChange = obj.on.log.clear();\n
"},{"location":"core/observers/#special-behaviors","title":"Special Behaviors","text":"The core library includes two special behaviors used to help manage observer inheritance. The best case is to manage inheritance using the methods described above, but these provide quick shorthand to help in certain scenarios. These are AssignFrom and CopyFrom.
"},{"location":"core/storage/","title":"@pnp/core : storage","text":"This module provides a thin wrapper over the browser local and session storage. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.
"},{"location":"core/storage/#pnpclientstorage","title":"PnPClientStorage","text":"The main export of this module, contains properties representing local and session storage.
import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\nconst myvalue = storage.local.get(\"mykey\");\n
"},{"location":"core/storage/#pnpclientstoragewrapper","title":"PnPClientStorageWrapper","text":"Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.
import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\n\n// get a value from storage\nconst value = storage.local.get(\"mykey\");\n\n// put a value into storage\nstorage.local.put(\"mykey2\", \"my value\");\n\n// put a value into storage with an expiration\nstorage.local.put(\"mykey2\", \"my value\", new Date());\n\n// put a simple object into storage\n// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects\nstorage.local.put(\"mykey3\", {\n key: \"value\",\n key2: \"value2\",\n});\n\n// remove a value from storage\nstorage.local.delete(\"mykey3\");\n\n// get an item or add it if it does not exist\n// returns a promise in case you need time to get the value for storage\n// optionally takes a third parameter specifying the expiration\nstorage.local.getOrPut(\"mykey4\", () => {\n return Promise.resolve(\"value\");\n});\n\n// delete expired items\nstorage.local.deleteExpired();\n
"},{"location":"core/storage/#cache-expiration","title":"Cache Expiration","text":"The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished by explicitly calling the deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.
import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\n\n// session storage\nstorage.session.deleteExpired();\n\n// local storage\nstorage.local.deleteExpired();\n\n// this returns a promise, so you can perform some activity after the expired items are removed:\nstorage.local.deleteExpired().then(_ => {\n // init my application\n});\n
In previous versions we included code to automatically remove expired items. Due to a lack of necessity we removed that, but you can recreate the concept as shown below:
function expirer(timeout = 3000) {\n\n // session storage\n storage.session.deleteExpired();\n\n // local storage\n storage.local.deleteExpired();\n\n setTimeout(() => expirer(timeout), timeout);\n}\n
"},{"location":"core/timeline/","title":"@pnp/core : timeline","text":"Timeline provides base functionality for ochestrating async operations. A timeline defines a set of moments to which observers can be registered. Observers are functions that can act independently or together during a moment in the timeline. The model is event-like but each moment's implementation can be unique in how it interacts with the registered observers. Keep reading under Define Moments to understand more about what a moment is and how to create one.
The easiest way to understand Timeline is to walk through implementing a simple one below. You also review Queryable to see how we use Timeline internally to the library.
"},{"location":"core/timeline/#create-a-timeline","title":"Create a Timeline","text":"Implementing a timeline involves several steps, each explained below.
A timeline is made up of a set of moments which are themselves defined by a plain object with one or more properties, each of which is a function. You can use predefined moments, or create your own to meet your exact requirements. Below we define two moments within the MyMoments
object, first and second. These names are entirely your choice and the order moments are defined in the plain object carries no meaning.
The first
moment uses a pre-defined moment implementation asyncReduce
. This moment allows you to define a state based on the arguments of the observer function, in this case FirstObserver
. asyncReduce
takes those arguments, does some processing, and returns a promise resolving an array matching the input arguments in order and type with optionally changed values. Those values become the arguments to the next observer registered to that moment.
import { asyncReduce, ObserverAction, Timeline } from \"@pnp/core\";\n\n// the first observer is a function taking a number and async returning a number in an array\n// all asyncReduce observers must follow this pattern of returning async a tuple matching the args\nexport type FirstObserver = (this: any, counter: number) => Promise<[number]>;\n\n// the second observer is a function taking a number and returning void\nexport type SecondObserver = (this: any, result: number) => void;\n\n// this is a custom moment definition as an example.\nexport function report<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {\n\n return function (observers: T[], ...args: any[]): void {\n\n const obs = [...observers];\n\n // for this \n if (obs.length > 0) {\n Reflect.apply(obs[0], this, args);\n }\n };\n}\n\n// this plain object defines the moments which will be available in our timeline\n// the property name \"first\" and \"second\" will be the moment names, used when we make calls such as instance.on.first and instance.on.second\nconst TestingMoments = {\n first: asyncReduce<FirstObserver>(),\n second: report<SecondObserver>(),\n} as const;\n// note as well the use of as const, this allows TypeScript to properly resolve all the complex typings and not treat the plain object as \"any\"\n
"},{"location":"core/timeline/#subclass-timeline","title":"Subclass Timeline","text":"After defining our moments we need to subclass Timeline to define how those moments emit through the lifecycle of the Timeline. Timeline has a single abstract method \"execute\" you must implement. You will also need to provide a way for callers to trigger the protected \"start\" method.
// our implementation of timeline, note we use `typeof TestingMoments` and ALSO pass the testing moments object to super() in the constructor\nclass TestTimeline extends Timeline<typeof TestingMoments> {\n\n // we create two unique refs for our implementation we will use\n // to resolve the execute promise\n private InternalResolveEvent = Symbol.for(\"Resolve\");\n private InternalRejectEvent = Symbol.for(\"Reject\");\n\n constructor() {\n // we need to pass the moments to the base Timeline\n super(TestingMoments);\n }\n\n // we implement the execute the method to define when, in what order, and how our moments are called. This give you full control within the Timeline framework\n // to determine your implementation's behavior\n protected async execute(init?: any): Promise<any> {\n\n // we can always emit log to any subscribers\n this.log(\"Starting\", 0);\n\n // set our timeline to start in the next tick\n setTimeout(async () => {\n\n try {\n\n // we emit our \"first\" event\n let [value] = await this.emit.first(init);\n\n // we emit our \"second\" event\n [value] = await this.emit.second(value);\n\n // we reolve the execute promise with the final value\n this.emit[this.InternalResolveEvent](value);\n\n } catch (e) {\n\n // we emit our reject event\n this.emit[this.InternalRejectEvent](e);\n // we emit error to any subscribed observers\n this.error(e);\n }\n }, 0);\n\n // return a promise which we will resolve/reject during the timeline lifecycle\n return new Promise((resolve, reject) => {\n this.on[this.InternalResolveEvent].replace(resolve);\n this.on[this.InternalRejectEvent].replace(reject);\n });\n }\n\n // provide a method to trigger our timeline, this could be protected or called directly by the user, your choice\n public go(startValue = 0): Promise<number> {\n\n // here we take a starting number\n return this.start(startValue);\n }\n}\n
"},{"location":"core/timeline/#using-your-timeline","title":"Using your Timeline","text":"import { TestTimeline } from \"./file.js\";\n\nconst tl = new TestTimeline();\n\n// register observer\ntl.on.first(async (n) => [++n]);\n\n// register observer\ntl.on.second(async (n) => [++n]);\n\n// h === 2\nconst h = await tl.go(0);\n\n// h === 7\nconst h2 = await tl.go(5);\n
"},{"location":"core/timeline/#understanding-the-timeline-lifecycle","title":"Understanding the Timeline Lifecycle","text":"Now that you implemented a simple timeline let's take a minute to understand the lifecycle of a timeline execution. There are four moments always defined for every timeline: init, dispose, log, and error. Of these init and dispose are used within the lifecycle, while log and error are used as you need.
"},{"location":"core/timeline/#timeline-lifecycle","title":"Timeline Lifecycle","text":"As well the moments log and error exist on every Timeline derived class and can occur at any point during the lifecycle.
"},{"location":"core/timeline/#observer-inheritance","title":"Observer Inheritance","text":"Let's say that you want to contruct a system whereby you can create Timeline based instances from other Timeline based instances - which is what Queryable does. Imagine we have a class with a pseudo-signature like:
class ExampleTimeline extends Timeline<typeof SomeMoments> {\n\n // we create two unique refs for our implementation we will use\n // to resolve the execute promise\n private InternalResolveEvent = Symbol.for(\"Resolve\");\n private InternalRejectEvent = Symbol.for(\"Reject\");\n\n constructor(base: ATimeline) {\n\n // we need to pass the moments to the base Timeline\n super(TestingMoments, base.observers);\n }\n\n //...\n}\n
We can then use it like:
const tl1 = new ExampleTimeline();\ntl1.on.first(async (n) => [++n]);\ntl1.on.second(async (n) => [++n]);\n\n// at this point tl2's observer collection is a pointer to the same collection as tl1\nconst tl2 = new ExampleTimeline(tl1);\n\n// we add a second observer to first, it is applied to BOTH tl1 and tl2\ntl1.on.first(async (n) => [++n]);\n\n// BUT when we modify tl2's observers, either by adding or clearing a moment it begins to track its own collection\ntl2.on.first(async (n) => [++n]);\n
"},{"location":"core/util/","title":"@pnp/core : util","text":"This module contains utility methods that you can import individually from the core library.
"},{"location":"core/util/#combine","title":"combine","text":"Combines any number of paths, normalizing the slashes as required
import { combine } from \"@pnp/core\";\n\n// \"https://microsoft.com/something/more\"\nconst paths = combine(\"https://microsoft.com\", \"something\", \"more\");\n\n// \"also/works/with/relative\"\nconst paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\");\n
"},{"location":"core/util/#dateadd","title":"dateAdd","text":"Manipulates a date, please see the Stack Overflow discussion from which this method was taken.
import { dateAdd } from \"@pnp/core\";\n\nconst now = new Date();\n\nconst newData = dateAdd(now, \"minute\", 10);\n
"},{"location":"core/util/#getguid","title":"getGUID","text":"Creates a random guid, please see the Stack Overflow discussion from which this method was taken.
import { getGUID } from \"@pnp/core\";\n\nconst newGUID = getGUID();\n
"},{"location":"core/util/#getrandomstring","title":"getRandomString","text":"Gets a random string containing the number of characters specified.
import { getRandomString } from \"@pnp/core\";\n\nconst randomString = getRandomString(10);\n
"},{"location":"core/util/#hop","title":"hOP","text":"Shortcut for Object.hasOwnProperty. Determines if an object has a specified property.
import { HttpRequestError } from \"@pnp/queryable\";\nimport { hOP } from \"@pnp/core\";\n\nexport async function handleError(e: Error | HttpRequestError): Promise<void> {\n\n //Checks to see if the error object has a property called isHttpRequestError. Returns a bool.\n if (hOP(e, \"isHttpRequestError\")) {\n // Handle this type or error\n } else {\n // not an HttpRequestError so we do something else\n\n }\n}\n
"},{"location":"core/util/#jss","title":"jsS","text":"Shorthand for JSON.stringify
import { jsS } from \"@pnp/core\";\n\nconst s: string = jsS({ hello: \"world\" });\n
"},{"location":"core/util/#isarray","title":"isArray","text":"Determines if a supplied variable represents an array.
import { isArray } from \"@pnp/core\";\n\nconst x = [1, 2, 3];\n\nif (isArray(x)){\n console.log(\"I am an array\");\n} else {\n console.log(\"I am not an array\");\n}\n
"},{"location":"core/util/#isfunc","title":"isFunc","text":"Determines if a supplied variable represents a function.
import { isFunc } from \"@pnp/core\";\n\npublic testFunction() {\n console.log(\"test function\");\n return\n}\n\nif (isFunc(testFunction)){\n console.log(\"this is a function\");\n testFunction();\n}\n
"},{"location":"core/util/#isurlabsolute","title":"isUrlAbsolute","text":"Determines if a supplied url is absolute, returning true; otherwise returns false.
import { isUrlAbsolute } from \"@pnp/core\";\n\nconst webPath = 'https://{tenant}.sharepoint.com/sites/dev/';\n\nif (isUrlAbsolute(webPath)){\n console.log(\"URL is absolute\");\n}else{\n console.log(\"URL is not absolute\");\n}\n
"},{"location":"core/util/#objectdefinednotnull","title":"objectDefinedNotNull","text":"Determines if an object is defined and not null.
import { objectDefinedNotNull } from \"@pnp/core\";\n\nconst obj = {\n prop: 1\n};\n\nif (objectDefinedNotNull(obj)){\n console.log(\"Not null\");\n} else {\n console.log(\"Null\");\n}\n
"},{"location":"core/util/#stringisnullorempty","title":"stringIsNullOrEmpty","text":"Determines if a supplied string is null or empty.
import { stringIsNullOrEmpty } from \"@pnp/core\";\n\nconst x: string = \"hello\";\n\nif (stringIsNullOrEmpty(x)){\n console.log(\"Null or empty\");\n} else {\n console.log(\"Not null or empty\");\n}\n
"},{"location":"core/util/#gethashcode","title":"getHashCode","text":"Gets a (mostly) unique hashcode for a specified string.
Taken from: https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
import { getHashCode } from \"@pnp/core\";\n\nconst x: string = \"hello\";\n\nconst hash = getHashCode(x);\n
"},{"location":"core/util/#delay","title":"delay","text":"Provides an awaitable delay specified in milliseconds.
import { delay } from \"@pnp/core\";\n\n// wait 1 second\nawait delay(1000);\n\n// wait 10 second\nawait delay(10000);\n
"},{"location":"graph/behaviors/","title":"@pnp/graph : behaviors","text":"The article describes the behaviors exported by the @pnp/graph
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/nodejs.
The DefaultInit
behavior, itself a composed behavior includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit and ensures the request url is absolute.
import { graphfi, DefaultInit } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultInit());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#defaultheaders","title":"DefaultHeaders","text":"The DefaultHeaders
behavior uses InjectHeaders to set the Content-Type header.
import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nawait graph.users();\n
DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.
"},{"location":"graph/behaviors/#paged","title":"Paged","text":"Added in 3.4.0
The Paged behavior allows you to access the information in a collection through a series of pages. While you can use it directly, you will likely use the paged
method of the collections which handles things for you.
Note that not all entity types support count
and where it is unsupported it will return 0.
Basic example, read all users:
import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allUsers = [];\nlet users = await graph.users.top(300).paged();\n\nallUsers.push(...users.value);\n\nwhile (users.hasNext) {\n users = await users.next();\n allUsers.push(...users.value);\n}\n\nconsole.log(`All users: ${JSON.stringify(allUsers)}`);\n
Beyond the basics other query operations are supported such as filter and select.
import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allUsers = [];\nlet users = await graph.users.top(50).select(\"userPrincipalName\", \"displayName\").filter(\"startswith(displayName, 'A')\").paged();\n\nallUsers.push(...users.value);\n\nwhile (users.hasNext) {\n users = await users.next();\n allUsers.push(...users.value);\n}\n\nconsole.log(`All users: ${JSON.stringify(allUsers)}`);\n
And similarly for groups, showing the same pattern for different types of collections
import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allGroups = [];\nlet groups = await graph.groups.paged();\n\nallGroups.push(...groups.value);\n\nwhile (groups.hasNext) {\n groups = await groups.next();\n allGroups.push(...groups.value);\n}\n\nconsole.log(`All groups: ${JSON.stringify(allGroups)}`);\n
"},{"location":"graph/behaviors/#endpoint","title":"Endpoint","text":"This behavior is used to change the endpoint to which requests are made, either \"beta\" or \"v1.0\". This allows you to easily switch back and forth between the endpoints as needed.
import { graphfi, Endpoint } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst beta = graphfi().using(Endpoint(\"beta\"));\n\nconst vOne = graphfi().using(Endpoint(\"v1.0\"));\n\nawait beta.users();\n\nawait vOne.users();\n
It can also be used at any point in the fluid chain to switch an isolated request to a different endpoint.
import { graphfi, Endpoint } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// will point to v1 by default\nconst graph = graphfi().using();\n\nconst user = graph.users.getById(\"{id}\");\n\n// this only applies to the \"user\" instance now\nconst userInfoFromBeta = user.using(Endpoint(\"beta\"))();\n
Finally, if you always want to make your requests to the beta end point (as an example) it is more efficient to set it in the graphfi factory.
import { graphfi } from \"@pnp/graph\";\n\nconst beta = graphfi(\"https://graph.microsoft.com/beta\");\n
"},{"location":"graph/behaviors/#graphbrowser","title":"GraphBrowser","text":"A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.
The baseUrl prop can be used to configure the graph endpoint to which requests will be sent.
If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.
import { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphBrowser());\n\nawait graph.users();\n
You can also set a baseUrl. This is equivelent to calling graphfi with an absolute url.
import { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphBrowser({ baseUrl: \"https://graph.microsoft.com/v1.0\" }));\n\n// this is the same as the above, and maybe a litter easier to read, and is more efficient\n// const graph = graphfi(\"https://graph.microsoft.com/v1.0\").using(GraphBrowser());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#spfx","title":"SPFx","text":"This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx
is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. It also replaces any authentication present with a method to get a token from the SPFx aadTokenProviderFactory.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi(...).using(SPFx(this.context));\n\nawait graph.users();\n
Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.
import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'\nimport { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'\n\nconst sp = spfi().using(spSPFx(this.context));\nconst graph = graphfi().using(graphSPFx(this.context));\n
If you want to use a different form of authentication you can apply that behavior after SPFx
to override it. In this case we are using the client MSAL authentication.
Added in 3.12
Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi(...).using(SPFxToken(this.context));\n\nawait graph.users();\n
import { graphfi } from \"@pnp/graph\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi().using(SPFx(this.context), MSAL({ /* proper MSAL settings */}));\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#telemetry","title":"Telemetry","text":"This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.
You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.
import { graphfi, Telemetry } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(Telemetry());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#consistencylevel","title":"ConsistencyLevel","text":"Using this behavior you can set the consistency level of your requests. You likely won't need to use this directly as we include it where needed.
Basic usage:
import { graphfi, ConsistencyLevel } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(ConsistencyLevel());\n\nawait graph.users();\n
If in the future there is another value other than \"eventual\" you can supply it to the behavior. For now only \"eventual\" is a valid value, which is the default, so you do not need to pass it as a param.
import { graphfi, ConsistencyLevel } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(ConsistencyLevel(\"{level value}\"));\n\nawait graph.users();\n
"},{"location":"graph/bookings/","title":"@pnp/graph/bookings","text":"Represents the Bookings services available to a user.
You can learn more by reading the Official Microsoft Graph Documentation.
"},{"location":"graph/bookings/#ibookingcurrencies-ibookingcurrency-ibookingbusinesses-ibookingbusiness-ibookingappointments-ibookingappointment-ibookingcustomers-ibookingcustomer-ibookingservices-ibookingservice-ibookingstaffmembers-ibookingstaffmember-ibookingcustomquestions-ibookingcustomquestion","title":"IBookingCurrencies, IBookingCurrency, IBookingBusinesses, IBookingBusiness, IBookingAppointments, IBookingAppointment, IBookingCustomers, IBookingCustomer, IBookingServices, IBookingService, IBookingStaffMembers, IBookingStaffMember, IBookingCustomQuestions, IBookingCustomQuestion","text":""},{"location":"graph/bookings/#get-booking-currencies","title":"Get Booking Currencies","text":"Get the supported currencies
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\n\nconst graph = graphfi(...);\n\n// Get all the currencies\nconst currencies = await graph.bookingCurrencies();\n// get the details of the first currency\nconst currency = await graph.bookingCurrencies.getById(currencies[0].id)();\n
"},{"location":"graph/bookings/#work-with-booking-businesses","title":"Work with Booking Businesses","text":"Get the bookings businesses
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\n\nconst graph = graphfi(...);\n\n// Get all the businesses\nconst businesses = await graph.bookingBusinesses();\n// get the details of the first business\nconst business = graph.bookingBusinesses.getById(businesses[0].id)();\nconst businessDetails = await business();\n// get the business calendar\nconst calView = await business.calendarView(\"2022-06-01\", \"2022-08-01\")();\n// publish the business\nawait business.publish();\n// unpublish the business\nawait business.unpublish();\n
"},{"location":"graph/bookings/#work-with-booking-services","title":"Work with Booking Services","text":"Get the bookings business services
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingService } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business services\nconst services = await business.services();\n// add a service\nconst newServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};\nconst newService = services.add(newServiceDesc);\n// get service by id\nconst service = await business.services.getById({service id})();\n// update service\nconst updateServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};\nconst update = await business.services.getById({service id}).update(updateServiceDesc);\n// delete service\nawait business.services.getById({service id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-customers","title":"Work with Booking Customers","text":"Get the bookings business customers
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingCustomer } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business customers\nconst customers = await business.customers();\n// add a customer\nconst newCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};\nconst newCustomer = customers.add(newCustomerDesc);\n// get customer by id\nconst customer = await business.customers.getById({customer id})();\n// update customer\nconst updateCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};\nconst update = await business.customers.getById({customer id}).update(updateCustomerDesc);\n// delete customer\nawait business.customers.getById({customer id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-staffmembers","title":"Work with Booking StaffMembers","text":"Get the bookings business staffmembers
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingStaffMember } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business staff members\nconst staffmembers = await business.staffMembers();\n// add a staff member\nconst newStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};\nconst newStaffMember = staffmembers.add(newStaffMemberDesc);\n// get staff member by id\nconst staffmember = await business.staffMembers.getById({staff member id})();\n// update staff member\nconst updateStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};\nconst update = await business.staffMembers.getById({staff member id}).update(updateStaffMemberDesc);\n// delete staffmember\nawait business.staffMembers.getById({staff member id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-appointments","title":"Work with Booking Appointments","text":"Get the bookings business appointments
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingAppointment } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business appointments\nconst appointments = await business.appointments();\n// add a appointment\nconst newAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};\nconst newAppointment = appointments.add(newAppointmentDesc);\n// get appointment by id\nconst appointment = await business.appointments.getById({appointment id})();\n// cancel the appointment\nawait appointment.cancel();\n// update appointment\nconst updateAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};\nconst update = await business.appointments.getById({appointment id}).update(updateAppointmentDesc);\n// delete appointment\nawait business.appointments.getById({appointment id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-custom-questions","title":"Work with Booking Custom Questions","text":"Get the bookings business custom questions
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingCustomQuestion } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business custom questions\nconst customQuestions = await business.customQuestions();\n// add a custom question\nconst newCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};\nconst newCustomQuestion = customQuestions.add(newCustomQuestionDesc);\n// get custom question by id\nconst customquestion = await business.customQuestions.getById({customquestion id})();\n// update custom question\nconst updateCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};\nconst update = await business.customQuestions.getById({custom question id}).update(updateCustomQuestionDesc);\n// delete custom question\nawait business.customQuestions.getById({customquestion id}).delete();\n
"},{"location":"graph/calendars/","title":"@pnp/graph/calendars","text":"More information can be found in the official Graph documentation:
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars();\n\nconst myCalendars = await graph.me.calendars();\n\n
"},{"location":"graph/calendars/#get-a-specific-calendar-for-a-user","title":"Get a Specific Calendar For a User","text":"import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';\n\nconst calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)();\n\nconst myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();\n
"},{"location":"graph/calendars/#get-a-users-default-calendar","title":"Get a User's Default Calendar","text":"import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar();\n\nconst myCalendar = await graph.me.calendar();\n
"},{"location":"graph/calendars/#get-events-for-a-users-default-calendar","title":"Get Events For a User's Default Calendar","text":"import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\n// You can get the default calendar events\nconst events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events();\n// or get all events for the user\nconst events = await graph.users.getById('user@tenant.onmicrosoft.com').events();\n\n// You can get my default calendar events\nconst events = await graph.me.calendar.events();\n// or get all events for me\nconst events = await graph.me.events();\n
"},{"location":"graph/calendars/#get-events-by-id","title":"Get Events By ID","text":"You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar.
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA==';\n\nconst EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';\n\n// Get events by ID\nconst event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID);\n\nconst events = await graph.me.events.getByID(EventID);\n\n// Get an event by ID from a specific calendar\nconst event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID);\n\nconst events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);\n\n
"},{"location":"graph/calendars/#create-events","title":"Create Events","text":"This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add(\n{\n \"subject\": \"Let's go for lunch\",\n \"body\": {\n \"contentType\": \"HTML\",\n \"content\": \"Does late morning work for you?\"\n },\n \"start\": {\n \"dateTime\": \"2017-04-15T12:00:00\",\n \"timeZone\": \"Pacific Standard Time\"\n },\n \"end\": {\n \"dateTime\": \"2017-04-15T14:00:00\",\n \"timeZone\": \"Pacific Standard Time\"\n },\n \"location\":{\n \"displayName\":\"Harry's Bar\"\n },\n \"attendees\": [\n {\n \"emailAddress\": {\n \"address\":\"samanthab@contoso.onmicrosoft.com\",\n \"name\": \"Samantha Booth\"\n },\n \"type\": \"required\"\n }\n ]\n});\n
"},{"location":"graph/calendars/#update-events","title":"Update Events","text":"This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({\n reminderMinutesBeforeStart: 99,\n});\n
"},{"location":"graph/calendars/#delete-event","title":"Delete Event","text":"This will work on any IEvents
objects (e.g. anything accessed using an events
key).
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete();\n\nawait graph.me.events.getById(EVENT_ID).delete();\n
"},{"location":"graph/calendars/#get-calendar-for-a-group","title":"Get Calendar for a Group","text":"import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/groups';\n\nconst graph = graph.using(SPFx(this.context));\n\nconst calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();\n
"},{"location":"graph/calendars/#get-events-for-a-group","title":"Get Events for a Group","text":"import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/groups';\n\nconst graph = graphfi(...);\n\n// You can do one of\nconst events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events();\n// or\nconst events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();\n
"},{"location":"graph/calendars/#get-calendar-view","title":"Get Calendar View","text":"Gets the events in a calendar during a specified date range.
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\n// basic request, note need to invoke the returned queryable\nconst view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")();\n\n// you can use select, top, etc to filter your returned results\nconst view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)();\n\n// you can specify times along with the dates\nconst view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")();\n\nconst view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();\n
"},{"location":"graph/calendars/#find-rooms","title":"Find Rooms","text":"Gets the emailAddress
objects that represent all the meeting rooms in the user's tenant or in a specific room list.
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n// basic request, note need to invoke the returned queryable\nconst rooms1 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms()();\n// you can pass a room list to filter results\nconst rooms2 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms('roomlist@tenant.onmicrosoft.com')();\n// you can use select, top, etc to filter your returned results\nconst rooms3 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms().select('name').top(10)();\n
"},{"location":"graph/calendars/#get-event-instances","title":"Get Event Instances","text":"Get the instances (occurrences) of an event for a specified time range.
If the event is a seriesMaster
type, this returns the occurrences and exceptions of the event in the specified time range.
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\nconst event = graph.me.events.getById('');\n// basic request, note need to invoke the returned queryable\nconst instances = await event.instances(\"2020-01-01\", \"2020-03-01\")();\n// you can use select, top, etc to filter your returned results\nconst instances2 = await event.instances(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)();\n// you can specify times along with the dates\nconst instance3 = await event.instances(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); \n
"},{"location":"graph/cloud-communications/","title":"@pnp/graph/cloud-communications","text":"The ability to retrieve information about a user's presence, including their availability and user activity.
More information can be found in the official Graph documentation:
Gets a list of all the contacts for the user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/cloud-communications\";\n\nconst graph = graphfi(...);\n\nconst presenceMe = await graph.me.presence();\n\nconst presenceThem = await graph.users.getById(\"99999999-9999-9999-9999-999999999999\").presence();\n\n
"},{"location":"graph/cloud-communications/#get-presence-for-multiple-users","title":"Get presence for multiple users","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/cloud-communications\";\n\nconst graph = graphfi(...);\n\nconst presenceList = await graph.communications.getPresencesByUserId([\"99999999-9999-9999-9999-999999999999\"]);\n\n
"},{"location":"graph/columns/","title":"Graph Columns","text":"More information can be found in the official Graph documentation:
"},{"location":"graph/columns/#get-columns","title":"Get Columns","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteColumns = await graph.site.getById(\"{site identifier}\").columns();\nconst listColumns = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns();\nconst contentTypeColumns = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns();\n
"},{"location":"graph/columns/#get-columns-by-id","title":"Get Columns by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.getById(\"{column identifier}\")();\nconst listColumn = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\")();\nconst contentTypeColumn = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns.getById(\"{column identifier}\")();\n
"},{"location":"graph/columns/#add-a-columns-sites-and-list","title":"Add a Columns (Sites and List)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst sampleColumn: ColumnDefinition = {\n description: \"PnPTestColumn Description\",\n enforceUniqueValues: false,\n hidden: false,\n indexed: false,\n name: \"PnPTestColumn\",\n displayName: \"PnPTestColumn\",\n text: {\n allowMultipleLines: false,\n appendChangesToExistingText: false,\n linesForEditing: 0,\n maxLength: 255,\n },\n};\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.add(sampleColumn);\nconst listColumn = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns.add(sampleColumn);\n
"},{"location":"graph/columns/#add-a-column-reference-content-types","title":"Add a Column Reference (Content Types)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for content types\nimport \"@pnp/graph/content-ypes\";\n\nconst graph = graphfi(...);\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.getById(\"{column identifier}\")();\nconst contentTypeColumn = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns.addRef(siteColumn);\n
"},{"location":"graph/columns/#update-a-column-sites-and-list","title":"Update a Column (Sites and List)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst updatedSiteColumn = await site.columns.getById(\"{column identifier}\").update({ displayName: \"New Name\" });\nconst updateListColumn = await site.lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\").update({ displayName: \"New Name\" });\n
"},{"location":"graph/columns/#delete-a-column","title":"Delete a Column","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst siteColumn = await site.columns.getById(\"{column identifier}\").delete();\nconst listColumn = await site.lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\").delete();\nconst contentTypeColumn = await site.contentTypes.getById(\"{content type identifier}\").columns.getById(\"{column identifier}\").delete();\n
"},{"location":"graph/contacts/","title":"@pnp/graph/contacts","text":"The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graphfi(). Through the methods described you can add and edit both contacts and folders in a users Outlook.
More information can be found in the official Graph documentation:
To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"
"},{"location":"graph/contacts/#get-all-of-the-contacts","title":"Get all of the Contacts","text":"Gets a list of all the contacts for the user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\"\nimport \"@pnp/graph/contacts\"\n\nconst graph = graphfi(...);\n\nconst contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts();\n\nconst contacts2 = await graph.me.contacts();\n\n
"},{"location":"graph/contacts/#get-contact-by-id","title":"Get Contact by Id","text":"Gets a specific contact by ID for the user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)();\n\nconst contact2 = await graph.me.contacts.getById(contactID)();\n\n
"},{"location":"graph/contacts/#add-a-new-contact","title":"Add a new Contact","text":"Adds a new contact for the user.
import { graphfi } from \"@pnp/graph\";\nimport { EmailAddress } from \"@microsoft/microsoft-graph-types\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\nconst addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\n
"},{"location":"graph/contacts/#update-a-contact","title":"Update a Contact","text":"Updates a specific contact by ID for teh designated user
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" });\n\nconst updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" });\n\n
"},{"location":"graph/contacts/#delete-a-contact","title":"Delete a Contact","text":"Delete a contact from the list of contacts for a user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete();\n\nconst delContact2 = await graph.me.contacts.getById(contactID).delete();\n\n
"},{"location":"graph/contacts/#get-all-of-the-contact-folders","title":"Get all of the Contact Folders","text":"Get all the folders for the designated user's contacts
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders();\n\nconst contactFolders2 = await graph.me.contactFolders();\n\n
"},{"location":"graph/contacts/#get-contact-folder-by-id","title":"Get Contact Folder by Id","text":"Get a contact folder by ID for the specified user
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)();\n\nconst contactFolder2 = await graph.me.contactFolders.getById(folderID)();\n\n
"},{"location":"graph/contacts/#add-a-new-contact-folder","title":"Add a new Contact Folder","text":"Add a new folder in the users contacts
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\";\n\nconst addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID);\n\nconst addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID);\n\n
"},{"location":"graph/contacts/#update-a-contact-folder","title":"Update a Contact Folder","text":"Update an existing folder in the users contacts
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });\n\nconst updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });\n\n
"},{"location":"graph/contacts/#delete-a-contact-folder","title":"Delete a Contact Folder","text":"Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete();\n\nconst delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();\n\n
"},{"location":"graph/contacts/#get-all-of-the-contacts-from-the-contact-folder","title":"Get all of the Contacts from the Contact Folder","text":"Get all the contacts in a folder
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts();\n\nconst contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();\n\n
"},{"location":"graph/contacts/#get-child-folders-of-the-contact-folder","title":"Get Child Folders of the Contact Folder","text":"Get child folders from contact folder
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders();\n\nconst childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();\n\n
"},{"location":"graph/contacts/#add-a-new-child-folder","title":"Add a new Child Folder","text":"Add a new child folder to a contact folder
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);\n\nconst addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);\n
"},{"location":"graph/contacts/#get-child-folder-by-id","title":"Get Child Folder by Id","text":"Get child folder by ID from user contacts
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\nconst subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\";\n\nconst childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)();\n\nconst childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();\n
"},{"location":"graph/contacts/#add-contact-in-child-folder-of-contact-folder","title":"Add Contact in Child Folder of Contact Folder","text":"Add a new contact to a child folder
import { graphfi } from \"@pnp/graph\";\nimport { EmailAddress } from \"./@microsoft/microsoft-graph-types\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\nconst subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\";\n\nconst addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\nconst addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\n
"},{"location":"graph/content-types/","title":"Graph Content Types","text":"More information can be found in the official Graph documentation:
"},{"location":"graph/content-types/#get-content-types","title":"Get Content Types","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes();\nconst listContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes();\n
"},{"location":"graph/content-types/#get-content-types-by-id","title":"Get Content Types by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\")();\nconst listContentType = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\")();\n
"},{"location":"graph/content-types/#add-a-content-type-site","title":"Add a Content Type (Site)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst sampleContentType: ContentType = {\n name: \"PnPTestContentType\",\n description: \"PnPTestContentType Description\",\n base: {\n name: \"Item\",\n id: \"0x01\",\n },\n group: \"PnPTest Content Types\",\n id: \"0x0100CDB27E23CEF44850904C80BD666FA645\",\n};\n\nconst siteContentType = await graph.sites.getById(\"{site identifier}\").contentTypes.add(sampleContentType);\n
"},{"location":"graph/content-types/#add-a-content-type-copy-list","title":"Add a Content Type - Copy (List)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/lists\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\n//Get a list of compatible site content types for the list\nconst siteContentType = await graph.site.getById(\"{site identifier}\").getApplicableContentTypesForList(\"{list identifier}\")();\n//Get a specific content type from the site.\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\")();\nconst listContentType = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.addCopy(siteContentType);\n
"},{"location":"graph/content-types/#update-a-content-type-sites-and-list","title":"Update a Content Type (Sites and List)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst updatedSiteContentType = await site.contentTypes.getById(\"{content type identifier}\").update({ description: \"New Description\" });\nconst updateListContentType = await site.lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\").update({ description: \"New Description\" });\n
"},{"location":"graph/content-types/#delete-a-content-type","title":"Delete a Content Type","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nawait graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").delete();\nawait graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\").delete();\n
"},{"location":"graph/content-types/#get-compatible-content-types-from-hub","title":"Get Compatible Content Types from Hub","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst listContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getCompatibleHubContentTypes();\n
"},{"location":"graph/content-types/#addsync-content-types-from-hub-site-and-list","title":"Add/Sync Content Types from Hub (Site and List)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst hubSiteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.addCopyFromContentTypeHub(hubSiteContentTypes[0].Id);\n\nconst hubListContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst listContentType = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.addCopyFromContentTypeHub(hubListContentTypes[0].Id);\n
"},{"location":"graph/content-types/#site-content-type-ispublished-publish-unpublish","title":"Site Content Type (isPublished, Publish, Unpublish)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteContentType = graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\");\nconst isPublished = await siteContentType.isPublished();\nawait siteContentType.publish();\nawait siteContentType.unpublish();;\n
"},{"location":"graph/content-types/#associate-content-type-with-hub-sites","title":"Associate Content Type with Hub Sites","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst hubSiteUrls: string[] = [hubSiteUrl1, hubSiteUrl2, hubSiteUrl3];\nconst propagateToExistingLists = true;\n// NOTE: the site must be the content type hub\nconst contentTypeHub = graph.site.getById(\"{content type hub site identifier}\");\nconst siteContentType = await contentTypeHub.contentTypes.getById(\"{content type identifier}\").associateWithHubSites(hubSiteUrls, propagateToExistingLists);\n
"},{"location":"graph/content-types/#copy-a-file-to-a-default-content-location-in-a-content-type","title":"Copy a file to a default content location in a content type","text":"Not fully implemented, requires Files support
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\n// Not fully implemented\nconst sourceFile: ItemReference = {};\nconst destinationFileName: string = \"NewFileName\";\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst siteContentType = await site.contentTypes.getById(\"{content type identifier}\").copyToDefaultContentLocation(sourceFile, destinationFileName);\n
"},{"location":"graph/directoryobjects/","title":"@pnp/graph/directoryObjects","text":"Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types.
More information can be found in the official Graph documentation:
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf();\n\nconst memberOf2 = await graph.me.memberOf();\n\n
"},{"location":"graph/directoryobjects/#return-all-the-groups-the-user-group-or-directoryobject-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","title":"Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups();\n\nconst memberGroups2 = await graph.me.getMemberGroups();\n\n// Returns only security enabled groups\nconst memberGroups3 = await graph.me.getMemberGroups(true);\n\nconst memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();\n\n
"},{"location":"graph/directoryobjects/#returns-all-the-groups-administrative-units-and-directory-roles-that-a-user-group-or-directory-object-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","title":"Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects();\n\nconst memberObjects2 = await graph.me.getMemberObjects();\n\n// Returns only security enabled groups\nconst memberObjects3 = await graph.me.getMemberObjects(true);\n\nconst memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();\n
"},{"location":"graph/directoryobjects/#check-for-membership-in-a-specified-list-of-groups","title":"Check for membership in a specified list of groups","text":"And returns from that list those groups of which the specified user, group, or directory object is a member
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n\nconst checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n\nconst checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n
"},{"location":"graph/directoryobjects/#get-directoryobject-by-id","title":"Get directoryObject by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/directory-objects\";\n\nconst graph = graphfi(...);\n\nconst dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');\n\n
"},{"location":"graph/directoryobjects/#delete-directoryobject","title":"Delete directoryObject","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/directory-objects\";\n\nconst graph = graphfi(...);\n\nconst deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()\n\n
"},{"location":"graph/groups/","title":"@pnp/graph/groups","text":"Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent.
Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups.
You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation.
"},{"location":"graph/groups/#igroup-igroups","title":"IGroup, IGroups","text":""},{"location":"graph/groups/#add-a-group","title":"Add a Group","text":"Add a new group.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport { GroupType } from '@pnp/graph/groups';\n\nconst graph = graphfi(...);\n\nconst groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365);\nconst group = await groupAddResult.group();\n
"},{"location":"graph/groups/#delete-a-group","title":"Delete a Group","text":"Deletes an existing group.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete();\n
"},{"location":"graph/groups/#update-group-properties","title":"Update Group Properties","text":"Updates an existing group.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue});\n
"},{"location":"graph/groups/#add-favorite","title":"Add favorite","text":"Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite();\n
"},{"location":"graph/groups/#remove-favorite","title":"Remove favorite","text":"Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite();\n
"},{"location":"graph/groups/#reset-unseen-count","title":"Reset Unseen Count","text":"Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount();\n
"},{"location":"graph/groups/#subscribe-by-mail","title":"Subscribe By Mail","text":"Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail();\n
"},{"location":"graph/groups/#unsubscribe-by-mail","title":"Unsubscribe By Mail","text":"Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail();\n
"},{"location":"graph/groups/#get-calendar-view","title":"Get Calendar View","text":"Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst startDate = new Date(\"2020-04-01\");\nconst endDate = new Date(\"2020-03-01\");\n\nconst events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate);\n
"},{"location":"graph/groups/#group-photo-operations","title":"Group Photo Operations","text":"See Photos
"},{"location":"graph/groups/#group-membership","title":"Group Membership","text":"Get the members and/or owners of a group.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/members\";\n\nconst graph = graphfi(...);\nconst members = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").members();\nconst owners = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").owners();\n
"},{"location":"graph/groups/#get-the-team-site-for-a-group","title":"Get the Team Site for a Group","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites/group\";\n\nconst graph = graphfi(...);\n\nconst teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root();\nconst url = teamSite.webUrl\n
"},{"location":"graph/insights/","title":"@pnp/graph/insights","text":"This module helps you get Insights in form of Trending, Used and Shared. The results are based on relationships calculated using advanced analytics and machine learning techniques.
"},{"location":"graph/insights/#iinsights","title":"IInsights","text":""},{"location":"graph/insights/#get-all-trending-documents","title":"Get all Trending documents","text":"Returns documents from OneDrive and SharePoint sites trending around a user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst trending = await graph.me.insights.trending()\n\nconst trending = await graph.users.getById(\"userId\").insights.trending()\n
"},{"location":"graph/insights/#get-a-trending-document-by-id","title":"Get a Trending document by Id","text":"Using the getById method to get a trending document by Id.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst trendingDoc = await graph.me.insights.trending.getById('Id')()\n\nconst trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-trending-document","title":"Get the resource from Trending document","text":"Using the resources method to get the resource from a trending document.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.trending.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource()\n
"},{"location":"graph/insights/#get-all-used-documents","title":"Get all Used documents","text":"Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst used = await graph.me.insights.used()\n\nconst used = await graph.users.getById(\"userId\").insights.used()\n
"},{"location":"graph/insights/#get-a-used-document-by-id","title":"Get a Used document by Id","text":"Using the getById method to get a used document by Id.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst usedDoc = await graph.me.insights.used.getById('Id')()\n\nconst usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-used-document","title":"Get the resource from Used document","text":"Using the resources method to get the resource from a used document.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.used.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource()\n
"},{"location":"graph/insights/#get-all-shared-documents","title":"Get all Shared documents","text":"Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst shared = await graph.me.insights.shared()\n\nconst shared = await graph.users.getById(\"userId\").insights.shared()\n
"},{"location":"graph/insights/#get-a-shared-document-by-id","title":"Get a Shared document by Id","text":"Using the getById method to get a shared document by Id.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst sharedDoc = await graph.me.insights.shared.getById('Id')()\n\nconst sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-a-shared-document","title":"Get the resource from a Shared document","text":"Using the resources method to get the resource from a shared document.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.shared.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()\n
"},{"location":"graph/invitations/","title":"@pnp/graph/invitations","text":"The ability invite an external user via the invitation manager
"},{"location":"graph/invitations/#iinvitations","title":"IInvitations","text":""},{"location":"graph/invitations/#create-invitation","title":"Create Invitation","text":"Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/invitations\";\n\nconst graph = graphfi(...);\n\nconst invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');\n\n
"},{"location":"graph/items/","title":"@pnp/graph/items","text":"Currently, there is no module in graph to access all items directly. Please, instead, default to search by path using the following methods.
"},{"location":"graph/items/#get-list-items","title":"Get list items","text":"
import { Site } from \"@pnp/graph/sites\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst items = await Site(sites, \"lists/{listid}/items\")();\n
"},{"location":"graph/items/#get-fileitem-version-information","title":"Get File/Item version information","text":"import { Site } from \"@pnp/graph/sites\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst users = await Site(sites, \"lists/{listid}/items/{item id}/versions\")();\n
"},{"location":"graph/items/#get-list-items-with-fields-included","title":"Get list items with fields included","text":"import { Site } from \"@pnp/graph/sites\";\nimport \"@pnp/graph/lists\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst listItems : IList[] = await Site(sites, \"lists/{site id}/items?$expand=fields\")();\n
"},{"location":"graph/items/#hint-note-that-you-can-just-use-normal-graph-queries-in-this-search","title":"Hint: Note that you can just use normal graph queries in this search.","text":""},{"location":"graph/lists/","title":"@pnp/graph/lists","text":"More information can be found in the official Graph documentation:
"},{"location":"graph/lists/#get-lists","title":"Get Lists","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteLists = await graph.site.getById(\"{site identifier}\").lists();\n
"},{"location":"graph/lists/#get-list-by-id","title":"Get List by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst listInfo = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\")();\n
"},{"location":"graph/lists/#add-a-list","title":"Add a List","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst sampleList: List = {\n displayName: \"PnPGraphTestList\",\n list: { \"template\": \"genericList\" },\n};\n\nconst list = await graph.sites.getById(\"{site identifier}\").lists.add(listTemplate);\n
"},{"location":"graph/lists/#update-a-list","title":"Update a List","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst list = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").update({ displayName: \"MyNewListName\" });\n
"},{"location":"graph/lists/#delete-a-list","title":"Delete a List","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nawait graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").delete();\n
"},{"location":"graph/lists/#get-list-columns","title":"Get List Columns","text":"For more information about working please see documentation on columns
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\nimport \"@pnp/graph/columns\";\n\nconst graph = graphfi(...);\n\nawait graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns();\n
"},{"location":"graph/lists/#get-list-items","title":"Get List Items","text":"Currently, recieving list items via @pnpjs/graph API is not possible.
This can currently be done with a call by path as documented under @pnpjs/graph/items
"},{"location":"graph/messages/","title":"Graph Messages (Mail)","text":"More information can be found in the official Graph documentation:
"},{"location":"graph/messages/#get-users-messages","title":"Get User's Messages","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/messages\";\n\nconst graph = graphfi(...);\n\nconst currentUser = graph.me;\nconst messages = await currentUser.messages();\n
"},{"location":"graph/onedrive/","title":"@pnp/graph/onedrive","text":"The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive.
"},{"location":"graph/onedrive/#iinvitations","title":"IInvitations","text":""},{"location":"graph/onedrive/#get-the-default-drive","title":"Get the default drive","text":"Using the drive you can get the users default drive from Onedrive, or the groups or sites default document library.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst otherUserDrive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive();\n\nconst currentUserDrive = await graph.me.drive();\n\nconst groupDrive = await graph.groups.getById(\"{group identifier}\").drive();\n\nconst siteDrive = await graph.sites.getById(\"{site identifier}\").drive();\n
"},{"location":"graph/onedrive/#get-all-of-the-drives","title":"Get all of the drives","text":"Using the drives() you can get the users available drives from Onedrive
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst otherUserDrive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives();\n\nconst currentUserDrive = await graph.me.drives();\n\nconst groupDrives = await graph.groups.getById(\"{group identifier}\").drives();\n\nconst siteDrives = await graph.sites.getById(\"{site identifier}\").drives();\n\n
"},{"location":"graph/onedrive/#get-drive-by-id","title":"Get drive by Id","text":"Using the drives.getById() you can get one of the available drives in Outlook
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst drive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\")();\n\nconst drive = await graph.me.drives.getById(\"{drive id}\")();\n\nconst drive = await graph.drives.getById(\"{drive id}\")();\n\n
"},{"location":"graph/onedrive/#get-the-associated-list-of-a-drive","title":"Get the associated list of a drive","text":"Using the list() you get the associated list information
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst list = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").list();\n\nconst list = await graph.me.drives.getById(\"{drive id}\").list();\n\n
Using the getList(), from the lists implementation, you get the associated IList object. Form more infomration about acting on the IList object see @pnpjs/graph/lists
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst listObject: IList = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").getList();\n\nconst listOBject: IList = await graph.me.drives.getById(\"{drive id}\").getList();\n\nconst list = await listObject();\n
"},{"location":"graph/onedrive/#get-the-recent-files","title":"Get the recent files","text":"Using the recent() you get the recent files
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst files = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").recent();\n\nconst files = await graph.me.drives.getById(\"{drive id}\").recent();\n\n
"},{"location":"graph/onedrive/#get-the-files-shared-with-me","title":"Get the files shared with me","text":"Using the sharedWithMe() you get the files shared with the user
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst shared = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").sharedWithMe();\n\nconst shared = await graph.me.drives.getById(\"{drive id}\").sharedWithMe();\n\n// By default, sharedWithMe return items shared within your own tenant. To include items shared from external tenants include the options object.\n\nconst options: ISharingWithMeOptions = {allowExternal: true};\nconst shared = await graph.me.drives.getById(\"{drive id}\").sharedWithMe(options);\n\n
"},{"location":"graph/onedrive/#get-the-following-files","title":"Get the following files","text":"List the items that have been followed by the signed in user.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst files = await graph.me.drives.getById(\"{drive id}\").following();\n\n
"},{"location":"graph/onedrive/#get-the-root-folder","title":"Get the Root folder","text":"Using the root() you get the root folder
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst root = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root();\nconst root = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.root();\n\nconst root = await graph.me.drives.getById(\"{drive id}\").root();\nconst root = await graph.me.drive.root();\n\nconst root = await graph.sites.getById(\"{site id}\").drives.getById(\"{drive id}\").root();\nconst root = await graph.sites.getById(\"{site id}\").drive.root();\n\nconst root = await graph.groups.getById(\"{site id}\").drives.getById(\"{drive id}\").root();\nconst root = await graph.groups.getById(\"{site id}\").drive.root();\n\n
"},{"location":"graph/onedrive/#get-the-children","title":"Get the Children","text":"Using the children() you get the children
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst rootChildren = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children();\n\nconst rootChildren = await graph.me.drives.getById(\"{drive id}\").root.children();\n\nconst itemChildren = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").children();\n\nconst itemChildren = await graph.me.drives.getById(\"{drive id}\").root.items.getById(\"{item id}\").children();\n\n
"},{"location":"graph/onedrive/#get-the-children-by-path","title":"Get the children by path","text":"Using the drive.getItemsByPath() you can get the contents of a particular folder path
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getItemsByPath(\"MyFolder/MySubFolder\")();\n\nconst item = await graph.me.drives.getItemsByPath(\"MyFolder/MySubFolder\")();\n\n
"},{"location":"graph/onedrive/#add-item","title":"Add Item","text":"Using the add you can add an item, for more options please user the upload method instead.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\";\nimport {IDriveItemAddResult} from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst add1: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children.add(\"test.txt\", \"My File Content String\");\nconst add2: IDriveItemAddResult = await graph.me.drives.getById(\"{drive id}\").root.children.add(\"filename.txt\", \"My File Content String\");\n
"},{"location":"graph/onedrive/#uploadreplace-drive-item-content","title":"Upload/Replace Drive Item Content","text":"Using the .upload method you can add or update the content of an item.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\";\nimport {IFileOptions, IDriveItemAddResult} from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// file path is only file name\nconst fileOptions: IFileOptions = {\n content: \"This is some test content\",\n filePathName: \"pnpTest.txt\",\n contentType: \"text/plain;charset=utf-8\"\n}\n\nconst uDriveRoot: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.root.upload(fileOptions);\n\nconst uFolder: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.getItemById(\"{folder id}\").upload(fileOptions);\n\nconst uDriveIdRoot: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.upload(fileOptions);\n\n// file path includes folders\nconst fileOptions2: IFileOptions = {\n content: \"This is some test content\",\n filePathName: \"folderA/pnpTest.txt\",\n contentType: \"text/plain;charset=utf-8\"\n}\n\nconst uFileOptions: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.upload(fileOptions2);\n
"},{"location":"graph/onedrive/#add-folder","title":"Add folder","text":"Using addFolder you can add a folder
import { graph } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\"\nimport {IDriveItemAddResult} from \"@pnp/graph/ondrive\";\n\nconst graph = graphfi(...);\n\nconst addFolder1: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children.addFolder('New Folder');\nconst addFolder2: IDriveItemAddResult = await graph.me.drives.getById(\"{drive id}\").root.children.addFolder('New Folder');\n\n
"},{"location":"graph/onedrive/#search-items","title":"Search items","text":"Using the search() you can search for items, and optionally select properties
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Where searchTerm is the query text used to search for items.\n// Values may be matched across several fields including filename, metadata, and file content.\n\nconst search = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.search(searchTerm)();\n\nconst search = await graph.me.drives.getById(\"{drive id}\").root.search(searchTerm)();\n\n
"},{"location":"graph/onedrive/#get-specific-item-in-drive","title":"Get specific item in drive","text":"Using the items.getById() you can get a specific item from the current drive
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\")();\n\nconst item = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\")();\n\n
"},{"location":"graph/onedrive/#get-specific-item-in-drive-by-path","title":"Get specific item in drive by path","text":"Using the drive.getItemByPath() you can get a specific item from the current drive
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getItemByPath(\"MyFolder/MySubFolder/myFile.docx\")();\n\nconst item = await graph.me.drives.getItemByPath(\"MyFolder/MySubFolder/myFile.docx\")();\n\n
"},{"location":"graph/onedrive/#get-drive-item-contents","title":"Get drive item contents","text":"Using the item.getContent() you can get the content of a file.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nprivate _readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => {\n resolve(reader.result as ArrayBuffer);\n };\n reader.onerror = reject;\n reader.readAsArrayBuffer(file);\n });\n}\n\n// Where itemId is the id of the item\nconst fileContents: Blob = await graph.me.drive.getItemById(itemId).getContent();\nconst content: ArrayBuffer = await this._readFileAsync(fileContents);\n\n// This is an example of decoding plain text from the ArrayBuffer\nconst decoder = new TextDecoder('utf-8');\nconst decodedContent = decoder.decode(content);\n
"},{"location":"graph/onedrive/#convert-drive-item-contents","title":"Convert drive item contents","text":"Using the item.convertContent() you can get a PDF version of the file. See official documentation for supported file types.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nprivate _readFileAsync(file: Blob): Promise<ArrayBuffer> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => {\n resolve(reader.result as ArrayBuffer);\n };\n reader.onerror = reject;\n reader.readAsArrayBuffer(file);\n });\n}\n\n// Where itemId is the id of the item\nconst fileContents: Blob = await graph.me.drive.getItemById(itemId).convertContent(\"pdf\");\nconst content: ArrayBuffer = await this._readFileAsync(fileContents);\n\n// Further manipulation of the array buffer will be needed based on your requriements.\n
"},{"location":"graph/onedrive/#get-thumbnails","title":"Get thumbnails","text":"Using the thumbnails() you get the thumbnails
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst thumbs = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").thumbnails();\n\nconst thumbs = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").thumbnails();\n\n
"},{"location":"graph/onedrive/#delete-drive-item","title":"Delete drive item","text":"Using the delete() you delete the current item
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst thumbs = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").delete();\n\nconst thumbs = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").delete();\n\n
"},{"location":"graph/onedrive/#update-drive-item-metadata","title":"Update drive item metadata","text":"Using the update() you update the current item
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst update = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").update({name: \"New Name\"});\n\nconst update = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").update({name: \"New Name\"});\n\n
"},{"location":"graph/onedrive/#move-drive-item","title":"Move drive item","text":"Using the move() you move the current item, and optionally update it
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Requires a parentReference to the destination folder location\nconst moveOptions: IItemOptions = {\n parentReference: {\n id?: {parentLocationId};\n driveId?: {parentLocationDriveId}};\n };\n name?: {newName};\n};\n\nconst move = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").move(moveOptions);\n\nconst move = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").move(moveOptions);\n\n
"},{"location":"graph/onedrive/#copy-drive-item","title":"Copy drive item","text":"Using the copy() you can copy the current item to a new location, returns the path to the new location
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Requires a parentReference to the destination folder location\nconst copyOptions: IItemOptions = {\n parentReference: {\n id?: {parentLocationId};\n driveId?: {parentLocationDriveId}};\n };\n name?: {newName};\n};\n\nconst copy = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").copy(copyOptions);\n\nconst copy = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").copy(copyOptions);\n\n
"},{"location":"graph/onedrive/#get-the-users-special-folders","title":"Get the users special folders","text":"Using the users default drive you can get special folders, including: Documents, Photos, CameraRoll, AppRoot, Music
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { SpecialFolder, IDriveItem } from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Get the special folder (App Root)\nconst driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.AppRoot)();\n\n// Get the special folder (Documents)\nconst driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.Documents)();\n\n// ETC\n
"},{"location":"graph/onedrive/#get-drive-item-preview","title":"Get drive item preview","text":"This action allows you to obtain a short-lived embeddable URL for an item in order to render a temporary preview.
If you want to obtain long-lived embeddable links, use the createLink API instead.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IPreviewOptions, IDriveItemPreviewInfo } from \"@pnp/graph/onedrive\";\nimport { ItemPreviewInfo } from \"@microsoft/microsoft-graph-types\"\n\nconst graph = graphfi(...);\n\nconst preview: ItemPreviewInfo = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview();\n\nconst preview: ItemPreviewInfo = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview();\n\nconst previewOptions: IPreviewOptions = {\n page: 1,\n zoom: 90\n}\n\nconst preview2: ItemPreviewInfo = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview(previewOptions);\n\n
"},{"location":"graph/onedrive/#track-changes","title":"Track Changes","text":"Track changes in a driveItem and its children over time.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IDeltaItems } from \"@pnp/graph/ondrive\";\n\nconst graph = graphfi(...);\n\n// Get the changes for the drive items from inception\nconst delta: IDeltaItems = await graph.me.drive.root.delta()();\nconst delta: IDeltaItems = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.delta()();\n\n// Get the changes for the drive items from token\nconst delta: IDeltaItems = await graph.me.drive.root.delta(\"{token}\")();\n
"},{"location":"graph/onedrive/#get-drive-item-analytics","title":"Get Drive Item Analytics","text":"Using the analytics() you get the ItemAnalytics for a DriveItem
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IAnalyticsOptions } from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Defaults to lastSevenDays\nconst analytics = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics()();\n\nconst analytics = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics()();\n\nconst analyticOptions: IAnalyticsOptions = {\n timeRange: \"allTime\"\n};\n\nconst analyticsAllTime = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics(analyticOptions)();\n
"},{"location":"graph/outlook/","title":"@pnp/graph/outlook","text":"Represents the Outlook services available to a user. Currently, only interacting with categories is supported.
You can learn more by reading the Official Microsoft Graph Documentation.
"},{"location":"graph/outlook/#iusers-iuser-ipeople","title":"IUsers, IUser, IPeople","text":""},{"location":"graph/outlook/#get-all-categories-user","title":"Get All Categories User","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories();\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories();\n
"},{"location":"graph/outlook/#add-category-user","title":"Add Category User","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nawait graph.me.outlook.masterCategories.add({\n displayName: 'Newsletters', \n color: 'preset2'\n});\n// Application permissions\nawait graph.users.getById('{user id}').outlook.masterCategories.add({\n displayName: 'Newsletters', \n color: 'preset2'\n});\n
"},{"location":"graph/outlook/#update-category","title":"Update Category","text":" Testing has shown that displayName
cannot be updated.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\nimport { OutlookCategory } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst categoryUpdate: OutlookCategory = {\n color: \"preset5\"\n}\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate);\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);\n
"},{"location":"graph/outlook/#delete-category","title":"Delete Category","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories.getById('{category id}').delete();\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();\n
"},{"location":"graph/photos/","title":"@pnp/graph/photos","text":"A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64.
You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.
"},{"location":"graph/photos/#iphoto","title":"IPhoto","text":""},{"location":"graph/photos/#current-user-photo","title":"Current User Photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.me.photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-user-photo-by-size","title":"Current User Photo by Size","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.me.photos.getBySize(\"48x48\").getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-group-photo","title":"Current Group Photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-group-photo-by-size","title":"Current Group Photo by Size","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photos.getBySize(\"120x120\").getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-team-photo","title":"Current Team Photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.teams.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#set-user-photo","title":"Set User Photo","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.me.photo.setContent(file);\n
"},{"location":"graph/photos/#set-group-photo","title":"Set Group Photo","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.setContent(file);\n
"},{"location":"graph/photos/#set-team-photo","title":"Set Team Photo","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.teams.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.setContent(file);\n
"},{"location":"graph/planner/","title":"@pnp/graph/planner","text":"The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner.
"},{"location":"graph/planner/#iinvitations","title":"IInvitations","text":""},{"location":"graph/planner/#get-plans-by-id","title":"Get Plans by Id","text":"Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst plan = await graph.planner.plans.getById('planId')();\n\n
"},{"location":"graph/planner/#add-new-plan","title":"Add new Plan","text":"Using the planner.plans.add() you can create a new Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newPlan = await graph.planner.plans.add('groupObjectId', 'title');\n\n
"},{"location":"graph/planner/#get-tasks-in-plan","title":"Get Tasks in Plan","text":"Using the tasks() you can get the Tasks in a Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planTasks = await graph.planner.plans.getById('planId').tasks();\n\n
"},{"location":"graph/planner/#get-buckets-in-plan","title":"Get Buckets in Plan","text":"Using the buckets() you can get the Buckets in a Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planBuckets = await graph.planner.plans.getById('planId').buckets();\n\n
"},{"location":"graph/planner/#get-details-in-plan","title":"Get Details in Plan","text":"Using the details() you can get the details in a Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planDetails = await graph.planner.plans.getById('planId').details();\n\n
"},{"location":"graph/planner/#delete-plan","title":"Delete Plan","text":"Using the delete() you can get delete a Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delPlan = await graph.planner.plans.getById('planId').delete('planEtag');\n\n
"},{"location":"graph/planner/#update-plan","title":"Update Plan","text":"Using the update() you can get update a Plan.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});\n\n
"},{"location":"graph/planner/#get-all-my-tasks-from-all-plans","title":"Get All My Tasks from all plans","text":"Using the tasks() you can get the Tasks across all plans
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planTasks = await graph.me.tasks()\n\n
"},{"location":"graph/planner/#get-task-by-id","title":"Get Task by Id","text":"Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst task = await graph.planner.tasks.getById('taskId')();\n\n
"},{"location":"graph/planner/#add-new-task","title":"Add new Task","text":"Using the planner.tasks.add() you can create a new Task.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newTask = await graph.planner.tasks.add('planId', 'title');\n\n
"},{"location":"graph/planner/#get-details-in-task","title":"Get Details in Task","text":"Using the details() you can get the details in a Task.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst taskDetails = await graph.planner.tasks.getById('taskId').details();\n\n
"},{"location":"graph/planner/#delete-task","title":"Delete Task","text":"Using the delete() you can get delete a Task.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');\n\n
"},{"location":"graph/planner/#update-task","title":"Update Task","text":"Using the update() you can get update a Task.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});\n\n
"},{"location":"graph/planner/#get-buckets-by-id","title":"Get Buckets by Id","text":"Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst bucket = await graph.planner.buckets.getById('bucketId')();\n\n
"},{"location":"graph/planner/#add-new-bucket","title":"Add new Bucket","text":"Using the planner.buckets.add() you can create a new Bucket.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newBucket = await graph.planner.buckets.add('name', 'planId');\n\n
"},{"location":"graph/planner/#update-bucket","title":"Update Bucket","text":"Using the update() you can get update a Bucket.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'});\n\n
"},{"location":"graph/planner/#delete-bucket","title":"Delete Bucket","text":"Using the delete() you can get delete a Bucket.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');\n\n
"},{"location":"graph/planner/#get-bucket-tasks","title":"Get Bucket Tasks","text":"Using the tasks() you can get Tasks in a Bucket.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();\n\n
"},{"location":"graph/planner/#get-plans-for-a-group","title":"Get Plans for a group","text":"Gets all the plans for a group
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst plans = await graph.groups.getById(\"b179a282-9f94-4bb5-a395-2a80de5a5a78\").plans();\n\n
"},{"location":"graph/search/","title":"@pnp/graph/search","text":"The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below.
"},{"location":"graph/search/#call-graphquery","title":"Call graph.query","text":"
This example shows calling the search API via the query
method of the root graph object.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/search\";\n\nconst graph = graphfi(...);\n\nconst results = await graph.query({\n entityTypes: [\"site\"],\n query: {\n queryString: \"test\"\n },\n});\n
Note: This library allows you to pass multiple search requests to the query
method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.
The shares module allows you to access shared files, or any file in the tenant using encoded file urls.
"},{"location":"graph/shares/#access-a-share-by-id","title":"Access a Share by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst shareInfo = await graph.shares.getById(\"{shareId}\")();\n
"},{"location":"graph/shares/#encode-a-sharing-link","title":"Encode a Sharing Link","text":"If you don't have a share id but have the absolute path to a file you can encode it into a sharing link, allowing you to access it directly using the /shares endpoint.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst shareLink: string = graph.shares.encodeSharingLink(\"https://{tenant}.sharepoint.com/sites/dev/Shared%20Documents/new.pptx\");\n\nconst shareInfo = await graph.shares.getById(shareLink)();\n
"},{"location":"graph/shares/#access-a-shares-driveitem-resource","title":"Access a Share's driveItem resource","text":"You can also access the full functionality of the driveItem via a share. Find more details on the capabilities of driveItem here.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst driveItemInfo = await graph.shares.getById(\"{shareId}\").driveItem();\n
"},{"location":"graph/sites/","title":"@pnp/graph/sites","text":"The search module allows you to access the Microsoft Graph Sites API.
"},{"location":"graph/sites/#call-graphsites","title":"Call graph.sites","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\n\nconst sitesInfo = await graph.sites();\n
"},{"location":"graph/sites/#call-graphsitesgetbyid","title":"Call graph.sites.getById","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\n\nconst siteInfo = await graph.sites.getById(\"{site identifier}\")();\n
"},{"location":"graph/sites/#call-graphsitesgetbyurl","title":"Call graph.sites.getByUrl","text":"Using the sites.getByUrl() you can get a site using url instead of identifier
If you get a site with this method, the graph does not support chaining a request further than .drive. We will review and try and create a work around for this issue.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\nconst sharepointHostName = \"contoso.sharepoint.com\";\nconst serverRelativeUrl = \"/sites/teamsite1\";\nconst siteInfo = await graph.sites.getByUrl(sharepointHostName, serverRelativeUrl)();\n
"},{"location":"graph/sites/#make-additional-calls-or-recieve-items-from-lists","title":"Make additional calls or recieve items from lists","text":"We don't currently implement all of the available options in graph for sites, rather focusing on the sp library. While we do accept PRs to add functionality, you can also make calls by path.
"},{"location":"graph/subscriptions/","title":"@pnp/graph/subscriptions","text":"The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft graph. Currently, subscriptions are enabled for the following resources:
Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst subscriptions = await graph.subscriptions();\n\n
"},{"location":"graph/subscriptions/#create-a-new-subscription","title":"Create a new Subscription","text":"Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\");\n\n
"},{"location":"graph/subscriptions/#get-subscription-by-id","title":"Get Subscription by Id","text":"Using the subscriptions.getById() you can get one of the subscriptions
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst subscription = await graph.subscriptions.getById('subscriptionId')();\n\n
"},{"location":"graph/subscriptions/#delete-a-subscription","title":"Delete a Subscription","text":"Using the subscriptions.getById().delete() you can remove one of the Subscriptions
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst delSubscription = await graph.subscriptions.getById('subscriptionId').delete();\n\n
"},{"location":"graph/subscriptions/#update-a-subscription","title":"Update a Subscription","text":"Using the subscriptions.getById().update() you can update one of the Subscriptions
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });\n\n
"},{"location":"graph/teams/","title":"@pnp/graph/teams","text":"The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams.
"},{"location":"graph/teams/#teams-the-user-is-a-member-of","title":"Teams the user is a member of","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams();\n\nconst myJoinedTeams = await graph.me.joinedTeams();\n\n
"},{"location":"graph/teams/#get-teams-by-id","title":"Get Teams by Id","text":"Using the teams.getById() you can get a specific Team.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();\n
"},{"location":"graph/teams/#create-new-teamgroup-method-1","title":"Create new Team/Group - Method #1","text":"The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.
"},{"location":"graph/teams/#create-a-team-via-a-specific-group","title":"Create a Team via a specific group","text":"Here we get the group via id and use createTeam
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({\n\"memberSettings\": {\n \"allowCreateUpdateChannels\": true\n},\n\"messagingSettings\": {\n \"allowUserEditMessages\": true,\n\"allowUserDeleteMessages\": true\n},\n\"funSettings\": {\n \"allowGiphy\": true,\n \"giphyContentRating\": \"strict\"\n}});\n
"},{"location":"graph/teams/#create-new-teamgroup-method-2","title":"Create new Team/Group - Method #2","text":"The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst team = {\n \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\",\n \"displayName\": \"PnPJS Test Team\",\n \"description\": \"PnPJS Test Team\u2019s Description\",\n \"members\": [\n {\n \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\",\n \"roles\": [\"owner\"],\n \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\",\n },\n ],\n };\n\nconst createdTeam: ITeamCreateResultAsync = await graph.teams.create(team);\n//To check the status of the team creation, call getOperationById for the newly created team.\nconst createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);\n
"},{"location":"graph/teams/#clone-a-team","title":"Clone a Team","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(\n'Cloned','description','apps,tabs,settings,channels,members','public');\n\n
"},{"location":"graph/teams/#get-teams-async-operation","title":"Get Teams Async Operation","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(\n'Cloned','description','apps,tabs,settings,channels,members','public');\nconst clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);\n
"},{"location":"graph/teams/#archive-a-team","title":"Archive a Team","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();\n
"},{"location":"graph/teams/#unarchive-a-team","title":"Unarchive a Team","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();\n
"},{"location":"graph/teams/#get-all-channels-of-a-team","title":"Get all channels of a Team","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();\n
"},{"location":"graph/teams/#get-primary-channel","title":"Get primary channel","text":"Using the teams.getById() you can get a specific Team.
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\nconst channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').primaryChannel();\n
"},{"location":"graph/teams/#get-channel-by-id","title":"Get channel by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();\n\n
"},{"location":"graph/teams/#create-a-new-channel","title":"Create a new Channel","text":"import { graphfi } from \"@pnp/graph\";\n\nconst graph = graphfi(...);\n\nconst newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');\n\n
"},{"location":"graph/teams/#list-messages","title":"List Messages","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst chatMessages = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages();\n
"},{"location":"graph/teams/#add-chat-message-to-channel","title":"Add chat message to Channel","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport { ChatMessage } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst message = {\n \"body\": {\n \"content\": \"Hello World\"\n }\n }\nconst chatMessage: ChatMessage = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages.add(message);\n
"},{"location":"graph/teams/#get-installed-apps","title":"Get installed Apps","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();\n\n
"},{"location":"graph/teams/#add-an-app","title":"Add an App","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');\n\n
"},{"location":"graph/teams/#remove-an-app","title":"Remove an App","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.delete();\n\n
"},{"location":"graph/teams/#get-tabs-from-a-channel","title":"Get Tabs from a Channel","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();\n\n
"},{"location":"graph/teams/#get-tab-by-id","title":"Get Tab by Id","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();\n\n
"},{"location":"graph/teams/#add-a-new-tab-to-channel","title":"Add a new Tab to Channel","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});\n\n
"},{"location":"graph/teams/#update-a-tab","title":"Update a Tab","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').update({\n displayName: \"New tab name\"\n});\n\n
"},{"location":"graph/teams/#remove-a-tab-from-channel","title":"Remove a Tab from channel","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').delete();\n\n
"},{"location":"graph/teams/#team-membership","title":"Team Membership","text":"Get the members and/or owners of a group.
See Groups
"},{"location":"graph/users/","title":"@pnp/graph/users","text":"Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services.
You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.
"},{"location":"graph/users/#iusers-iuser-ipeople","title":"IUsers, IUser, IPeople","text":""},{"location":"graph/users/#current-user","title":"Current User","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst currentUser = await graph.me();\n
"},{"location":"graph/users/#get-users-in-the-organization","title":"Get Users in the Organization","text":"If you want to get all users you will need to use paging
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst allUsers = await graph.users();\n
"},{"location":"graph/users/#get-a-user-by-email-address-or-user-id","title":"Get a User by email address (or user id)","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst matchingUser = await graph.users.getById('jane@contoso.com')();\n
"},{"location":"graph/users/#user-properties","title":"User Properties","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nawait graph.me.memberOf();\nawait graph.me.transitiveMemberOf();\n
"},{"location":"graph/users/#update-current-user","title":"Update Current User","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nawait graph.me.update({\n displayName: 'John Doe'\n});\n
"},{"location":"graph/users/#people","title":"People","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst people = await graph.me.people();\n\n// get the top 3 people\nconst people = await graph.me.people.top(3)();\n
"},{"location":"graph/users/#manager","title":"Manager","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst manager = await graph.me.manager();\n
"},{"location":"graph/users/#direct-reports","title":"Direct Reports","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst reports = await graph.me.directReports();\n
"},{"location":"graph/users/#photo","title":"Photo","text":"import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst currentUser = await graph.me.photo();\nconst specificUser = await graph.users.getById('jane@contoso.com').photo();\n
"},{"location":"graph/users/#user-photo-operations","title":"User Photo Operations","text":"See Photos
"},{"location":"graph/users/#user-presence-operation","title":"User Presence Operation","text":"See Cloud Communications
"},{"location":"graph/users/#user-messages-mail","title":"User Messages (Mail)","text":"See Messages
"},{"location":"graph/users/#user-onedrive","title":"User OneDrive","text":"See OneDrive
"},{"location":"logging/","title":"@pnp/logging","text":"The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.
"},{"location":"logging/#getting-started","title":"Getting Started","text":"Install the logging module, it has no other dependencies
npm install @pnp/logging --save
The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter.
/**\n * Interface that defines a log listener\n *\n */\nexport interface ILogListener {\n /**\n * Any associated data that a given logging listener may choose to log or ignore\n *\n * @param entry The information to be logged\n */\n log(entry: ILogEntry): void;\n}\n\n/**\n * Interface that defines a log entry\n *\n */\nexport interface ILogEntry {\n /**\n * The main message to be logged\n */\n message: string;\n /**\n * The level of information this message represents\n */\n level: LogLevel;\n /**\n * Any associated data that a given logging listener may choose to log or ignore\n */\n data?: any;\n}\n
"},{"location":"logging/#log-levels","title":"Log Levels","text":"export const enum LogLevel {\n Verbose = 0,\n Info = 1,\n Warning = 2,\n Error = 3,\n Off = 99,\n}\n
"},{"location":"logging/#writing-to-the-logger","title":"Writing to the Logger","text":"To write information to a logger you can use either write, writeJSON, or log.
import {\n Logger,\n LogLevel\n} from \"@pnp/logging\";\n\n// write logs a simple string as the message value of the LogEntry\nLogger.write(\"This is logging a simple string\");\n\n// optionally passing a level, default level is Verbose\nLogger.write(\"This is logging a simple string\", LogLevel.Error);\n\n// this will convert the object to a string using JSON.stringify and set the message with the result\nLogger.writeJSON({ name: \"value\", name2: \"value2\"});\n\n// optionally passing a level, default level is Verbose\nLogger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning);\n\n// specify the entire LogEntry interface using log\nLogger.log({\n data: { name: \"value\", name2: \"value2\"},\n level: LogLevel.Warning,\n message: \"This is my message\"\n});\n
"},{"location":"logging/#log-an-error","title":"Log an error","text":"There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property.
const e = Error(\"An Error\");\n\nLogger.error(e);\n
"},{"location":"logging/#subscribing-a-listener","title":"Subscribing a Listener","text":"By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.
import {\n Logger,\n ConsoleListener,\n LogLevel\n} from \"@pnp/logging\";\n\n// subscribe a listener\nLogger.subscribe(ConsoleListener());\n\n// set the active log level\nLogger.activeLogLevel = LogLevel.Info;\n
"},{"location":"logging/#available-listeners","title":"Available Listeners","text":"There are two listeners included in the library, ConsoleListener and FunctionListener.
"},{"location":"logging/#consolelistener","title":"ConsoleListener","text":"This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.
"},{"location":"logging/#configuration-options","title":"Configuration Options","text":"Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).
"},{"location":"logging/#using-a-prefix","title":"Using a Prefix","text":"To add a prefix to all output, supply a string in the constructor:
import {\n Logger,\n ConsoleListener,\n LogLevel\n} from \"@pnp/logging\";\n\nconst LOG_SOURCE: string = 'MyAwesomeWebPart';\nLogger.subscribe(ConsoleListener(LOG_SOURCE));\nLogger.activeLogLevel = LogLevel.Info;\n
With the above configuration, Logger.write(\"My special message\");
will be output to the console as:
MyAwesomeWebPart - My special message\n
"},{"location":"logging/#customizing-text-color","title":"Customizing Text Color","text":"You can also specify text color for your messages by supplying an IConsoleListenerColors
object. You can simply specify color
to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color
out and all other log levels will use the default color).
Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.):
import {\n Logger,\n ConsoleListener,\n LogLevel\n} from \"@pnp/logging\";\n\nconst LOG_SOURCE: string = 'MyAwesomeWebPart';\nLogger.subscribe(ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'}));\nLogger.activeLogLevel = LogLevel.Info;\n
With the above configuration:
Logger.write(\"My special message\");\nLogger.write(\"A warning!\", LogLevel.Warning);\n
Will result in messages that look like this:
Color options:
color
: Default text color for all logging levels unless they're specifiedverboseColor
: Text color to use for messages with LogLevel.VerboseinfoColor
: Text color to use for messages with LogLevel.InfowarningColor
: Text color to use for messages with LogLevel.WarningerrorColor
: Text color to use for messages with LogLevel.ErrorTo set colors without a prefix, specify either undefined
or an empty string for the first parameter:
Logger.subscribe(ConsoleListener(undefined, {color:'purple'}));\n
"},{"location":"logging/#functionlistener","title":"FunctionListener","text":"The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.
import {\n Logger,\n FunctionListener,\n ILogEntry\n} from \"@pnp/logging\";\n\nlet listener = new FunctionListener((entry: ILogEntry) => {\n\n // pass all logging data to an existing framework\n MyExistingCompanyLoggingFramework.log(entry.message);\n});\n\nLogger.subscribe(listener);\n
"},{"location":"logging/#create-a-custom-listener","title":"Create a Custom Listener","text":"If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface.
import {\n Logger,\n ILogListener,\n ILogEntry\n} from \"@pnp/logging\";\n\nclass MyListener implements ILogListener {\n\n log(entry: ILogEntry): void {\n // here you would do something with the entry\n }\n}\n\nLogger.subscribe(new MyListener());\n
"},{"location":"logging/#logging-behavior","title":"Logging Behavior","text":"To allow seamless logging with v3 we have introduced the PnPLogging
behavior. It takes a single augument representing the log level of that behavior, allowing you to be very selective in what logging you want to get. As well the log level applied here ignores any global level set with activeLogLevel
on Logger.
import { LogLevel, PnPLogging, Logger, ConsoleListener } from \"@pnp/logging\";\nimport { spfi, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\n// subscribe a listener\nLogger.subscribe(ConsoleListener());\n\n// at the root we only want to log errors, which will be sent to all subscribed loggers on Logger\nconst sp = spfi().using(SPFx(this.context), PnPLogging(LogLevel.Error));\n\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n// use verbose logging with this particular list because you are trying to debug something\nlist.using(PnPLogging(LogLevel.Verbose));\n\nconst listData = await list();\n
"},{"location":"msaljsclient/","title":"@pnp/msaljsclient","text":"This library provides a thin wrapper around the msal library to make it easy to integrate MSAL authentication in the browser.
You will first need to install the package:
npm install @pnp/msaljsclient --save
The configuration and authParams
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/sp/webs\";\n\nconst configuation = {\n auth: {\n authority: \"https://login.microsoftonline.com/common\",\n clientId: \"{client id}\",\n }\n};\n\nconst authParams = {\n scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n};\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPBrowser(), MSAL(configuration, authParams));\n\nconst webData = await sp.web();\n
Please see more scenarios in the authentication article.
"},{"location":"news/2020-year-in-review/","title":"2020 Year End Report","text":"Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.
This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules.
We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community.
Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios.
Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.
"},{"location":"news/2020-year-in-review/#usage","title":"Usage","text":"In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227.
These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November.
1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds
"},{"location":"news/2020-year-in-review/#releases","title":"Releases","text":"We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
"},{"location":"news/2020-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp):","text":"Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836With 2020 our total all time downloads of @pnp/sp is now at: 949,638
Stats from https://npm-stat.com/
"},{"location":"news/2020-year-in-review/#future-plans","title":"Future Plans","text":"Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date.
Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.
"},{"location":"news/2020-year-in-review/#new-lead-maintainer","title":"New Lead Maintainer","text":"With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work.
Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean.
We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.
"},{"location":"news/2020-year-in-review/#contributors","title":"Contributors","text":"As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.
"},{"location":"news/2020-year-in-review/#sponsors","title":"Sponsors","text":"
We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
Thank You
"},{"location":"news/2020-year-in-review/#closing","title":"Closing","text":"
In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
Wishing you the very best for 2021,
The PnPjs Team
"},{"location":"news/2021-year-in-review/","title":"2021 Year End Report","text":"Welcome to our second year in review report for PnPjs. 2021 found us planning, building, testing, and documenting a whole new version of PnPjs. The goal is to deliver a much improved and flexible experience and none of that would have been possible without the support and participation of everyone in the PnP community - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.
Because of the huge useage we've seen with the library and issues we found implementing some of the much requested enhancements, we felt we really needed to start from the ground up and rearchitect the library completely. This new design, built on the concept of a \"Timeline\", enabled us to build a significantly lighter weight solution that is more extensible than ever. And bonus, we were able to keep the overall development experience largly unchanged, so that makes transitioning all that much easier. In addition we took extra effort to validate our development efforts by making sure all our tests passed so that we could better ensure quality of the library. Check out our Transition Guide and ChangeLog for all the details.
In other news, we fixed 47 reported bugs, answered 89 questions, and made 51 suggested enhancements to version 2 of the library - all driven by feedback from users and the community.
"},{"location":"news/2021-year-in-review/#usage","title":"Usage","text":"In 2021 we transitioned from rapid growth to slower growth but maintaining a request/month rate over 11 billion, approaching 13 billion by the end of the year. These requests came from more than 25 thousand tenants including some of the largest M365 customers. Due to some data cleanup we don't have the full year's information, but the below graph shows the final 7 months of the year.
"},{"location":"news/2021-year-in-review/#releases","title":"Releases","text":"We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
"},{"location":"news/2021-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp)","text":"Month Count * Month Count January 49,446 * July 73,491 February 56,054 * August 74,236 March 66,113 * September 69,179 April 58,526 * October 77,645 May 62,747 * November 74,966 June 69,349 * December 61,995 Grand Total 793,747For comparison our total downloads in 2020 was 543,836.
With 2021 our total all time downloads of @pnp/sp is now at: 1,743,385
In 2020 the all time total was 949,638.
Stats from https://npm-stat.com/
"},{"location":"news/2021-year-in-review/#future-plans","title":"Future Plans","text":"Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.
"},{"location":"news/2021-year-in-review/#contributors","title":"Contributors","text":"As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.
"},{"location":"news/2021-year-in-review/#sponsors","title":"Sponsors","text":"
We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
Thank You
"},{"location":"news/2021-year-in-review/#closing","title":"Closing","text":"
In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
Wishing you the very best for 2022,
The PnPjs Team
"},{"location":"news/2022-year-in-review/","title":"2022 Year End Report","text":"Wow, what a year for PnPjs! We released our latest major version 3.0 on Valentine's Day 2022 which included significant performance improvements, a completely rewritten internal architecture, and reduced the bundled library size by two-thirds. As well we continued out monthly releases bringing enhancements and bug fixes to our users on a continual basis.
But before we go any further we once again say Thank You!!! to everyone that has used, contributed to, and provided feedback on the library. This journey is not possible without you, and this last year you have driven us to be our best.
Version 3 introduces a completely new design for the internals of the library, easily allowing consumers to customize any part of the request process to their needs. Centered around an extensible Timeline and extended for http requests by Queryable this new pattern reduced code duplication, interlock, and complexity significantly. It allows everything in the request flow to be controlled through behaviors, which are plain functions acting at the various stages of the request. Using this model we reimagined batching, caching, authentication, and parsing in simpler, composable ways. If you have not yet updated to version 3, we encourage you to do so. You can review the transition guide to get started.
As one last treat, we set up nightly builds so that each day you can get a fresh version with any updates merged the previous day. This is super helpful if you're waiting for a specific fix or feature for your project. It allows for easier testing of new features through the full dev lifecycle, as well.
In other news, we fixed 54 reported bugs, answered 123 questions, and made 54 suggested enhancements to version 3 of the library - all driven by feedback from users and the community.
"},{"location":"news/2022-year-in-review/#usage","title":"Usage","text":"In 2022 we continued to see steady usage and growth maintaining a requst/month rate over 30 billion for much of the year. These requets came from ~29K tenants a month, including some of our largest M365 customers.
"},{"location":"news/2022-year-in-review/#releases","title":"Releases","text":"We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.
"},{"location":"news/2022-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp)","text":"Month Count * Month Count January 70,863 * July 63,844 February 76,649 * August 75,713 March 83,902 * September 71,447 April 70,429 * October 84,744 May 72,406 * November 82,459 June 71,375 * December 65,785 Grand Total 889,616For comparison our total downloads in 2021 was 793,747.
With 2022 our total all time downloads of @pnp/sp is now at: 2,543,639
In 2021 the all time total was 1,743,385.
Stats from https://npm-stat.com/
"},{"location":"news/2022-year-in-review/#future-plans","title":"Future Plans","text":"Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.
"},{"location":"news/2022-year-in-review/#contributors","title":"Contributors","text":"As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2021! If you are interested in becoming a contributor check out our guide on ways to get started.
"},{"location":"news/2022-year-in-review/#sponsors","title":"Sponsors","text":"
We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.
Thank You
"},{"location":"news/2022-year-in-review/#closing","title":"Closing","text":"
In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.
Wishing you the very best for 2023,
The PnPjs Team
"},{"location":"nodejs/behaviors/","title":"@pnp/nodejs : behaviors","text":"The article describes the behaviors exported by the @pnp/nodejs
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/graph.
This behavior, for use in nodejs, provides basic fetch support through the node-fetch
package. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing
For fetch configuration in browsers please see @pnp/queryable behaviors.
import { NodeFetch } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetch());\n\nawait sp.webs();\n
import { NodeFetch } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetch({ replace: false }));\n\nawait sp.webs();\n
"},{"location":"nodejs/behaviors/#nodefetchwithretry","title":"NodeFetchWithRetry","text":"This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.
import { NodeFetchWithRetry } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetchWithRetry());\n\nawait sp.webs();\n
You can also control how the behavior works through its props. The replace
value works as described above for NodeFetch. interval
specifies the initial dynamic back off value in milliseconds. This value is ignored if a \"Retry-After\" header exists in the response. retries
indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.
import { NodeFetchWithRetry } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetchWithRetry({\n retries: 2,\n interval: 400,\n replace: true,\n}));\n\nawait sp.webs();\n
"},{"location":"nodejs/behaviors/#graphdefault","title":"GraphDefault","text":"The GraphDefault
behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse, graph's DefaultHeaders, and graph's DefaultInit. It is configured using a props argument:
interface IGraphDefaultProps {\n baseUrl?: string;\n msal: {\n config: Configuration;\n scopes?: string[];\n };\n}\n
You can use the baseUrl property to specify either v1.0 or beta - or one of the special graph urls.
import { GraphDefault } from \"@pnp/nodejs\";\nimport { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users/index.js\";\n\nconst graph = graphfi().using(GraphDefault({\n // use the German national graph endpoint\n baseUrl: \"https://graph.microsoft.de/v1.0\",\n msal: {\n config: { /* my msal config */ },\n }\n}));\n\nawait graph.me();\n
"},{"location":"nodejs/behaviors/#msal","title":"MSAL","text":"This behavior provides a thin wrapper around the @azure/msal-node
library. The options you provide are passed directly to msal, and all options are available.
import { MSAL } from \"@pnp/nodejs\";\nimport { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users/index.js\";\n\nconst graph = graphfi().using(MSAL(config: { /* my msal config */ }, scopes: [\"https://graph.microsoft.com/.default\"]);\n\nawait graph.me();\n
"},{"location":"nodejs/behaviors/#spdefault","title":"SPDefault","text":"The SPDefault
behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse,sp's DefaultHeaders, and sp's DefaultInit. It is configured using a props argument:
interface ISPDefaultProps {\n baseUrl?: string;\n msal: {\n config: Configuration;\n scopes: string[];\n };\n}\n
You can use the baseUrl property to specify the absolute site/web url to which queries should be set.
import { SPDefault } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(SPDefault({\n msal: {\n config: { /* my msal config */ },\n scopes: [\"Scope.Value\", \"Scope2.Value\"],\n }\n}));\n\nawait sp.web();\n
"},{"location":"nodejs/behaviors/#streamparse","title":"StreamParse","text":"StreamParse
is a specialized parser allowing request results to be read as a nodejs stream. The return value when using this parser will be of the shape:
{\n body: /* The .body property of the Response object */,\n knownLength: /* number value calculated from the Response's content-length header */\n}\n
import { StreamParse } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(StreamParse());\n\nconst streamResult = await sp.someQueryThatReturnsALargeFile();\n\n// read the stream as text\nconst txt = await new Promise<string>((resolve) => {\n let data = \"\";\n streamResult.body.on(\"data\", (chunk) => data += chunk);\n streamResult.body.on(\"end\", () => resolve(data));\n});\n
"},{"location":"nodejs/sp-extensions/","title":"@pnp/nodejs - sp extensions","text":"By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api.
"},{"location":"nodejs/sp-extensions/#ifilegetstream","title":"IFile.getStream","text":"Allows you to read a response body as a nodejs PassThrough stream.
// by importing the the library the node specific extensions are automatically applied\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n // config\n}));\n\n// get the stream\nconst streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();\n\n// see if we have a known length\nconsole.log(streamResult.knownLength);\n\n// read the stream\n// this is a very basic example - you can do tons more with streams in node\nconst txt = await new Promise<string>((resolve) => {\n let data = \"\";\n stream.body.on(\"data\", (chunk) => data += chunk);\n stream.body.on(\"end\", () => resolve(data));\n});\n
"},{"location":"nodejs/sp-extensions/#ifilesaddchunked","title":"IFiles.addChunked","text":"import { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs/index.js\";\nimport \"@pnp/sp/folders/web.js\";\nimport \"@pnp/sp/folders/list.js\";\nimport \"@pnp/sp/files/web.js\";\nimport \"@pnp/sp/files/folder.js\";\nimport * as fs from \"fs\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n // config\n}));\n\n// NOTE: you must supply the highWaterMark to determine the block size for stream uploads\nconst stream = fs.createReadStream(\"{file path}\", { highWaterMark: 10485760 });\nconst files = sp.web.defaultDocumentLibrary.rootFolder.files;\n\n// passing the chunkSize parameter has no affect when using a stream, use the highWaterMark as shown above when creating the stream\nawait files.addChunked(name, stream, null, true);\n
"},{"location":"nodejs/sp-extensions/#ifilesetstreamcontentchunked","title":"IFile.setStreamContentChunked","text":"import { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs/index.js\";\nimport \"@pnp/sp/folders/web.js\";\nimport \"@pnp/sp/folders/list.js\";\nimport \"@pnp/sp/files/web.js\";\nimport \"@pnp/sp/files/folder.js\";\nimport * as fs from \"fs\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n // config\n}));\n\n// NOTE: you must supply the highWaterMark to determine the block size for stream uploads\nconst stream = fs.createReadStream(\"{file path}\", { highWaterMark: 10485760 });\nconst file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\");\n\nawait file.setStreamContentChunked(stream);\n
"},{"location":"nodejs/sp-extensions/#explicit-import","title":"Explicit import","text":"If you don't need to import anything from the library, but would like to include the extensions just import the library as shown.
import \"@pnp/nodejs\";\n\n// get the stream\nconst streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();\n
"},{"location":"nodejs/sp-extensions/#accessing-sp-extension-namespace","title":"Accessing SP Extension Namespace","text":"There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\".
import { SPNS } from \"@pnp/nodejs-commonjs\";\n\nconst parser = new SPNS.StreamParser();\n
"},{"location":"queryable/behaviors/","title":"@pnp/queryable : behaviors","text":"The article describes the behaviors exported by the @pnp/queryable
library. Please also see available behaviors in @pnp/core, @pnp/nodejs, @pnp/sp, and @pnp/graph.
Generally you won't need to use these behaviors individually when using the defaults supplied by the library, but when appropriate you can create your own composed behaviors using these as building blocks.
"},{"location":"queryable/behaviors/#bearer-token","title":"Bearer Token","text":"Allows you to inject an existing bearer token into the request. This behavior will not replace any existing authentication behaviors, so you may want to ensure they are cleared if you are supplying your own tokens, regardless of their source. This behavior does no caching or performs any operation other than including your token in an authentication heading.
import { BearerToken } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BearerToken(\"HereIsMyBearerTokenStringFromSomeSource\"));\n\n// optionally clear any configured authentication as you are supplying a token so additional calls shouldn't be needed\n// but take care as other behaviors may add observers to auth\nsp.on.auth.clear();\n\n// the bearer token supplied above will be applied to all requests made from `sp`\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#browserfetch","title":"BrowserFetch","text":"This behavior, for use in web browsers, provides basic fetch support through the browser's fetch global method. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing
For fetch configuration in nodejs please see @pnp/nodejs behaviors.
import { BrowserFetch } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetch());\n\nconst webInfo = await sp.webs();\n
import { BrowserFetch } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetch({ replace: false }));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#browserfetchwithretry","title":"BrowserFetchWithRetry","text":"This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.
import { BrowserFetchWithRetry } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetchWithRetry());\n\nconst webInfo = await sp.webs();\n
You can also control how the behavior works through its props. The replace
value works as described above for BrowserFetch. interval
specifies the initial dynamic back off value in milliseconds. This value is ignored if a \"Retry-After\" header exists in the response. retries
indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.
import { BrowserFetchWithRetry } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetchWithRetry({\n retries: 2,\n interval: 400,\n replace: true,\n}));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#caching","title":"Caching","text":"This behavior allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map. It is a good idea to include caching in your projects to improve performance. By default items in the cache will expire after 5 minutes.
import { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(Caching());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n
"},{"location":"queryable/behaviors/#custom-key-function","title":"Custom Key Function","text":"You can also supply custom functionality to control how keys are generated and calculate the expirations.
The cache key factory has the form (url: string) => string
and you must ensure your keys are unique enough that you won't have collisions.
The expire date factory has the form (url: string) => Date
and should return the Date when the cached data should expire. If you know that some particular data won't expire often you can set this date far in the future, or for more frequently updated information you can set it lower. If you set the expiration too short there is no reason to use caching as any stored information will likely always be expired. Additionally, you can set the storage to use local storage which will persist across sessions.
Note that for sp.search() requests if you want to specify a key you will need to use the CacheKey behavior below, the keyFactory value will be overwritten
import { getHashCode, PnPClientStorage, dateAdd, TimelinePipe } from \"@pnp/core\";\nimport { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(Caching({\n store: \"local\",\n // use a hascode for the key\n keyFactory: (url) => getHashCode(url.toLowerCase()).toString(),\n // cache for one minute\n expireFunc: (url) => dateAdd(new Date(), \"minute\", 1),\n}));\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n
As with any behavior you have the option to only apply caching to certain requests:
import { getHashCode, dateAdd } from \"@pnp/core\";\nimport { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// caching will only apply to requests using `cachingList` as the base of the fluent chain\nconst cachingList = sp.web.lists.getByTitle(\"{List Title}\").using(Caching());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst itemsInfo = await cachingList.items();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst itemsInfo2 = await cachingList.items();\n
"},{"location":"queryable/behaviors/#bindcachingcore","title":"bindCachingCore","text":"Added in 3.10.0
The bindCachingCore
method is supplied to allow all caching behaviors to share a common logic around the handling of ICachingProps. Usage of this function is not required to build your own caching method. However, it does provide consistent logic and will incoroporate any future enhancements. It can be used to create your own caching behavior. Here we show how we use the binding function within Caching
as a basic example.
The bindCachingCore
method is designed for use in a pre
observer and the first two parameters are the url and init passed to pre. The third parameter is an optional Partial. It returns a tuple with three values. The first is a calculated value indicating if this request should be cached based on the internal default logic of the library, you can use this value in conjunction with your own logic. The second value is a function that will get a cached value, note no key is passed - the key is calculated and held within bindCachingCore
. The third value is a function to which you pass a value to cache. The key and expiration are similarly calculated and held within bindCachingCore
.
import { TimelinePipe } from \"@pnp/core\";\nimport { bindCachingCore, ICachingProps, Queryable } from \"@pnp/queryable\";\n\nexport function Caching(props?: ICachingProps): TimelinePipe<Queryable> {\n\n return (instance: Queryable) => {\n\n instance.on.pre(async function (this: Queryable, url: string, init: RequestInit, result: any): Promise<[string, RequestInit, any]> {\n\n const [shouldCache, getCachedValue, setCachedValue] = bindCachingCore(url, init, props);\n\n // only cache get requested data or where the CacheAlways header is present (allows caching of POST requests)\n if (shouldCache) {\n\n const cached = getCachedValue();\n\n // we need to ensure that result stays \"undefined\" unless we mean to set null as the result\n if (cached === null) {\n\n // if we don't have a cached result we need to get it after the request is sent. Get the raw value (un-parsed) to store into cache\n this.on.rawData(noInherit(async function (response) {\n setCachedValue(response);\n }));\n\n } else {\n // if we find it in cache, override send request, and continue flow through timeline and parsers.\n this.on.auth.clear();\n this.on.send.replace(async function (this: Queryable) {\n return new Response(cached, {});\n });\n }\n }\n\n return [url, init, result];\n });\n\n return instance;\n };\n}\n
"},{"location":"queryable/behaviors/#cachekey","title":"CacheKey","text":"Added in 3.5.0
This behavior allows you to set a pre-determined cache key for a given request. It needs to be used PER request otherwise the value will be continuously overwritten.
import { Caching, CacheKey } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\n// note the application of the behavior on individual requests, if you share a CacheKey behavior across requests you'll encounter conflicts\nconst webInfo = await sp.web.using(CacheKey(\"MyWebInfoCacheKey\"))();\n\nconst listsInfo = await sp.web.lists.using(CacheKey(\"MyListsInfoCacheKey\"))();\n
"},{"location":"queryable/behaviors/#cachealways","title":"CacheAlways","text":"Added in 3.8.0
This behavior allows you to force caching for a given request. This should not be used for update/create operations as the request will not execute if a result is found in the cache
import { Caching, CacheAlways } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\nconst webInfo = await sp.web.using(CacheAlways())();\n
"},{"location":"queryable/behaviors/#cachenever","title":"CacheNever","text":"Added in 3.10.0
This behavior allows you to force skipping caching for a given request.
import { Caching, CacheNever } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\nconst webInfo = await sp.web.using(CacheNever())();\n
"},{"location":"queryable/behaviors/#caching-pessimistic-refresh","title":"Caching Pessimistic Refresh","text":"This behavior is slightly different than our default Caching behavior in that it will always return the cached value if there is one, but also asyncronously update the cached value in the background. Like the default CAchine behavior it allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map.
If you do not provide an expiration function then the cache will be updated asyncronously on every call, if you do provide an expiration then the cached value will only be updated, although still asyncronously, only when the cache has expired.
import { CachingPessimisticRefresh } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(CachingPessimisticRefresh());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n
Again as with the default Caching behavior you can provide custom functions for key generation and expiration. Please see the Custom Key Function documentation above for more details.
"},{"location":"queryable/behaviors/#injectheaders","title":"InjectHeaders","text":"Adds any specified headers to a given request. Can be used multiple times with a timeline. The supplied headers are added to all requests, and last applied wins - meaning if two InjectHeaders are included in the pipeline which inlcude a value for the same header, the second one applied will be used.
import { InjectHeaders } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(InjectHeaders({\n \"X-Something\": \"a value\",\n \"MyCompanySpecialAuth\": \"special company token\",\n}));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#parsers","title":"Parsers","text":"Parsers convert the returned fetch Response into something usable. We have included the most common parsers we think you'll need - but you can always write your own parser based on the signature of the parse moment.
All of these parsers when applied through using will replace any other observers on the parse moment.
"},{"location":"queryable/behaviors/#defaultparse","title":"DefaultParse","text":"Performs error handling and parsing of JSON responses. This is the one you'll use for most of your requests and it is included in all the defaults.
import { DefaultParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(DefaultParse());\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#textparse","title":"TextParse","text":"Checks for errors and parses the results as text with no further manipulation.
import { TextParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(TextParse());\n
"},{"location":"queryable/behaviors/#blobparse","title":"BlobParse","text":"Checks for errors and parses the results a Blob with no further manipulation.
import { BlobParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BlobParse());\n
"},{"location":"queryable/behaviors/#jsonparse","title":"JSONParse","text":"Checks for errors and parses the results as JSON with no further manipulation. Meaning you will get the raw JSON response vs DefaultParse which will remove wrapping JSON.
import { JSONParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(JSONParse());\n
"},{"location":"queryable/behaviors/#bufferparse","title":"BufferParse","text":"Checks for errors and parses the results a Buffer with no further manipulation.
import { BufferParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BufferParse());\n
"},{"location":"queryable/behaviors/#headerparse","title":"HeaderParse","text":"Checks for errors and parses the headers of the Response as the result. This is a specialised parses which can be used in those infrequent scenarios where you need information from the headers of a response.
import { HeaderParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(HeaderParse());\n
"},{"location":"queryable/behaviors/#jsonheaderparse","title":"JSONHeaderParse","text":"Checks for errors and parses the headers of the Respnose as well as the JSON and returns an object with both values.
import { JSONHeaderParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(JSONHeaderParse());\n\n...sp.data\n...sp.headers\n
"},{"location":"queryable/behaviors/#resolvers","title":"Resolvers","text":"These two behaviors are special and should always be included when composing your own defaults. They implement the expected behavior of resolving or rejecting the promise returned when executing a timeline. They are implemented as behaviors should there be a need to do something different the logic is not locked into the core of the library.
"},{"location":"queryable/behaviors/#resolveondata-rejectonerror","title":"ResolveOnData, RejectOnError","text":"import { ResolveOnData, RejectOnError } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(ResolveOnData(), RejectOnError());\n
"},{"location":"queryable/behaviors/#timeout","title":"Timeout","text":"The Timeout behavior allows you to include a timeout in requests. You can specify either a number, representing the number of milliseconds until the request should timeout or an AbortSignal.
In Nodejs you will need to polyfill AbortController
if your version (<15) does not include it when using Timeout and passing a number. If you are supplying your own AbortSignal you do not.
import { Timeout } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\n// requests should timeout in 5 seconds\nconst sp = spfi(...).using(Timeout(5000));\n
import { Timeout } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst controller = new AbortController();\n\nconst sp = spfi(...).using(Timeout(controller.signal));\n\n// abort requests after 6 seconds using our own controller\nconst timer = setTimeout(() => {\n controller.abort();\n}, 6000);\n\n// this request will be cancelled if it doesn't complete in 6 seconds\nconst webInfo = await sp.webs();\n\n// be a good citizen and cancel unneeded timers\nclearTimeout(timer);\n
"},{"location":"queryable/behaviors/#cancelable","title":"Cancelable","text":"Updated as Beta 2 in 3.5.0
This behavior allows you to cancel requests before they are complete. It is similar to timeout however you control when and if the request is canceled. Please consider this behavior as beta while we work to stabalize the functionality.
"},{"location":"queryable/behaviors/#known-issues","title":"Known Issues","text":"import { Cancelable, CancelablePromise } from \"@pnp/queryable\";\nimport { IWebInfo } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(Cancelable());\n\nconst p: CancelablePromise<IWebInfo> = <any>sp.web();\n\nsetTimeout(() => {\n\n // you should await the cancel operation to ensure it completes\n await p.cancel();\n}, 200);\n\n// this is awaiting the results of the request\nconst webInfo: IWebInfo = await p;\n
"},{"location":"queryable/behaviors/#cancel-long-running-operations","title":"Cancel long running operations","text":"Some operations such as chunked uploads that take longer to complete are good candidates for canceling based on user input such as a button select.
import { Cancelable, CancelablePromise } from \"@pnp/queryable\";\nimport { IFileAddResult } from \"@pnp/sp/files\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport { getRandomString } from \"@pnp/core\";\nimport { createReadStream } from \"fs\";\n\nconst sp = spfi().using(Cancelable());\n\nconst file = createReadStream(join(\"C:/some/path\", \"test.mp4\"));\n\nconst p: CancelablePromise<IFileAddResult> = <any>sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents\").files.addChunked(`te's't-${getRandomString(4)}.mp4`, <any>file);\n\nsetTimeout(() => {\n\n // you should await the cancel operation to ensure it completes\n await p.cancel();\n}, 10000);\n\n// this is awaiting the results of the request\nawait p;\n
"},{"location":"queryable/extensions/","title":"Extensions","text":"Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invokable and allow you to control any behavior of the library with extensions.
"},{"location":"queryable/extensions/#types-of-extensions","title":"Types of Extensions","text":"There are two types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.
"},{"location":"queryable/extensions/#function-extensions","title":"Function Extensions","text":"The first type is a simple function with a signature:
(op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void\n
This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.
"},{"location":"queryable/extensions/#named-extensions","title":"Named Extensions","text":"Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.
import { extendFactory } from \"@pnp/queryable\";\nimport { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\";\nimport { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\";\n\n// create a plain object with the props and methods we want to add/change\nconst myExtensions = {\n // override the lists property\n get lists(this: IWeb): ILists {\n // we will always order our lists by title and select just the Title for ALL calls (just as an example)\n return Lists(this).orderBy(\"Title\").select(\"Title\");\n },\n // override the getByTitle method\n getByTitle: function (this: ILists, title: string): IList {\n // in our example our list has moved, so we rewrite the request on the fly\n if (title === \"List1\") {\n return List(this, `getByTitle('List2')`);\n } else {\n // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n // so you need to ensure you patch/include any original functionality you need\n return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);\n }\n },\n};\n\n// register all the named Extensions\nextendFactory(Web, myExtensions);\n\n// this will use our extension to ensure the lists are ordered\nconst lists = await sp.web.lists();\n\nconsole.log(JSON.stringify(lists, null, 2));\n\n// we will get the items from List1 but within the extension it is rewritten as List2\nconst items = await sp.web.lists.getByTitle(\"List1\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#proxyhandler-extensions","title":"ProxyHandler Extensions","text":"You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.
import { extendFactory } from \"@pnp/queryable\";\nimport { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\";\nimport { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\";\n\nconst myExtensions = {\n get: (target, p: string | number | symbol, _receiver: any) => {\n switch (p) {\n case \"getByTitle\":\n return (title: string) => {\n\n // in our example our list has moved, so we rewrite the request on the fly\n if (title === \"LookupList\") {\n return List(target, `getByTitle('OrderByList')`);\n } else {\n // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n // so you need to ensure you patch/include any original functionality you need\n return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);\n }\n };\n }\n },\n};\n\nextendFactory(Web, myExtensions);\n\nconst lists = sp.web.lists;\nconst items = await lists.getByTitle(\"LookupList\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#registering-extensions","title":"Registering Extensions","text":"You can register Extensions on an invocable factory or on a per-object basis, and you can register a single extension or an array of Extensions.
"},{"location":"queryable/extensions/#factory-registration","title":"Factory Registration","text":"The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IWeb, Web } from \"@pnp/sp/webs\";\nimport { ILists, Lists } from \"@pnp/sp/lists\";\nimport { extendFactory } from \"@pnp/queryable\";\nimport { sp } from \"@pnp/sp\";\n\nconst sp = spfi().using(...);\n\n// sets up the types correctly when importing across your application\ndeclare module \"@pnp/sp/webs/types\" {\n\n // we need to extend the interface\n interface IWeb {\n orderedLists: ILists;\n }\n}\n\n// sets up the types correctly when importing across your application\ndeclare module \"@pnp/sp/lists/types\" {\n\n // we need to extend the interface\n interface ILists {\n getOrderedListsQuery: (this: ILists) => ILists;\n }\n}\n\nextendFactory(Web, {\n // add an ordered lists property\n get orderedLists(this: IWeb): ILists {\n return this.lists.getOrderedListsQuery();\n },\n});\n\nextendFactory(Lists, {\n // add an ordered lists property\n getOrderedListsQuery(this: ILists): ILists {\n return this.top(10).orderBy(\"Title\").select(\"Title\");\n },\n});\n\n// regardless of how we access the web and lists collections our extensions remain with all new instance based on\nconst web = Web([sp.web, \"https://tenant.sharepoint.com/sites/dev/\"]);\nconst lists1 = await web.orderedLists();\nconsole.log(JSON.stringify(lists1, null, 2));\n\nconst lists2 = await Web([sp.web, \"https://tenant.sharepoint.com/sites/dev/\"]).orderedLists();\nconsole.log(JSON.stringify(lists2, null, 2));\n\nconst lists3 = await sp.web.orderedLists();\nconsole.log(JSON.stringify(lists3, null, 2));\n
"},{"location":"queryable/extensions/#instance-registration","title":"Instance Registration","text":"You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.
Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.
Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.
import { extendObj } from \"@pnp/queryable\";\nimport { sp, List, ILists } from \"@pnp/sp/presets/all\";\n\nconst myExtensions = {\n getByTitle: function (this: ILists, title: string) {\n // in our example our list has moved, so we rewrite the request on the fly\n if (title === \"List1\") {\n return List(this, \"getByTitle('List2')\");\n } else {\n // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n // so you need to ensure you patch/include any original functionality you need\n return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);\n }\n },\n};\n\nconst lists = extendObj(sp.web.lists, myExtensions);\nconst items = await lists.getByTitle(\"LookupList\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#enable-disable-extensions-and-clear-global-extensions","title":"Enable & Disable Extensions and Clear Global Extensions","text":"Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.
import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\";\n\n// disable Extensions\ndisableExtensions();\n\n// enable Extensions\nenableExtensions();\n
"},{"location":"queryable/queryable/","title":"@pnp/queryable/queryable","text":"Queryable is the base class for both the sp and graph fluent interfaces and provides the structure to which observers are registered. As a background to understand more of the mechanics please see the articles on Timeline, moments, and observers. For reuse it is recommended to compose your observer registrations with behaviors.
"},{"location":"queryable/queryable/#queryable-constructor","title":"Queryable Constructor","text":"By design the library is meant to allow creating the next part of a url from the current part. In this way each queryable instance is built from a previous instance. As such understanding the Queryable constructor's behavior is important. The constructor takes two parameters, the first required and the second optional.
The first parameter can be another queryable, a string, or a tuple of [Queryable, string].
Parameter Behavior Queryable The new queryable inherits all of the supplied queryable's observers. Any supplied path (second constructor param) is appended to the supplied queryable's url becoming the url of the newly constructed queryable string The new queryable will have NO registered observers. Any supplied path (second constructor param) is appended to the string becoming the url of the newly constructed queryable [Queryable, string] The observers from the supplied queryable are used by the new queryable. The url is a combination of the second tuple argument (absolute url string) and any supplied path.The tuple constructor call can be used to rebase a queryable to call a different host in an otherwise identical way to another queryable. When using the tuple constructor the url provided must be absolute.
"},{"location":"queryable/queryable/#examples","title":"Examples","text":"// represents a fully configured queryable with url and registered observers\n// url: https://something.com\nconst baseQueryable;\n\n// child1 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://something.com/subpath\"\nconst child1 = Child(baseQueryable, \"subpath\");\n\n// child2 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://something.com\"\nconst child2 = Child(baseQueryable);\n\n// nonchild1 will:\n// - have NO registered observers or connection to baseQueryable\n// - have a url of \"https://somethingelse.com\"\nconst nonchild1 = Child(\"https://somethingelse.com\");\n\n// nonchild2 will:\n// - have NO registered observers or connection to baseQueryable\n// - have a url of \"https://somethingelse.com/subpath\"\nconst nonchild2 = Child(\"https://somethingelse.com\", \"subpath\");\n\n// rebased1 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://somethingelse.com\"\nconst rebased1 = Child([baseQueryable, \"https://somethingelse.com\"]);\n\n// rebased2 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://somethingelse.com/subpath\"\nconst rebased2 = Child([baseQueryable, \"https://somethingelse.com\"], \"subpath\");\n
"},{"location":"queryable/queryable/#queryable-lifecycle","title":"Queryable Lifecycle","text":"The Queryable lifecycle is:
construct
(Added in 3.5.0)init
pre
auth
send
parse
post
data
dispose
As well log
and error
can emit at any point during the lifecycle.
If you see an error thrown with the message No observers registered for this request.
it means at the time of execution the given object has no actions to take. Because all the request logic is defined within observers, an absence of observers is likely an error condition. If the object was created by a method within the library please report an issue as it is likely a bug. If you created the object through direct use of one of the factory functions, please be sure you have registered observers with using
or on
as appropriate. More information on observers is available in this article.
If you for some reason want to execute a queryable with no registred observers, you can simply register a noop observer to any of the moments.
"},{"location":"queryable/queryable/#queryable-observers","title":"Queryable Observers","text":"This section outlines how to write observers for the Queryable lifecycle, and the expectations of each moment's observer behaviors.
In the below samples consider the variable query
to mean any valid Queryable derived object.
Anything can log to a given timeline's log using the public log
method and to intercept those message you can subscribed to the log event.
The log
observer's signature is: (this: Timeline<T>, message: string, level: number) => void
query.on.log((message, level) => {\n\n // log only warnings or errors\n if (level > 1) {\n console.log(message);\n }\n});\n
The level value is a number indicating the severity of the message. Internally we use the values from the LogLevel enum in @pnp/logging: Verbose = 0, Info = 1, Warning = 2, Error = 3. Be aware that nothing enforces those values other than convention and log can be called with any value for level.
As well we provide easy support to use PnP logging within a Timeline derived class:
import { LogLevel, PnPLogging } from \"@pnp/logging\";\n\n// any messages of LogLevel Info or higher (1) will be logged to all subscribers of the logging framework\nquery.using(PnPLogging(LogLevel.Info));\n
More details on the pnp logging framework
"},{"location":"queryable/queryable/#error","title":"error","text":"Errors can happen at anytime and for any reason. If you are using the RejectOnError
behavior, and both sp and graph include that in the defaults, the request promise will be rejected as expected and you can handle the error that way.
The error
observer's signature is: (this: Timeline<T>, err: string | Error) => void
import { spfi, DefaultInit, DefaultHeaders } from \"@pnp/sp\";\nimport { BrowserFetchWithRetry, DefaultParse } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultInit(), DefaultHeaders(), BrowserFetchWithRetry(), DefaultParse());\n\ntry {\n\n const result = await sp.web();\n\n} catch(e) {\n\n // any errors emitted will result in the promise being rejected\n // and ending up in the catch block as expected\n}\n
In addition to the default behavior you can register your own observers on error
, though it is recommended you leave the default behavior in place.
query.on.error((err) => {\n\n if (err) {\n console.error(err);\n // do other stuff with the error (send it to telemetry)\n }\n});\n
"},{"location":"queryable/queryable/#construct","title":"construct","text":"Added in 3.5.0
This moment exists to assist behaviors that need to transfer some information from a parent to a child through the fluent chain. We added this to support cancelable scopes for the Cancelable behavior, but it may have other uses. It is invoked AFTER the new instance is fully realized via new
and supplied with the parameters used to create the new instance. As with all moments the \"this\" within the observer is the current (NEW) instance.
For your observers on the construct method to work correctly they must be registered before the instance is created.
The construct moment is NOT async and is designed to support simple operations.
query.on.construct(function (this: Queryable, init: QueryableInit, path?: string): void {\n if (typeof init !== \"string\") {\n\n // get a ref to the parent Queryable instance used to create this new instance\n const parent = isArray(init) ? init[0] : init;\n\n if (Reflect.has(parent, \"SomeSpecialValueKey\")) {\n\n // copy that specail value to the new child\n this[\"SomeSpecialValueKey\"] = parent[\"SomeSpecialValueKey\"];\n }\n } \n});\n\nquery.on.pre(async function(url, init, result) {\n\n // we have access to the copied special value throughout the lifecycle\n this.log(this[\"SomeSpecialValueKey\"]);\n\n return [url, init, result];\n});\n\nquery.on.dispose(() => {\n\n // be a good citizen and clean up your behavior's values when you're done\n delete this[\"SomeSpecialValueKey\"];\n});\n
"},{"location":"queryable/queryable/#init","title":"init","text":"Along with dispose
, init
is a special moment that occurs before any of the other lifecycle providing a first chance at doing any tasks before the rest of the lifecycle starts. It is not await aware so only sync operations are supported in init by design.
The init
observer's signature is: (this: Timeline<T>) => void
In the case of init you manipulate the Timeline instance itself
query.on.init(function (this: Queryable) {\n\n // init is a great place to register additioanl observers ahead of the lifecycle\n this.on.pre(async function (this: Quyerable, url, init, result) {\n // stuff happens\n return [url, init, result];\n });\n});\n
"},{"location":"queryable/queryable/#pre","title":"pre","text":"Pre is used by observers to configure the request before sending. Note there is a dedicated auth moment which is prefered by convention to handle auth related tasks.
The pre
observer's signature is: (this: IQueryable, url: string, init: RequestInit, result: any) => Promise<[string, RequestInit, any]>
The pre
, auth
, parse
, and post
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
Example of when to use pre are updates to the init, caching scenarios, or manipulation of the url (ensuring it is absolute). The init passed to pre (and auth) is the same object that will be eventually passed to fetch, meaning you can add any properties/congifuration you need. The result should always be left undefined unless you intend to end the lifecycle. If pre completes and result has any value other than undefined that value will be emitted to data
and the timeline lifecycle will end.
query.on.pre(async function(url, init, result) {\n\n init.cache = \"no-store\";\n\n return [url, init, result];\n});\n\nquery.on.pre(async function(url, init, result) {\n\n // setting result causes no moments after pre to be emitted other than data\n // once data is emitted (resolving the request promise by default) the lifecycle ends\n result = \"My result\";\n\n return [url, init, result];\n});\n
"},{"location":"queryable/queryable/#auth","title":"auth","text":"Auth functions very much like pre
except it does not have the option to set the result, and the url is considered immutable by convention. Url manipulation should be done in pre. Having a seperate moment for auth allows for easily changing auth specific behavior without having to so a lot of complicated parsing of pre
observers.
The auth
observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<[URL, RequestInit]>
.
The pre
, auth
, parse
, and post
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
query.on.auth(async function(url, init) {\n\n // some code to get a token\n const token = getToken();\n\n init.headers[\"Authorization\"] = `Bearer ${token}`;\n\n return [url, init];\n});\n
"},{"location":"queryable/queryable/#send","title":"send","text":"Send is implemented using the request moment which uses the first registered observer and invokes it expecting an async Response.
The send
observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<Response>
.
query.on.send(async function(url, init) {\n\n // this could represent reading a file, querying a database, or making a web call\n return fetch(url.toString(), init);\n});\n
"},{"location":"queryable/queryable/#parse","title":"parse","text":"Parse is responsible for turning the raw Response into something usable. By default we handle errors and parse JSON responses, but any logic could be injected here. Perhaps your company encrypts things and you need to decrypt them before parsing further.
The parse
observer's signature is: (this: IQueryable, url: URL, response: Response, result: any | undefined) => Promise<[URL, Response, any]>
.
The pre
, auth
, parse
, and post
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
// you should be careful running multiple parse observers so we replace with our functionality\n// remember every registered observer is run, so if you set result and a later observer sets a\n// different value last in wins.\nquery.on.parse.replace(async function(url, response, result) {\n\n if (response.ok) {\n\n result = await response.json();\n\n } else {\n\n // just an example\n throw Error(response.statusText);\n }\n\n return [url, response, result];\n});\n
"},{"location":"queryable/queryable/#post","title":"post","text":"Post is run after parse, meaning you should have a valid fully parsed result, and provides a final opportunity to do caching, some final checks, or whatever you might need immediately prior to the request promise resolving with the value. It is recommened to NOT manipulate the result within post though nothing prevents you from doing so.
The post
observer's signature is: (this: IQueryable, url: URL, result: any | undefined) => Promise<[URL, any]>
.
The pre
, auth
, parse
, and post
are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.
query.on.post(async function(url, result) {\n\n // here we do some caching of a result\n const key = hash(url);\n cache(key, result); \n\n return [url, result];\n});\n
"},{"location":"queryable/queryable/#data","title":"data","text":"Data is called with the result of the Queryable lifecycle produced by send
, understood by parse
, and passed through post
. By default the request promise will resolve with the value, but you can add any additional observers you need.
The data
observer's signature is: (this: IQueryable, result: T) => void
.
Clearing the data moment (ie. .on.data.clear()) after the lifecycle has started will result in the request promise never resolving
query.on.data(function(result) {\n\n console.log(`Our result! ${JSON.stringify(result)}`);\n});\n
"},{"location":"queryable/queryable/#dispose","title":"dispose","text":"Along with init
, dispose
is a special moment that occurs after all other lifecycle moments have completed. It is not await aware so only sync operations are supported in dispose by design.
The dispose
observer's signature is: (this: Timeline<T>) => void
In the case of dispose you manipulate the Timeline instance itself
query.on.dispose(function (this: Queryable) {\n\n // maybe your queryable calls a database?\n db.connection.close();\n});\n
"},{"location":"queryable/queryable/#other-methods","title":"Other Methods","text":"Queryable exposes some additional methods beyond the observer registration.
"},{"location":"queryable/queryable/#concat","title":"concat","text":"Appends the supplied string to the url without mormalizing slashes.
// url: something.com/items\nquery.concat(\"(ID)\");\n// url: something.com/items(ID)\n
"},{"location":"queryable/queryable/#torequesturl","title":"toRequestUrl","text":"Converts the queryable's internal url parameters (url and query) into a relative or absolute url.
const s = query.toRequestUrl();\n
"},{"location":"queryable/queryable/#query","title":"query","text":"Map used to manage any query string parameters that will be included. Anything added here will be represented in toRequestUrl
's output.
query.query.add(\"$select\", \"Title\");\n
"},{"location":"queryable/queryable/#tourl","title":"toUrl","text":"Returns the url currently represented by the Queryable, without the querystring part
const s = query.toUrl();\n
"},{"location":"sp/alias-parameters/","title":"@pnp/sp - Aliased Parameters","text":"Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders.
To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query.
"},{"location":"sp/alias-parameters/#construct-a-parameter-alias","title":"Construct a parameter alias","text":"Pattern: !@{label name}::{value}
Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\"
"},{"location":"sp/alias-parameters/#example-without-aliasing","title":"Example without aliasing","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nconst sp = spfi(...);\n\n// still works as expected, no aliasing\nconst query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3\n\nconst r = await query();\nconsole.log(r);\n
"},{"location":"sp/alias-parameters/#example-with-aliasing","title":"Example with aliasing","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// same query with aliasing\nconst query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3\n\nconst r = await query();\nconsole.log(r);\n
"},{"location":"sp/alias-parameters/#example-with-aliasing-and-batching","title":"Example with aliasing and batching","text":"Aliasing is supported with batching as well:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// same query with aliasing and batching\nconst [batchedWeb, execute] = await sp.web.batched();\n\nconst query = batchedWeb.web.getFolderByServerRelativePath(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3\n\nquery().then(r => {\n\n console.log(r);\n});\n\nexecute();\n
"},{"location":"sp/alm/","title":"@pnp/sp/appcatalog","text":"The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.
"},{"location":"sp/alm/#understanding-the-app-catalog-hierarchy","title":"Understanding the App Catalog Hierarchy","text":"Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.
"},{"location":"sp/alm/#referencing-an-app-catalog","title":"Referencing an App Catalog","text":"There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.
"},{"location":"sp/alm/#get-tenant-app-catalog","title":"Get tenant app catalog","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// get the current context web's app catalog\n// this will be the site collection app catalog\nconst availableApps = await sp.tenantAppcatalog();\n
"},{"location":"sp/alm/#get-site-collection-appcatalog","title":"Get site collection AppCatalog","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// get the current context web's app catalog\nconst availableApps = await sp.web.appcatalog();\n
"},{"location":"sp/alm/#get-site-collection-appcatalog-by-url","title":"Get site collection AppCatalog by URL","text":"If you know the url of the site collection whose app catalog you want you can use the following code. First you need to use one of the methods to access a web. Once you have the web instance you can call the .appcatalog
property on that web instance.
If a given site collection does not have an app catalog trying to access it will throw an error.
import { spfi } from \"@pnp/sp\";\nimport { Web } from '@pnp/sp/webs';\n\nconst sp = spfi(...);\nconst web = Web([sp.web, \"https://mytenant.sharepoint.com/sites/mysite\"]);\nconst catalog = await web.appcatalog();\n
The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.
"},{"location":"sp/alm/#list-available-apps","title":"List Available Apps","text":"The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.
// get available apps\nawait catalog();\n\n// get available apps selecting two fields\nawait catalog.select(\"Title\", \"Deployed\")();\n
"},{"location":"sp/alm/#add-an-app","title":"Add an App","text":"This action must be performed in the context of the tenant app catalog
// this represents the file bytes of the app package file\nconst blob = new Blob();\n\n// there is an optional third argument to control overwriting existing files\nconst r = await catalog.add(\"myapp.app\", blob);\n\n// this is at its core a file add operation so you have access to the response data as well\n// as a File instance representing the created file\nconsole.log(JSON.stringify(r.data, null, 4));\n\n// all file operations are available\nconst nameData = await r.file.select(\"Name\")();\n
"},{"location":"sp/alm/#get-an-app","title":"Get an App","text":"You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions
const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")();\n
"},{"location":"sp/alm/#perform-app-actions","title":"Perform app actions","text":"Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block.
const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\";\n\n// deploy\nawait catalog.getAppById(myAppId).deploy();\n\n// retract\nawait catalog.getAppById(myAppId).retract();\n\n// install\nawait catalog.getAppById(myAppId).install();\n\n// uninstall\nawait catalog.getAppById(myAppId).uninstall();\n\n// upgrade\nawait catalog.getAppById(myAppId).upgrade();\n\n// remove\nawait catalog.getAppById(myAppId).remove();\n\n
"},{"location":"sp/alm/#synchronize-a-solutionapp-to-the-microsoft-teams-app-catalog","title":"Synchronize a solution/app to the Microsoft Teams App Catalog","text":"By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id.
// Using the app id\nawait catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\");\n\n// Using the SharePoint apps item id\nawait catalog.syncSolutionToTeams(\"123\", true);\n
"},{"location":"sp/alm/#notes","title":"Notes","text":"The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.
"},{"location":"sp/attachments/#get-attachments","title":"Get attachments","text":"import { spfi } from \"@pnp/sp\";\nimport { IAttachmentInfo } from \"@pnp/sp/attachments\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\n// get all the attachments\nconst info: IAttachmentInfo[] = await item.attachmentFiles();\n\n// get a single file by file name\nconst info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")();\n\n// select specific properties using odata operators and use Pick to type the result\nconst info3: Pick<IAttachmentInfo, \"ServerRelativeUrl\">[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")();\n
"},{"location":"sp/attachments/#add-an-attachment","title":"Add an Attachment","text":"You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.
import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.add(\"file2.txt\", \"Here is my content\");\n
"},{"location":"sp/attachments/#read-attachment-content","title":"Read Attachment Content","text":"You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.
import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nconst text = await item.attachmentFiles.getByName(\"file.txt\").getText();\n\n// use this in the browser, does not work in nodejs\nconst blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob();\n\n// use this in nodejs\nconst buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer();\n\n// file must be valid json\nconst json = await item.attachmentFiles.getByName(\"file.json\").getJSON();\n
"},{"location":"sp/attachments/#update-attachment-content","title":"Update Attachment Content","text":"You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library.
import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\");\n
"},{"location":"sp/attachments/#delete-attachment","title":"Delete Attachment","text":"import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").delete();\n
"},{"location":"sp/attachments/#recycle-attachment","title":"Recycle Attachment","text":"Delete the attachment and send it to recycle bin
import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").recycle();\n
"},{"location":"sp/attachments/#recycle-multiple-attachments","title":"Recycle Multiple Attachments","text":"Delete multiple attachments and send them to recycle bin
import { spfi } from \"@pnp/sp\";\nimport { IList } from \"@pnp/sp/lists/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst item = await batchedSP.web.lists.getByTitle(\"MyList\").items.getById(2);\n\nitem.attachmentFiles.getByName(\"1.txt\").recycle();\nitem.attachmentFiles.getByName(\"2.txt\").recycle();\n\nawait execute();\n
"},{"location":"sp/behaviors/","title":"@pnp/sp : behaviors","text":"The article describes the behaviors exported by the @pnp/sp
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/graph, and @pnp/nodejs.
The DefaultInit
behavior, is a composed behavior which includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit.
import { spfi, DefaultInit } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultInit());\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#defaultheaders","title":"DefaultHeaders","text":"The DefaultHeaders
behavior uses InjectHeaders to set the Accept, Content-Type, and User-Agent headers.
import { spfi, DefaultHeaders } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultHeaders());\n\nawait sp.web();\n
DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.
"},{"location":"sp/behaviors/#requestdigest","title":"RequestDigest","text":"The RequestDigest
behavior ensures that the \"X-RequestDigest\" header is included for requests where it is needed. If you are using MSAL, supplying your own tokens, or doing a GET request it is not required. As well it cache's the digests to reduce the number of requests.
Optionally you can provide a function to supply your own digests. The logic followed by the behavior is to check the cache, run a hook if provided, and finally make a request to \"/_api/contextinfo\" for the value.
import { spfi, RequestDigest } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(RequestDigest());\n\nawait sp.web();\n
With a hook:
import { dateAdd } from \"@pnp/core\";\nimport { spfi, RequestDigest } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(RequestDigest((url, init) => {\n\n // the url will be a URL instance representing the request url\n // init will be the RequestInit\n\n return {\n expiration: dateAdd(new Date(), \"minute\", 20);\n value: \"MY VALID REQUEST DIGEST VALUE\";\n }\n}));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#spbrowser","title":"SPBrowser","text":"A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.
The baseUrl prop can be used to configure a fallback when making urls absolute.
If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.
You should set a baseUrl as shown below.
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// you should use the baseUrl value when working in a SPA to ensure it is always properly set for all requests\nconst sp = spfi().using(SPBrowser({ baseUrl: \"https://tenant.sharepoint.com/sites/dev\" }));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#spfx","title":"SPFx","text":"This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx
is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. A hook is supplied to RequestDigest that will attempt to use any existing legacyPageContext formDigestValue it can find, otherwise defaults to the base RequestDigest behavior. It also sets a pre handler to ensure the url is absolute, using the SPFx context's pageContext.web.absoluteUrl as the base.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst sp = spfi(...).using(SPFx(this.context));\n\nawait sp.web();\n
Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.
import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'\nimport { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'\n\nconst sp = spfi().using(spSPFx(this.context));\nconst graph = graphfi().using(graphSPFx(this.context));\n
"},{"location":"sp/behaviors/#spfxtoken","title":"SPFxToken","text":"Added in 3.12
Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst sp = spfi(...).using(SPFxToken(this.context));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#telemetry","title":"Telemetry","text":"This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.
You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.
import { spfi, Telemetry } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(Telemetry());\n\nawait sp.web();\n
"},{"location":"sp/clientside-pages/","title":"@pnp/sp/clientside-pages","text":"The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts.
"},{"location":"sp/clientside-pages/#create-a-new-page","title":"Create a new Page","text":"You can create a new client-side page in several ways, all are equivalent.
"},{"location":"sp/clientside-pages/#create-using-iwebaddclientsidepage","title":"Create using IWeb.addClientsidePage","text":"import { spfi, SPFI } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\nimport { PromotedState } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// Create a page providing a file name\nconst page = await sp.web.addClientsidePage(\"mypage1\");\n\n// ... other operations on the page as outlined below\n\n// the page is initially not published, you must publish it so it appears for others users\nawait page.save();\n\n// include title and page layout\nconst page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\");\n\n// you must publish the new page\nawait page2.save();\n\n// include title, page layout, and specifying the publishing status (Added in 2.0.4)\nconst page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish);\n\n// you must publish the new page, after which the page will immediately be promoted to a news article\nawait page3.save();\n
"},{"location":"sp/clientside-pages/#create-using-createclientsidepage-method","title":"Create using CreateClientsidePage method","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { Web } from \"@pnp/sp/webs\";\nimport { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\");\n\n// you must publish the new page\nawait page1.save(true);\n\n// specify the page layout type parameter\nconst page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\");\n\n// you must publish the new page\nawait page2.save();\n\n// specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4)\nconst page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish);\n\n// you must publish the new page, after which the page will immediately be promoted to a news article\nawait page2half.save();\n\n// use the web factory to create a page in a specific web\nconst page3 = await CreateClientsidePage(Web([sp, \"https://{absolute web url}\"]), \"mypage4\", \"My Page Title\");\n\n// you must publish the new page\nawait page3.save();\n
"},{"location":"sp/clientside-pages/#create-using-iwebaddfullpageapp","title":"Create using IWeb.addFullPageApp","text":"Using this method you can easily create a full page app page given the component id. Don't forget the page will not be published and you will need to call save.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page = await sp.web.addFullPageApp(\"name333\", \"My Title\", \"2CE4E250-B997-11EB-A9D2-C9D2FF95D000\");\n// ... other page actions\n// you must save the page to publish it\nawait page.save();\n
"},{"location":"sp/clientside-pages/#load-pages","title":"Load Pages","text":"There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.
"},{"location":"sp/clientside-pages/#load-using-iwebloadclientsidepage","title":"Load using IWeb.loadClientsidePage","text":"This method takes a server relative path to the page to load.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\n\nconst sp = spfi(...);\n\n// use from the sp.web fluent chain\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n\n// use the web factory to target a specific web\nconst page2 = await Web([sp.web, \"https://{absolute web url}\"]).loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n
"},{"location":"sp/clientside-pages/#load-using-clientsidepagefromfile","title":"Load using ClientsidePageFromFile","text":"This method takes an IFile instance and loads an IClientsidePage instance.
import { spfi } from \"@pnp/sp\";\nimport { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\n\nconst sp = spfi(...);\n\nconst page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\"));\n
"},{"location":"sp/clientside-pages/#edit-sections-and-columns","title":"Edit Sections and Columns","text":"Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page
that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12\nconst section1 = page.addSection();\nsection1.addColumn(6);\nsection1.addColumn(6);\n\n// create a three column layout in a new section\nconst section2 = page.addSection();\nsection2.addColumn(4);\nsection2.addColumn(4);\nsection2.addColumn(4);\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#manipulate-sections-and-columns","title":"Manipulate Sections and Columns","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// drop all the columns in this section\n// this will also DELETE all controls contained in the columns\npage.sections[1].columns.length = 0;\n\n// create a new column layout\npage.sections[1].addColumn(4);\npage.sections[1].addColumn(8);\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#vertical-section","title":"Vertical Section","text":"The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// add or get a vertical section (handles case where section already exists)\nconst vertSection = page.addVerticalSection();\n\n// ****************************************************************\n\n// if you know or want to test if a vertical section is present:\nif (page.hasVerticalSection) {\n\n // access the vertical section (this method will NOT create the section if it does not exist)\n page.verticalSection.addControl(new ClientsideText(\"hello\"));\n} else {\n\n const vertSection = page.addVerticalSection();\n vertSection.addControl(new ClientsideText(\"hello\"));\n}\n
"},{"location":"sp/clientside-pages/#reorder-sections","title":"Reorder Sections","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// swap the order of two sections\n// this will preserve the controls within the columns\npage.sections = [page.sections[1], page.sections[0]];\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#reorder-columns","title":"Reorder Columns","text":"The sections and columns are arrays, so normal array operations work as expected
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// swap the order of two columns\n// this will preserve the controls within the columns\npage.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]];\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#clientside-controls","title":"Clientside Controls","text":"Once you have your sections and columns defined you will want to add/edit controls within those columns.
"},{"location":"sp/clientside-pages/#add-text-content","title":"Add Text Content","text":"import { spfi } from \"@pnp/sp\";\nimport { ClientsideText, IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\npage.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\"));\n\nawait page.save();\n
"},{"location":"sp/clientside-pages/#add-controls","title":"Add Controls","text":"Adding controls involves loading the available client-side part definitions from the server or creating a text part.
import \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\nimport { spfi } from \"@pnp/sp\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// this will be a ClientsidePageComponent array\n// this can be cached on the client in production scenarios\nconst partDefs = await sp.web.getClientsideWebParts();\n\n// find the definition we want, here by id\nconst partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\");\n\n// optionally ensure you found the def\nif (partDef.length < 1) {\n // we didn't find it so we throw an error\n throw new Error(\"Could not find the web part\");\n}\n\n// create a ClientWebPart instance from the definition\nconst part = ClientsideWebpart.fromComponentDef(partDef[0]);\n\n// set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video.\n// the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting\n// the properties correctly\npart.setProperties<{ embedCode: string }>({\n embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\",\n});\n\n// we add that part to a new section\npage.addSection().addControl(part);\n\nawait page.save();\n
"},{"location":"sp/clientside-pages/#handle-different-webparts-settings","title":"Handle Different Webpart's Settings","text":"There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title.
import { spfi } from \"@pnp/sp\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\n// we create a class to wrap our functionality in a reusable way\nclass ListWebpart extends ClientsideWebpart {\n\n constructor(control: ClientsideWebpart) {\n super((<any>control).json);\n }\n\n // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts\n public get DisplayTitle(): string {\n return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\";\n }\n\n public set DisplayTitle(value: string) {\n this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value;\n }\n}\n\nconst sp = spfi(...);\n\n// now we load our page\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\");\n\n// get our part and pass it to the constructor of our wrapper class\nconst part = new ListWebpart(page.sections[0].columns[0].getControl(0));\n\npart.DisplayTitle = \"My New Title!\";\n\nawait page.save();\n
Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.
"},{"location":"sp/clientside-pages/#page-operations","title":"Page Operations","text":"There are other operation you can perform on a page in addition to manipulating the content.
"},{"location":"sp/clientside-pages/#pagelayout","title":"pageLayout","text":"You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.pageLayout;\n\n// set the value\npage.pageLayout = \"Article\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#bannerimageurl","title":"bannerImageUrl","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.bannerImageUrl;\n\n// set the value\npage.bannerImageUrl = \"/server/relative/path/to/image.png\";\nawait page.save();\n
Banner images need to exist within the same site collection as the page where you want to use them.
"},{"location":"sp/clientside-pages/#thumbnailurl","title":"thumbnailUrl","text":"Allows you to set the thumbnail used for the page independently of the banner.
If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.thumbnailUrl;\n\n// set the value\npage.thumbnailUrl = \"/server/relative/path/to/image.png\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#topicheader","title":"topicHeader","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.topicHeader;\n\n// set the value\npage.topicHeader = \"My cool header!\";\nawait page.save();\n\n// clear the topic header and hide it\npage.topicHeader = \"\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#title","title":"title","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.title;\n\n// set the value\npage.title = \"My page title\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#description","title":"description","text":"Descriptions are limited to 255 chars
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.description;\n\n// set the value\npage.description = \"A description\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#layouttype","title":"layoutType","text":"Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.layoutType;\n\n// set the value\npage.layoutType = \"ColorBlock\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#headertextalignment","title":"headerTextAlignment","text":"Sets the header text alignment to one of \"Left\" or \"Center\"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.headerTextAlignment;\n\n// set the value\npage.headerTextAlignment = \"Center\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#showtopicheader","title":"showTopicHeader","text":"Sets if the topic header is displayed on a page.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.showTopicHeader;\n\n// show the header\npage.showTopicHeader = true;\nawait page.save();\n\n// hide the header\npage.showTopicHeader = false;\nawait page.save();\n
"},{"location":"sp/clientside-pages/#showpublishdate","title":"showPublishDate","text":"Sets if the publish date is displayed on a page.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.showPublishDate;\n\n// show the date\npage.showPublishDate = true;\nawait page.save();\n\n// hide the date\npage.showPublishDate = false;\nawait page.save();\n
"},{"location":"sp/clientside-pages/#get-set-author-details","title":"Get / Set author details","text":"import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/site-users\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the author details (string | null)\nconst value = page.authorByLine;\n\n// set the author by user id\nconst user = await sp.web.currentUser.select(\"Id\", \"LoginName\")();\nconst userId = user.Id;\nconst userLogin = user.LoginName;\n\nawait page.setAuthorById(userId);\nawait page.save();\n\nawait page.setAuthorByLoginName(userLogin);\nawait page.save();\n
you must still save the page after setting the author to persist your changes as shown in the example.
"},{"location":"sp/clientside-pages/#load","title":"load","text":"Loads the page from the server. This will overwrite any local unsaved changes.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.load();\n
"},{"location":"sp/clientside-pages/#save","title":"save","text":"Uncustomized home pages (i.e the home page that is generated with a site out of the box) cannot be updated by this library without becoming corrupted.
Saves any changes to the page, optionally keeping them in draft state.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// changes are published\nawait page.save();\n\n// changes remain in draft\nawait page.save(false);\n
"},{"location":"sp/clientside-pages/#discardpagecheckout","title":"discardPageCheckout","text":"Discards any current checkout of the page by the current user.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.discardPageCheckout();\n
"},{"location":"sp/clientside-pages/#schedulepublish","title":"schedulePublish","text":"Schedules the page for publishing.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// date and time to publish the page in UTC.\nconst publishDate = new Date(\"1/1/1901\");\n\nconst scheduleVersion: string = await page.schedulePublish(publishDate);\n
"},{"location":"sp/clientside-pages/#promotetonews","title":"promoteToNews","text":"Promotes the page as a news article.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.promoteToNews();\n
"},{"location":"sp/clientside-pages/#enablecomments-disablecomments","title":"enableComments & disableComments","text":"Used to control the availability of comments on a page.
import { spfi } from \"@pnp/sp\";\n// you need to import the comments sub-module or use the all preset\nimport \"@pnp/sp/comments/clientside-page\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// turn on comments\nawait page.enableComments();\n\n// turn off comments\nawait page.disableComments();\n
"},{"location":"sp/clientside-pages/#findcontrolbyid","title":"findControlById","text":"Finds a control within the page by id.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage, ClientsideText } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nconst control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");\n\n// you can also type the control\nconst control = page.findControlById<ClientsideText>(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");\n
"},{"location":"sp/clientside-pages/#findcontrol","title":"findControl","text":"Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// find the first control whose order is 9\nconst control = page.findControl((c) => c.order === 9);\n\n// iterate all the controls and output the id to the console\npage.findControl((c) => {\n console.log(c.id);\n return false;\n});\n
"},{"location":"sp/clientside-pages/#like-unlike","title":"like & unlike","text":"Updates the page's like value for the current user.
// our page instance\nconst page: IClientsidePage;\n\n// like this page\nawait page.like();\n\n// unlike this page\nawait page.unlike();\n
"},{"location":"sp/clientside-pages/#getlikedbyinformation","title":"getLikedByInformation","text":"Gets the likes information for this page.
// our page instance\nconst page: IClientsidePage;\n\nconst info = await page.getLikedByInformation();\n
"},{"location":"sp/clientside-pages/#copy","title":"copy","text":"Creates a copy of the page, including all controls.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// creates a published copy of the page\nconst pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\");\n\n// creates a draft (unpublished) copy of the page\nconst pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false);\n\n// edits to pageCopy2 ...\n\n// publish the page\npageCopy2.save();\n
"},{"location":"sp/clientside-pages/#copyto","title":"copyTo","text":"Copies the contents of a page to another existing page instance.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instances, loaded in any of the ways shown above\nconst source: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\nconst target: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/target.aspx\");\nconst target2: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/target2.aspx\");\n\n// creates a published copy of the page\nawait source.copyTo(target);\n\n// creates a draft (unpublished) copy of the page\nawait source.copyTo(target2, false);\n\n// edits to target2...\n\n// publish the page\ntarget2.save();\n
"},{"location":"sp/clientside-pages/#setbannerimage","title":"setBannerImage","text":"Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent.
Banner images need to exist within the same site collection as the page where you want to use them.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\npage.setBannerImage(\"/server/relative/path/to/image.png\");\n\n// save the changes\nawait page.save();\n\n// set additional props\npage.setBannerImage(\"/server/relative/path/to/image.png\", {\n altText: \"Image description\",\n imageSourceType: 2,\n translateX: 30,\n translateY: 1234,\n});\n\n// save the changes\nawait page.save();\n
This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file
- likely from a file input or similar.
import { join } from \"path\";\nimport { createReadStream } from \"fs\";\nimport { spfi, SPFI, SPFx } from \"@pnp/sp\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { LogLevel } from \"@pnp/logging\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst buffer = readFileSync(\"c:/temp/key.pem\");\n\nconst config:any = {\n auth: {\n authority: \"https://login.microsoftonline.com/{my tenant}/\",\n clientId: \"{application (client) id}\",\n clientCertificate: {\n thumbprint: \"{certificate thumbprint, displayed in AAD}\",\n privateKey: buffer.toString(),\n },\n },\n system: {\n loggerOptions: {\n loggerCallback(loglevel: any, message: any, containsPii: any) {\n console.log(message);\n },\n piiLoggingEnabled: false,\n logLevel: LogLevel.Verbose\n }\n }\n};\n\n// configure your node options\nconst sp = spfi('{site url}').using(SPDefault({\n baseUrl: '{site url}',\n msal: {\n config: config,\n scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]\n }\n}));\n\n\n// add the banner image\nconst dirname = join(\"C:/path/to/file\", \"img-file.jpg\");\n\nconst chunkedFile = createReadStream(dirname);\n\nconst far = await sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents\").files.addChunked( \"banner.jpg\", chunkedFile );\n\n// add the page\nconst page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\");\n\n// set the banner image\npage.setBannerImage(far.data.ServerRelativeUrl);\n\n// publish the page\nawait page.save();\n
"},{"location":"sp/clientside-pages/#setbannerimagefromexternalurl","title":"setBannerImageFromExternalUrl","text":"Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there.
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// you must await this method\nawait page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\");\n\n// save the changes\nawait page.save();\n
You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// you must await this method\nawait page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", {\n altText: \"Image description\",\n imageSourceType: 2,\n translateX: 30,\n translateY: 1234,\n});\n\n// save the changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#recycle","title":"recycle","text":"Allows you to recycle a page without first needing to use getItem
// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.recycle();\n
"},{"location":"sp/clientside-pages/#delete","title":"delete","text":"Allows you to delete a page without first needing to use getItem
// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.delete();\n
"},{"location":"sp/clientside-pages/#saveastemplate","title":"saveAsTemplate","text":"Save page as a template from which other pages can be created. If it doesn't exist a special folder \"Templates\" will be added to the doc lib
// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.saveAsTemplate();\n// save a template, but don't publish it allowing you to make changes before it is available to users\n// you \nawait page.saveAsTemplate(false);\n// ... changes to the page\n// you must publish the template so it is available\nawait page.save();\n
"},{"location":"sp/clientside-pages/#share","title":"share","text":"Allows sharing a page with one or more email addresses, optionall including a message in the email
// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.share([\"email@place.com\", \"email2@otherplace.com\"]);\n// optionally include a message\nawait page.share([\"email@place.com\", \"email2@otherplace.com\"], \"Please check out this cool page!\");\n
"},{"location":"sp/clientside-pages/#add-repost-page","title":"Add Repost Page","text":"You can use the addRepostPage
method to add a report page. The method returns the absolute url of the created page. All properties are optional but it is recommended to include as much as possible to improve the quality of the repost card's display.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\nconst page = await sp.web.addRepostPage({\n BannerImageUrl: \"https://some.absolute/path/to/an/image.jpg\",\n IsBannerImageUrlExternal: true,\n Description: \"My Description\",\n Title: \"This is my title!\",\n OriginalSourceUrl: \"https://absolute/path/to/article\",\n});\n
To specify an existing item in another list all of the four properties OriginalSourceSiteId, OriginalSourceWebId, OriginalSourceListId, and OriginalSourceItemId are required.
"},{"location":"sp/column-defaults/","title":"@pnp/sp/column-defaults","text":"The column defaults sub-module allows you to manage the default column values on a library or library folder.
"},{"location":"sp/column-defaults/#get-folder-defaults","title":"Get Folder Defaults","text":"You can get the default values for a specific folder as shown below:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nconst defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues();\n\n/*\nThe resulting structure will have the form:\n\n[\n {\n \"name\": \"{field internal name}\",\n \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n \"value\": \"{the default value}\"\n },\n {\n \"name\": \"{field internal name}\",\n \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n \"value\": \"{the default value}\"\n }\n]\n*/\n
"},{"location":"sp/column-defaults/#set-folder-defaults","title":"Set Folder Defaults","text":"When setting the defaults for a folder you need to include the field's internal name and the value.
For more examples of other field types see the section Pattern for setting defaults on various column types
Note: Be very careful when setting the path as the site collection url is case sensitive
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{\n name: \"TextField\",\n value: \"Something\",\n},\n{\n name: \"NumberField\",\n value: 14,\n}]);\n
"},{"location":"sp/column-defaults/#get-library-defaults","title":"Get Library Defaults","text":"You can also get all of the defaults for the entire library.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nconst defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues();\n\n/*\nThe resulting structure will have the form:\n\n[\n {\n \"name\": \"{field internal name}\",\n \"path\": \"/sites/dev/DefaultColumnValues\",\n \"value\": \"{the default value}\"\n },\n {\n \"name\": \"{field internal name}\",\n \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n \"value\": \"{a different default value}\"\n }\n]\n*/\n
"},{"location":"sp/column-defaults/#set-library-defaults","title":"Set Library Defaults","text":"You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value.
For more examples of other field types see the section Pattern for setting defaults on various column types
Note: Be very careful when setting the path as the site collection url is case sensitive
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{\n name: \"TextField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: \"#PnPjs Rocks!\",\n}]);\n
"},{"location":"sp/column-defaults/#clear-folder-defaults","title":"Clear Folder Defaults","text":"If you want to clear all of the folder defaults you can use the clear method:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues();\n
"},{"location":"sp/column-defaults/#clear-library-defaults","title":"Clear Library Defaults","text":"If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]);\n
"},{"location":"sp/column-defaults/#pattern-for-setting-defaults-on-various-column-types","title":"Pattern for setting defaults on various column types","text":"The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types.
[{\n // Text/Boolean/CurrencyDateTime/Choice/User\n name: \"TextField\":\n path: \"/sites/dev/DefaultColumnValues\",\n value: \"#PnPjs Rocks!\",\n}, {\n //Number\n name: \"NumberField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: 42,\n}, {\n //Date\n name: \"NumberField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: \"1900-01-01T00:00:00Z\",\n}, {\n //Date - Today\n name: \"NumberField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: \"[today]\",\n}, {\n //MultiChoice\n name: \"MultiChoiceField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: [\"Item 1\", \"Item 2\"],\n}, {\n //MultiChoice - single value\n name: \"MultiChoiceField\",\n path: \"/sites/dev/DefaultColumnValues/folder2\",\n value: [\"Item 1\"],\n}, {\n //Taxonomy - single value\n name: \"TaxonomyField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: {\n wssId:\"-1\",\n termName: \"TaxValueName\",\n termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\"\n }\n}, {\n //Taxonomy - multiple value\n name: \"TaxonomyMultiField\",\n path: \"/sites/dev/DefaultColumnValues\",\n value: [{\n wssId:\"-1\",\n termName: \"TaxValueName\",\n termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\"\n },{\n wssId:\"-1\",\n termName: \"TaxValueName2\",\n termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\"\n },]\n}]);\n
"},{"location":"sp/column-defaults/#taxonomy-full-example","title":"Taxonomy Full Example","text":"This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/column-defaults\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get the term's info we want to use as the default\nconst term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")();\n\n// get the default term label\nconst defLabel = term.labels.find(v => v.isDefault);\n\n// set the default value using -1, the term id, and the term's default label name\nawait sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{\n name: \"MetaDataColumnInternalName\",\n value: {\n wssId: \"-1\",\n termId: term.id,\n termName: defLabel.name,\n }\n}])\n\n// check that the defaults have updated\nconst newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();\n
"},{"location":"sp/comments-likes/","title":"@pnp/sp/comments and likes","text":"Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles.
These APIs are currently in BETA and are subject to change or may not work on all tenants.
"},{"location":"sp/comments-likes/#clientsidepage-comments","title":"ClientsidePage Comments","text":"
The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.
"},{"location":"sp/comments-likes/#add-comments","title":"Add Comments","text":"You can add a comment using the addComment method as shown
import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\n//add a comment as text\nconst comment = await page.addComment(\"A test comment\");\n\n//or you can include the @mentions. html anchor required to include mention in text body.\nconst mentionHtml = `<a data-sp-mention-user-id=\"test@contoso.com\" href=\"mailto:test@contoso.com.com\" tabindex=\"-1\">Test User</a>`;\n\nconst commentInfo: Partial<ICommentInfo> = { text: `${mentionHtml} This is the test comment with at mentions`, \n mentions: [{ loginName: 'test@contoso.com', email: 'test@contoso.com', name: 'Test User' }], };\nconst comment = await page.addComment(commentInfo);\n
"},{"location":"sp/comments-likes/#get-page-comments","title":"Get Page Comments","text":"import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\n\nconst comments = await page.getComments();\n
"},{"location":"sp/comments-likes/#enablecomments-disablecomments","title":"enableComments & disableComments","text":"Used to control the availability of comments on a page
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n// you need to import the comments sub-module or use the all preset\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// turn on comments\nawait page.enableComments();\n\n// turn off comments\nawait page.disableComments();\n
"},{"location":"sp/comments-likes/#getbyid","title":"GetById","text":"import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\nconst comment = await page.addComment(\"A test comment\");\n\nconst commentData = await page.getCommentById(parseInt(comment.id, 10));\n
"},{"location":"sp/comments-likes/#clear-comments","title":"Clear Comments","text":""},{"location":"sp/comments-likes/#item-comments","title":"Item Comments","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments/item\";\n\nconst sp = spfi(...);\n\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem();\n\n// as an example, or any of the below options\nawait item.like();\n
The below examples use a variable named \"item\" which is taken to represent an IItem instance.
"},{"location":"sp/comments-likes/#comments","title":"Comments","text":""},{"location":"sp/comments-likes/#get-item-comments","title":"Get Item Comments","text":"const comments = await item.comments();\n
You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].replies.add({ text: \"#PnPjs is pretty ok!\" });\n\n//load the top 20 replies and comments for an item including likedBy information\nconst comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)();\n
"},{"location":"sp/comments-likes/#add-comment","title":"Add Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { ICommentInfo } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\n// you can add a comment as a string\nconst comment = await item.comments.add(\"string comment\");\n\n\n
"},{"location":"sp/comments-likes/#delete-a-comment","title":"Delete a Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].delete()\n
"},{"location":"sp/comments-likes/#like-comment","title":"Like Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].like();\n
"},{"location":"sp/comments-likes/#unlike-comment","title":"Unlike Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\ncomments[0].unlike()\n
"},{"location":"sp/comments-likes/#reply-to-a-comment","title":"Reply to a Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\nconst comment = await comments[0].comments.add({ text: \"#PnPjs is pretty ok!\" });\n
"},{"location":"sp/comments-likes/#load-replies-to-a-comment","title":"Load Replies to a Comment","text":"import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\nconst replies = await comments[0].replies();\n
"},{"location":"sp/comments-likes/#likeunlike","title":"Like/Unlike","text":"You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments\";\nimport { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst item = sp.web.lists.getByTitle(\"PnP List\").items.getById(1);\n\n// like an item\nawait item.like();\n\n// unlike an item\nawait item.unlike();\n\n// get the liked by information\nconst likedByInfo: ILikedByInformation = await item.getLikedByInformation();\n
To like/unlike a client-side page and get liked by information.
import { spfi } from \"@pnp/sp\";\nimport { ILikedByInformation } from \"@pnp/sp/comments\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// like a page\nawait page.like();\n\n// unlike a page\nawait page.unlike();\n\n// get the liked by information\nconst likedByInfo: ILikedByInformation = await page.getLikedByInformation();\n
"},{"location":"sp/comments-likes/#rate","title":"Rate","text":"You can rate list items with a numeric values between 1 and 5.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments\";\nimport { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst item = sp.web.lists.getByTitle(\"PnP List\").items.getById(1);\n\n// rate an item\nawait item.rate(2);\n
"},{"location":"sp/content-types/","title":"@pnp/sp/content-types","text":"Content Types are used to define sets of columns in SharePoint.
"},{"location":"sp/content-types/#icontenttypes","title":"IContentTypes","text":""},{"location":"sp/content-types/#add-an-existing-content-type-to-a-collection","title":"Add an existing Content Type to a collection","text":"The following example shows how to add the built in Picture Content Type to the Documents library.
const sp = spfi(...);\n\nsp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\");\n
"},{"location":"sp/content-types/#get-a-content-type-by-id","title":"Get a Content Type by Id","text":"import { IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\nconst d: IContentType = await sp.web.contentTypes.getById(\"0x01\")();\n\n// log content type name to console\nconsole.log(d.name);\n
"},{"location":"sp/content-types/#update-a-content-type","title":"Update a Content Type","text":"import { IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\nawait sp.web.contentTypes.getById(\"0x01\").update({EditFormClientSideComponentId: \"9dfdb916-7380-4b69-8d92-bc711f5fa339\"});\n
"},{"location":"sp/content-types/#add-a-new-content-type","title":"Add a new Content Type","text":"To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.
const sp = spfi(...);\n\nsp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\");\n
It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API).
const sp = spfi(...);\n\n//Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings)\nsp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true });\n
"},{"location":"sp/content-types/#icontenttype","title":"IContentType","text":""},{"location":"sp/content-types/#get-the-field-links","title":"Get the field links","text":"Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get field links from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks();\n\n// log collection of fieldlinks to console\nconsole.log(d);\n
"},{"location":"sp/content-types/#get-content-type-fields","title":"Get Content Type fields","text":"To get a collection with all fields on the Content Type, simply use this method.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get fields from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").fields();\n\n// log collection of fields to console\nconsole.log(d);\n
"},{"location":"sp/content-types/#get-parent-content-type","title":"Get parent Content Type","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get parent Content Type from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").parent();\n\n// log name of parent Content Type to console\nconsole.log(d.Name)\n
"},{"location":"sp/content-types/#get-content-type-workflow-associations","title":"Get Content Type Workflow associations","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get workflow associations from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations();\n\n// log collection of workflow associations to console\nconsole.log(d);\n
"},{"location":"sp/context-info/","title":"@pnp/sp/ - context-info","text":"Starting with 3.8.0 we've moved context information to its own sub-module. You can now import context-info
and use it on any SPQueryable derived object to understand the context. Some examples are below.
The information returned by the method is defined by the IContextInfo interface.
export interface IContextInfo {\n FormDigestTimeoutSeconds: number;\n FormDigestValue: number;\n LibraryVersion: string;\n SiteFullUrl: string;\n SupportedSchemaVersions: string[];\n WebFullUrl: string;\n}\n
"},{"location":"sp/context-info/#get-context-for-a-web","title":"Get Context for a web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\nconst info = await sp.web.getContextInfo();\n
"},{"location":"sp/context-info/#get-context-from-lists","title":"Get Context from lists","text":"This pattern works as well for any SPQueryable derived object, allowing you to gain context no matter with which fluent objects you are working.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\nconst info = await sp.web.lists.getContextInfo();\n
"},{"location":"sp/context-info/#get-context-from-url","title":"Get Context from URL","text":"Often you will have an absolute URL to a file or path and would like to create an IWeb or IFile. You can use the fileFromPath or folderFromPath to get an IFile/IFolder, or you can use getContextInfo
to create a new web within the context of the file path.
import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\n// supply an absolute path to get associated context info, this works across site collections\nconst { WebFullUrl } = await sp.web.getContextInfo(\"https://tenant.sharepoint.com/sites/dev/shared documents/file.docx\");\n\n// create a new web pointing to the web where the file is stored\nconst web = Web([sp.web, decodeURI(WebFullUrl)]);\n\nconst webInfo = await web();\n
"},{"location":"sp/favorites/","title":"@pnp/sp/ - favorites","text":"The favorites API allows you to fetch and manipulate followed sites and list items (also called saved for later). Note, all of these methods only work with the context of a logged in user, and not with app-only permissions.
"},{"location":"sp/favorites/#get-current-users-followed-sites","title":"Get current user's followed sites","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst favSites = await sp.favorites.getFollowedSites();\n
"},{"location":"sp/favorites/#add-a-site-to-current-users-followed-sites","title":"Add a site to current user's followed sites","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst tenantUrl = \"contoso.sharepoint.com\";\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\"\nconst webUrl = \"https://contoso.sharepoint.com/sites/favsite\"\n\nconst favSiteInfo = await sp.favorites.getFollowedSites.add(tenantUrl, siteId, webId, webUrl);\n
"},{"location":"sp/favorites/#remove-a-site-from-current-users-followed-sites","title":"Remove a site from current user's followed sites","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst tenantUrl = \"contoso.sharepoint.com\";\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\"\nconst webUrl = \"https://contoso.sharepoint.com/sites/favsite\"\n\nawait sp.favorites.getFollowedSites.remove(tenantUrl, siteId, webId, webUrl);\n
"},{"location":"sp/favorites/#get-current-users-followed-list-items","title":"Get current user's followed list items","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst favListItems = await sp.favorites.getFollowedListItems();\n
"},{"location":"sp/favorites/#add-an-item-to-current-users-followed-list-items","title":"Add an item to current user's followed list items","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\";\nconst listId = \"f09fe67e-0160-4fcc-9144-905bd4889f31\";\nconst listItemUniqueId = \"1425C841-626A-44C9-8731-DA8BDC0882D1\";\n\nconst favListItemInfo = await sp.favorites.getFollowedListItems.add(siteId, webId, listId, listItemUniqueId);\n
"},{"location":"sp/favorites/#remove-an-item-from-current-users-followed-list-items","title":"Remove an item from current user's followed list items","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\";\nconst listId = \"f09fe67e-0160-4fcc-9144-905bd4889f31\";\nconst listItemUniqueId = \"1425C841-626A-44C9-8731-DA8BDC0882D1\";\n\nconst favListItemInfo = await sp.favorites.getFollowedListItems.remove(siteId, webId, listId, listItemUniqueId);\n
"},{"location":"sp/features/","title":"@pnp/sp/features","text":"Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.
"},{"location":"sp/features/#ifeatures","title":"IFeatures","text":"Represents a collection of features. SharePoint Sites and Webs will have a collection of features
"},{"location":"sp/features/#getbyid","title":"getById","text":"Gets the information about a feature for the given GUID
import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nconst webFeature = await sp.web.features.getById(webFeatureId)();\n\nconst siteFeatureId = \"guid-of-site-scope-feature\";\nconst siteFeature = await sp.site.features.getById(siteFeatureId)();\n
"},{"location":"sp/features/#add","title":"add","text":"Adds (activates) a feature at the Site or Web level
import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nlet res = await sp.web.features.add(webFeatureId);\n// Activate with force\nres = await sp.web.features.add(webFeatureId, true);\n
"},{"location":"sp/features/#remove","title":"remove","text":"Removes and deactivates the specified feature from the SharePoint Site or Web
import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nlet res = await sp.web.features.remove(webFeatureId);\n// Deactivate with force\nres = await sp.web.features.remove(webFeatureId, true);\n
"},{"location":"sp/features/#ifeature","title":"IFeature","text":"Represents an instance of a SharePoint feature.
"},{"location":"sp/features/#deactivate","title":"deactivate","text":"
Deactivates the specified feature from the SharePoint Site or Web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/features\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nsp.web.features.remove(webFeatureId);\n\n// Deactivate with force\nsp.web.features.remove(webFeatureId, true);\n
"},{"location":"sp/fields/","title":"@pnp/sp/fields","text":"Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.
"},{"location":"sp/fields/#ifields","title":"IFields","text":""},{"location":"sp/fields/#get-field-by-id","title":"Get Field by Id","text":"Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/fields\";\n\n// set up sp root object\nconst sp = spfi(...);\n// get the field by Id for web\nconst field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\");\n// get the field by Id for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")();\n\n// we can use this 'field' variable to execute more queries on the field:\nconst r = await field.select(\"Title\")();\n\n// show the response from the server\nconsole.log(r.Title);\n
"},{"location":"sp/fields/#get-field-by-title","title":"Get Field by Title","text":"You can also get a field from the collection by title.
import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\"\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n// get the field with the title 'Author' for web\nconst field: IField = sp.web.fields.getByTitle(\"Author\");\n// get the field with the title 'Title' for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Title\")();\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#get-field-by-internal-name-or-title","title":"Get Field by Internal Name or Title","text":"You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different.
import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\"\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n// get the field with the internal name 'ModifiedBy' for web\nconst field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\");\n// get the field with the internal name 'ModifiedBy' for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")();\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#create-a-field-using-an-xml-schema","title":"Create a Field using an XML schema","text":"Create a new field by defining an XML schema that assigns all the properties for the field.
import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldAddResult } from \"@pnp/sp/fields/types\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// define the schema for your new field, in this case a date field with a default date of today.\nconst fieldSchema = `<Field ID=\"{03b09ff4-d99d-45ed-841d-3855f77a2483}\" StaticName=\"MyField\" Name=\"MyField\" DisplayName=\"My New Field\" FriendlyDisplayFormat=\"Disabled\" Format=\"DateOnly\" Type=\"DateTime\" Group=\"My Group\"><Default>[today]</Default></Field>`;\n\n// create the new field in the web\nconst field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema);\n// create the new field in the list 'My List'\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema);\n\n// we can use this 'field' variable to run more queries on the list:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-new-field","title":"Add a New Field","text":"Use the add method to create a new field where you define the field type
import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n// create a new field called 'My Field' in the list 'My List'\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-site-field-to-a-list","title":"Add a Site Field to a List","text":"Use the createFieldAsXml method to add a site field to a list.
import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n// add the site field 'My Field' to the list 'My List'\nconst r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml as string);\n\n// log the field Id to console\nconsole.log(r.data.Id);\n
"},{"location":"sp/fields/#add-a-text-field","title":"Add a Text Field","text":"Use the addText method to create a new text field.
import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new text field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", { MaxLength: 255, Group: \"My Group\" });\n// create a new text field called 'My Field' in the list 'My List'.\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", { MaxLength: 255, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-calculated-field","title":"Add a Calculated Field","text":"Use the addCalculated method to create a new calculated field.
import { spfi } from \"@pnp/sp\";\nimport { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new calculated field called 'My Field' in web\nconst field = await sp.web.fields.addCalculated(\"My Field\", { Formula: \"=Modified+1\", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: \"MyGroup\" });\n// create a new calculated field called 'My Field' in the list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", { Formula: \"=Modified+1\", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: \"MyGroup\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-datetime-field","title":"Add a Date/Time Field","text":"Use the addDateTime method to create a new date/time field.
import { spfi } from \"@pnp/sp\";\nimport { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new date/time field called 'My Field' in web\nconst field = await sp.web.fields.addDateTime(\"My Field\", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: \"My Group\" });\n// create a new date/time field called 'My Field' in the list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-currency-field","title":"Add a Currency Field","text":"Use the addCurrency method to create a new currency field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new currency field called 'My Field' in web\nconst field = await sp.web.fields.addCurrency(\"My Field\", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: \"My Group\" });\n// create a new currency field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-an-image-field","title":"Add an Image Field","text":"Use the addImageField method to create a new image field.
import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new image field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.addImageField(\"My Field\");\n// create a new image field called 'My Field' in the list 'My List'.\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addImageField(\"My Field\");\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-line-text-field","title":"Add a Multi-line Text Field","text":"Use the addMultilineText method to create a new multi-line text field.
For Enhanced Rich Text mode, see the next section.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new multi-line text field called 'My Field' in web\nconst field = await sp.web.fields.addMultilineText(\"My Field\", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: \"My Group\" });\n// create a new multi-line text field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-line-text-field-with-enhanced-rich-text","title":"Add a Multi-line Text Field with Enhanced Rich Text","text":"The REST endpoint doesn't support setting the RichTextMode
field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n//Create a new multi-line text field called 'My Field' in web\nconst field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(\n `<Field Type=\"Note\" Name=\"MyField\" DisplayName=\"My Field\" Required=\"FALSE\" RichText=\"TRUE\" RichTextMode=\"FullHtml\" />`\n);\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-number-field","title":"Add a Number Field","text":"Use the addNumber method to create a new number field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new number field called 'My Field' in web\nconst field = await sp.web.fields.addNumber(\"My Field\", { MinimumValue: 1, MaximumValue: 100, Group: \"My Group\" });\n// create a new number field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", { MinimumValue: 1, MaximumValue: 100, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-url-field","title":"Add a URL Field","text":"Use the addUrl method to create a new url field.
import { spfi } from \"@pnp/sp\";\nimport { UrlFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new url field called 'My Field' in web\nconst field = await sp.web.fields.addUrl(\"My Field\", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: \"My Group\" });\n// create a new url field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-user-field","title":"Add a User Field","text":"Use the addUser method to create a new user field.
import { spfi } from \"@pnp/sp\";\nimport { FieldUserSelectionMode } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new user field called 'My Field' in web\nconst field = await sp.web.fields.addUser(\"My Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: \"My Group\" });\n// create a new user field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n\n// **\n// Adding a lookup that supports multiple values takes two calls:\nconst fieldAddResult = await sp.web.fields.addUser(\"Multi User Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly });\nawait fieldAddResult.field.update({ AllowMultipleValues: true }, \"SP.FieldUser\");\n
"},{"location":"sp/fields/#add-a-lookup-field","title":"Add a Lookup Field","text":"Use the addLookup method to create a new lookup field.
import { spfi } from \"@pnp/sp\";\nimport { FieldTypes } from \"@pnp/sp/fields/types\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst list = await sp.web.lists.getByTitle(\"My Lookup List\")();\n// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web.\nconst field = await sp.web.fields.addLookup(\"My Field\", { LookupListId: list.data.Id, LookupFieldName: \"Title\" });\n// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", {LookupListId: list.data.Id, LookupFieldName: \"Title\"});\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n\n// **\n// Adding a lookup that supports multiple values takes two calls:\nconst fieldAddResult = await sp.web.fields.addLookup(\"Multi Lookup Field\", { LookupListId: list.data.Id, LookupFieldName: \"Title\" });\nawait fieldAddResult.field.update({ AllowMultipleValues: true }, \"SP.FieldLookup\");\n
"},{"location":"sp/fields/#add-a-choice-field","title":"Add a Choice Field","text":"Use the addChoice method to create a new choice field.
import { spfi } from \"@pnp/sp\";\nimport { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];\n// create a new choice field called 'My Field' in web\nconst field = await sp.web.fields.addChoice(\"My Field\", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: \"My Group\" });\n// create a new choice field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-choice-field","title":"Add a Multi-Choice Field","text":"Use the addMultiChoice method to create a new multi-choice field.
import { spfi } from \"@pnp/sp\";\nimport { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];\n// create a new multi-choice field called 'My Field' in web\nconst field = await sp.web.fields.addMultiChoice(\"My Field\", { Choices: choices, FillInChoice: false, Group: \"My Group\" });\n// create a new multi-choice field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", { Choices: choices, FillInChoice: false, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-boolean-field","title":"Add a Boolean Field","text":"Use the addBoolean method to create a new boolean field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new boolean field called 'My Field' in web\nconst field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" });\n// create a new boolean field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-dependent-lookup-field","title":"Add a Dependent Lookup Field","text":"Use the addDependentLookupField method to create a new dependent lookup field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst field = await sp.web.fields.addLookup(\"My Field\", { LookupListId: list.Id, LookupFieldName: \"Title\" });\n// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web.\nconst fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.data.Id as string, \"Description\");\n// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", { LookupListId: list.Id, LookupFieldName: \"Title\" });\nconst fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.data.Id as string, \"Description\");\n\n// we can use this 'fieldDep' variable to run more queries on the field:\nconst r = await fieldDep.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-location-field","title":"Add a Location Field","text":"Use the addLocation method to create a new location field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new location field called 'My Field' in web\nconst field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" });\n// create a new location field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#delete-a-field","title":"Delete a Field","text":"Use the delete method to delete a field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nawait sp.web.fields.addBoolean(\"Temp Field\", { Group: \"My Group\" });\nawait sp.web.fields.addBoolean(\"Temp Field 2\", { Group: \"My Group\" });\nawait sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"Temp Field\", { Group: \"My Group\" });\nawait sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"Temp Field 2\", { Group: \"My Group\" });\n\n// delete one or more fields from web, returns boolean\nconst result = await sp.web.fields.getByTitle(\"Temp Field\").delete();\nconst result2 = await sp.web.fields.getByTitle(\"Temp Field 2\").delete();\n\n\n// delete one or more fields from list 'My List', returns boolean\nconst result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Temp Field\").delete();\nconst result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Temp Field 2\").delete();\n
"},{"location":"sp/fields/#update-a-field","title":"Update a Field","text":"Use the update method to update a field.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// update the field called 'My Field' with a description in web, returns FieldUpdateResult\nconst fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" });\n// update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult\nconst fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" });\n\n// if you need to update a field with properties for a specific field type you can optionally include the field type as a second param\n// if you do not include it we will look up the type, but that adds a call to the server\nconst fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\");\n
"},{"location":"sp/fields/#show-a-field-in-the-display-form","title":"Show a Field in the Display Form","text":"Use the setShowInDisplayForm method to add a field to the display form.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in display form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true);\n// show field called 'My Field' in display form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true);\n
"},{"location":"sp/fields/#show-a-field-in-the-edit-form","title":"Show a Field in the Edit Form","text":"Use the setShowInEditForm method to add a field to the edit form.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in edit form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true);\n// show field called 'My Field' in edit form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true);\n
"},{"location":"sp/fields/#show-a-field-in-the-new-form","title":"Show a Field in the New Form","text":"Use the setShowInNewForm method to add a field to the display form.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in new form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true);\n// show field called 'My Field' in new form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);\n
"},{"location":"sp/files/","title":"@pnp/sp/files","text":"One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.
"},{"location":"sp/files/#reading-files","title":"Reading Files","text":"Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst blob: Blob = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.avi\").getBlob();\n\nconst buffer: ArrayBuffer = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.avi\").getBuffer();\n\nconst json: any = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.json\").getJSON();\n\nconst text: string = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.txt\").getText();\n\n// all of these also work from a file object no matter how you access it\nconst text2: string = await sp.web.getFolderByServerRelativePath(\"/sites/dev/documents\").files.getByUrl(\"file.txt\").getText();\n
"},{"location":"sp/files/#getfilebyurl","title":"getFileByUrl","text":"This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\n\nconst sp = spfi(...);\n\nconst url = \"{absolute file url OR sharing url}\";\n\n// file is an IFile and supports all the file operations\nconst file = sp.web.getFileByUrl(url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefromserverrelativepath","title":"fileFromServerRelativePath","text":"Added in 3.3.0
Utility method allowing you to get an IFile reference using any SPQueryable as a base and the server relative path to the file. Helpful when you do not have convenient access to an IWeb to use getFileByServerRelativePath
.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromServerRelativePath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromServerRelativePath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefromabsolutepath","title":"fileFromAbsolutePath","text":"Added in 3.8.0
Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.
Works across site collections within the same tenant
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromAbsolutePath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromAbsolutePath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefrompath","title":"fileFromPath","text":"Added in 3.8.0
Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute OR server relative path to the file.
Works across site collections within the same tenant
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromPath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromPath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n\nconst url2 = \"/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file2 = fileFromPath(sp.web, url2);\n\n// for example\nconst fileContent2 = await file2.getText();\n
"},{"location":"sp/files/#adding-files","title":"Adding Files","text":"Likewise you can add files using one of two methods, addUsingPath or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.
The addUsingPath method, supports the percent or pound characters in file names.
When using EnsureUniqueFileName property, you must omit the Overwrite parameter.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n//Sample uses pure JavaScript to access the input tag of type=\"file\" ->https://www.w3schools.com/tags/att_input_type_file.asp \nlet file = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst fileNamePath = encodeURI(file.name);\nlet result: IFileAddResult;\n// you can adjust this number to control what size files are uploaded in chunks\nif (file.size <= 10485760) {\n // small upload\n result = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files.addUsingPath(fileNamePath, file, { Overwrite: true });\n} else {\n // large upload\n result = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files.addChunked(fileNamePath, file, data => {\n console.log(`progress`);\n }, true);\n}\n\nconsole.log(`Result of file upload: ${JSON.stringify(result)}`);\n
"},{"location":"sp/files/#adding-a-file-using-nodejs-streams","title":"Adding a file using Nodejs Streams","text":"If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams.
// triggers auto-application of extensions, in this case to add getStream\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/nodejs\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/folders/list\";\nimport \"@pnp/sp/files/folder\";\nimport { createReadStream } from 'fs';\n\n// get a stream of an existing file\nconst stream = createReadStream(\"c:/temp/file.txt\");\n\n// now add the stream as a new file\nconst sp = spfi(...);\n\nconst fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.addChunked( \"new.txt\", stream, undefined, true );\n
"},{"location":"sp/files/#setting-associated-item-values","title":"Setting Associated Item Values","text":"You can also update the file properties of a newly uploaded file using code similar to the below snippet:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\nconst file = await sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file.name\", \"content\", {Overwrite: true});\nconst item = await file.file.getItem();\nawait item.update({\n Title: \"A Title\",\n OtherField: \"My Other Value\"\n});\n
"},{"location":"sp/files/#update-file-content","title":"Update File Content","text":"You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\");\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/documents/test.mp4\").setContentChunked(file);\n
"},{"location":"sp/files/#check-in-check-out-and-approve-deny","title":"Check in, Check out, and Approve & Deny","text":"The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.
"},{"location":"sp/files/#check-in","title":"Check In","text":"Check in takes two optional arguments, comment and check in type.
import { spfi } from \"@pnp/sp\";\nimport { CheckinType } from \"@pnp/sp/files\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// default options with empty comment and CheckinType.Major\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin();\nconsole.log(\"File checked in!\");\n\n// supply a comment (< 1024 chars) and using default check in type CheckinType.Major\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\");\nconsole.log(\"File checked in!\");\n\n// Supply both comment and check in type\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite);\nconsole.log(\"File checked in!\");\n
"},{"location":"sp/files/#check-out","title":"Check Out","text":"Check out takes no arguments.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nsp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkout();\nconsole.log(\"File checked out!\");\n
"},{"location":"sp/files/#approve-and-deny","title":"Approve and Deny","text":"You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\");\nconsole.log(\"File approved!\");\n\n// deny with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").deny();\nconsole.log(\"File denied!\");\n\n// deny with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\");\nconsole.log(\"File denied!\");\n
"},{"location":"sp/files/#publish-and-unpublish","title":"Publish and Unpublish","text":"You can both publish and unpublish a file using the library. Both methods take an optional comment argument.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// publish with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").publish();\nconsole.log(\"File published!\");\n\n// publish with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\");\nconsole.log(\"File published!\");\n\n// unpublish with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").unpublish();\nconsole.log(\"File unpublished!\");\n\n// unpublish with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\");\nconsole.log(\"File unpublished!\");\n
"},{"location":"sp/files/#advanced-upload-options","title":"Advanced Upload Options","text":"Both the addChunked and setContentChunked methods support options beyond just supplying the file content.
"},{"location":"sp/files/#progress-function","title":"progress function","text":"A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:
(data: ChunkedFileUploadProgressData) => void
The data interface is:
export interface ChunkedFileUploadProgressData {\n stage: \"starting\" | \"continue\" | \"finishing\";\n blockNumber: number;\n totalBlocks: number;\n chunkSize: number;\n currentPointer: number;\n fileSize: number;\n}\n
"},{"location":"sp/files/#chunksize","title":"chunkSize","text":"This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.
"},{"location":"sp/files/#getitem","title":"getItem","text":"This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.
import { spFI, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem();\nconsole.log(item);\n\nconst item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\");\nconsole.log(item2);\n\n// you can also chain directly off this item instance\nconst perms = await item.getCurrentUserEffectivePermissions();\nconsole.log(perms);\n
You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.
import { spFI, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// also supports typing the objects so your type will be a union type\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\");\n\n// You get intellisense and proper typing of the returned object\nconsole.log(`Id: ${item.Id} -- ${item.Title}`);\n\n// You can also chain directly off this item instance\nconst perms = await item.getCurrentUserEffectivePermissions();\nconsole.log(perms);\n
"},{"location":"sp/files/#move-by-path","title":"move by path","text":"It's possible to move a file to a new destination within a site collection
If you change the filename during the move operation this is considered an \"edit\" and the file's modified information will be updated regardless of the \"RetainEditorAndModifiedOnMove\" setting.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true);\n
Added in 3.7.0
You can also supply a set of detailed options to better control the move process:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/new-file.docx\").moveByPath(destinationUrl, false, {\n KeepBoth: false,\n RetainEditorAndModifiedOnMove: true,\n ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/files/#copy","title":"copy","text":"It's possible to copy a file to a new destination within a site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false);\n
"},{"location":"sp/files/#copy-by-path","title":"copy by path","text":"It's possible to copy a file to a new destination within the same or a different site collection.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true);\n
Added in 3.7.0
You can also supply a set of detailed options to better control the copy process:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, {\n KeepBoth: false,\n ResetAuthorAndCreatedOnCopy: true,\n ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/files/#getfilebyid","title":"getFileById","text":"You can get a file by Id from a web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport { IFile } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\n
"},{"location":"sp/files/#delete","title":"delete","text":"Deletes a file
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nawait sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"filename.txt\").delete();\n
"},{"location":"sp/files/#delete-with-params","title":"delete with params","text":"Deletes a file with options
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nawait sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"filename.txt\").deleteWithParams({\n BypassSharedLock: true,\n});\n
"},{"location":"sp/files/#exists","title":"exists","text":"Checks to see if a file exists
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nconst exists = await sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"name.txt\").exists();\n
"},{"location":"sp/files/#lockedbyuser","title":"lockedByUser","text":"Gets the user who currently has this file locked for shared use
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nconst user = await sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"name.txt\").getLockedByUser();\n
"},{"location":"sp/folders/","title":"@pnp/sp/folders","text":"Folders serve as a container for your files and list items.
"},{"location":"sp/folders/#ifolders","title":"IFolders","text":"Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties.
"},{"location":"sp/folders/#get-folders-collection-for-various-sharepoint-objects","title":"Get folders collection for various SharePoint objects","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// gets web's folders\nconst webFolders = await sp.web.folders();\n\n// gets list's folders\nconst listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders();\n\n// gets item's folders\nconst itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders();\n
"},{"location":"sp/folders/#folderfromserverrelativepath","title":"folderFromServerRelativePath","text":"Added in 3.3.0
Utility method allowing you to get an IFolder reference using any SPQueryable as a base and the server relative path to the folder. Helpful when you do not have convenient access to an IWeb to use getFolderByServerRelativePath
.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromServerRelativePath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"/sites/dev/documents/folder4\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromServerRelativePath(sp.web, url);\n
"},{"location":"sp/folders/#folderfromabsolutepath","title":"folderFromAbsolutePath","text":"Added in 3.8.0
Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.
Works across site collections within the same tenant
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromAbsolutePath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromAbsolutePath(sp.web, url);\n\n// for example\nconst folderInfo = await folder();\n
"},{"location":"sp/folders/#folderfrompath","title":"folderFromPath","text":"Added in 3.8.0
Utility method allowing you to get an IFolder reference using any SPQueryable as a base and an absolute OR server relative path to the file.
Works across site collections within the same tenant
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromPath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromPath(sp.web, url);\n\n// for example\nconst folderInfo = await folder();\n\nconst url2 = \"/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder2 = folderFromPath(sp.web, url2);\n\n// for example\nconst folderInfo2 = await folder2();\n
"},{"location":"sp/folders/#add","title":"add","text":"Adds a new folder to collection of folders
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// creates a new folder for web with specified url\nconst folderAddResult = await sp.web.folders.addUsingPath(\"folder url\");\n
"},{"location":"sp/folders/#getbyurl","title":"getByUrl","text":"Gets a folder instance from a collection by folder's name
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder = await sp.web.folders.getByUrl(\"folder name\")();\n
"},{"location":"sp/folders/#ifolder","title":"IFolder","text":"Represents an instance of a SharePoint folder.
"},{"location":"sp/folders/#get-a-folder-object-associated-with-different-sharepoint-artifacts-web-list-list-item","title":"Get a folder object associated with different SharePoint artifacts (web, list, list item)","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// web's folder\nconst rootFolder = await sp.web.rootFolder();\n\n// list's folder\nconst listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder();\n\n// item's folder\nconst itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder();\n
"},{"location":"sp/folders/#getitem","title":"getItem","text":"Gets list item associated with a folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folderItem = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").getItem();\n
"},{"location":"sp/folders/#storagemetrics","title":"storageMetrics","text":"Added in 3.8.0
Gets a set of metrics describing the total file size contained in the folder.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst metrics = await sp.web.getFolderByServerRelativePath(\"/sites/dev/shared documents/target\").storageMetrics();\n\n// you can also select specific metrics if desired:\nconst metrics2 = await sp.web.getFolderByServerRelativePath(\"/sites/dev/shared documents/target\").storageMetrics.select(\"TotalSize\")();\n
"},{"location":"sp/folders/#move-by-path","title":"move by path","text":"It's possible to move a folder to a new destination within the same or a different site collection
If you change the filename during the move operation this is considered an \"edit\" and the file's modified information will be updated regardless of the \"RetainEditorAndModifiedOnMove\" setting.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new folder\nconst destinationUrl = `/sites/my-site/SiteAssets/new-folder`;\n\nawait sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").moveByPath(destinationUrl, true);\n
Added in 3.8.0
You can also supply a set of detailed options to better control the move process:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/folder`;\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents/folder\").moveByPath(destinationUrl, {\n KeepBoth: false,\n RetainEditorAndModifiedOnMove: true,\n ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/folders/#copy-by-path","title":"copy by path","text":"It's possible to copy a folder to a new destination within the same or a different site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new folder\nconst destinationUrl = `/sites/my-site/SiteAssets/new-folder`;\n\nawait sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").copyByPath(destinationUrl, true);\n
Added in 3.8.0
You can also supply a set of detailed options to better control the copy process:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/folder`;\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents/folder\").copyByPath(destinationUrl, false, {\n KeepBoth: false,\n ResetAuthorAndCreatedOnCopy: true,\n ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/folders/#delete","title":"delete","text":"Deletes a folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").delete();\n
"},{"location":"sp/folders/#delete-with-params","title":"delete with params","text":"Deletes a folder with options
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").deleteWithParams({\n BypassSharedLock: true,\n DeleteIfEmpty: true,\n });\n
"},{"location":"sp/folders/#recycle","title":"recycle","text":"Recycles a folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").recycle();\n
"},{"location":"sp/folders/#serverrelativeurl","title":"serverRelativeUrl","text":"Gets folder's server relative url
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst relUrl = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").select('ServerRelativeUrl')();\n
"},{"location":"sp/folders/#update","title":"update","text":"Updates folder's properties
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({\n \"Name\": \"New name\",\n });\n
"},{"location":"sp/folders/#contenttypeorder","title":"contentTypeOrder","text":"Gets content type order of a folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").select('contentTypeOrder')();\n
"},{"location":"sp/folders/#folders","title":"folders","text":"Gets all child folders associated with the current folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folders = await sp.web.rootFolder.folders();\n
"},{"location":"sp/folders/#files","title":"files","text":"Gets all files inside a folder
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/files/folder\";\n\nconst sp = spfi(...);\n\nconst files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files();\n
"},{"location":"sp/folders/#listitemallfields","title":"listItemAllFields","text":"Gets this folder's list item field values
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields();\n
"},{"location":"sp/folders/#parentfolder","title":"parentFolder","text":"Gets the parent folder, if available
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder();\n
"},{"location":"sp/folders/#properties","title":"properties","text":"Gets this folder's properties
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties();\n
"},{"location":"sp/folders/#uniquecontenttypeorder","title":"uniqueContentTypeOrder","text":"Gets a value that specifies the content type order.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").select('uniqueContentTypeOrder')();\n
"},{"location":"sp/folders/#rename-a-folder","title":"Rename a folder","text":"You can rename a folder by updating FileLeafRef
property:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\");\n\nconst item = await folder.getItem();\nconst result = await item.update({ FileLeafRef: \"Folder2\" });\n
"},{"location":"sp/folders/#create-a-folder-with-custom-content-type","title":"Create a folder with custom content type","text":"Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst newFolderResult = await sp.web.rootFolder.folders.getByUrl(\"Shared Documents\").folders.addUsingPath(\"My New Folder\");\nconst item = await newFolderResult.folder.listItemAllFields();\n\nawait sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({\n ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\",\n MyFolderField: \"field value\",\n Title: \"My New Folder\",\n});\n
"},{"location":"sp/folders/#addsubfolderusingpath","title":"addSubFolderUsingPath","text":"You can use the addSubFolderUsingPath method to add a folder with some special chars supported
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// add a folder to site assets\nconst folder: IFolder = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").addSubFolderUsingPath(\"folder name\");\n
"},{"location":"sp/folders/#getfolderbyid","title":"getFolderById","text":"You can get a folder by Id from a web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\n
"},{"location":"sp/folders/#getparentinfos","title":"getParentInfos","text":"Gets information about folder, including details about the parent list, parent list root folder, and parent web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\nawait folder.getParentInfos();\n
"},{"location":"sp/forms/","title":"@pnp/sp/forms","text":"Forms in SharePoint are the Display, New, and Edit forms associated with a list.
"},{"location":"sp/forms/#iforms","title":"IForms","text":""},{"location":"sp/forms/#get-form-by-id","title":"Get Form by Id","text":"Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/forms\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the field by Id for web\nconst form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();\n
"},{"location":"sp/groupSiteManager/","title":"GroupSiteManager","text":""},{"location":"sp/groupSiteManager/#pnpspgroupsitemanager","title":"@pnp/sp/groupsitemanager","text":"The @pnp/sp/groupsitemanager
package represents calls to _api/groupsitemanager
endpoint and is accessible from any site url.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/groupsitemanager\";\n\nconst sp = spfi(...);\n\n// call method to check if the current user can create Microsoft 365 groups\nconst isUserAllowed = await sp.groupSiteManager.canUserCreateGroup();\n\n// call method to delete a group-connected site\nawait sp.groupSiteManager.delete(\"https://contoso.sharepoint.com/sites/hrteam\");\n\n//call method to gets labels configured for the tenant\nconst orgLabels = await sp.groupSiteManager.getAllOrgLabels(0);\n\n//call method to get information regarding site groupification configuration for the current site context\nconst groupCreationContext = await sp.groupSiteManager.getGroupCreationContext();\n\n//call method to get information regarding site groupification configuration for the current site context\nconst siteData = await sp.groupSiteManager.getGroupSiteConversionData();\n\n// call method to get teams membership for a user\nconst userTeams = await sp.groupSiteManager.getUserTeamConnectedMemberGroups(\"meganb@contoso.onmicrosoft.com\");\n\n// call method to get shared channel memberhsip for user\nconst sharedChannels = await sp.groupSiteManager.getUserSharedChannelMemberGroups(\"meganb@contoso.onmicrosoft.com\");\n\n//call method to get valid site url from Alias\nconst siteUrl = await sp.groupSiteManager.getValidSiteUrlFromAlias(\"contoso\");\n\n//call method to check if teamify prompt is hidden\nconst isTeamifyPromptHidden = await sp.groupSiteManager.isTeamifyPromptHidden(\"https://contoso.sharepoint.com/sites/hrteam\");\n
For more information on the methods available and how to use them, please review the code comments in the source.
"},{"location":"sp/hubsites/","title":"@pnp/sp/hubsites","text":"This module helps you with working with hub sites in your tenant.
"},{"location":"sp/hubsites/#ihubsites","title":"IHubSites","text":""},{"location":"sp/hubsites/#get-a-listing-of-all-hub-sites","title":"Get a Listing of All Hub sites","text":"import { spfi } from \"@pnp/sp\";\nimport { IHubSiteInfo } from \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\n// invoke the hub sites object\nconst hubsites: IHubSiteInfo[] = await sp.hubSites();\n\n// you can also use select to only return certain fields:\nconst hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")();\n
"},{"location":"sp/hubsites/#get-hub-site-by-id","title":"Get Hub site by Id","text":"Using the getById method on the hubsites module to get a hub site by site Id (guid).
import { spfi } from \"@pnp/sp\";\nimport { IHubSiteInfo } from \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\nconst hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")();\n\n// log hub site title to console\nconsole.log(hubsite.Title);\n
"},{"location":"sp/hubsites/#get-isite-instance","title":"Get ISite instance","text":"We provide a helper method to load the ISite instance from the HubSite
import { spfi } from \"@pnp/sp\";\nimport { ISite } from \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\nconst site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite();\n\nconst siteData = await site();\n\nconsole.log(siteData.Title);\n
"},{"location":"sp/hubsites/#get-hub-site-data-for-a-web","title":"Get Hub site data for a web","text":"import { spfi } from \"@pnp/sp\";\nimport { IHubSiteWebData } from \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n\nconst webData: Partial<IHubSiteWebData> = await sp.web.hubSiteData();\n\n// you can also force a refresh of the hub site data\nconst webData2: Partial<IHubSiteWebData> = await sp.web.hubSiteData(true);\n
"},{"location":"sp/hubsites/#synchubsitetheme","title":"syncHubSiteTheme","text":"Allows you to apply theme updates from the parent hub site collection.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n\nawait sp.web.syncHubSiteTheme();\n
"},{"location":"sp/hubsites/#hub-site-site-methods","title":"Hub site Site Methods","text":"You manage hub sites at the Site level.
"},{"location":"sp/hubsites/#joinhubsite","title":"joinHubSite","text":"Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// join a site to a hub site\nawait sp.site.joinHubSite(\"{parent hub site id}\");\n\n// remove a site from a hub site\nawait sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\");\n
"},{"location":"sp/hubsites/#registerhubsite","title":"registerHubSite","text":"Registers the current site collection as hub site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// register current site as a hub site\nawait sp.site.registerHubSite();\n
"},{"location":"sp/hubsites/#unregisterhubsite","title":"unRegisterHubSite","text":"Un-registers the current site collection as hub site collection.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// make a site no longer a hub\nawait sp.site.unRegisterHubSite();\n
"},{"location":"sp/items/","title":"@pnp/sp/items","text":""},{"location":"sp/items/#get","title":"GET","text":"Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.
"},{"location":"sp/items/#basic-get","title":"Basic Get","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// get all the items from a list\nconst items: any[] = await sp.web.lists.getByTitle(\"My List\").items();\nconsole.log(items);\n\n// get a specific item by id.\nconst item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)();\nconsole.log(item);\n\n// use odata operators for more efficient queries\nconst items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)();\nconsole.log(items2);\n
"},{"location":"sp/items/#get-paged-items","title":"Get Paged Items","text":"Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// basic case to get paged items form a list\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged();\n\n// you can also provide a type for the returned values instead of any\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>();\n\n// the query also works with select to choose certain fields and top to set the page size\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>();\n\n// the results object will have two properties and one method:\n\n// the results property will be an array of the items returned\nif (items.results.length > 0) {\n console.log(\"We got results!\");\n\n for (let i = 0; i < items.results.length; i++) {\n // type checking works here if we specify the return type\n console.log(items.results[i].Title);\n }\n}\n\n// the hasNext property is used with the getNext method to handle paging\n// hasNext will be true so long as there are additional results\nif (items.hasNext) {\n\n // this will carry over the type specified in the original query for the results array\n items = await items.getNext();\n console.log(items.results.length);\n}\n
"},{"location":"sp/items/#getlistitemchangessincetoken","title":"getListItemChangesSinceToken","text":"The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// Using RowLimit. Enables paging\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'});\n\n// Use QueryOptions to make a XML-style query.\n// Because it's XML we need to escape special characters\n// Instead of & we use & in the query\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext=\"Paged=TRUE&p_ID=5\" />'});\n\n// Get everything. Using null with ChangeToken gets everything\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null});\n\n
"},{"location":"sp/items/#get-all-items","title":"Get All Items","text":"Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used.
In v3 there is a separate import for get-all to include the functionality. This is to remove the code from bundles for folks who do not need it.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/items/get-all\";\n\nconst sp = spfi(...);\n\n// basic usage\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll();\nconsole.log(allItems.length);\n\n// set page size\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000);\nconsole.log(allItems.length);\n\n// use select and top. top will set page size and override the any value passed to getAll\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll();\nconsole.log(allItems.length);\n\n// we can also use filter as a supported odata operation, but this will likely fail on large lists\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll();\nconsole.log(allItems.length);\n
"},{"location":"sp/items/#retrieving-lookup-fields","title":"Retrieving Lookup Fields","text":"When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")();\nconsole.log(items);\n\nconst item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")();\nconsole.log(item);\n
"},{"location":"sp/items/#filter-using-metadata-fields","title":"Filter using Metadata fields","text":"To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({\n ViewXml: `<View><Query><Where><Eq><FieldRef Name=\"MetaData\"/><Value Type=\"TaxonomyFieldType\">Term 2</Value></Eq></Where></Query></View>`,\n});\n
"},{"location":"sp/items/#retrieving-publishingpageimage","title":"Retrieving PublishingPageImage","text":"The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { Web } from \"@pnp/sp/webs\";\n\ntry {\n const sp = spfi(\"https://{publishing site url}\").using(SPFx(this.context));\n\n const r = await sp.web.lists.getByTitle(\"Pages\").items\n .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\")\n .expand(\"FieldValuesAsText\")\n ();\n\n // look through the returned items.\n for (var i = 0; i < r.length; i++) {\n\n // the title field value\n console.log(r[i].Title);\n\n // find the value in the MetaInfo string using regex\n const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);\n if (matches !== null && matches.length > 1) {\n\n // this wil be the value of the PublishingPageImage field\n console.log(matches[1]);\n }\n }\n}\ncatch (e) {\n console.error(e);\n}\n
"},{"location":"sp/items/#add-items","title":"Add Items","text":"There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { IItemAddResult } from \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// add an item to the list\nconst iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({\n Title: \"Title\",\n Description: \"Description\"\n});\n\nconsole.log(iar);\n
"},{"location":"sp/items/#content-type","title":"Content Type","text":"You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({\n Title: \"Test 1\",\n ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\"\n});\n
"},{"location":"sp/items/#user-fields","title":"User Fields","text":"There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.
Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an array. Examples for both are shown below.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { getGUID } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nconst i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({\n Title: getGUID(),\n User1Id: 9, // allows a single user\n User2Id: [16, 45] // allows multiple users\n});\n\nconsole.log(i);\n
If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{\n FieldName: \"UserField\",\n FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]),\n},\n{\n FieldName: \"Title\",\n FieldValue: \"Test - Updated\",\n}]);\n
"},{"location":"sp/items/#lookup-fields","title":"Lookup Fields","text":"What is said for User Fields is, in general, relevant to Lookup Fields:
Id
suffix should be appended to the end of lookups EntityPropertyName
in payloadsimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { getGUID } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"LookupFields\").items.add({\n Title: getGUID(),\n LookupFieldId: 2, // allows a single lookup value\n MultiLookupFieldId: [1, 56] // allows multiple lookup value\n});\n
"},{"location":"sp/items/#add-multiple-items","title":"Add Multiple Items","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst list = batchedSP.web.lists.getByTitle(\"rapidadd\");\n\nlet res = [];\n\nlist.items.add({ Title: \"Batch 6\" }).then(r => res.push(r));\n\nlist.items.add({ Title: \"Batch 7\" }).then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n ///Do something with the results\n}\n
"},{"location":"sp/items/#update-items","title":"Update Items","text":"The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.
Note: For updating certain types of fields, see the Add examples above. The payload will be the same you will just need to replace the .add method with .getById({itemId}).update.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nconst i = await list.items.getById(1).update({\n Title: \"My New Title\",\n Description: \"Here is a new description\"\n});\n\nconsole.log(i);\n
"},{"location":"sp/items/#getting-and-updating-a-collection-using-filter","title":"Getting and updating a collection using filter","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// you are getting back a collection here\nconst items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")();\n\n// see if we got something\nif (items.length > 0) {\n const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({\n Title: \"Updated Title\",\n });\n\n console.log(JSON.stringify(updatedItem));\n}\n
"},{"location":"sp/items/#update-multiple-items","title":"Update Multiple Items","text":"This approach avoids multiple calls for the same list's entity type name.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\"\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst list = batchedSP.web.lists.getByTitle(\"rapidupdate\");\n\nlist.items.getById(1).update({ Title: \"Batch 6\" }).then(b => {\n console.log(b);\n});\n\nlist.items.getById(2).update({ Title: \"Batch 7\" }).then(b => {\n console.log(b);\n});\n\n// Executes the batched calls\nawait execute();\n\nconsole.log(\"Done\");\n
"},{"location":"sp/items/#update-taxonomy-field","title":"Update Taxonomy field","text":"Note: Updating Taxonomy field for a File item should be handled differently. Instead of using update(), use validateUpdateListItem(). Please see below
List Item
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"Demo\").items.getById(1).update({\n MetaDataColumn: { Label: \"Demo\", TermGuid: '883e4c81-e8f9-4f19-b90b-6ab805c9f626', WssId: '-1' }\n});\n\n
File List Item
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nawait (await sp.web.getFileByServerRelativePath(\"/sites/demo/DemoLibrary/File.txt\").getItem()).validateUpdateListItem([{\n FieldName: \"MetaDataColumn\",\n FieldValue:\"Demo|883e4c81-e8f9-4f19-b90b-6ab805c9f626\", //Label|TermGuid\n}]);\n
"},{"location":"sp/items/#update-multi-value-taxonomy-field","title":"Update Multi-value Taxonomy field","text":"Based on this excellent article from Beau Cameron.
As he says you must update a hidden field to get this to work via REST. My meta data field accepting multiple values is called \"MultiMetaData\".
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// first we need to get the hidden field's internal name.\n// The Title of that hidden field is, in my case and in the linked article just the visible field name with \"_0\" appended.\nconst fields = await sp.web.lists.getByTitle(\"TestList\").fields.filter(\"Title eq 'MultiMetaData_0'\").select(\"Title\", \"InternalName\")();\n// get an item to update, here we just create one for testing\nconst newItem = await sp.web.lists.getByTitle(\"TestList\").items.add({\n Title: \"Testing\",\n});\n// now we have to create an update object\n// to do that for each field value you need to serialize each as -1;#{field label}|{field id} joined by \";#\"\n// update with the values you want, this also works in the add call directly to avoid a second call\nconst updateVal = {};\nupdateVal[fields[0].InternalName] = \"-1;#New Term|bb046161-49cc-41bd-a459-5667175920d4;#-1;#New 2|0069972e-67f1-4c5e-99b6-24ac5c90b7c9\";\n// execute the update call\nawait newItem.item.update(updateVal);\n
"},{"location":"sp/items/#update-bcs-field","title":"Update BCS Field","text":"Please see the issue for full details.
You will need to use validateUpdateListItem
to ensure hte BCS field is updated correctly.
const update = await sp.web.lists.getByTitle(\"Price\").items.getById(7).select('*,External').validateUpdateListItem([\n {FieldName:\"External\",FieldValue:\"Fauntleroy Circus\"},\n {FieldName:\"Customers_ID\", FieldValue:\"__bk410024003500240054006500\"}\n ]); \n
"},{"location":"sp/items/#recycle","title":"Recycle","text":"To send an item to the recycle bin use recycle.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nconst recycleBinIdentifier = await list.items.getById(1).recycle();\n
"},{"location":"sp/items/#delete","title":"Delete","text":"Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nawait list.items.getById(1).delete();\n
"},{"location":"sp/items/#delete-with-params","title":"Delete With Params","text":"Deletes the item object with options.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nawait list.items.getById(1).deleteWithParams({\n BypassSharedLock: true,\n });\n
The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true
"},{"location":"sp/items/#resolving-field-names","title":"Resolving field names","text":"It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName
value should be used.
The easiest way to get know EntityPropertyName is to use the following snippet:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst response =\n await sp.web.lists\n .getByTitle('[Lists_Title]')\n .fields\n .select('Title, EntityPropertyName')\n .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)\n ();\n\nconsole.log(response.map(field => {\n return {\n Title: field.Title,\n EntityPropertyName: field.EntityPropertyName\n };\n}));\n
Lookup fields' names should be ended with additional Id
suffix. E.g. for Editor
EntityPropertyName EditorId
should be used.
Gets information about an item, including details about the parent list, parent list root folder, and parent web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)();\nawait item.getParentInfos();\n
"},{"location":"sp/lists/","title":"@pnp/sp/lists","text":"Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.
"},{"location":"sp/lists/#ilists","title":"ILists","text":""},{"location":"sp/lists/#get-list-by-id","title":"Get List by Id","text":"Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the list by Id\nconst list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\");\n\n// we can use this 'list' variable to execute more queries on the list:\nconst r = await list.select(\"Title\")();\n\n// show the response from the server\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#get-list-by-title","title":"Get List by Title","text":"You can also get a list from the collection by title.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the default document library 'Documents'\nconst list = sp.web.lists.getByTitle(\"Documents\");\n\n// we can use this 'list' variable to run more queries on the list:\nconst r = await list.select(\"Id\")();\n\n// log the list Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/lists/#add-list","title":"Add List","text":"You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// create a new list, passing only the title\nconst listAddResult = await sp.web.lists.add(\"My new list\");\n\n// we can work with the list created using the IListAddResult.list property:\nconst r = await listAddResult.list.select(\"Title\")();\n\n// log newly created list title to console\nconsole.log(r.Title);\n});\n
You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs.
// this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings)\nconst listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true });\n\n// get the Id of the newly added document library\nconst r = await listAddResult.list.select(\"Id\")();\n\n// log id to console\nconsole.log(r.Id);\n
"},{"location":"sp/lists/#ensure-that-a-list-exists-by-title","title":"Ensure that a List exists (by title)","text":"Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default):\nconst listEnsureResult = await sp.web.lists.ensure(\"My List\");\n\n// check if the list was created, or if it already existed:\nif (listEnsureResult.created) {\n console.log(\"My List was created!\");\n} else {\n console.log(\"My List already existed!\");\n}\n\n// work on the created/updated list\nconst r = await listEnsureResult.list.select(\"Id\")();\n\n// log the Id\nconsole.log(r.Id);\n
If the list already exists, the other settings you provide will be used to update the existing list.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// add a new list to the lists collection of the web\nsp.web.lists.add(\"My List 2\").then(async () => {\n\n// then call ensure on the created list with an updated description\nconst listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\");\n\n// get the updated description\nconst r = await listEnsureResult.list.select(\"Description\")();\n\n// log the updated description\nconsole.log(r.Description);\n});\n
"},{"location":"sp/lists/#ensure-site-assets-library-exist","title":"Ensure Site Assets Library exist","text":"Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// get Site Assets library\nconst siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary();\n\n// get the Title\nconst r = await siteAssetsList.select(\"Title\")();\n\n// log Title\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#ensure-site-pages-library-exist","title":"Ensure Site Pages Library exist","text":"Gets a list that is the default location for wiki pages.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// get Site Pages library\nconst siteAssetsList = await sp.web.lists.ensureSitePagesLibrary();\n\n// get the Title\nconst r = await siteAssetsList.select(\"Title\")();\n\n// log Title\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#ilist","title":"IList","text":"Scenario Import Statement Selective 1 import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\";"},{"location":"sp/lists/#update-a-list","title":"Update a list","text":"Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\")
import { IListUpdateResult } from \"@pnp/sp/lists\";\n\n// create a TypedHash object with the properties to update\nconst updateProperties = {\n Description: \"This list title and description has been updated using PnPjs.\",\n Title: \"Updated title\",\n};\n\n// update the list with the properties above\nlist.update(updateProperties).then(async (l: IListUpdateResult) => {\n\n // get the updated title and description\n const r = await l.list.select(\"Title\", \"Description\")();\n\n // log the updated properties to the console\n console.log(r.Title);\n console.log(r.Description);\n});\n
"},{"location":"sp/lists/#get-changes-on-a-list","title":"Get changes on a list","text":"From the change log, you can get a collection of changes that have occurred within the list based on the specified query.
import { IChangeQuery } from \"@pnp/sp\";\n\n// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore\nconst changeQuery: IChangeQuery = {\n Add: true,\n ChangeTokenEnd: null,\n ChangeTokenStart: null,\n DeleteObject: true,\n Rename: true,\n Restore: true,\n};\n\n// get list changes\nconst r = await list.getChanges(changeQuery);\n\n// log changes to console\nconsole.log(r);\n
To get changes from a specific time range you can use the ChangeTokenStart or a combination of ChangeTokenStart and ChangeTokenEnd.
import { IChangeQuery } from \"@pnp/sp\";\n\n//Resource is the list Id (as Guid)\nconst resource = list.Id;\nconst changeStart = new Date(\"2022-02-22\").getTime();\nconst changeTokenStart = `1;3;${resource};${changeStart};-1`;\n\n// build the changeQuery object, here we look at changes regarding Add and Update for Items.\nconst changeQuery: IChangeQuery = {\n Add: true,\n Update: true,\n Item: true,\n ChangeTokenEnd: null,\n ChangeTokenStart: { StringValue: changeTokenStart },\n};\n\n// get list changes\nconst r = await list.getChanges(changeQuery);\n\n// log changes to console\nconsole.log(r);\n
"},{"location":"sp/lists/#get-list-items-using-a-caml-query","title":"Get list items using a CAML Query","text":"You can get items from SharePoint using a CAML Query.
import { ICamlQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst caml: ICamlQuery = {\n ViewXml: \"<View><ViewFields><FieldRef Name='Title' /></ViewFields><RowLimit>5</RowLimit></View>\",\n};\n\n// get list items\nconst r = await list.getItemsByCAMLQuery(caml);\n\n// log resulting array to console\nconsole.log(r);\n
If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment)
import { ICamlQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst caml: ICamlQuery = {\n ViewXml: \"<View><ViewFields><FieldRef Name='Title' /><FieldRef Name='RoleAssignments' /></ViewFields><RowLimit>5</RowLimit></View>\",\n};\n\n// get list items\nconst r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\");\n\n// log resulting item array to console\nconsole.log(r);\n
"},{"location":"sp/lists/#get-list-items-changes-using-a-token","title":"Get list items changes using a Token","text":"import { IChangeLogItemQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst changeLogItemQuery: IChangeLogItemQuery = {\n Contains: `<Contains><FieldRef Name=\"Title\"/><Value Type=\"Text\">Item16</Value></Contains>`,\n QueryOptions: `<QueryOptions>\n <IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns>\n <DateInUtc>False</DateInUtc>\n <IncludePermissions>TRUE</IncludePermissions>\n <IncludeAttachmentUrls>FALSE</IncludeAttachmentUrls>\n <Folder>My List</Folder></QueryOptions>`,\n};\n\n// get list items\nconst r = await list.getListItemChangesSinceToken(changeLogItemQuery);\n\n// log resulting XML to console\nconsole.log(r);\n
"},{"location":"sp/lists/#recycle-a-list","title":"Recycle a list","text":"Removes the list from the web's list collection and puts it in the recycle bin.
await list.recycle();\n
"},{"location":"sp/lists/#render-list-data","title":"Render list data","text":"import { IRenderListData } from \"@pnp/sp/lists\";\n\n// render list data, top 5 items\nconst r: IRenderListData = await list.renderListData(\"<View><RowLimit>5</RowLimit></View>\");\n\n// log array of items in response\nconsole.log(r.Row);\n
"},{"location":"sp/lists/#render-list-data-as-stream","title":"Render list data as stream","text":"import { IRenderListDataParameters } from \"@pnp/sp/lists\";\n// setup parameters object\nconst renderListDataParams: IRenderListDataParameters = {\n ViewXml: \"<View><RowLimit>5</RowLimit></View>\",\n};\n// render list data as stream\nconst r = await list.renderListDataAsStream(renderListDataParams);\n// log array of items in response\nconsole.log(r.Row);\n
You can also supply other options to renderListDataAsStream including override parameters and query params. This can be helpful when looking to apply sorting to the returned data.
import { IRenderListDataParameters } from \"@pnp/sp/lists\";\n// setup parameters object\nconst renderListDataParams: IRenderListDataParameters = {\n ViewXml: \"<View><RowLimit>5</RowLimit></View>\",\n};\nconst overrideParams = {\n ViewId = \"{view guid}\"\n};\n// OR if you don't want to supply override params use null\n// overrideParams = null;\n// Set the query params using a map\nconst query = new Map<string, string>();\nquery.set(\"SortField\", \"{AField}\");\nquery.set(\"SortDir\", \"Desc\");\n// render list data as stream\nconst r = await list.renderListDataAsStream(renderListDataParams, overrideParams, query);\n// log array of items in response\nconsole.log(r.Row);\n
"},{"location":"sp/lists/#reserve-list-item-id-for-idempotent-list-item-creation","title":"Reserve list item Id for idempotent list item creation","text":"const listItemId = await list.reserveListItemId();\n\n// log id to console\nconsole.log(listItemId);\n
"},{"location":"sp/lists/#add-a-list-item-using-path-folder-validation-and-set-field-values","title":"Add a list item using path (folder), validation and set field values","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")();\nconst formValues: IListItemFormUpdateValue[] = [\n {\n FieldName: \"Title\",\n FieldValue: title,\n },\n ];\n\nlist.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)\n\n
"},{"location":"sp/lists/#content-types-imports","title":"content-types imports","text":""},{"location":"sp/lists/#contenttypes","title":"contentTypes","text":"Get all content types for a list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nimport \"@pnp/sp/content-types/list\";\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.contentTypes();\n
"},{"location":"sp/lists/#fields-imports","title":"fields imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";"},{"location":"sp/lists/#fields","title":"fields","text":"Get all the fields for a list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nimport \"@pnp/sp/fields/list\";\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.fields();\n
Add a field to the site, then add the site field to a list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst fld = await sp.site.rootWeb.fields.addText(\"MyField\");\nawait sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml);\n
"},{"location":"sp/lists/#folders","title":"folders","text":"Get the root folder of a list.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/folders/list\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.rootFolder();\n
"},{"location":"sp/lists/#forms","title":"forms","text":"import \"@pnp/sp/forms/list\";\n\nconst r = await list.forms();\n
"},{"location":"sp/lists/#items","title":"items","text":"Get a collection of list items.
import \"@pnp/sp/items/list\";\n\nconst r = await list.items();\n
"},{"location":"sp/lists/#views","title":"views","text":"Get the default view of the list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views/list\";\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst views = await list.views();\nconst defaultView = await list.defaultView();\n
Get a list view by Id
const view = await list.getView(defaultView.Id).select(\"Title\")();\n
"},{"location":"sp/lists/#security-imports","title":"security imports","text":"To work with list security, you can import the list methods as follows:
import \"@pnp/sp/security/list\";\n
For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.
"},{"location":"sp/lists/#subscriptions","title":"subscriptions","text":"Get all subscriptions on the list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions/list\";\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst subscriptions = await list.subscriptions();\n
"},{"location":"sp/lists/#usercustomactions","title":"userCustomActions","text":"Get a collection of the list's user custom actions.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/user-custom-actions/web\"\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.userCustomActions();\n
"},{"location":"sp/lists/#getparentinfos","title":"getParentInfos","text":"Gets information about an list, including details about the parent list root folder, and parent web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nawait list.getParentInfos();\n
"},{"location":"sp/navigation/","title":"@pnp/sp - navigation","text":""},{"location":"sp/navigation/#navigation-service","title":"Navigation Service","text":""},{"location":"sp/navigation/#getmenustate","title":"getMenuState","text":"The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.
The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma
NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\n// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.\nconst state = await sp.navigation.getMenuState();\n\n// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5\nconst state2 = await sp.navigation.getMenuState(\"1002\", 5);\n\n// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5\nconst state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\");\n
"},{"location":"sp/navigation/#getmenunodekey","title":"getMenuNodeKey","text":"Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\nconst key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\");\n
"},{"location":"sp/navigation/#web-navigation","title":"Web Navigation","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/webs\";import \"@pnp/sp/navigation\"; The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.
"},{"location":"sp/navigation/#get-navigation","title":"Get navigation","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\nconst top = await sp.web.navigation.topNavigationBar();\nconst quick = await sp.web.navigation.quicklaunch();\n
For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n// note we are just getting a ref to the nav object, not executing a request\nconst nav = sp.web.navigation.topNavigationBar;\n// -- OR -- \n// note we are just getting a ref to the nav object, not executing a request\nconst nav = sp.web.navigation.quicklaunch;\n
"},{"location":"sp/navigation/#getbyid","title":"getById","text":"import \"@pnp/sp/navigation\";\n\nconst node = await nav.getById(3)();\n
"},{"location":"sp/navigation/#add","title":"add","text":"import \"@pnp/sp/navigation\";\n\nconst result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true);\n\nconst nodeDataRaw = result.data;\n\n// request the data from the created node\nconst nodeData = result.node();\n
"},{"location":"sp/navigation/#moveafter","title":"moveAfter","text":"Places a navigation node after another node in the tree
import \"@pnp/sp/navigation\";\n\nconst node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true);\nconst node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true);\nconst node1 = await node1result.node();\nconst node2 = await node2result.node();\n\nawait nav.moveAfter(node1.Id, node2.Id);\n
"},{"location":"sp/navigation/#delete","title":"Delete","text":"Deletes a given node
import \"@pnp/sp/navigation\";\n\nconst node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true);\nlet nodes = await nav();\n// check we added a node\nlet index = nodes.findIndex(n => n.Id === node1result.data.Id)\n// index >= 0\n\n// delete a node\nawait nav.getById(node1result.data.Id).delete();\n\nnodes = await nav();\nindex = nodes.findIndex(n => n.Id === node1result.data.Id)\n// index = -1\n
"},{"location":"sp/navigation/#update","title":"Update","text":"You are able to update various properties of a given node, such as the the Title, Url, IsVisible.
You may update the Audience Targeting value for the node by passing in Microsoft Group IDs in the AudienceIds array. Be aware, Audience Targeting must already be enabled on the navigation.
import \"@pnp/sp/navigation\";\n\n\nawait nav.getById(4).update({\n Title: \"A new title\",\n AudienceIds:[\"d50f9511-b811-4d76-b20a-0d6e1c8095f7\"],\n Url:\"/sites/dev/SitePages/home.aspx\",\n IsVisible:false\n});\n
"},{"location":"sp/navigation/#children","title":"Children","text":"The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch.
import \"@pnp/sp/navigation\";\n\nconst childrenData = await nav.getById(1).children();\n\n// add a child\nawait nav.getById(1).children.add(\"Title\", \"Url\", true);\n
"},{"location":"sp/permissions/","title":"@pnp/sp - permissions","text":"A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.
Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.
"},{"location":"sp/permissions/#get-role-assignments","title":"Get Role Assignments","text":"This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst roles = await sp.web.roleAssignments();\n
"},{"location":"sp/permissions/#first-unique-ancestor-securable-object","title":"First Unique Ancestor Securable Object","text":"This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst obj = await sp.web.firstUniqueAncestorSecurableObject();\n
"},{"location":"sp/permissions/#user-effective-permissions","title":"User Effective Permissions","text":"This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\");\n\nconst perms2 = await sp.web.getCurrentUserEffectivePermissions();\n
"},{"location":"sp/permissions/#user-has-permissions","title":"User Has Permissions","text":"Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems);\n\nconst perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems);\n
"},{"location":"sp/permissions/#has-permissions","title":"Has Permissions","text":"If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.getCurrentUserEffectivePermissions();\nif (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {\n // ...\n}\n
"},{"location":"sp/profiles/","title":"@pnp/sp/profiles","text":"The profile services allows you to work with the SharePoint User Profile Store.
"},{"location":"sp/profiles/#profiles","title":"Profiles","text":"Profiles is accessed directly from the root sp object.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/profiles\";\n
"},{"location":"sp/profiles/#get-edit-profile-link-for-the-current-user","title":"Get edit profile link for the current user","text":"getEditProfileLink(): Promise<string>\n
const sp = spfi(...);\nconst editProfileLink = await sp.profiles.getEditProfileLink();\n
"},{"location":"sp/profiles/#is-my-people-list-public","title":"Is My People List Public","text":"Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not
getIsMyPeopleListPublic(): Promise<boolean>\n
const sp = spfi(...);\nconst isPublic = await sp.profiles.getIsMyPeopleListPublic();\n
"},{"location":"sp/profiles/#find-out-if-the-current-user-is-followed-by-another-user","title":"Find out if the current user is followed by another user","text":"Provides a boolean that indicates if the current users is followed by a specific user.
amIFollowedBy(loginName: string): Promise<boolean>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst isFollowedBy = await sp.profiles.amIFollowedBy(loginName);\n
"},{"location":"sp/profiles/#find-out-if-i-am-following-a-specific-user","title":"Find out if I am following a specific user","text":"Provides a boolean that indicates if the current users is followed by a specific user.
amIFollowing(loginName: string): Promise<boolean>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst following = await sp.profiles.amIFollowing(loginName);\n
"},{"location":"sp/profiles/#get-the-tags-i-follow","title":"Get the tags I follow","text":"Gets the tags the current user is following. Accepts max count, default is 20.
getFollowedTags(maxCount = 20): Promise<string[]>\n
const sp = spfi(...);\nconst tags = await sp.profiles.getFollowedTags();\n
"},{"location":"sp/profiles/#get-followers-for-a-specific-user","title":"Get followers for a specific user","text":"Gets the people who are following the specified user.
getFollowersFor(loginName: string): Promise<any[]>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst followers = await sp.profiles.getFollowersFor(loginName);\nfollowers.forEach((value) => {\n ...\n});\n
"},{"location":"sp/profiles/#get-followers-for-the-current","title":"Get followers for the current","text":"Gets the people who are following the current user.
myFollowers(): ISPCollection\n
const sp = spfi(...);\nconst folowers = await sp.profiles.myFollowers();\n
"},{"location":"sp/profiles/#get-the-properties-for-the-current-user","title":"Get the properties for the current user","text":"Gets user properties for the current user.
myProperties(): ISPInstance\n
const sp = spfi(...);\nconst profile = await sp.profiles.myProperties();\nconsole.log(profile.DisplayName);\nconsole.log(profile.Email);\nconsole.log(profile.Title);\nconsole.log(profile.UserProfileProperties.length);\n\n// Properties are stored in Key/Value pairs,\n// so parse into an object called userProperties\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n props[prop.Key] = prop.Value;\n});\nprofile.userProperties = props;\nconsole.log(\"Account Name: \" + profile.userProperties.AccountName);\n
// you can also select properties to return before\nconst sp = spfi(...);\nconst profile = await sp.profiles.myProperties.select(\"Title\", \"Email\")();\nconsole.log(profile.Email);\nconsole.log(profile.Title);\n
"},{"location":"sp/profiles/#gets-people-specified-user-is-following","title":"Gets people specified user is following","text":"getPeopleFollowedBy(loginName: string): Promise<any[]>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst folowers = await sp.profiles.getFollowersFor(loginName);\nfollowers.forEach((value) => {\n ...\n});\n
"},{"location":"sp/profiles/#gets-properties-for-a-specified-user","title":"Gets properties for a specified user","text":"getPropertiesFor(loginName: string): Promise<any>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst profile = await sp.profiles.getPropertiesFor(loginName);\nconsole.log(profile.DisplayName);\nconsole.log(profile.Email);\nconsole.log(profile.Title);\nconsole.log(profile.UserProfileProperties.length);\n\n// Properties are stored in inconvenient Key/Value pairs,\n// so parse into an object called userProperties\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n props[prop.Key] = prop.Value;\n});\n\nprofile.userProperties = props;\nconsole.log(\"Account Name: \" + profile.userProperties.AccountName);\n
"},{"location":"sp/profiles/#gets-most-popular-tags","title":"Gets most popular tags","text":"Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first
trendingTags(): Promise<IHashTagCollection>\n
const sp = spfi(...);\nconst tags = await sp.profiles.trendingTags();\ntags.Items.forEach((tag) => {\n ...\n});\n
"},{"location":"sp/profiles/#gets-specified-user-profile-property-for-the-specified-user","title":"Gets specified user profile property for the specified user","text":"getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst propertyName = \"AccountName\";\nconst property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);\n
"},{"location":"sp/profiles/#hide-specific-user-from-list-of-suggested-people","title":"Hide specific user from list of suggested people","text":"Removes the specified user from the user's list of suggested people to follow.
hideSuggestion(loginName: string): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nawait sp.profiles.hideSuggestion(loginName);\n
"},{"location":"sp/profiles/#is-one-user-following-another","title":"Is one user following another","text":"Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower.
isFollowing(follower: string, followee: string): Promise<boolean>\n
const sp = spfi(...);\nconst follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\";\nconst isFollowing = await sp.profiles.isFollowing(follower, followee);\n
"},{"location":"sp/profiles/#set-user-profile-picture","title":"Set User Profile Picture","text":"Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB.
setMyProfilePic(profilePicSource: Blob): Promise<void>\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/profiles\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// get the blob object through a request or from a file input\nconst blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob();\n\nawait sp.profiles.setMyProfilePic(blob);\n
"},{"location":"sp/profiles/#sets-single-value-user-profile-property","title":"Sets single value User Profile property","text":"accountName The account name of the user propertyName Property name propertyValue Property value
setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nawait sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\");\n
"},{"location":"sp/profiles/#sets-a-mult-value-user-profile-property","title":"Sets a mult-value User Profile property","text":"accountName The account name of the user propertyName Property name propertyValues Property values
setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst propertyName = \"SPS-Skills\";\nconst propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"];\nawait sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues);\nconst profile = await sp.profiles.getPropertiesFor(loginName);\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n props[prop.Key] = prop.Value;\n});\nprofile.userProperties = props;\nconsole.log(profile.userProperties[propertyName]);\n
"},{"location":"sp/profiles/#create-personal-site-for-specified-users","title":"Create Personal Site for specified users","text":"Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for
createPersonalSiteEnqueueBulk(...emails: string[]): Promise<void>\n
const sp = spfi(...);\nlet userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"];\nawait sp.profiles.createPersonalSiteEnqueueBulk(userEmails);\n
"},{"location":"sp/profiles/#get-the-user-profile-of-the-owner-for-the-current-site","title":"Get the user profile of the owner for the current site","text":"ownerUserProfile(): Promise<IUserProfile>\n
const sp = spfi(...);\nconst profile = await sp.profiles.ownerUserProfile();\n
"},{"location":"sp/profiles/#get-the-user-profile-of-the-current-user","title":"Get the user profile of the current user","text":"userProfile(): Promise<any>\n
const sp = spfi(...);\nconst profile = await sp.profiles.userProfile();\n
"},{"location":"sp/profiles/#create-personal-site-for-current-user","title":"Create personal site for current user","text":"createPersonalSite(interactiveRequest = false): Promise<void>\n
const sp = spfi(...);\nawait sp.profiles.createPersonalSite();\n
"},{"location":"sp/profiles/#make-all-profile-data-public-or-private","title":"Make all profile data public or private","text":"Set the privacy settings for all social data.
shareAllSocialData(share: boolean): Promise<void>\n
const sp = spfi(...);\nawait sp.profiles.shareAllSocialData(true);\n
"},{"location":"sp/profiles/#resolve-a-user-or-group","title":"Resolve a user or group","text":"Resolves user or group using specified query parameters
clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity>\n
const sp = spfi(...);\nconst result = await sp.profiles.clientPeoplePickerResolveUser({\n AllowEmailAddresses: true,\n AllowMultipleEntities: false,\n MaximumEntitySuggestions: 25,\n QueryString: 'mbowen@contoso.com'\n});\n
"},{"location":"sp/profiles/#search-a-user-or-group","title":"Search a user or group","text":"Searches for users or groups using specified query parameters
clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity[]>\n
const sp = spfi(...);\nconst result = await sp.profiles.clientPeoplePickerSearchUser({\n AllowEmailAddresses: true,\n AllowMultipleEntities: false,\n MaximumEntitySuggestions: 25,\n QueryString: 'John'\n});\n
"},{"location":"sp/publishing-sitepageservice/","title":"@pnp/sp/publishing-sitepageservice","text":"Through the REST api you are able to call a SP.Publishing.SitePageService method GetCurrentUserMemberships. This method allows you to fetch identifiers of unified groups to which current user belongs. It's an alternative for using graph.me.transitiveMemberOf() method from graph package. Note, method only works with the context of a logged in user, and not with app-only permissions.
"},{"location":"sp/publishing-sitepageservice/#get-current-users-group-memberships","title":"Get current user's group memberships","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/publishing-sitepageservice\";\n\nconst sp = spfi(...);\n\nconst groupIdentifiers = await sp.publishingSitePageService.getCurrentUserMemberships();\n
"},{"location":"sp/recycle-bin/","title":"@pnp/sp/recycle-bin","text":"The contents of the recycle bin.
"},{"location":"sp/recycle-bin/#irecyclebin-irecyclebinitem","title":"IRecycleBin, IRecycleBinItem","text":""},{"location":"sp/recycle-bin/#work-with-the-contents-of-the-webs-recycle-bin","title":"Work with the contents of the web's Recycle Bin","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/recycle-bin\";\n\nconst sp = spfi(...);\n\n// gets contents of the web's recycle bin\nconst bin = await sp.web.recycleBin();\n\n// gets a specific item from the recycle bin\nconst rbItem = await sp.web.recycleBin.getById(bin[0].id);\n\n// delete the item from the recycle bin\nawait rbItem.delete();\n\n// restore the item from the recycle bin\nawait rbItem.restore();\n\n// move the item to the second-stage (site) recycle bin.\nawait rbItem.moveToSecondStage();\n\n// deletes everything in the recycle bin\nawait sp.web.recycleBin.deleteAll();\n\n// restores everything in the recycle bin\nawait sp.web.recycleBin.restoreAll();\n\n// moves contents of recycle bin to second-stage (site) recycle bin.\nawait sp.web.recycleBin.moveAllToSecondStage();\n\n// deletes contents of the second-stage (site) recycle bin.\nawait sp.web.recycleBin.deleteAllSecondStageItems();\n
"},{"location":"sp/recycle-bin/#work-with-the-contents-of-the-second-stage-site-recycle-bin","title":"Work with the contents of the Second-stage (site) Recycle Bin","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/recycle-bin\";\n\nconst sp = spfi(...);\n\n// gets contents of the second-stage recycle bin\nconst ssBin = await sp.site.recycleBin();\n\n// gets a specific item from the second-stage recycle bin\nconst rbItem = await sp.site.recycleBin.getById(ssBin[0].id);\n\n// delete the item from the second-stage recycle bin\nawait rbItem.delete();\n\n// restore the item from the second-stage recycle bin\nawait rbItem.restore();\n\n// deletes everything in the second-stage recycle bin\nawait sp.site.recycleBin.deleteAll();\n\n// restores everything in the second-stage recycle bin\nawait sp.site.recycleBin.restoreAll();\n
"},{"location":"sp/regional-settings/","title":"@pnp/sp/regional-settings","text":"The regional settings module helps with managing dates and times across various timezones.
"},{"location":"sp/regional-settings/#iregionalsettings","title":"IRegionalSettings","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\n// get all the web's regional settings\nconst s = await sp.web.regionalSettings();\n\n// select only some settings to return\nconst s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")();\n
"},{"location":"sp/regional-settings/#installed-languages","title":"Installed Languages","text":"You can get a list of the installed languages in the web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\nconst s = await sp.web.regionalSettings.getInstalledLanguages();\n
The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.
"},{"location":"sp/regional-settings/#timezones","title":"TimeZones","text":"You can also get information about the selected timezone in the web and all of the defined timezones.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\n// get the web's configured timezone\nconst s = await sp.web.regionalSettings.timeZone();\n\n// select just the Description and Id\nconst s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")();\n\n// get all the timezones\nconst s3 = await sp.web.regionalSettings.timeZones();\n\n// get a specific timezone by id\n// list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx\nconst s4 = await sp.web.regionalSettings.timeZones.getById(23);\nconst s5 = await s.localTimeToUTC(new Date());\n\n// convert a given date from web's local time to UTC time\nconst s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date());\n\n// convert a given date from UTC time to web's local time\nconst s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date())\nconst s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))\n
"},{"location":"sp/regional-settings/#title-and-description-resources","title":"Title and Description Resources","text":"Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings\";\n\nconst sp = spfi(...);\n\n//\n// The below methods appears on\n// - Web\n// - List\n// - Field\n// - ContentType\n// - User Custom Action\n//\n// after you import @pnp/sp/regional-settings\n//\n// you can also import just parts of the regional settings:\n// import \"@pnp/sp/regional-settings/web\";\n// import \"@pnp/sp/regional-settings/list\";\n// import \"@pnp/sp/regional-settings/content-type\";\n// import \"@pnp/sp/regional-settings/field\";\n// import \"@pnp/sp/regional-settings/user-custom-actions\";\n\n\nconst title = await sp.web.titleResource(\"en-us\");\nconst title2 = await sp.web.titleResource(\"de-de\");\n\nconst description = await sp.web.descriptionResource(\"en-us\");\nconst description2 = await sp.web.descriptionResource(\"de-de\");\n
You can only read the values through the REST API, not set the value.
"},{"location":"sp/related-items/","title":"@pnp/sp/related-items","text":"The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.
"},{"location":"sp/related-items/#setup","title":"Setup","text":"Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work.
import { spfi, SPFx, extractWebUrl } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/related-items/web\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\nimport \"@pnp/sp/files/list\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { getRandomString } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\n// setup some lists (or just use existing ones this is just to show the complete process)\n// we need two lists to use for creating related items, they need to use template 107 (task list)\nconst ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107);\nconst ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107);\n\nconst sourceList = ler1.list;\nconst targetList = ler2.list;\n\nconst sourceListName = await sourceList.select(\"Id\")().then(r => r.Id);\nconst targetListName = await targetList.select(\"Id\")().then(r => r.Id);\n\n// or whatever you need to get the web url, both our example lists are in the same web.\nconst webUrl = sp.web.toUrl();\n\n// ...individual samples start here\n
"},{"location":"sp/related-items/#addsinglelink","title":"addSingleLink","text":"const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n
"},{"location":"sp/related-items/#addsinglelinktourl","title":"addSingleLinkToUrl","text":"This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document.
// get a file's server relative url in some manner, here we add one\nconst file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data);\n// add an item or get an item from the task list\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\nawait sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);\n
"},{"location":"sp/related-items/#addsinglelinkfromurl","title":"addSingleLinkFromUrl","text":"This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.
"},{"location":"sp/related-items/#deletesinglelink","title":"deleteSingleLink","text":"This method allows you to delete a link previously created.
const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add the link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\n// delete the link\nawait sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n
"},{"location":"sp/related-items/#getrelateditems","title":"getRelatedItems","text":"Gets the related items for an item
import { IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\nconst targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);\n\nconst items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id);\n\n// items.length === 2\n
Related items are defined by the IRelatedItem interface
export interface IRelatedItem {\n ListId: string;\n ItemId: number;\n Url: string;\n Title: string;\n WebId: string;\n IconUrl: string;\n}\n
"},{"location":"sp/related-items/#getpageonerelateditems","title":"getPageOneRelatedItems","text":"Gets an abbreviated set of related items
import { IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\nconst targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);\n\nconst items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id);\n\n// items.length === 2\n
"},{"location":"sp/search/","title":"@pnp/sp/search","text":"Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.
"},{"location":"sp/search/#search","title":"Search","text":"Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// text search using SharePoint default values for other parameters\nconst results: SearchResults = await sp.search(\"test\");\n\nconsole.log(results.ElapsedTime);\nconsole.log(results.RowCount);\nconsole.log(results.PrimarySearchResults);\n\n\n// define a search query object matching the ISearchQuery interface\nconst results2: SearchResults = await sp.search(<ISearchQuery>{\n Querytext: \"test\",\n RowLimit: 10,\n EnableInterleaving: true,\n});\n\nconsole.log(results2.ElapsedTime);\nconsole.log(results2.RowCount);\nconsole.log(results2.PrimarySearchResults);\n\n// define a query using a builder\nconst builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites;\nconst results3 = await sp.search(builder);\n\nconsole.log(results3.ElapsedTime);\nconsole.log(results3.RowCount);\nconsole.log(results3.PrimarySearchResults);\n
"},{"location":"sp/search/#search-result-caching","title":"Search Result Caching","text":"Starting with v3 you can use any of the caching behaviors with search and the results will be cached. Please see here for more details on caching options.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...).using(Caching());\n\nsp.search({/* ... query */}).then((r: SearchResults) => {\n\n console.log(r.ElapsedTime);\n console.log(r.RowCount);\n console.log(r.PrimarySearchResults);\n});\n\n// use a query builder\nconst builder = SearchQueryBuilder(\"test\").rowLimit(3);\n\n// supply a search query builder and caching options\nconst results2 = await sp.search(builder);\n\nconsole.log(results2.TotalRows);\n
"},{"location":"sp/search/#paging-with-searchresultsgetpage","title":"Paging with SearchResults.getPage","text":"Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// this will hold our current results\nlet currentResults: SearchResults = null;\nlet page = 1;\n\n// triggered on page load or through some other means\nfunction onStart() {\n\n // construct our query that will be used throughout the paging process, likely from user input\n const q = SearchQueryBuilder(\"test\").rowLimit(5);\n const results = await sp.search(q);\n currentResults = results; // set the current results\n page = 1; // reset page counter\n // update UI...\n}\n\n// triggered by an event\nasync function next() {\n\n currentResults = await currentResults.getPage(++page);\n // update UI...\n}\n\n// triggered by an event\nasync function prev() {\n\n currentResults = await currentResults.getPage(--page);\n // update UI...\n}\n
"},{"location":"sp/search/#searchquerybuilder","title":"SearchQueryBuilder","text":"The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// basic usage\nlet q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic;\n\nsp.search(q).then(h => { /* ... */ });\n\n// provide a default query text at creation\nlet q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic;\n\nconst results: SearchResults = await sp.search(q2);\n\n// provide query text and a template for\n// shared settings across queries that can\n// be overwritten by individual builders\nconst appSearchSettings: ISearchQuery = {\n EnablePhonetic: true,\n HiddenConstraints: \"reports\"\n};\n\nlet q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules;\nlet q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming;\nconst results2 = await sp.search(q3);\nconst results3 = sp.search(q4);\n
"},{"location":"sp/search/#search-suggest","title":"Search Suggest","text":"Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\nconst results = await sp.searchSuggest(\"test\");\n\nconst results2 = await sp.searchSuggest({\n querytext: \"test\",\n count: 5,\n} as ISuggestQuery);\n
"},{"location":"sp/search/#search-factory","title":"Search Factory","text":"You can also configure a search or suggest query against any valid SP url using the factory methods.
In this case you'll need to ensure you add observers, or use the tuple constructor to inherit
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/search\";\nimport { Search, Suggest } from \"@pnp/sp/search\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\nconst sp = spfi(...);\n\n// set the url for search\nconst searcher = Search([sp.web, \"https://mytenant.sharepoint.com/sites/dev\"]);\n\n// this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder)\nconst results = await searcher(\"test\");\n\n// you can reuse the ISearch instance\nconst results2 = await searcher(\"another query\");\n\n// same process works for Suggest\nconst suggester = Suggest([sp.web, \"https://mytenant.sharepoint.com/sites/dev\"]);\n\nconst suggestions = await suggester({ querytext: \"test\" });\n\n// resetting the observers on the instance\nconst searcher2 = Search(\"https://mytenant.sharepoint.com/sites/dev\").using(SPDefault({\n msal: {\n config: {...},\n scopes: [...],\n },\n}));\n\nconst results3 = await searcher2(\"test\");\n
"},{"location":"sp/security/","title":"@pnp/sp/security","text":"There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below.
Site permissions are managed on the root web of the site collection.
"},{"location":"sp/security/#a-note-on-selective-imports-for-security","title":"A Note on Selective Imports for Security","text":"Because the method are shared you can opt to import only the methods for one of the instances.
import \"@pnp/sp/security/web\";\nimport \"@pnp/sp/security/list\";\nimport \"@pnp/sp/security/item\";\n
Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module:
import \"@pnp/sp/security\";\n
"},{"location":"sp/security/#securable-methods","title":"Securable Methods","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/security/list\";\nimport \"@pnp/sp/site-users/web\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// ensure we have a list\nconst ler = await sp.web.lists.ensure(\"SecurityTestingList\");\nconst list: IList = ler.list;\n\n// role assignments (see section below)\nawait list.roleAssignments();\n\n// data will represent one of the possible parents Site, Web, or List\nconst data = await list.firstUniqueAncestorSecurableObject();\n\n// getUserEffectivePermissions\nconst users = await sp.web.siteUsers.top(1).select(\"LoginName\")();\nconst perms = await list.getUserEffectivePermissions(users[0].LoginName);\n\n// getCurrentUserEffectivePermissions\nconst perms2 = list.getCurrentUserEffectivePermissions();\n\n// userHasPermissions\nconst v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems)\n\n// currentUserHasPermissions\nconst v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems)\n\n// breakRoleInheritance\nawait list.breakRoleInheritance();\n// copy existing permissions\nawait list.breakRoleInheritance(true);\n// copy existing permissions and reset all child securables to the new permissions\nawait list.breakRoleInheritance(true, true);\n\n// resetRoleInheritance\nawait list.resetRoleInheritance();\n
"},{"location":"sp/security/#web-specific-methods","title":"Web Specific methods","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/security/web\";\n\nconst sp = spfi(...);\n\n// role definitions (see section below)\nconst defs = await sp.web.roleDefinitions();\n
"},{"location":"sp/security/#role-assignments","title":"Role Assignments","text":"Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/security/web\";\nimport \"@pnp/sp/site-users/web\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// ensure we have a list\nconst ler = await sp.web.lists.ensure(\"SecurityTestingList\");\nconst list: IList = ler.list;\n\n// list role assignments\nconst assignments = await list.roleAssignments();\n\n// add a role assignment\nconst defs = await sp.web.roleDefinitions();\nconst user = await sp.web.currentUser();\nconst r = await list.roleAssignments.add(user.Id, defs[0].Id);\n\n// remove a role assignment\nconst { Id: fullRoleDefId } = await sp.web.roleDefinitions.getByName('Full Control')();\nconst ras = await list.roleAssignments();\n// filter/find the role assignment you want to remove\n// here we just grab the first\nconst ra = ras.find(v => true);\nconst r = await list.roleAssignments.remove(ra.PrincipalId, fullRoleDefId);\n\n// read role assignment info\nconst info = await list.roleAssignments.getById(ra.Id)();\n\n// get the groups\nconst info2 = await list.roleAssignments.getById(ra.Id).groups();\n\n// get the bindings\nconst info3 = await list.roleAssignments.getById(ra.Id).bindings();\n\n// delete a role assignment (same as remove)\nconst ras = await list.roleAssignments();\n// filter/find the role assignment you want to remove\n// here we just grab the first\nconst ra = ras.find(v => true);\n\n// delete it\nawait list.roleAssignments.getById(ra.Id).delete();\n
"},{"location":"sp/security/#role-definitions","title":"Role Definitions","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/security/web\";\n\nconst sp = spfi(...);\n\n// read role definitions\nconst defs = await sp.web.roleDefinitions();\n\n// get by id\nconst def = await sp.web.roleDefinitions.getById(5)();\nconst def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")();\n\n// get by name\nconst def = await sp.web.roleDefinitions.getByName(\"Full Control\")();\nconst def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")();\n\n// get by type\nconst def = await sp.web.roleDefinitions.getByType(5)();\nconst def = await sp.web.roleDefinitions.getByType(5).select(\"Name\", \"Order\")();\n\n// add\n// name The new role definition's name\n// description The new role definition's description\n// order The order in which the role definition appears\n// basePermissions The permissions mask for this role definition\nconst rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 });\n\n\n\n// the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example\n\n// delete\nawait sp.web.roleDefinitions.getById(5).delete();\n\n// update\nconst res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });\n
"},{"location":"sp/security/#get-list-items-with-unique-permissions","title":"Get List Items with Unique Permissions","text":"In order to get a list of items that have unique permissions you have to specifically select the '' field and then filter on the client.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/security/items\";\n\nconst sp = spfi(...);\n\nconst listItems = await sp.web.lists.getByTitle(\"pnplist\").items.select(\"Id, HasUniqueRoleAssignments\")();\n\n//Loop over list items filtering for HasUniqueRoleAssignments value\n\n
"},{"location":"sp/sharing/","title":"@pnp/sp/sharing","text":"Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.
One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.
"},{"location":"sp/sharing/#imports","title":"Imports","text":"In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.
"},{"location":"sp/sharing/#import-all","title":"Import All","text":"To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module:
import \"@pnp/sp/sharing\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.siteUsers.getByEmail(\"user@site.com\")();\nconst r = await sp.web.shareWith(user.LoginName);\n
"},{"location":"sp/sharing/#selective-import","title":"Selective Import","text":"Import only the web's sharing methods into the library
import \"@pnp/sp/sharing/web\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.siteUsers.getByEmail(\"user@site.com\")();\nconst r = await sp.web.shareWith(user.LoginName);\n
"},{"location":"sp/sharing/#getsharelink","title":"getShareLink","text":"Applies to: Item, Folder, File
Creates a sharing link for the given resource with an optional expiration.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\";\nimport { dateAdd } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView);\n\nconsole.log(JSON.stringify(result, null, 2));\n\n\nconst result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5));\n\nconsole.log(JSON.stringify(result2, null, 2));\n
"},{"location":"sp/sharing/#sharewith","title":"shareWith","text":"Applies to: Item, Folder, File, Web
Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames
parameter or an array of loginnames
. The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/files/web\";\nimport { ISharingResult, SharingRole } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\");\n\nconsole.log(JSON.stringify(result, null, 2));\n\n// Share and allow editing\nconst result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);\n\nconsole.log(JSON.stringify(result2, null, 2));\n\n\n// share folder\nconst result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\");\n\n// Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children)\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true);\n\n// Share a file\nawait sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\");\n\n// Share a file with edit permissions\nawait sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);\n
"},{"location":"sp/sharing/#shareobject-shareobjectraw","title":"shareObject & shareObjectRaw","text":"Applies to: Web
Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { ISharingResult, SharingRole } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Share an object in this web\nconst result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View);\n\n// Share an object with all settings available\nawait sp.web.shareObjectRaw({\n url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\",\n peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }],\n roleValue: \"role: 1973741327\",\n groupId: 0,\n propagateAcl: false,\n sendEmail: true,\n includeAnonymousLinkInEmail: false,\n emailSubject: \"subject\",\n emailBody: \"body\",\n useSimplifiedRoles: true,\n});\n
"},{"location":"sp/sharing/#unshareobject","title":"unshareObject","text":"Applies to: Web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { ISharingResult } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\");\n
"},{"location":"sp/sharing/#checksharingpermissions","title":"checkSharingPermissions","text":"Applies to: Item, Folder, File
Checks Permissions on the list of Users and returns back role the users have on the Item.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing/folders\";\nimport \"@pnp/sp/folders/web\";\nimport { SharingEntityPermission } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// check the sharing permissions for a folder\nconst perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]);\n
"},{"location":"sp/sharing/#getsharinginformation","title":"getSharingInformation","text":"Applies to: Item, Folder, File
Get Sharing Information.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingInformation } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Get the sharing information for a folder\nconst info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation();\n\n// get sharing informaiton with a request object\nconst info2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation({\n maxPrincipalsToReturn: 10,\n populateInheritedLinks: true,\n});\n\n// get sharing informaiton using select and expand, NOTE expand comes first in the API signature\nconst info3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation({}, [\"permissionsInformation\"], [\"permissionsInformation\",\"anyoneLinkTrackUsers\"]);\n
"},{"location":"sp/sharing/#getobjectsharingsettings","title":"getObjectSharingSettings","text":"Applies to: Item, Folder, File
Gets the sharing settings
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { IObjectSharingSettings } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Gets the sharing object settings\nconst settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings();\n
"},{"location":"sp/sharing/#unshare","title":"unshare","text":"Applies to: Item, Folder, File
Unshares a given resource
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingResult } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare();\n
"},{"location":"sp/sharing/#deletesharinglinkbykind","title":"deleteSharingLinkByKind","text":"Applies to: Item, Folder, File
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);\n
"},{"location":"sp/sharing/#unsharelink","title":"unshareLink","text":"Applies to: Item, Folder, File
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { SharingLinkKind } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit);\n\n// specify the sharing link id if available\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");\n
"},{"location":"sp/site-designs/","title":"@pnp/sp/site-designs","text":"You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information.
"},{"location":"sp/site-designs/#site-designs","title":"Site Designs","text":""},{"location":"sp/site-designs/#create-a-new-site-design","title":"Create a new site design","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// WebTemplate: 64 Team site template, 68 Communication site template\nconst siteDesign = await sp.siteDesigns.createSiteDesign({\n SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"],\n Title: \"SiteDesign001\",\n WebTemplate: \"64\",\n});\n\nconsole.log(siteDesign.Title);\n
"},{"location":"sp/site-designs/#applying-a-site-design-to-a-site","title":"Applying a site design to a site","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Limited to 30 actions in a site script, but runs synchronously\nawait sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\");\n\n// Better use the following method for 300 actions in a site script\nconst task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n
"},{"location":"sp/site-designs/#retrieval","title":"Retrieval","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Retrieving all site designs\nconst allSiteDesigns = await sp.siteDesigns.getSiteDesigns();\nconsole.log(`Total site designs: ${allSiteDesigns.length}`);\n\n// Retrieving a single site design by Id\nconst siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconsole.log(siteDesign.Title);\n
"},{"location":"sp/site-designs/#update-and-delete","title":"Update and delete","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Update\nconst updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" });\n\n// Delete\nawait sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n
"},{"location":"sp/site-designs/#setting-rightspermissions","title":"Setting Rights/Permissions","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Get\nconst rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconsole.log(rights.length > 0 ? rights[0].PrincipalName : \"\");\n\n// Grant\nawait sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]);\n\n// Revoke\nawait sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]);\n\n// Reset all view rights\nconst rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nawait sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName));\n
"},{"location":"sp/site-designs/#get-a-history-of-site-designs-that-have-run-on-a-web","title":"Get a history of site designs that have run on a web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\nconst runs = await sp.web.getSiteDesignRuns();\nconst runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\");\n\n// Get runs specific to a site design\nconst runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconst runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n\n// For more information about the site script actions\nconst runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);\nconst runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);\n\n
"},{"location":"sp/site-groups/","title":"@pnp/sp/site-groups","text":"The site groups module provides methods to manage groups for a sharepoint site.
"},{"location":"sp/site-groups/#isitegroups","title":"ISiteGroups","text":""},{"location":"sp/site-groups/#get-all-site-groups","title":"Get all site groups","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// gets all site groups of the web\nconst groups = await sp.web.siteGroups();\n
"},{"location":"sp/site-groups/#get-the-associated-groups-of-a-web","title":"Get the associated groups of a web","text":"You can get the associated Owner, Member and Visitor groups of a web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Gets the associated visitors group of a web\nconst visitorGroup = await sp.web.associatedVisitorGroup();\n\n// Gets the associated members group of a web\nconst memberGroup = await sp.web.associatedMemberGroup();\n\n// Gets the associated owners group of a web\nconst ownerGroup = await sp.web.associatedOwnerGroup();\n\n
"},{"location":"sp/site-groups/#create-the-default-associated-groups-for-a-web","title":"Create the default associated groups for a web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Breaks permission inheritance and creates the default associated groups for the web\n\n// Login name of the owner\nconst owner1 = \"owner@example.onmicrosoft.com\";\n\n// Specify true, the permissions should be copied from the current parent scope, else false\nconst copyRoleAssignments = false;\n\n// Specify true to make all child securable objects inherit role assignments from the current object\nconst clearSubScopes = true;\n\nawait sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes);\n
"},{"location":"sp/site-groups/#create-a-new-site-group","title":"Create a new site group","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Creates a new site group with the specified title\nawait sp.web.siteGroups.add({\"Title\":\"new group name\"});\n
"},{"location":"sp/site-groups/#isitegroup","title":"ISiteGroup","text":"Scenario Import Statement Selective 2 import \"@pnp/sp/webs\";import \"@pnp/sp/site-groups\"; Selective 3 import \"@pnp/sp/webs\";import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\";"},{"location":"sp/site-groups/#getting-and-updating-the-groups-of-a-sharepoint-web","title":"Getting and updating the groups of a sharepoint web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// get the group using a group id\nconst groupID = 33;\nlet grp = await sp.web.siteGroups.getById(groupID)();\n\n// get the group using the group's name\nconst groupName = \"ClassicTeam Visitors\";\ngrp = await sp.web.siteGroups.getByName(groupName)();\n\n// update a group\nawait sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"});\n\n// delete a group from the site using group id\nawait sp.web.siteGroups.removeById(groupID);\n\n// delete a group from the site using group name\nawait sp.web.siteGroups.removeByLoginName(groupName);\n
"},{"location":"sp/site-groups/#getting-all-users-of-a-group","title":"Getting all users of a group","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// get all users of group\nconst groupID = 7;\nconst users = await sp.web.siteGroups.getById(groupID).users();\n
"},{"location":"sp/site-groups/#updating-the-owner-of-a-site-group","title":"Updating the owner of a site group","text":"Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// Update the owner with a user id\nawait sp.web.siteGroups.getById(7).setUserAsOwner(4);\n
"},{"location":"sp/site-scripts/","title":"@pnp/sp/site-scripts","text":""},{"location":"sp/site-scripts/#create-a-new-site-script","title":"Create a new site script","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\nconst sitescriptContent = {\n \"$schema\": \"schema.json\",\n \"actions\": [\n {\n \"themeName\": \"Theme Name 123\",\n \"verb\": \"applyTheme\",\n },\n ],\n \"bindata\": {},\n \"version\": 1,\n};\n\nconst siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent);\n\nconsole.log(siteScript.Title);\n
"},{"location":"sp/site-scripts/#retrieval","title":"Retrieval","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Retrieving all site scripts\nconst allSiteScripts = await sp.siteScripts.getSiteScripts();\nconsole.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\");\n\n// Retrieving a single site script by Id\nconst siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");\nconsole.log(siteScript.Title);\n
"},{"location":"sp/site-scripts/#update-and-delete","title":"Update and delete","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Update\nconst updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" });\nconsole.log(updatedSiteScript.Title);\n\n// Delete\nawait sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");\n
"},{"location":"sp/site-scripts/#get-site-script-from-a-list","title":"Get site script from a list","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Using the absolute URL of the list\nconst ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\");\n\n// Using the PnPjs web object to fetch the site script from a specific list\nconst ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript();\n
"},{"location":"sp/site-scripts/#get-site-script-from-a-web","title":"Get site script from a web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst extractInfo = {\n IncludeBranding: true,\n IncludeLinksToExportedItems: true,\n IncludeRegionalSettings: true,\n IncludeSiteExternalSharingCapability: true,\n IncludeTheme: true,\n IncludedLists: [\"Lists/MyList\"]\n};\n\nconst ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo);\n\n// Using the PnPjs web object to fetch the site script from a specific web\nconst ss2 = await sp.web.getSiteScript(extractInfo);\n
"},{"location":"sp/site-scripts/#execute-site-script-action","title":"Execute Site Script Action","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\nconst siteScript = \"your site script action...\";\n\nconst ss = await sp.siteScripts.executeSiteScriptAction(siteScript);\n
"},{"location":"sp/site-scripts/#execute-site-script-for-a-specific-web","title":"Execute site script for a specific web","text":"import { spfi } from \"@pnp/sp\";\nimport { SiteScripts } \"@pnp/sp/site-scripts\";\n\nconst siteScript = \"your site script action...\";\n\nconst scriptService = SiteScripts(\"https://absolute/url/to/web\");\n\nconst ss = await scriptService.executeSiteScriptAction(siteScript);\n
"},{"location":"sp/site-users/","title":"@pnp/sp/site-users","text":"The site users module provides methods to manage users for a sharepoint site.
"},{"location":"sp/site-users/#isiteusers","title":"ISiteUsers","text":""},{"location":"sp/site-users/#get-all-site-user","title":"Get all site user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst users = await sp.web.siteUsers();\n
"},{"location":"sp/site-users/#get-current-user","title":"Get Current user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet user = await sp.web.currentUser();\n
"},{"location":"sp/site-users/#get-user-by-id","title":"Get user by id","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst id = 6;\nuser = await sp.web.getUserById(id)();\n
"},{"location":"sp/site-users/#ensure-user","title":"Ensure user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst username = \"usernames@microsoft.com\";\nresult = await sp.web.ensureUser(username);\n
"},{"location":"sp/site-users/#isiteuser","title":"ISiteUser","text":"Scenario Import Statement Selective 2 import \"@pnp/sp/webs\";import \"@pnp/sp/site-users\"; Selective 3 import \"@pnp/sp/webs\";import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\";"},{"location":"sp/site-users/#get-user-groups","title":"Get user Groups","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet groups = await sp.web.currentUser.groups();\n
"},{"location":"sp/site-users/#add-user-to-site-collection","title":"Add user to Site collection","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.ensureUser(\"userLoginname\")\nconst users = await sp.web.siteUsers;\n\nawait users.add(user.data.LoginName);\n
"},{"location":"sp/site-users/#get-user","title":"Get user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\n// get user object by id\nconst user = await sp.web.siteUsers.getById(6)();\n\n//get user object by Email\nconst user = await sp.web.siteUsers.getByEmail(\"user@mail.com\")();\n\n//get user object by LoginName\nconst user = await sp.web.siteUsers.getByLoginName(\"userLoginName\")();\n
"},{"location":"sp/site-users/#update-user","title":"Update user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet userProps = await sp.web.currentUser();\nuserProps.Title = \"New title\";\nawait sp.web.currentUser.update(userProps);\n
"},{"location":"sp/site-users/#remove-user","title":"Remove user","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\n// remove user by id\nawait sp.web.siteUsers.removeById(6);\n\n// remove user by LoginName\nawait sp.web.siteUsers.removeByLoginName(6);\n
"},{"location":"sp/site-users/#isiteuserprops","title":"ISiteUserProps","text":"User properties:
Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Titleinterface ISiteUserProps {\n\n /**\n * Contains Site user email\n *\n */\n Email: string;\n\n /**\n * Contains Site user Id\n *\n */\n Id: number;\n\n /**\n * Site user IsHiddenInUI\n *\n */\n IsHiddenInUI: boolean;\n\n /**\n * Site user IsShareByEmailGuestUser\n *\n */\n IsShareByEmailGuestUser: boolean;\n\n /**\n * Describes if Site user Is Site Admin\n *\n */\n IsSiteAdmin: boolean;\n\n /**\n * Site user LoginName\n *\n */\n LoginName: string;\n\n /**\n * Site user Principal type\n *\n */\n PrincipalType: number | PrincipalType;\n\n /**\n * Site user Title\n *\n */\n Title: string;\n}\n
"},{"location":"sp/sites/","title":"@pnp/sp/site - Site properties","text":"Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.
"},{"location":"sp/sites/#get-context-information-for-the-current-site-collection","title":"Get context information for the current site collection","text":"Using the library, you can get the context information of the current site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { IContextInfo } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst oContext: IContextInfo = await sp.site.getContextInfo();\nconsole.log(oContext.FormDigestValue);\n
"},{"location":"sp/sites/#get-document-libraries-of-a-web","title":"Get document libraries of a web","text":"Using the library, you can get a list of the document libraries present in the a given web.
Note: Works only in SharePoint online
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { IDocumentLibraryInformation } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\");\n\n//we got the array of document library information\ndocLibs.forEach((docLib: IDocumentLibraryInformation) => {\n // do something with each library\n});\n
"},{"location":"sp/sites/#open-web-by-id","title":"Open Web By Id","text":"Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\");\n\n//we got all the data from the web as well\nconsole.log(w.data);\n\n// we can chain\nconst w2 = await w.web.select(\"Title\")();\n
"},{"location":"sp/sites/#get-absolute-web-url-from-page-url","title":"Get absolute web url from page url","text":"Using the library, you can get the absolute web url by providing a page url
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\");\n\nconsole.log(d); //https://tenant.sharepoint.com/sites/test\n
"},{"location":"sp/sites/#access-the-root-web","title":"Access the root web","text":"There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// use for rootweb information access\nconst rootwebData = await sp.site.rootWeb();\n\n// use for chaining\nconst rootweb = await sp.site.getRootWeb();\nconst listData = await rootWeb.lists.getByTitle(\"MyList\")();\n
"},{"location":"sp/sites/#create-a-modern-communication-site","title":"Create a modern communication site","text":"Note: Works only in SharePoint online
Creates a modern communication site.
Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g.https://yourtenant.sharepoint.com/sites/mysitecollection
) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com \nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst result = await sp.site.createCommunicationSite(\n \"Title\",\n 1033,\n true,\n \"https://tenant.sharepoint.com/sites/commSite\",\n \"Description\",\n \"HBI\",\n \"f6cc5403-0d63-442e-96c0-285923709ffc\",\n \"a00ec589-ea9f-4dba-a34e-67e78d41e509\",\n \"user@TENANT.onmicrosoft.com\");\n\n
"},{"location":"sp/sites/#create-from-props","title":"Create from Props","text":"You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps
method.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// in this case you supply a single struct deinfing the creation props\nconst result = await sp.site.createCommunicationSiteFromProps({\n Owner: \"patrick@three18studios.com\",\n Title: \"A Test Site\",\n Url: \"https://{tenant}.sharepoint.com/sites/commsite2\",\n WebTemplate: \"STS#3\",\n});\n
"},{"location":"sp/sites/#create-a-modern-team-site","title":"Create a modern team site","text":"Note: Works only in SharePoint online. It wont work with App only tokens
Creates a modern team site backed by O365 group.
Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst result = await sp.site.createModernTeamSite(\n \"displayName\",\n \"alias\",\n true,\n 1033,\n \"description\",\n \"HBI\",\n [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"],\n \"a00ec589-ea9f-4dba-a34e-67e78d41e509\",\n \"f6cc5403-0d63-442e-96c0-285923709ffc\"\n );\n\nconsole.log(d);\n
"},{"location":"sp/sites/#create-from-props_1","title":"Create from Props","text":"You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps
method.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// in this case you supply a single struct deinfing the creation props\nconst result = await sp.site.createModernTeamSiteFromProps({\n alias: \"JenniferGarner\",\n displayName: \"A Test Site\",\n owners: [\"patrick@three18studios.com\"],\n});\n
"},{"location":"sp/sites/#delete-a-site-collection","title":"Delete a site collection","text":"Using the library, you can delete a specific site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { Site } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// Delete the current site\nawait sp.site.delete();\n\n// Specify which site to delete\nconst siteUrl = \"https://tenant.sharepoint.com/sites/subsite\";\nconst site2 = Site(siteUrl);\nawait site2.delete();\n
"},{"location":"sp/sites/#check-if-a-site-collection-exists","title":"Check if a Site Collection Exists","text":"Using the library, you can check if a specific site collection exist or not on your tenant
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// Specify which site to verify\nconst siteUrl = \"https://tenant.sharepoint.com/sites/subsite\";\nconst exists = await sp.site.exists(siteUrl);\nconsole.log(exists);\n
"},{"location":"sp/sites/#set-the-site-logo","title":"Set the site logo","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport {ISiteLogoProperties, SiteLogoAspect, SiteLogoType} from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n//set the web's site logo\nconst logoProperties: ISiteLogoProperties = {\n relativeLogoUrl: \"/sites/mySite/SiteAssets/site_logo.png\", \n aspect: SiteLogoAspect.Rectangular, \n type: SiteLogoType.WebLogo\n};\nawait sp.site.setSiteLogo(logoProperties);\n
"},{"location":"sp/social/","title":"@pnp/sp/ - social","text":"The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions.
"},{"location":"sp/social/#getfollowedsitesuri","title":"getFollowedSitesUri","text":"Gets a URI to a site that lists the current user's followed sites.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst uri = await sp.social.getFollowedSitesUri();\n
"},{"location":"sp/social/#getfolloweddocumentsuri","title":"getFollowedDocumentsUri","text":"Gets a URI to a site that lists the current user's followed documents.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst uri = await sp.social.getFollowedDocumentsUri();\n
"},{"location":"sp/social/#follow","title":"follow","text":"Makes the current user start following a user, document, site, or tag
import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// follow a site\nconst r1 = await sp.social.follow({\n ActorType: SocialActorType.Site,\n ContentUri: \"htts://tenant.sharepoint.com/sites/site\",\n});\n\n// follow a person\nconst r2 = await sp.social.follow({\n AccountName: \"i:0#.f|membership|person@tenant.com\",\n ActorType: SocialActorType.User,\n});\n\n// follow a doc\nconst r3 = await sp.social.follow({\n ActorType: SocialActorType.Document,\n ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\",\n});\n\n// follow a tag\n// You need the tag GUID to start following a tag.\n// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.\n// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.\n// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid\nconst r4 = await sp.social.follow({\n ActorType: SocialActorType.Tag,\n TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\",\n});\n
"},{"location":"sp/social/#isfollowed","title":"isFollowed","text":"Indicates whether the current user is following a specified user, document, site, or tag
import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// pass the same social actor struct as shown in follow example for each type\nconst r = await sp.social.isFollowed({\n AccountName: \"i:0#.f|membership|person@tenant.com\",\n ActorType: SocialActorType.User,\n});\n
"},{"location":"sp/social/#stopfollowing","title":"stopFollowing","text":"Makes the current user stop following a user, document, site, or tag
import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// pass the same social actor struct as shown in follow example for each type\nconst r = await sp.social.stopFollowing({\n AccountName: \"i:0#.f|membership|person@tenant.com\",\n ActorType: SocialActorType.User,\n});\n
"},{"location":"sp/social/#my","title":"my","text":""},{"location":"sp/social/#get","title":"get","text":"Gets this user's social information
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst r = await sp.social.my();\n
"},{"location":"sp/social/#followed","title":"followed","text":"Gets users, documents, sites, and tags that the current user is following based on the supplied flags.
import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get all the followed documents\nconst r1 = await sp.social.my.followed(SocialActorTypes.Document);\n\n// get all the followed documents and sites\nconst r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);\n\n// get all the followed sites updated in the last 24 hours\nconst r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);\n
"},{"location":"sp/social/#followedcount","title":"followedCount","text":"Works as followed but returns on the count of actors specified by the query
import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.followedCount(SocialActorTypes.Document);\n
"},{"location":"sp/social/#followers","title":"followers","text":"Gets the users who are following the current user.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.followers();\n
"},{"location":"sp/social/#suggestions","title":"suggestions","text":"Gets users who the current user might want to follow.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.suggestions();\n
"},{"location":"sp/sp-utilities-utility/","title":"@pnp/sp/utilities","text":"Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.
"},{"location":"sp/sp-utilities-utility/#sendemail","title":"sendEmail","text":"This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).
"},{"location":"sp/sp-utilities-utility/#emailproperties","title":"EmailProperties","text":"export interface TypedHash<T> {\n [key: string]: T;\n}\n\nexport interface EmailProperties {\n\n To: string[];\n CC?: string[];\n BCC?: string[];\n Subject: string;\n Body: string;\n AdditionalHeaders?: TypedHash<string>;\n From?: string;\n}\n
"},{"location":"sp/sp-utilities-utility/#usage","title":"Usage","text":"You must define the To, Subject, and Body values - the remaining are optional.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\nimport { IEmailProperties } from \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nconst emailProps: IEmailProperties = {\n To: [\"user@site.com\"],\n CC: [\"user2@site.com\", \"user3@site.com\"],\n BCC: [\"user4@site.com\", \"user5@site.com\"],\n Subject: \"This email is about...\",\n Body: \"Here is the body. <b>It supports html</b>\",\n AdditionalHeaders: {\n \"content-type\": \"text/html\"\n }\n};\n\nawait sp.utility.sendEmail(emailProps);\nconsole.log(\"Email Sent!\");\n
"},{"location":"sp/sp-utilities-utility/#getcurrentuseremailaddresses","title":"getCurrentUserEmailAddresses","text":"This method returns the current user's email addresses known to SharePoint.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet addressString: string = await sp.utility.getCurrentUserEmailAddresses();\n\n// and use it with sendEmail\nawait sp.utility.sendEmail({\n To: [addressString],\n Subject: \"This email is about...\",\n Body: \"Here is the body. <b>It supports html</b>\",\n AdditionalHeaders: {\n \"content-type\": \"text/html\"\n },\n});\n
"},{"location":"sp/sp-utilities-utility/#resolveprincipal","title":"resolvePrincipal","text":"Gets information about a principal that matches the specified Search criteria
import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true);\n\nconsole.log(principal);\n
"},{"location":"sp/sp-utilities-utility/#searchprincipals","title":"searchPrincipals","text":"Gets information about the principals that match the specified Search criteria.
import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10);\n\nconsole.log(principals);\n
"},{"location":"sp/sp-utilities-utility/#createemailbodyforinvitation","title":"createEmailBodyForInvitation","text":"Gets the external (outside the firewall) URL to a document or resource in a site.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\");\nconsole.log(url);\n
"},{"location":"sp/sp-utilities-utility/#expandgroupstoprincipals","title":"expandGroupsToPrincipals","text":"Resolves the principals contained within the supplied groups
import { spfi, SPFx, IPrincipalInfo } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]);\nconsole.log(principals);\n\n// optionally supply a max results count. Default is 30.\nlet principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10);\nconsole.log(principals);\n
"},{"location":"sp/sp-utilities-utility/#createwikipage","title":"createWikiPage","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\nimport { ICreateWikiPageResult } from \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({\n ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\",\n WikiHtmlContent: \"This is my <b>page</b> content. It supports rich html.\",\n});\n\n// newPage contains the raw data returned by the service\nconsole.log(newPage.data);\n\n// newPage contains a File instance you can use to further update the new page\nlet file = await newPage.file();\nconsole.log(file);\n
"},{"location":"sp/subscriptions/","title":"@pnp/sp/subscriptions","text":"Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.
"},{"location":"sp/subscriptions/#isubscriptions","title":"ISubscriptions","text":""},{"location":"sp/subscriptions/#add-a-webhook","title":"Add a webhook","text":"Using this library, you can add a webhook to a specified list within the SharePoint site.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nimport { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\";\nimport \"@pnp/sp/subscriptions/list\";\n\nconst sp = spfi(...);\n\n// This is the URL which will be called by SharePoint when there is a change in the list\nconst notificationUrl = \"<notification-url>\";\n\n// Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date.\nconst expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString();\n\n// Adds a webhook to the Documents library\nvar res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate);\n
"},{"location":"sp/subscriptions/#get-all-webhooks-added-to-a-list","title":"Get all webhooks added to a list","text":"Read all the webhooks' details which are associated to the list
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions\";\n\nconst sp = spfi(...);\n\nconst res = await sp.web.lists.getByTitle(\"Documents\").subscriptions();\n
"},{"location":"sp/subscriptions/#isubscription","title":"ISubscription","text":"This interface provides the methods for managing a particular webhook.
Scenario Import Statement Selective import \"@pnp/sp/webs\";import \"@pnp/sp/lists\";import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\";import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\";"},{"location":"sp/subscriptions/#managing-a-webhook","title":"Managing a webhook","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions\";\n\nconst sp = spfi(...);\n\n// Get details of a webhook based on its ID\nconst webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\";\nconst webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)();\n\n// Update a webhook\nconst newDate = dateAdd(new Date(), \"day\" , 150).toISOString();\nconst updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate);\n\n// Delete a webhook\nawait sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();\n
"},{"location":"sp/taxonomy/","title":"@pnp/sp/taxonomy","text":"Provides access to the v2.1 api term store
"},{"location":"sp/taxonomy/#docs-updated-with-v209-release-as-the-underlying-api-changed","title":"Docs updated with v2.0.9 release as the underlying API changed","text":"NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabilize this note will be removed.
"},{"location":"sp/taxonomy/#term-store","title":"Term Store","text":"
Access term store data from the root sp object as shown below.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermStoreInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term store data\nconst info: ITermStoreInfo = await sp.termStore();\n
"},{"location":"sp/taxonomy/#searchterm","title":"searchTerm","text":"Added in 3.3.0
Search for terms starting with provided label under entire termStore or a termSet or a parent term.
The following properties are valid for the supplied query: label: string
, setId?: string
, parentTermId?: string
, languageTag?: string
, stringMatchOption?: \"ExactMatch\" | \"StartsWith\"
.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// minimally requires the label\nconst results1 = await sp.termStore.searchTerm({\n label: \"test\",\n});\n\n// other properties can be included as needed\nconst results2 = await sp.termStore.searchTerm({\n label: \"test\",\n setId: \"{guid}\",\n});\n\n// other properties can be included as needed\nconst results3 = await sp.termStore.searchTerm({\n label: \"test\",\n setId: \"{guid}\",\n stringMatchOption: \"ExactMatch\",\n});\n
"},{"location":"sp/taxonomy/#update","title":"update","text":"Added in 3.10.0
Allows you to update language setttings for the store
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\nawait sp.termStore.update({\n defaultLanguageTag: \"en-US\",\n languageTags: [\"en-US\", \"en-IE\", \"de-DE\"],\n});\n
"},{"location":"sp/taxonomy/#term-groups","title":"Term Groups","text":"Access term group information
"},{"location":"sp/taxonomy/#list","title":"List","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term groups\nconst info: ITermGroupInfo[] = await sp.termStore.groups();\n
"},{"location":"sp/taxonomy/#get-by-id","title":"Get By Id","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term groups data\nconst info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add","title":"Add","text":"Added in 3.10.0
Allows you to add a term group to a store.
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\nconst groupInfo: ITermGroupInfo = await sp.termStore.groups.add({\n displayName: \"Accounting\",\n description: \"Term Group for Accounting\",\n name: \"accounting1\",\n scope: \"global\",\n});\n
"},{"location":"sp/taxonomy/#term-group","title":"Term Group","text":""},{"location":"sp/taxonomy/#delete","title":"Delete","text":"Added in 3.10.0
Allows you to add a term group to a store.
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nawait sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#term-sets","title":"Term Sets","text":"Access term set information
"},{"location":"sp/taxonomy/#list_1","title":"List","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermSetInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get set info\nconst info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets();\n
"},{"location":"sp/taxonomy/#get-by-id_1","title":"Get By Id","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermSetInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term set data by group id then by term set id\nconst info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n\n// get term set data by term set id\nconst infoByTermSetId: ITermSetInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add_1","title":"Add","text":"Added in 3.10.0
Allows you to add a term set.
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\n// when adding a set directly from the root .sets property, you must include the \"parentGroup\" property\nconst setInfo = await sp.termStore.sets.add({\n parentGroup: {\n id: \"338666a8-1111-2222-3333-f72471314e72\"\n },\n contact: \"steve\",\n description: \"description\",\n isAvailableForTagging: true,\n isOpen: true,\n localizedNames: [{\n name: \"MySet\",\n languageTag: \"en-US\",\n }],\n properties: [{\n key: \"key1\",\n value: \"value1\",\n }]\n});\n\n// when adding a termset through a group's sets property you do not specify the \"parentGroup\" property\nconst setInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.add({\n contact: \"steve\",\n description: \"description\",\n isAvailableForTagging: true,\n isOpen: true,\n localizedNames: [{\n name: \"MySet2\",\n languageTag: \"en-US\",\n }],\n properties: [{\n key: \"key1\",\n value: \"value1\",\n }]\n});\n
"},{"location":"sp/taxonomy/#getallchildrenasorderedtree","title":"getAllChildrenAsOrderedTree","text":"This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change.
Starting with version 2.6.0 you can now include an optional param to retrieve all of the term's properties and localProperties in the tree. Default is false.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\nimport { dateAdd, PnPClientStorage } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\n// here we get all the children of a given set\nconst childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree();\n\n// here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available\nconst store = new PnPClientStorage();\n\n// our tree likely doesn't change much in 30 minutes for most applications\n// adjust to be longer or shorter as needed\nconst cachedTree = await store.local.getOrPut(\"myKey\", () => {\n return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree();\n}, dateAdd(new Date(), \"minute\", 30));\n\n// you can also get all the properties and localProperties\nconst set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\");\nconst childTree = await set.getAllChildrenAsOrderedTree({ retrieveProperties: true });\n
"},{"location":"sp/taxonomy/#termset","title":"TermSet","text":"Access term set information
"},{"location":"sp/taxonomy/#update_1","title":"Update","text":"Added in 3.10.0
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termSetInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n properties: [{\n key: \"MyKey2\",\n value: \"MyValue2\",\n }],\n});\n\nconst termSetInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n properties: [{\n key: \"MyKey3\",\n value: \"MyValue3\",\n }],\n});\n
"},{"location":"sp/taxonomy/#delete_1","title":"Delete","text":"Added in 3.10.0
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nawait sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n\nawait sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#terms","title":"Terms","text":"Access term set information
"},{"location":"sp/taxonomy/#list_2","title":"List","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// list all the terms that are direct children of this set\nconst infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children();\n
"},{"location":"sp/taxonomy/#list-terms","title":"List (terms)","text":"You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// list all the terms available in this term set by group id then by term set id\nconst infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();\n\n// list all the terms available in this term set by term set id\nconst infosByTermSetId: ITermInfo[] = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();\n
"},{"location":"sp/taxonomy/#get-by-id_2","title":"Get By Id","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term set data\nconst info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add_2","title":"Add","text":"Added in 3.10.0
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst newTermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children.add({\n labels: [\n {\n isDefault: true,\n languageTag: \"en-us\",\n name: \"New Term\",\n }\n ]\n});\n\nconst newTermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children.add({\n labels: [\n {\n isDefault: true,\n languageTag: \"en-us\",\n name: \"New Term 2\",\n }\n ]\n});\n
"},{"location":"sp/taxonomy/#term","title":"Term","text":""},{"location":"sp/taxonomy/#update_2","title":"Update","text":"Note that when updating a Term if you update the properties
it replaces the collection, so a merge of existing + new needs to be handled by your application.
Added in 3.10.0
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n properties: [{\n key: \"something\",\n value: \"a value 2\",\n }],\n});\n\nconst termInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n properties: [{\n key: \"something\",\n value: \"a value\",\n }],\n});\n
"},{"location":"sp/taxonomy/#delete_2","title":"Delete","text":"Added in 3.10.0
import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n\nconst termInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#get-term-parent","title":"Get Term Parent","text":"Behavior Change in 2.1.0
The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get a ref to the set\nconst set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\");\n\n// get a term's information and expand parent to get the parent info as well\nconst w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")();\n\n// get a ref to the parent term\nconst parent = set.getTermById(w.parent.id);\n\n// make a request for the parent term's info - this data currently match the results in the expand call above, but this\n// is to demonstrate how to gain a ref to the parent and select its data\nconst parentInfo = await parent.select(\"Id\", \"Descriptions\")();\n
"},{"location":"sp/tenant-properties/","title":"@pnp/sp/web - tenant properties","text":"You can set, read, and remove tenant properties using the methods shown below:
"},{"location":"sp/tenant-properties/#setstorageentity","title":"setStorageEntity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst w = await sp.getTenantAppCatalogWeb();\n\n// specify required key and value\nawait w.setStorageEntity(\"Test1\", \"Value 1\");\n\n// specify optional description and comments\nawait w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\");\n
"},{"location":"sp/tenant-properties/#getstorageentity","title":"getStorageEntity","text":"This method can be used from any web to retrieve values previously set.
import { spfi, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\nimport { IStorageEntity } from \"@pnp/sp/webs\"; \n\nconst sp = spfi(...);\n\nconst prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\");\n\nconsole.log(prop.Value);\n
"},{"location":"sp/tenant-properties/#removestorageentity","title":"removeStorageEntity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst w = await sp.getTenantAppCatalogWeb();\n\nawait w.removeStorageEntity(\"Test1\");\n
"},{"location":"sp/user-custom-actions/","title":"@pnp/sp/user-custom-actions","text":"Represents a custom action associated with a SharePoint list, web or site collection.
"},{"location":"sp/user-custom-actions/#iusercustomactions","title":"IUserCustomActions","text":""},{"location":"sp/user-custom-actions/#get-a-collection-of-user-custom-actions-from-a-web","title":"Get a collection of User Custom Actions from a web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst userCustomActions = sp.web.userCustomActions();\n
"},{"location":"sp/user-custom-actions/#add-a-new-user-custom-action","title":"Add a new User Custom Action","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\nimport { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions';\n\nconst sp = spfi(...);\n\nconst newValues: TypedHash<string> = {\n \"Title\": \"New Title\",\n \"Description\": \"New Description\",\n \"Location\": \"ScriptLink\",\n \"ScriptSrc\": \"https://...\"\n};\n\nconst response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);\n
"},{"location":"sp/user-custom-actions/#get-a-user-custom-action-by-id","title":"Get a User Custom Action by ID","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\");\n\nconst ucaData = await uca();\n
"},{"location":"sp/user-custom-actions/#clear-the-user-custom-action-collection","title":"Clear the User Custom Action collection","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\n// Site collection level\nawait sp.site.userCustomActions.clear();\n\n// Site (web) level\nawait sp.web.userCustomActions.clear();\n\n// List level\nawait sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear();\n
"},{"location":"sp/user-custom-actions/#iusercustomaction","title":"IUserCustomAction","text":""},{"location":"sp/user-custom-actions/#update-an-existing-user-custom-action","title":"Update an existing User Custom Action","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\nimport { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions';\n\nconst sp = spfi(...);\n\nconst uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\");\n\nconst newValues: TypedHash<string> = {\n \"Title\": \"New Title\",\n \"Description\": \"New Description\",\n \"ScriptSrc\": \"https://...\"\n};\n\nconst response: IUserCustomActionUpdateResult = uca.update(newValues);\n
"},{"location":"sp/views/","title":"@pnp/sp/views","text":"Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.
"},{"location":"sp/views/#iviews","title":"IViews","text":""},{"location":"sp/views/#get-views-in-a-list","title":"Get views in a list","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get all the views and their properties\nconst views1 = await list.views();\n\n// you can use odata select operations to get just a set a fields\nconst views2 = await list.views.select(\"Id\", \"Title\")();\n\n// get the top three views\nconst views3 = await list.views.top(3)();\n
"},{"location":"sp/views/#add-a-view","title":"Add a View","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// create a new view with default fields and properties\nconst result = await list.views.add(\"My New View\");\n\n// create a new view with specific properties\nconst result2 = await list.views.add(\"My New View 2\", false, {\n RowLimit: 10,\n ViewQuery: \"<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>\",\n});\n\n// manipulate the view's fields\nawait result2.view.fields.removeAll();\n\nawait Promise.all([\n result2.view.fields.add(\"Title\"),\n result2.view.fields.add(\"Modified\"),\n]);\n
"},{"location":"sp/views/#iview","title":"IView","text":""},{"location":"sp/views/#get-a-views-information","title":"Get a View's Information","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\")();\n\nconst result2 = await list.views.getByTitle(\"My View\")();\n\nconst result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")();\n\nconst result4 = await list.defaultView();\n\nconst result5 = await list.getView(\"{GUID view id}\")();\n
"},{"location":"sp/views/#fields","title":"fields","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\").fields();\n
"},{"location":"sp/views/#update","title":"update","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\").update({\n RowLimit: 20,\n});\n
"},{"location":"sp/views/#renderashtml","title":"renderAsHtml","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml();\n
"},{"location":"sp/views/#setviewxml","title":"setViewXml","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst viewXml = \"...\";\n\nawait sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml);\n
"},{"location":"sp/views/#delete","title":"delete","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst viewXml = \"...\";\n\nawait sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete();\n
"},{"location":"sp/views/#viewfields","title":"ViewFields","text":""},{"location":"sp/views/#getschemaxml","title":"getSchemaXml","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml();\n
"},{"location":"sp/views/#add","title":"add","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\");\n
"},{"location":"sp/views/#move","title":"move","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0);\n
"},{"location":"sp/views/#remove","title":"remove","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\");\n
"},{"location":"sp/views/#removeall","title":"removeAll","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();\n
"},{"location":"sp/webs/","title":"@pnp/sp/webs","text":"Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.
"},{"location":"sp/webs/#iwebs","title":"IWebs","text":""},{"location":"sp/webs/#add-web","title":"Add Web","text":"Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions.
import { spfi } from \"@pnp/sp\";\nimport { IWebAddResult } from \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.webs.add(\"title\", \"subweb1\");\n\n// show the response from the server when adding the web\nconsole.log(result.data);\n\n// we can immediately operate on the new web\nresult.web.select(\"Title\")().then((w: IWebInfo) => {\n\n // show our title\n console.log(w.Title);\n});\n
import { spfi } from \"@pnp/sp\";\nimport { IWebAddResult } from \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// create a German language wiki site with title, url, description, which does not inherit permissions\nsp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => {\n\n // ...\n});\n
"},{"location":"sp/webs/#iweb","title":"IWeb","text":""},{"location":"sp/webs/#access-a-web","title":"Access a Web","text":"There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed.
Access the web from the imported \"spfi\" object using selective import:
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web();\n
Access the web from the imported \"spfi\" object using the 'all' preset
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/presets/all\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web();\n
Access the web using any SPQueryable as a base
In this scenario you might be deep in your code without access to the original start of the fluid chain (i.e. the instance produced from spfi). You can pass any queryable to the Web or Site factory and get back a valid IWeb instance. In this case all of the observers registered to the supplied instance will be referenced by the IWeb, and the url will be rebased to ensure a valid path.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// we have a ref to the IItems instance\nconst items = await sp.web.lists.getByTitle(\"Generic\").items;\n\n// we create a new IWeb instance using the items as a base\nconst web = Web(items);\n\n// gets the web info\nconst webInfo = await web();\n\n// get a reference to a different list\nconst list = web.lists.getByTitle(\"DifferentList\");\n
Access a web using the Web factory method
There are several ways to use the Web
factory directly and have some special considerations unique to creating IWeb
instances from Web
. The easiest is to supply the absolute URL of the web you wish to target, as seen in the first example below. When supplying a path parameter to Web
you need to include the _api/web
part in the appropriate location as the library can't from strings determine how to append the path. Example 2 below shows a wrong usage of the Web factory as we cannot determine how the path part should be appended. Examples 3 and 4 show how to include the _api/web
part for both subwebs or queries within the given web.
When in doubt, supply the absolute url to the web as the first parameter as shown in example 1 below
import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\n// creates a web:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/_api/web\"\n// - has no registered observers\nconst web1 = Web(\"https://tenant.sharepoint.com/sites/myweb\");\n\n// creates a web that will not work due to missing the _api/web portion\n// this is because we don't know that the extra path should come before/after the _api/web portion\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - has no registered observers\nconst web2-WRONG = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"some sub path\");\n\n// creates a web:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/some sub web/_api/web\"\n// including the _api/web ensures the path you are providing is correct and can be parsed by the library\n// - has no registered observers\nconst web3 = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"some sub web/_api/web\");\n\n// creates a web that actually points to the lists endpoint:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/_api/web/lists\"\n// including the _api/web ensures the path you are providing is correct and can be parsed by the library\n// - has no registered observers\nconst web4 = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"_api/web/lists\");\n
The above examples show you how to use the constructor to create the base url for the Web
although none of them are usable as is until you add observers. You can do so by either adding them explicitly with a using...
import { spfi, SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\nconst web1 = Web(\"https://tenant.sharepoint.com/sites/myweb\").using(SPFx(this.context));\n
or by copying them from another SPQueryable instance...
import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n//sp.web is of type SPQueryable; using tuple pattern pass SPQueryable and the web's url\nconst web = Web([sp.web, \"https://tenant.sharepoint.com/sites/otherweb\"]);\n
"},{"location":"sp/webs/#webs","title":"webs","text":"Access the child webs collection of this web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst webs = await web.webs();\n
"},{"location":"sp/webs/#get-a-webs-properties","title":"Get A Web's properties","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// basic get of the webs properties\nconst props = await sp.web();\n\n// use odata operators to get specific fields\nconst props2 = await sp.web.select(\"Title\")();\n\n// type the result to match what you are requesting\nconst props3 = await sp.web.select(\"Title\")<{ Title: string }>();\n
"},{"location":"sp/webs/#getparentweb","title":"getParentWeb","text":"Get the data and IWeb instance for the parent web for the given web instance
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = web.getParentWeb();\n
"},{"location":"sp/webs/#getsubwebsfilteredforcurrentuser","title":"getSubwebsFilteredForCurrentUser","text":"Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst subWebs = web.getSubwebsFilteredForCurrentUser()();\n\n// apply odata operations to the collection\nconst subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)();\n
Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.
"},{"location":"sp/webs/#allproperties","title":"allProperties","text":"Allows access to the web's all properties collection. This is readonly in REST.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst props = await web.allProperties();\n\n// select certain props\nconst props2 = await web.allProperties.select(\"prop1\", \"prop2\")();\n
"},{"location":"sp/webs/#webinfos","title":"webinfos","text":"Gets a collection of WebInfos for this web's subwebs
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n\nconst infos = await web.webinfos();\n\n// or select certain fields\nconst infos2 = await web.webinfos.select(\"Title\", \"Description\")();\n\n// or filter\nconst infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")();\n\n// or both\nconst infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")();\n\n// get the top 4 ordered by Title\nconst infos5 = await web.webinfos.top(4).orderBy(\"Title\")();\n
Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.
"},{"location":"sp/webs/#update","title":"update","text":"Updates this web instance with the supplied properties
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n// update the web's title and description\nconst result = await web.update({\n Title: \"New Title\",\n Description: \"My new description\",\n});\n\n// a project implementation could wrap the update to provide type information for your expected fields:\n\ninterface IWebUpdateProps {\n Title: string;\n Description: string;\n}\n\nfunction updateWeb(props: IWebUpdateProps): Promise<void> {\n web.update(props);\n}\n
"},{"location":"sp/webs/#delete-a-web","title":"Delete a Web","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n\nawait web.delete();\n
"},{"location":"sp/webs/#applytheme","title":"applyTheme","text":"Applies the theme specified by the contents of each of the files specified in the arguments to the site
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { combine } from \"@pnp/core\";\n\nconst sp = spfi(\"https://{tenant}.sharepoint.com/sites/dev/subweb\").using(SPFx(this.context));\nconst web = sp.web;\n\n// the urls to the color and font need to both be from the catalog at the root\n// these urls can be constants or calculated from existing urls\nconst colorUrl = combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\");\n// this gives us the same result\nconst fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\";\n\n// apply the font and color, no background image, and don't share this theme\nawait web.applyTheme(colorUrl, fontUrl, \"\", false);\n
"},{"location":"sp/webs/#applywebtemplate-availablewebtemplates","title":"applyWebTemplate & availableWebTemplates","text":"Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\nconst templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name));\n\n// apply the wiki template\nconst template = templates.length > 0 ? templates[0].Name : \"STS#0\";\n\nawait web.applyWebTemplate(template);\n
"},{"location":"sp/webs/#getchanges","title":"getChanges","text":"Returns the collection of changes from the change log that have occurred within the web, based on the specified query.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n// get the web changes including add, update, and delete\nconst changes = await web.getChanges({\n Add: true,\n ChangeTokenEnd: undefined,\n ChangeTokenStart: undefined,\n DeleteObject: true,\n Update: true,\n Web: true,\n });\n
"},{"location":"sp/webs/#maptoicon","title":"mapToIcon","text":"Returns the name of the image file for the icon that is used to represent the specified file
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { combine } from \"@pnp/core\";\n\nconst iconFileName = await web.mapToIcon(\"test.docx\");\n// iconPath === \"icdocx.png\"\n// which you can need to map to a real url\nconst iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`;\n\n// OR dynamically\nconst sp = spfi(...);\nconst webData = await sp.web.select(\"Url\")();\nconst iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName);\n\n// OR within SPFx using the context\nconst iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName);\n\n// You can also set size\n// 16x16 pixels = 0, 32x32 pixels = 1\nconst icon32FileName = await web.mapToIcon(\"test.docx\", 1);\n
"},{"location":"sp/webs/#storage-entities","title":"storage entities","text":"import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/appcatalog\";\nimport { IStorageEntity } from \"@pnp/sp/webs\";\n\n// needs to be unique, GUIDs are great\nconst key = \"my-storage-key\";\n\nconst sp = spfi(...);\n\n// read an existing entity\nconst entity: IStorageEntity = await sp.web.getStorageEntity(key);\n\n// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site\n// you can get the tenant app catalog using the getTenantAppCatalogWeb\nconst tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();\n\ntenantAppCatalogWeb.setStorageEntity(key, \"new value\");\n\n// set other properties\ntenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\");\n\nconst entity2: IStorageEntity = await sp.web.getStorageEntity(key);\n/*\nentity2 === {\n Value: \"another value\",\n Comment: \"comments\";\n Description: \"description\",\n};\n*/\n\n// you can also remove a storage entity\nawait tenantAppCatalogWeb.removeStorageEntity(key);\n
"},{"location":"sp/webs/#getappcatalog","title":"getAppCatalog","text":"Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IApp } from \"@pnp/sp/appcatalog\";\n\nconst sp = spfi(...);\n\nconst appWeb = sp.web.appcatalog;\nconst app: IApp = appWeb.getAppById(\"{your app id}\");\n// appWeb url === web url\n
"},{"location":"sp/webs/#client-side-pages","title":"client-side-pages","text":"You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\n\nconst sp = spfi(...);\n\n// simplest add a page example\nconst page = await sp.web.addClientsidePage(\"mypage1\");\n\n// simplest load a page example\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n
"},{"location":"sp/webs/#contenttypes","title":"contentTypes","text":"Allows access to the collection of content types in this web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/content-types/web\";\n\nconst sp = spfi(...);\n\nconst cts = await sp.web.contentTypes();\n\n// you can also select fields and use other odata operators\nconst cts2 = await sp.web.contentTypes.select(\"Name\")();\n
"},{"location":"sp/webs/#features","title":"features","text":"Allows access to the collection of content types in this web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/features/web\";\n\nconst sp = spfi(...);\n\nconst features = await sp.web.features();\n
"},{"location":"sp/webs/#fields","title":"fields","text":"Allows access to the collection of fields in this web.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/fields/web\";\n\nconst sp = spfi(...);\nconst fields = await sp.web.fields();\n
"},{"location":"sp/webs/#getfilebyserverrelativepath","title":"getFileByServerRelativePath","text":"Gets a file by server relative url if your file name contains # and % characters
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\nimport { IFile } from \"@pnp/sp/files/types\";\n\nconst sp = spfi(...);\nconst file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\");\n
"},{"location":"sp/webs/#folders","title":"folders","text":"Gets the collection of folders in this web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\n\nconst sp = spfi(...);\n\nconst folders = await sp.web.folders();\n\n// you can also filter and select as with any collection\nconst folders2 = await sp.web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")();\n\n// or get the most recently modified folder\nconst folders2 = await sp.web.folders.orderBy(\"TimeLastModified\").top(1)();\n
"},{"location":"sp/webs/#rootfolder","title":"rootFolder","text":"Gets the root folder of the web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\n\nconst sp = spfi(...);\n\nconst folder = await sp.web.rootFolder();\n
"},{"location":"sp/webs/#getfolderbyserverrelativepath","title":"getFolderByServerRelativePath","text":"Gets a folder by server relative url if your folder name contains # and % characters
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\");\n
"},{"location":"sp/webs/#hubsitedata","title":"hubSiteData","text":"Gets hub site data for the current web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n// get the data and force a refresh\nconst data = await sp.web.hubSiteData(true);\n
"},{"location":"sp/webs/#synchubsitetheme","title":"syncHubSiteTheme","text":"Applies theme updates from the parent hub site collection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\nawait sp.web.syncHubSiteTheme();\n
"},{"location":"sp/webs/#lists","title":"lists","text":"Gets the collection of all lists that are contained in the Web site
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { ILists } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst lists: ILists = sp.web.lists;\n\n// you can always order the lists and select properties\nconst data = await lists.select(\"Title\").orderBy(\"Title\")();\n\n// and use other odata operators as well\nconst data2 = await sp.web.lists.top(3).orderBy(\"LastItemModifiedDate\")();\n
"},{"location":"sp/webs/#siteuserinfolist","title":"siteUserInfoList","text":"Gets the UserInfo list of the site collection that contains the Web site
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.siteUserInfoList;\n\nconst data = await list();\n\n// or chain off that list to get additional details\nconst items = await list.items.top(2)();\n
"},{"location":"sp/webs/#defaultdocumentlibrary","title":"defaultDocumentLibrary","text":"Get a reference to the default document library of a web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.defaultDocumentLibrary;\n
"},{"location":"sp/webs/#customlisttemplates","title":"customListTemplates","text":"Gets the collection of all list definitions and list templates that are available
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst templates = await sp.web.customListTemplates();\n\n// odata operators chain off the collection as expected\nconst templates2 = await sp.web.customListTemplates.select(\"Title\")();\n
"},{"location":"sp/webs/#getlist","title":"getList","text":"Gets a list by server relative url (list's root folder)
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.getList(\"/sites/dev/lists/test\");\n\nconst listData = await list();\n
"},{"location":"sp/webs/#getcatalog","title":"getCatalog","text":"Returns the list gallery on the site
Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst templateCatalog: IList = await sp.web.getCatalog(111);\n\nconst themeCatalog: IList = await sp.web.getCatalog(123);\n
"},{"location":"sp/webs/#navigation","title":"navigation","text":"Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation/web\";\nimport { INavigation } from \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\nconst nav: INavigation = sp.web.navigation;\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation/web\";\nimport { IRegionalSettings } from \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\nconst settings: IRegionalSettings = sp.web.regionalSettings;\n\nconst settingsData = await settings();\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/related-items/web\";\nimport { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sp = spfi(...);\nconst manager: IRelatedItemManager = sp.web.relatedItems;\n\nconst data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4);\n
"},{"location":"sp/webs/#security-imports","title":"security imports","text":"Please see information around the available security methods in the security article.
"},{"location":"sp/webs/#sharing-imports","title":"sharing imports","text":"Please see information around the available sharing methods in the sharing article.
"},{"location":"sp/webs/#sitegroups","title":"siteGroups","text":"The site groups
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\nconst groups = await sp.web.siteGroups();\n\nconst groups2 = await sp.web.siteGroups.top(2)();\n
"},{"location":"sp/webs/#associatedownergroup","title":"associatedOwnerGroup","text":"The web's owner group
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedOwnerGroup();\n\nconst users = await sp.web.associatedOwnerGroup.users();\n
"},{"location":"sp/webs/#associatedmembergroup","title":"associatedMemberGroup","text":"The web's member group
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedMemberGroup();\n\nconst users = await sp.web.associatedMemberGroup.users();\n
"},{"location":"sp/webs/#associatedvisitorgroup","title":"associatedVisitorGroup","text":"The web's visitor group
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedVisitorGroup();\n\nconst users = await sp.web.associatedVisitorGroup.users();\n
"},{"location":"sp/webs/#createdefaultassociatedgroups","title":"createDefaultAssociatedGroups","text":"Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\");\n\n// copy the role assignments\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true);\n\n// don't clear sub assignments\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false);\n\n// specify secondary owner, don't copy permissions, clear sub scopes\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\");\n
"},{"location":"sp/webs/#siteusers","title":"siteUsers","text":"The site users
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst users = await sp.web.siteUsers();\n\nconst users2 = await sp.web.siteUsers.top(5)();\n\nconst users3 = await sp.web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)();\n
"},{"location":"sp/webs/#currentuser","title":"currentUser","text":"Information on the current user
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.currentUser();\n\n// check the login name of the current user\nconst user2 = await sp.web.currentUser.select(\"LoginName\")();\n
"},{"location":"sp/webs/#ensureuser","title":"ensureUser","text":"Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { IWebEnsureUserResult } from \"@pnp/sp/site-users/\";\n\nconst sp = spfi(...);\n\nconst result: IWebEnsureUserResult = await sp.web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\");\n
"},{"location":"sp/webs/#getuserbyid","title":"getUserById","text":"Returns the user corresponding to the specified member identifier for the current web
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { ISiteUser } from \"@pnp/sp/site-users/\";\n\nconst sp = spfi(...);\n\nconst user: ISiteUser = sp.web.getUserById(23);\n\nconst userData = await user();\n\nconst userData2 = await user.select(\"LoginName\")();\n
"},{"location":"sp/webs/#usercustomactions","title":"userCustomActions","text":"Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions/web\";\nimport { IUserCustomActions } from \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst actions: IUserCustomActions = sp.web.userCustomActions;\n\nconst actionsData = await actions();\n
"},{"location":"sp/webs/#iwebinfosdata","title":"IWebInfosData","text":"Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations.
interface IWebInfosData {\n Configuration: number;\n Created: string;\n Description: string;\n Id: string;\n Language: number;\n LastItemModifiedDate: string;\n LastItemUserModifiedDate: string;\n ServerRelativeUrl: string;\n Title: string;\n WebTemplate: string;\n WebTemplateId: number;\n}\n
"},{"location":"sp-admin/","title":"sp-admin","text":"The @pnp/sp-admin
library enables you to call the static SharePoint admin API's:
_api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
_api/Microsoft.Online.SharePoint.TenantAdministration.SiteProperties
_api/Microsoft.Online.SharePoint.TenantAdministration.Tenant
These APIs typically require an elevated level of permissions and should not be relied upon in general user facing solutions. Before using this library please understand the impact of what you are doing as you are updating settings at the tenant level for all users.
Warning
These APIs are officially not documented which means there is no SLA provided by Microsoft. Furthermore, they can be updated without notification.
"},{"location":"sp-admin/#use","title":"Use","text":"To use the library you first install the package:
npm install @pnp/sp-admin --save\n
Then import the package into your solution, it will attach a node to the sp fluent interface using selective imports.
import \"@pnp/sp-admin\";\n
"},{"location":"sp-admin/#basic-example","title":"Basic Example","text":"In this example we get all of the web templates' information.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// note the \"admin\" node now available\nconst templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();\n
"},{"location":"sp-admin/#tenant","title":"tenant","text":"The tenant
node represents calls to the _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant
api.
When calling the tenant
endpoint you must target the -admin site as shown here. If you do not you will get only errors.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(\"https://{tenant}-admin.sharepoint.com\");\n\n// The MSAL scope will be: \"https://{tenant}-admin.sharepoint.com/.default\"\n\n// default props\nconst defaultProps = await sp.admin.tenant();\n\n// all props\nconst allProps = await sp.admin.tenant.select(\"*\")();\n\n// select specific props\nconst selectedProps = await sp.admin.tenant.select(\"AllowEditing\", \"DefaultContentCenterSite\")();\n\n// call method\nconst templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();\n
"},{"location":"sp-admin/#office365tenant","title":"office365Tenant","text":"The office365Tenant
node represents calls to the _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
end point and is accessible from any site url.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// default props\nconst defaultProps = await sp.admin.office365Tenant();\n\n// all props\nconst allProps = await sp.admin.office365Tenant.select(\"*\")();\n\n// selected props\nconst selectedProps = await sp.admin.office365Tenant.select(\"AllowEditing\", \"DefaultContentCenterSite\")();\n\n// call method\nconst externalUsers = await sp.admin.office365Tenant.getExternalUsers();\n
"},{"location":"sp-admin/#siteproperties","title":"siteProperties","text":"The siteProperties
node is primarily for accessing detailed properties about a site and tenant.
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// default props\nconst defaultProps = await sp.admin.siteProperties();\n\n// all props\nconst allProps = await sp.admin.siteProperties.select(\"*\")();\n\n// selected props\nconst selectedProps = await sp.admin.siteProperties.select(\"LockState\")();\n\n// call method\nawait sp.admin.siteProperties.clearSharingLockDown(\"https://tenant.sharepoint.com/sites/site1\");\n
For more information on the methods available and how to use them, please review the code comments in the source.
"},{"location":"sp-admin/#call","title":"call","text":"All those nodes support a call
method to easily allow calling methods not explictly added to the library. If there is a method you use often that would be a good candidate to add, please open an issue or submit a PR. The call method is meant to help unblock folks before methods are added.
This sample shows using call to invoke the \"AddTenantCdnOrigin\" method of office365Tenant. While we already support for this method, it helps to show the relationship between call
and an existing method.
import { spfi } from \"@pnp/sp\";\nimport { SPOTenantCdnType } from '@pnp/sp-admin';\n\nconst sp = spfi(...);\n\n// call AddTenantCdnOrigin\nawait sp.admin.office365Tenant.call<void>(\"AddTenantCdnOrigin\", {\n \"cdnType\": SPOTenantCdnType.Public,\n \"originUrl\": \"*/clientsideassets\"\n});\n\nconst spTenant = spfi(\"https://{tenant}-admin.sharepoint.com\");\n\n// call GetSiteSubscriptionId which takes no args\nconst id = await spTenant.admin.tenant.call<string>(\"GetSiteSubscriptionId\");\n
"}]}
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
new file mode 100644
index 000000000..76991bd4a
--- /dev/null
+++ b/sitemap.xml
@@ -0,0 +1,573 @@
+
+The @pnp/sp-admin
library enables you to call the static SharePoint admin API's:
_api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
_api/Microsoft.Online.SharePoint.TenantAdministration.SiteProperties
_api/Microsoft.Online.SharePoint.TenantAdministration.Tenant
These APIs typically require an elevated level of permissions and should not be relied upon in general user facing solutions. Before using this library please understand the impact of what you are doing as you are updating settings at the tenant level for all users.
+Warning
+These APIs are officially not documented which means there is no SLA provided by Microsoft. Furthermore, they can be updated without notification.
+To use the library you first install the package:
+npm install @pnp/sp-admin --save
+
+Then import the package into your solution, it will attach a node to the sp fluent interface using selective imports.
+import "@pnp/sp-admin";
+
+In this example we get all of the web templates' information.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// note the "admin" node now available
+const templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();
+
+The tenant
node represents calls to the _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant
api.
++When calling the
+tenant
endpoint you must target the -admin site as shown here. If you do not you will get only errors.
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi("https://{tenant}-admin.sharepoint.com");
+
+// The MSAL scope will be: "https://{tenant}-admin.sharepoint.com/.default"
+
+// default props
+const defaultProps = await sp.admin.tenant();
+
+// all props
+const allProps = await sp.admin.tenant.select("*")();
+
+// select specific props
+const selectedProps = await sp.admin.tenant.select("AllowEditing", "DefaultContentCenterSite")();
+
+// call method
+const templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();
+
+The office365Tenant
node represents calls to the _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
end point and is accessible from any site url.
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// default props
+const defaultProps = await sp.admin.office365Tenant();
+
+// all props
+const allProps = await sp.admin.office365Tenant.select("*")();
+
+// selected props
+const selectedProps = await sp.admin.office365Tenant.select("AllowEditing", "DefaultContentCenterSite")();
+
+// call method
+const externalUsers = await sp.admin.office365Tenant.getExternalUsers();
+
+The siteProperties
node is primarily for accessing detailed properties about a site and tenant.
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// default props
+const defaultProps = await sp.admin.siteProperties();
+
+// all props
+const allProps = await sp.admin.siteProperties.select("*")();
+
+// selected props
+const selectedProps = await sp.admin.siteProperties.select("LockState")();
+
+// call method
+await sp.admin.siteProperties.clearSharingLockDown("https://tenant.sharepoint.com/sites/site1");
+
+++For more information on the methods available and how to use them, please review the code comments in the source.
+
All those nodes support a call
method to easily allow calling methods not explictly added to the library. If there is a method you use often that would be a good candidate to add, please open an issue or submit a PR. The call method is meant to help unblock folks before methods are added.
This sample shows using call to invoke the "AddTenantCdnOrigin" method of office365Tenant. While we already support for this method, it helps to show the relationship between call
and an existing method.
import { spfi } from "@pnp/sp";
+import { SPOTenantCdnType } from '@pnp/sp-admin';
+
+const sp = spfi(...);
+
+// call AddTenantCdnOrigin
+await sp.admin.office365Tenant.call<void>("AddTenantCdnOrigin", {
+ "cdnType": SPOTenantCdnType.Public,
+ "originUrl": "*/clientsideassets"
+});
+
+const spTenant = spfi("https://{tenant}-admin.sharepoint.com");
+
+// call GetSiteSubscriptionId which takes no args
+const id = await spTenant.admin.tenant.call<string>("GetSiteSubscriptionId");
+
+
+
+
+
+
+
+ Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders.
+To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query.
+Pattern: !@{label name}::{value}
+Example: "!@p1::\sites\dev" or "!@p2::\text.txt"
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+const sp = spfi(...);
+
+// still works as expected, no aliasing
+const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3
+
+const r = await query();
+console.log(r);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// same query with aliasing
+const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+const r = await query();
+console.log(r);
+
+Aliasing is supported with batching as well:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// same query with aliasing and batching
+const [batchedWeb, execute] = await sp.web.batched();
+
+const query = batchedWeb.web.getFolderByServerRelativePath("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+query().then(r => {
+
+ console.log(r);
+});
+
+execute();
+
+
+
+
+
+
+
+ The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.
+Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.
+There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// get the current context web's app catalog
+// this will be the site collection app catalog
+const availableApps = await sp.tenantAppcatalog();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// get the current context web's app catalog
+const availableApps = await sp.web.appcatalog();
+
+If you know the url of the site collection whose app catalog you want you can use the following code. First you need to use one of the methods to access a web. Once you have the web instance you can call the .appcatalog
property on that web instance.
++If a given site collection does not have an app catalog trying to access it will throw an error.
+
import { spfi } from "@pnp/sp";
+import { Web } from '@pnp/sp/webs';
+
+const sp = spfi(...);
+const web = Web([sp.web, "https://mytenant.sharepoint.com/sites/mysite"]);
+const catalog = await web.appcatalog();
+
+The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.
+The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.
+// get available apps
+await catalog();
+
+// get available apps selecting two fields
+await catalog.select("Title", "Deployed")();
+
+This action must be performed in the context of the tenant app catalog
+ +// this represents the file bytes of the app package file
+const blob = new Blob();
+
+// there is an optional third argument to control overwriting existing files
+const r = await catalog.add("myapp.app", blob);
+
+// this is at its core a file add operation so you have access to the response data as well
+// as a File instance representing the created file
+console.log(JSON.stringify(r.data, null, 4));
+
+// all file operations are available
+const nameData = await r.file.select("Name")();
+
+You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions
+const app = await catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c")();
+
+Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block.
+const myAppId = "5137dff1-0b79-4ebc-8af4-ca01f7bd393c";
+
+// deploy
+await catalog.getAppById(myAppId).deploy();
+
+// retract
+await catalog.getAppById(myAppId).retract();
+
+// install
+await catalog.getAppById(myAppId).install();
+
+// uninstall
+await catalog.getAppById(myAppId).uninstall();
+
+// upgrade
+await catalog.getAppById(myAppId).upgrade();
+
+// remove
+await catalog.getAppById(myAppId).remove();
+
+
+By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id.
+// Using the app id
+await catalog.syncSolutionToTeams("5137dff1-0b79-4ebc-8af4-ca01f7bd393c");
+
+// Using the SharePoint apps item id
+await catalog.syncSolutionToTeams("123", true);
+
+The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.
+import { spfi } from "@pnp/sp";
+import { IAttachmentInfo } from "@pnp/sp/attachments";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+// get all the attachments
+const info: IAttachmentInfo[] = await item.attachmentFiles();
+
+// get a single file by file name
+const info2: IAttachmentInfo = await item.attachmentFiles.getByName("file.txt")();
+
+// select specific properties using odata operators and use Pick to type the result
+const info3: Pick<IAttachmentInfo, "ServerRelativeUrl">[] = await item.attachmentFiles.select("ServerRelativeUrl")();
+
+You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.
+ +import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.add("file2.txt", "Here is my content");
+
+You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.
+import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+const text = await item.attachmentFiles.getByName("file.txt").getText();
+
+// use this in the browser, does not work in nodejs
+const blob = await item.attachmentFiles.getByName("file.mp4").getBlob();
+
+// use this in nodejs
+const buffer = await item.attachmentFiles.getByName("file.mp4").getBuffer();
+
+// file must be valid json
+const json = await item.attachmentFiles.getByName("file.json").getJSON();
+
+You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. +
+import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!");
+
+import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").delete();
+
+Delete the attachment and send it to recycle bin
+import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").recycle();
+
+Delete multiple attachments and send them to recycle bin
+import { spfi } from "@pnp/sp";
+import { IList } from "@pnp/sp/lists/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const item = await batchedSP.web.lists.getByTitle("MyList").items.getById(2);
+
+item.attachmentFiles.getByName("1.txt").recycle();
+item.attachmentFiles.getByName("2.txt").recycle();
+
+await execute();
+
+
+
+
+
+
+
+ The article describes the behaviors exported by the @pnp/sp
library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/graph, and @pnp/nodejs.
The DefaultInit
behavior, is a composed behavior which includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit.
import { spfi, DefaultInit } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultInit());
+
+await sp.web();
+
+The DefaultHeaders
behavior uses InjectHeaders to set the Accept, Content-Type, and User-Agent headers.
import { spfi, DefaultHeaders } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultHeaders());
+
+await sp.web();
+
+++DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.
+
The RequestDigest
behavior ensures that the "X-RequestDigest" header is included for requests where it is needed. If you are using MSAL, supplying your own tokens, or doing a GET request it is not required. As well it cache's the digests to reduce the number of requests.
Optionally you can provide a function to supply your own digests. The logic followed by the behavior is to check the cache, run a hook if provided, and finally make a request to "/_api/contextinfo" for the value.
+import { spfi, RequestDigest } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(RequestDigest());
+
+await sp.web();
+
+With a hook:
+import { dateAdd } from "@pnp/core";
+import { spfi, RequestDigest } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(RequestDigest((url, init) => {
+
+ // the url will be a URL instance representing the request url
+ // init will be the RequestInit
+
+ return {
+ expiration: dateAdd(new Date(), "minute", 20);
+ value: "MY VALID REQUEST DIGEST VALUE";
+ }
+}));
+
+await sp.web();
+
+A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.
+The baseUrl prop can be used to configure a fallback when making urls absolute.
+++If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.
+
You should set a baseUrl as shown below.
+import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// you should use the baseUrl value when working in a SPA to ensure it is always properly set for all requests
+const sp = spfi().using(SPBrowser({ baseUrl: "https://tenant.sharepoint.com/sites/dev" }));
+
+await sp.web();
+
+This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx
is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. A hook is supplied to RequestDigest that will attempt to use any existing legacyPageContext formDigestValue it can find, otherwise defaults to the base RequestDigest behavior. It also sets a pre handler to ensure the url is absolute, using the SPFx context's pageContext.web.absoluteUrl as the base.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const sp = spfi(...).using(SPFx(this.context));
+
+await sp.web();
+
+Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.
+import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'
+import { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'
+
+const sp = spfi().using(spSPFx(this.context));
+const graph = graphfi().using(graphSPFx(this.context));
+
+Added in 3.12
+Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const sp = spfi(...).using(SPFxToken(this.context));
+
+await sp.web();
+
+This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.
+++You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.
+
import { spfi, Telemetry } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(Telemetry());
+
+await sp.web();
+
+
+
+
+
+
+
+ The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts.
+ +You can create a new client-side page in several ways, all are equivalent.
+import { spfi, SPFI } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+import { PromotedState } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// Create a page providing a file name
+const page = await sp.web.addClientsidePage("mypage1");
+
+// ... other operations on the page as outlined below
+
+// the page is initially not published, you must publish it so it appears for others users
+await page.save();
+
+// include title and page layout
+const page2 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article");
+
+// you must publish the new page
+await page2.save();
+
+// include title, page layout, and specifying the publishing status (Added in 2.0.4)
+const page3 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article", PromotedState.PromoteOnPublish);
+
+// you must publish the new page, after which the page will immediately be promoted to a news article
+await page3.save();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { Web } from "@pnp/sp/webs";
+import { CreateClientsidePage, PromotedState } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page1 = await CreateClientsidePage(sp.web, "mypage2", "My Page Title");
+
+// you must publish the new page
+await page1.save(true);
+
+// specify the page layout type parameter
+const page2 = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article");
+
+// you must publish the new page
+await page2.save();
+
+// specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4)
+const page2half = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article", PromotedState.PromoteOnPublish);
+
+// you must publish the new page, after which the page will immediately be promoted to a news article
+await page2half.save();
+
+// use the web factory to create a page in a specific web
+const page3 = await CreateClientsidePage(Web([sp, "https://{absolute web url}"]), "mypage4", "My Page Title");
+
+// you must publish the new page
+await page3.save();
+
+Using this method you can easily create a full page app page given the component id. Don't forget the page will not be published and you will need to call save.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page = await sp.web.addFullPageApp("name333", "My Title", "2CE4E250-B997-11EB-A9D2-C9D2FF95D000");
+// ... other page actions
+// you must save the page to publish it
+await page.save();
+
+There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.
+This method takes a server relative path to the page to load.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+
+const sp = spfi(...);
+
+// use from the sp.web fluent chain
+const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+// use the web factory to target a specific web
+const page2 = await Web([sp.web, "https://{absolute web url}"]).loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+This method takes an IFile instance and loads an IClientsidePage instance.
+import { spfi } from "@pnp/sp";
+import { ClientsidePageFromFile } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+
+const sp = spfi(...);
+
+const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath("/sites/dev/sitepages/mypage3.aspx"));
+
+Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page
that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections.
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12
+const section1 = page.addSection();
+section1.addColumn(6);
+section1.addColumn(6);
+
+// create a three column layout in a new section
+const section2 = page.addSection();
+section2.addColumn(4);
+section2.addColumn(4);
+section2.addColumn(4);
+
+// publish our changes
+await page.save();
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// drop all the columns in this section
+// this will also DELETE all controls contained in the columns
+page.sections[1].columns.length = 0;
+
+// create a new column layout
+page.sections[1].addColumn(4);
+page.sections[1].addColumn(8);
+
+// publish our changes
+await page.save();
+
+The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// add or get a vertical section (handles case where section already exists)
+const vertSection = page.addVerticalSection();
+
+// ****************************************************************
+
+// if you know or want to test if a vertical section is present:
+if (page.hasVerticalSection) {
+
+ // access the vertical section (this method will NOT create the section if it does not exist)
+ page.verticalSection.addControl(new ClientsideText("hello"));
+} else {
+
+ const vertSection = page.addVerticalSection();
+ vertSection.addControl(new ClientsideText("hello"));
+}
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// swap the order of two sections
+// this will preserve the controls within the columns
+page.sections = [page.sections[1], page.sections[0]];
+
+// publish our changes
+await page.save();
+
+The sections and columns are arrays, so normal array operations work as expected
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// swap the order of two columns
+// this will preserve the controls within the columns
+page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]];
+
+// publish our changes
+await page.save();
+
+Once you have your sections and columns defined you will want to add/edit controls within those columns.
+import { spfi } from "@pnp/sp";
+import { ClientsideText, IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+page.addSection().addControl(new ClientsideText("@pnp/sp is a great library!"));
+
+await page.save();
+
+Adding controls involves loading the available client-side part definitions from the server or creating a text part.
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+import { spfi } from "@pnp/sp";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// this will be a ClientsidePageComponent array
+// this can be cached on the client in production scenarios
+const partDefs = await sp.web.getClientsideWebParts();
+
+// find the definition we want, here by id
+const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa");
+
+// optionally ensure you found the def
+if (partDef.length < 1) {
+ // we didn't find it so we throw an error
+ throw new Error("Could not find the web part");
+}
+
+// create a ClientWebPart instance from the definition
+const part = ClientsideWebpart.fromComponentDef(partDef[0]);
+
+// set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video.
+// the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting
+// the properties correctly
+part.setProperties<{ embedCode: string }>({
+ embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg",
+});
+
+// we add that part to a new section
+page.addSection().addControl(part);
+
+await page.save();
+
+There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title.
+import { spfi } from "@pnp/sp";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+// we create a class to wrap our functionality in a reusable way
+class ListWebpart extends ClientsideWebpart {
+
+ constructor(control: ClientsideWebpart) {
+ super((<any>control).json);
+ }
+
+ // add property getter/setter for what we need, in this case "listTitle" within searchablePlainTexts
+ public get DisplayTitle(): string {
+ return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || "";
+ }
+
+ public set DisplayTitle(value: string) {
+ this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value;
+ }
+}
+
+const sp = spfi(...);
+
+// now we load our page
+const page = await sp.web.loadClientsidePage("/sites/dev/SitePages/List-Web-Part.aspx");
+
+// get our part and pass it to the constructor of our wrapper class
+const part = new ListWebpart(page.sections[0].columns[0].getControl(0));
+
+part.DisplayTitle = "My New Title!";
+
+await page.save();
+
+++Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.
+
There are other operation you can perform on a page in addition to manipulating the content.
+You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.pageLayout;
+
+// set the value
+page.pageLayout = "Article";
+await page.save();
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.bannerImageUrl;
+
+// set the value
+page.bannerImageUrl = "/server/relative/path/to/image.png";
+await page.save();
+
+++Banner images need to exist within the same site collection as the page where you want to use them.
+
Allows you to set the thumbnail used for the page independently of the banner.
+++If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality.
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.thumbnailUrl;
+
+// set the value
+page.thumbnailUrl = "/server/relative/path/to/image.png";
+await page.save();
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.topicHeader;
+
+// set the value
+page.topicHeader = "My cool header!";
+await page.save();
+
+// clear the topic header and hide it
+page.topicHeader = "";
+await page.save();
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.title;
+
+// set the value
+page.title = "My page title";
+await page.save();
+
+++Descriptions are limited to 255 chars
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.description;
+
+// set the value
+page.description = "A description";
+await page.save();
+
+Sets the layout type of the page. The valid values are: "FullWidthImage", "NoImage", "ColorBlock", "CutInShape"
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.layoutType;
+
+// set the value
+page.layoutType = "ColorBlock";
+await page.save();
+
+Sets the header text alignment to one of "Left" or "Center"
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.headerTextAlignment;
+
+// set the value
+page.headerTextAlignment = "Center";
+await page.save();
+
+Sets if the topic header is displayed on a page.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.showTopicHeader;
+
+// show the header
+page.showTopicHeader = true;
+await page.save();
+
+// hide the header
+page.showTopicHeader = false;
+await page.save();
+
+Sets if the publish date is displayed on a page.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.showPublishDate;
+
+// show the date
+page.showPublishDate = true;
+await page.save();
+
+// hide the date
+page.showPublishDate = false;
+await page.save();
+
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/clientside-pages";
+import "@pnp/sp/site-users";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the author details (string | null)
+const value = page.authorByLine;
+
+// set the author by user id
+const user = await sp.web.currentUser.select("Id", "LoginName")();
+const userId = user.Id;
+const userLogin = user.LoginName;
+
+await page.setAuthorById(userId);
+await page.save();
+
+await page.setAuthorByLoginName(userLogin);
+await page.save();
+
+++you must still save the page after setting the author to persist your changes as shown in the example.
+
Loads the page from the server. This will overwrite any local unsaved changes.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.load();
+
+++Uncustomized home pages (i.e the home page that is generated with a site out of the box) cannot be updated by this library without becoming corrupted.
+
Saves any changes to the page, optionally keeping them in draft state.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// changes are published
+await page.save();
+
+// changes remain in draft
+await page.save(false);
+
+Discards any current checkout of the page by the current user.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.discardPageCheckout();
+
+Schedules the page for publishing.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// date and time to publish the page in UTC.
+const publishDate = new Date("1/1/1901");
+
+const scheduleVersion: string = await page.schedulePublish(publishDate);
+
+Promotes the page as a news article.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.promoteToNews();
+
+Used to control the availability of comments on a page.
+ +import { spfi } from "@pnp/sp";
+// you need to import the comments sub-module or use the all preset
+import "@pnp/sp/comments/clientside-page";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// turn on comments
+await page.enableComments();
+
+// turn off comments
+await page.disableComments();
+
+Finds a control within the page by id.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage, ClientsideText } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+const control = page.findControlById("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
+
+// you can also type the control
+const control = page.findControlById<ClientsideText>("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
+
+Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// find the first control whose order is 9
+const control = page.findControl((c) => c.order === 9);
+
+// iterate all the controls and output the id to the console
+page.findControl((c) => {
+ console.log(c.id);
+ return false;
+});
+
+Updates the page's like value for the current user.
+// our page instance
+const page: IClientsidePage;
+
+// like this page
+await page.like();
+
+// unlike this page
+await page.unlike();
+
+Gets the likes information for this page.
+// our page instance
+const page: IClientsidePage;
+
+const info = await page.getLikedByInformation();
+
+Creates a copy of the page, including all controls.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// creates a published copy of the page
+const pageCopy = await page.copy(sp.web, "newpagename", "New Page Title");
+
+// creates a draft (unpublished) copy of the page
+const pageCopy2 = await page.copy(sp.web, "newpagename", "New Page Title", false);
+
+// edits to pageCopy2 ...
+
+// publish the page
+pageCopy2.save();
+
+Copies the contents of a page to another existing page instance.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instances, loaded in any of the ways shown above
+const source: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+const target: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/target.aspx");
+const target2: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/target2.aspx");
+
+// creates a published copy of the page
+await source.copyTo(target);
+
+// creates a draft (unpublished) copy of the page
+await source.copyTo(target2, false);
+
+// edits to target2...
+
+// publish the page
+target2.save();
+
+Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent.
+++Banner images need to exist within the same site collection as the page where you want to use them.
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+page.setBannerImage("/server/relative/path/to/image.png");
+
+// save the changes
+await page.save();
+
+// set additional props
+page.setBannerImage("/server/relative/path/to/image.png", {
+ altText: "Image description",
+ imageSourceType: 2,
+ translateX: 30,
+ translateY: 1234,
+});
+
+// save the changes
+await page.save();
+
+This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file
- likely from a file input or similar.
import { join } from "path";
+import { createReadStream } from "fs";
+import { spfi, SPFI, SPFx } from "@pnp/sp";
+import { SPDefault } from "@pnp/nodejs";
+import { LogLevel } from "@pnp/logging";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/clientside-pages";
+
+const buffer = readFileSync("c:/temp/key.pem");
+
+const config:any = {
+ auth: {
+ authority: "https://login.microsoftonline.com/{my tenant}/",
+ clientId: "{application (client) id}",
+ clientCertificate: {
+ thumbprint: "{certificate thumbprint, displayed in AAD}",
+ privateKey: buffer.toString(),
+ },
+ },
+ system: {
+ loggerOptions: {
+ loggerCallback(loglevel: any, message: any, containsPii: any) {
+ console.log(message);
+ },
+ piiLoggingEnabled: false,
+ logLevel: LogLevel.Verbose
+ }
+ }
+};
+
+// configure your node options
+const sp = spfi('{site url}').using(SPDefault({
+ baseUrl: '{site url}',
+ msal: {
+ config: config,
+ scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]
+ }
+}));
+
+
+// add the banner image
+const dirname = join("C:/path/to/file", "img-file.jpg");
+
+const chunkedFile = createReadStream(dirname);
+
+const far = await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents").files.addChunked( "banner.jpg", chunkedFile );
+
+// add the page
+const page = await sp.web.addClientsidePage("MyPage", "Page Title");
+
+// set the banner image
+page.setBannerImage(far.data.ServerRelativeUrl);
+
+// publish the page
+await page.save();
+
+Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there.
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// you must await this method
+await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg");
+
+// save the changes
+await page.save();
+
+You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// you must await this method
+await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg", {
+ altText: "Image description",
+ imageSourceType: 2,
+ translateX: 30,
+ translateY: 1234,
+});
+
+// save the changes
+await page.save();
+
+Allows you to recycle a page without first needing to use getItem
+// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.recycle();
+
+Allows you to delete a page without first needing to use getItem
+// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.delete();
+
+Save page as a template from which other pages can be created. If it doesn't exist a special folder "Templates" will be added to the doc lib
+// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.saveAsTemplate();
+// save a template, but don't publish it allowing you to make changes before it is available to users
+// you
+await page.saveAsTemplate(false);
+// ... changes to the page
+// you must publish the template so it is available
+await page.save();
+
+Allows sharing a page with one or more email addresses, optionall including a message in the email
+// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.share(["email@place.com", "email2@otherplace.com"]);
+// optionally include a message
+await page.share(["email@place.com", "email2@otherplace.com"], "Please check out this cool page!");
+
+You can use the addRepostPage
method to add a report page. The method returns the absolute url of the created page. All properties are optional but it is recommended to include as much as possible to improve the quality of the repost card's display.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+const page = await sp.web.addRepostPage({
+ BannerImageUrl: "https://some.absolute/path/to/an/image.jpg",
+ IsBannerImageUrlExternal: true,
+ Description: "My Description",
+ Title: "This is my title!",
+ OriginalSourceUrl: "https://absolute/path/to/article",
+});
+
+++ + + + + + +To specify an existing item in another list all of the four properties OriginalSourceSiteId, OriginalSourceWebId, OriginalSourceListId, and OriginalSourceItemId are required.
+
The column defaults sub-module allows you to manage the default column values on a library or library folder.
+ +You can get the default values for a specific folder as shown below:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+const defaults = await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").getDefaultColumnValues();
+
+/*
+The resulting structure will have the form:
+
+[
+ {
+ "name": "{field internal name}",
+ "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+ "value": "{the default value}"
+ },
+ {
+ "name": "{field internal name}",
+ "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+ "value": "{the default value}"
+ }
+]
+*/
+
+When setting the defaults for a folder you need to include the field's internal name and the value.
+++For more examples of other field types see the section Pattern for setting defaults on various column types
+Note: Be very careful when setting the path as the site collection url is case sensitive
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").setDefaultColumnValues([{
+ name: "TextField",
+ value: "Something",
+},
+{
+ name: "NumberField",
+ value: 14,
+}]);
+
+You can also get all of the defaults for the entire library.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+const defaults = await sp.web.lists.getByTitle("DefaultColumnValues").getDefaultColumnValues();
+
+/*
+The resulting structure will have the form:
+
+[
+ {
+ "name": "{field internal name}",
+ "path": "/sites/dev/DefaultColumnValues",
+ "value": "{the default value}"
+ },
+ {
+ "name": "{field internal name}",
+ "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+ "value": "{a different default value}"
+ }
+]
+*/
+
+You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value.
+++For more examples of other field types see the section Pattern for setting defaults on various column types
+Note: Be very careful when setting the path as the site collection url is case sensitive
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([{
+ name: "TextField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: "#PnPjs Rocks!",
+}]);
+
+If you want to clear all of the folder defaults you can use the clear method:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").clearDefaultColumnValues();
+
+If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([]);
+
+The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types.
+[{
+ // Text/Boolean/CurrencyDateTime/Choice/User
+ name: "TextField":
+ path: "/sites/dev/DefaultColumnValues",
+ value: "#PnPjs Rocks!",
+}, {
+ //Number
+ name: "NumberField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: 42,
+}, {
+ //Date
+ name: "NumberField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: "1900-01-01T00:00:00Z",
+}, {
+ //Date - Today
+ name: "NumberField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: "[today]",
+}, {
+ //MultiChoice
+ name: "MultiChoiceField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: ["Item 1", "Item 2"],
+}, {
+ //MultiChoice - single value
+ name: "MultiChoiceField",
+ path: "/sites/dev/DefaultColumnValues/folder2",
+ value: ["Item 1"],
+}, {
+ //Taxonomy - single value
+ name: "TaxonomyField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: {
+ wssId:"-1",
+ termName: "TaxValueName",
+ termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
+ }
+}, {
+ //Taxonomy - multiple value
+ name: "TaxonomyMultiField",
+ path: "/sites/dev/DefaultColumnValues",
+ value: [{
+ wssId:"-1",
+ termName: "TaxValueName",
+ termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
+ },{
+ wssId:"-1",
+ termName: "TaxValueName2",
+ termId: "95d4c307-dde5-49d8-b861-392e145d94d3"
+ },]
+}]);
+
+This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/column-defaults";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get the term's info we want to use as the default
+const term = await sp.termStore.sets.getById("ea6fc521-d293-4f3d-9e84-f3a5bc0936ce").getTermById("775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a")();
+
+// get the default term label
+const defLabel = term.labels.find(v => v.isDefault);
+
+// set the default value using -1, the term id, and the term's default label name
+await sp.web.lists.getByTitle("MetaDataDocLib").rootFolder.setDefaultColumnValues([{
+ name: "MetaDataColumnInternalName",
+ value: {
+ wssId: "-1",
+ termId: term.id,
+ termName: defLabel.name,
+ }
+}])
+
+// check that the defaults have updated
+const newDefaults = await sp.web.lists.getByTitle("MetaDataDocLib").getDefaultColumnValues();
+
+
+
+
+
+
+
+ Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles.
+These APIs are currently in BETA and are subject to change or may not work on all tenants.
+ +The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.
+You can add a comment using the addComment method as shown
+import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+//add a comment as text
+const comment = await page.addComment("A test comment");
+
+//or you can include the @mentions. html anchor required to include mention in text body.
+const mentionHtml = `<a data-sp-mention-user-id="test@contoso.com" href="mailto:test@contoso.com.com" tabindex="-1">Test User</a>`;
+
+const commentInfo: Partial<ICommentInfo> = { text: `${mentionHtml} This is the test comment with at mentions`,
+ mentions: [{ loginName: 'test@contoso.com', email: 'test@contoso.com', name: 'Test User' }], };
+const comment = await page.addComment(commentInfo);
+
+import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+
+const comments = await page.getComments();
+
+Used to control the availability of comments on a page
+import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+// you need to import the comments sub-module or use the all preset
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// turn on comments
+await page.enableComments();
+
+// turn off comments
+await page.disableComments();
+
+import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+const comment = await page.addComment("A test comment");
+
+const commentData = await page.getCommentById(parseInt(comment.id, 10));
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+import "@pnp/sp/items";
+import "@pnp/sp/comments/item";
+
+const sp = spfi(...);
+
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/SitePages/Test_8q5L.aspx").getItem();
+
+// as an example, or any of the below options
+await item.like();
+
+The below examples use a variable named "item" which is taken to represent an IItem instance.
+const comments = await item.comments();
+
+You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
+
+//load the top 20 replies and comments for an item including likedBy information
+const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20)();
+
+import { spfi } from "@pnp/sp";
+import { ICommentInfo } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+// you can add a comment as a string
+const comment = await item.comments.add("string comment");
+
+
+
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].delete()
+
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].like();
+
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+comments[0].unlike()
+
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+const comment = await comments[0].comments.add({ text: "#PnPjs is pretty ok!" });
+
+import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+const replies = await comments[0].replies();
+
+You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/comments";
+import { ILikeData, ILikedByInformation } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const item = sp.web.lists.getByTitle("PnP List").items.getById(1);
+
+// like an item
+await item.like();
+
+// unlike an item
+await item.unlike();
+
+// get the liked by information
+const likedByInfo: ILikedByInformation = await item.getLikedByInformation();
+
+To like/unlike a client-side page and get liked by information.
+import { spfi } from "@pnp/sp";
+import { ILikedByInformation } from "@pnp/sp/comments";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// like a page
+await page.like();
+
+// unlike a page
+await page.unlike();
+
+// get the liked by information
+const likedByInfo: ILikedByInformation = await page.getLikedByInformation();
+
+You can rate list items with a numeric values between 1 and 5.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/comments";
+import { ILikeData, ILikedByInformation } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const item = sp.web.lists.getByTitle("PnP List").items.getById(1);
+
+// rate an item
+await item.rate(2);
+
+
+
+
+
+
+
+ Content Types are used to define sets of columns in SharePoint.
+The following example shows how to add the built in Picture Content Type to the Documents library.
+const sp = spfi(...);
+
+sp.web.lists.getByTitle("Documents").contentTypes.addAvailableContentType("0x010102");
+
+import { IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+const d: IContentType = await sp.web.contentTypes.getById("0x01")();
+
+// log content type name to console
+console.log(d.name);
+
+import { IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+await sp.web.contentTypes.getById("0x01").update({EditFormClientSideComponentId: "9dfdb916-7380-4b69-8d92-bc711f5fa339"});
+
+To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.
+const sp = spfi(...);
+
+sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type");
+
+It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API).
+const sp = spfi(...);
+
+//Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings)
+sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type", "This is my content type.", "_PnP Content Types", { ReadOnly: true });
+
+Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get field links from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").fieldLinks();
+
+// log collection of fieldlinks to console
+console.log(d);
+
+To get a collection with all fields on the Content Type, simply use this method.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get fields from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").fields();
+
+// log collection of fields to console
+console.log(d);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get parent Content Type from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").parent();
+
+// log name of parent Content Type to console
+console.log(d.Name)
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get workflow associations from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").workflowAssociations();
+
+// log collection of workflow associations to console
+console.log(d);
+
+
+
+
+
+
+
+ Starting with 3.8.0 we've moved context information to its own sub-module. You can now import context-info
and use it on any SPQueryable derived object to understand the context. Some examples are below.
The information returned by the method is defined by the IContextInfo interface.
+export interface IContextInfo {
+ FormDigestTimeoutSeconds: number;
+ FormDigestValue: number;
+ LibraryVersion: string;
+ SiteFullUrl: string;
+ SupportedSchemaVersions: string[];
+ WebFullUrl: string;
+}
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+const info = await sp.web.getContextInfo();
+
+This pattern works as well for any SPQueryable derived object, allowing you to gain context no matter with which fluent objects you are working.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+const info = await sp.web.lists.getContextInfo();
+
+Often you will have an absolute URL to a file or path and would like to create an IWeb or IFile. You can use the fileFromPath or folderFromPath to get an IFile/IFolder, or you can use getContextInfo
to create a new web within the context of the file path.
import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+// supply an absolute path to get associated context info, this works across site collections
+const { WebFullUrl } = await sp.web.getContextInfo("https://tenant.sharepoint.com/sites/dev/shared documents/file.docx");
+
+// create a new web pointing to the web where the file is stored
+const web = Web([sp.web, decodeURI(WebFullUrl)]);
+
+const webInfo = await web();
+
+
+
+
+
+
+
+ The favorites API allows you to fetch and manipulate followed sites and list items (also called saved for later). Note, all of these methods only work with the context of a logged in user, and not with app-only permissions.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const favSites = await sp.favorites.getFollowedSites();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const tenantUrl = "contoso.sharepoint.com";
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1"
+const webUrl = "https://contoso.sharepoint.com/sites/favsite"
+
+const favSiteInfo = await sp.favorites.getFollowedSites.add(tenantUrl, siteId, webId, webUrl);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const tenantUrl = "contoso.sharepoint.com";
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1"
+const webUrl = "https://contoso.sharepoint.com/sites/favsite"
+
+await sp.favorites.getFollowedSites.remove(tenantUrl, siteId, webId, webUrl);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const favListItems = await sp.favorites.getFollowedListItems();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1";
+const listId = "f09fe67e-0160-4fcc-9144-905bd4889f31";
+const listItemUniqueId = "1425C841-626A-44C9-8731-DA8BDC0882D1";
+
+const favListItemInfo = await sp.favorites.getFollowedListItems.add(siteId, webId, listId, listItemUniqueId);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1";
+const listId = "f09fe67e-0160-4fcc-9144-905bd4889f31";
+const listItemUniqueId = "1425C841-626A-44C9-8731-DA8BDC0882D1";
+
+const favListItemInfo = await sp.favorites.getFollowedListItems.remove(siteId, webId, listId, listItemUniqueId);
+
+
+
+
+
+
+
+ Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.
+Represents a collection of features. SharePoint Sites and Webs will have a collection of features
+Gets the information about a feature for the given GUID
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+const webFeature = await sp.web.features.getById(webFeatureId)();
+
+const siteFeatureId = "guid-of-site-scope-feature";
+const siteFeature = await sp.site.features.getById(siteFeatureId)();
+
+Adds (activates) a feature at the Site or Web level
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+let res = await sp.web.features.add(webFeatureId);
+// Activate with force
+res = await sp.web.features.add(webFeatureId, true);
+
+Removes and deactivates the specified feature from the SharePoint Site or Web
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+let res = await sp.web.features.remove(webFeatureId);
+// Deactivate with force
+res = await sp.web.features.remove(webFeatureId, true);
+
+Represents an instance of a SharePoint feature.
+ +Deactivates the specified feature from the SharePoint Site or Web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/features";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+sp.web.features.remove(webFeatureId);
+
+// Deactivate with force
+sp.web.features.remove(webFeatureId, true);
+
+
+
+
+
+
+
+ Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.
+Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
+import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/fields";
+
+// set up sp root object
+const sp = spfi(...);
+// get the field by Id for web
+const field: IField = sp.web.fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
+// get the field by Id for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483")();
+
+// we can use this 'field' variable to execute more queries on the field:
+const r = await field.select("Title")();
+
+// show the response from the server
+console.log(r.Title);
+
+You can also get a field from the collection by title.
+import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists"
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+// get the field with the title 'Author' for web
+const field: IField = sp.web.fields.getByTitle("Author");
+// get the field with the title 'Title' for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByTitle("Title")();
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different.
+import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists"
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+// get the field with the internal name 'ModifiedBy' for web
+const field: IField = sp.web.fields.getByInternalNameOrTitle("ModifiedBy");
+// get the field with the internal name 'ModifiedBy' for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByInternalNameOrTitle("ModifiedBy")();
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Create a new field by defining an XML schema that assigns all the properties for the field.
+import { spfi } from "@pnp/sp";
+import { IField, IFieldAddResult } from "@pnp/sp/fields/types";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// define the schema for your new field, in this case a date field with a default date of today.
+const fieldSchema = `<Field ID="{03b09ff4-d99d-45ed-841d-3855f77a2483}" StaticName="MyField" Name="MyField" DisplayName="My New Field" FriendlyDisplayFormat="Disabled" Format="DateOnly" Type="DateTime" Group="My Group"><Default>[today]</Default></Field>`;
+
+// create the new field in the web
+const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema);
+// create the new field in the list 'My List'
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(fieldSchema);
+
+// we can use this 'field' variable to run more queries on the list:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the add method to create a new field where you define the field type
+import { spfi } from "@pnp/sp";
+import { IField, IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+// create a new field called 'My Field' in the list 'My List'
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the createFieldAsXml method to add a site field to a list.
+import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+// add the site field 'My Field' to the list 'My List'
+const r = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(field.data.SchemaXml as string);
+
+// log the field Id to console
+console.log(r.data.Id);
+
+Use the addText method to create a new text field.
+import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new text field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.addText("My Field", { MaxLength: 255, Group: "My Group" });
+// create a new text field called 'My Field' in the list 'My List'.
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.addText("My Field", { MaxLength: 255, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addCalculated method to create a new calculated field.
+import { spfi } from "@pnp/sp";
+import { DateTimeFieldFormatType, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new calculated field called 'My Field' in web
+const field = await sp.web.fields.addCalculated("My Field", { Formula: "=Modified+1", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: "MyGroup" });
+// create a new calculated field called 'My Field' in the list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addCalculated("My Field", { Formula: "=Modified+1", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: "MyGroup" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addDateTime method to create a new date/time field.
+import { spfi } from "@pnp/sp";
+import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new date/time field called 'My Field' in web
+const field = await sp.web.fields.addDateTime("My Field", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: "My Group" });
+// create a new date/time field called 'My Field' in the list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addDateTime("My Field", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addCurrency method to create a new currency field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new currency field called 'My Field' in web
+const field = await sp.web.fields.addCurrency("My Field", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: "My Group" });
+// create a new currency field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addCurrency("My Field", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addImageField method to create a new image field.
+import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new image field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.addImageField("My Field");
+// create a new image field called 'My Field' in the list 'My List'.
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.addImageField("My Field");
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addMultilineText method to create a new multi-line text field.
+++For Enhanced Rich Text mode, see the next section.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new multi-line text field called 'My Field' in web
+const field = await sp.web.fields.addMultilineText("My Field", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: "My Group" });
+// create a new multi-line text field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addMultilineText("My Field", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+The REST endpoint doesn't support setting the RichTextMode
field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+//Create a new multi-line text field called 'My Field' in web
+const field = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(
+ `<Field Type="Note" Name="MyField" DisplayName="My Field" Required="FALSE" RichText="TRUE" RichTextMode="FullHtml" />`
+);
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addNumber method to create a new number field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new number field called 'My Field' in web
+const field = await sp.web.fields.addNumber("My Field", { MinimumValue: 1, MaximumValue: 100, Group: "My Group" });
+// create a new number field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addNumber("My Field", { MinimumValue: 1, MaximumValue: 100, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addUrl method to create a new url field.
+import { spfi } from "@pnp/sp";
+import { UrlFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new url field called 'My Field' in web
+const field = await sp.web.fields.addUrl("My Field", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: "My Group" });
+// create a new url field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addUrl("My Field", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addUser method to create a new user field.
+import { spfi } from "@pnp/sp";
+import { FieldUserSelectionMode } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new user field called 'My Field' in web
+const field = await sp.web.fields.addUser("My Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: "My Group" });
+// create a new user field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addUser("My Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+// **
+// Adding a lookup that supports multiple values takes two calls:
+const fieldAddResult = await sp.web.fields.addUser("Multi User Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly });
+await fieldAddResult.field.update({ AllowMultipleValues: true }, "SP.FieldUser");
+
+Use the addLookup method to create a new lookup field.
+import { spfi } from "@pnp/sp";
+import { FieldTypes } from "@pnp/sp/fields/types";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const list = await sp.web.lists.getByTitle("My Lookup List")();
+// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web.
+const field = await sp.web.fields.addLookup("My Field", { LookupListId: list.data.Id, LookupFieldName: "Title" });
+// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLookup("My Field", {LookupListId: list.data.Id, LookupFieldName: "Title"});
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+// **
+// Adding a lookup that supports multiple values takes two calls:
+const fieldAddResult = await sp.web.fields.addLookup("Multi Lookup Field", { LookupListId: list.data.Id, LookupFieldName: "Title" });
+await fieldAddResult.field.update({ AllowMultipleValues: true }, "SP.FieldLookup");
+
+Use the addChoice method to create a new choice field.
+import { spfi } from "@pnp/sp";
+import { ChoiceFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
+// create a new choice field called 'My Field' in web
+const field = await sp.web.fields.addChoice("My Field", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: "My Group" });
+// create a new choice field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addChoice("My Field", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addMultiChoice method to create a new multi-choice field.
+import { spfi } from "@pnp/sp";
+import { ChoiceFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
+// create a new multi-choice field called 'My Field' in web
+const field = await sp.web.fields.addMultiChoice("My Field", { Choices: choices, FillInChoice: false, Group: "My Group" });
+// create a new multi-choice field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addMultiChoice("My Field", { Choices: choices, FillInChoice: false, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addBoolean method to create a new boolean field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new boolean field called 'My Field' in web
+const field = await sp.web.fields.addBoolean("My Field", { Group: "My Group" });
+// create a new boolean field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addBoolean("My Field", { Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addDependentLookupField method to create a new dependent lookup field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const field = await sp.web.fields.addLookup("My Field", { LookupListId: list.Id, LookupFieldName: "Title" });
+// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web.
+const fieldDep = await sp.web.fields.addDependentLookupField("My Dep Field", field.data.Id as string, "Description");
+// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLookup("My Field", { LookupListId: list.Id, LookupFieldName: "Title" });
+const fieldDep2 = await sp.web.lists.getByTitle("My List").fields.addDependentLookupField("My Dep Field", field2.data.Id as string, "Description");
+
+// we can use this 'fieldDep' variable to run more queries on the field:
+const r = await fieldDep.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the addLocation method to create a new location field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new location field called 'My Field' in web
+const field = await sp.web.fields.addLocation("My Field", { Group: "My Group" });
+// create a new location field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLocation("My Field", { Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+Use the delete method to delete a field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+await sp.web.fields.addBoolean("Temp Field", { Group: "My Group" });
+await sp.web.fields.addBoolean("Temp Field 2", { Group: "My Group" });
+await sp.web.lists.getByTitle("My List").fields.addBoolean("Temp Field", { Group: "My Group" });
+await sp.web.lists.getByTitle("My List").fields.addBoolean("Temp Field 2", { Group: "My Group" });
+
+// delete one or more fields from web, returns boolean
+const result = await sp.web.fields.getByTitle("Temp Field").delete();
+const result2 = await sp.web.fields.getByTitle("Temp Field 2").delete();
+
+
+// delete one or more fields from list 'My List', returns boolean
+const result = await sp.web.lists.getByTitle("My List").fields.getByTitle("Temp Field").delete();
+const result2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("Temp Field 2").delete();
+
+Use the update method to update a field.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// update the field called 'My Field' with a description in web, returns FieldUpdateResult
+const fieldUpdate = await sp.web.fields.getByTitle("My Field").update({ Description: "My Description" });
+// update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult
+const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").update({ Description: "My Description" });
+
+// if you need to update a field with properties for a specific field type you can optionally include the field type as a second param
+// if you do not include it we will look up the type, but that adds a call to the server
+const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Look up Field").update({ RelationshipDeleteBehavior: 1 }, "SP.FieldLookup");
+
+Use the setShowInDisplayForm method to add a field to the display form.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in display form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInDisplayForm(true);
+// show field called 'My Field' in display form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInDisplayForm(true);
+
+Use the setShowInEditForm method to add a field to the edit form.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in edit form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInEditForm(true);
+// show field called 'My Field' in edit form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInEditForm(true);
+
+Use the setShowInNewForm method to add a field to the display form.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in new form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInNewForm(true);
+// show field called 'My Field' in new form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInNewForm(true);
+
+
+
+
+
+
+
+ One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.
+Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const blob: Blob = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.avi").getBlob();
+
+const buffer: ArrayBuffer = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.avi").getBuffer();
+
+const json: any = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.json").getJSON();
+
+const text: string = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.txt").getText();
+
+// all of these also work from a file object no matter how you access it
+const text2: string = await sp.web.getFolderByServerRelativePath("/sites/dev/documents").files.getByUrl("file.txt").getText();
+
+This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+
+const sp = spfi(...);
+
+const url = "{absolute file url OR sharing url}";
+
+// file is an IFile and supports all the file operations
+const file = sp.web.getFileByUrl(url);
+
+// for example
+const fileContent = await file.getText();
+
+Added in 3.3.0
+Utility method allowing you to get an IFile reference using any SPQueryable as a base and the server relative path to the file. Helpful when you do not have convenient access to an IWeb to use getFileByServerRelativePath
.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromServerRelativePath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromServerRelativePath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+Added in 3.8.0
+ +Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.
+++Works across site collections within the same tenant
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromAbsolutePath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromAbsolutePath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+Added in 3.8.0
+ +Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute OR server relative path to the file.
+++Works across site collections within the same tenant
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromPath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromPath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+const url2 = "/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file2 = fileFromPath(sp.web, url2);
+
+// for example
+const fileContent2 = await file2.getText();
+
+Likewise you can add files using one of two methods, addUsingPath or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.
+The addUsingPath method, supports the percent or pound characters in file names.
+When using EnsureUniqueFileName property, you must omit the Overwrite parameter.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+//Sample uses pure JavaScript to access the input tag of type="file" ->https://www.w3schools.com/tags/att_input_type_file.asp
+let file = <HTMLInputElement>document.getElementById("thefileinput");
+const fileNamePath = encodeURI(file.name);
+let result: IFileAddResult;
+// you can adjust this number to control what size files are uploaded in chunks
+if (file.size <= 10485760) {
+ // small upload
+ result = await sp.web.getFolderByServerRelativePath("Shared Documents").files.addUsingPath(fileNamePath, file, { Overwrite: true });
+} else {
+ // large upload
+ result = await sp.web.getFolderByServerRelativePath("Shared Documents").files.addChunked(fileNamePath, file, data => {
+ console.log(`progress`);
+ }, true);
+}
+
+console.log(`Result of file upload: ${JSON.stringify(result)}`);
+
+If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams.
+ +// triggers auto-application of extensions, in this case to add getStream
+import { spfi } from "@pnp/sp";
+import "@pnp/nodejs";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/folders/list";
+import "@pnp/sp/files/folder";
+import { createReadStream } from 'fs';
+
+// get a stream of an existing file
+const stream = createReadStream("c:/temp/file.txt");
+
+// now add the stream as a new file
+const sp = spfi(...);
+
+const fr = await sp.web.lists.getByTitle("Documents").rootFolder.files.addChunked( "new.txt", stream, undefined, true );
+
+You can also update the file properties of a newly uploaded file using code similar to the below snippet:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+const file = await sp.web.getFolderByServerRelativePath("/sites/dev/Shared%20Documents/test/").files.addUsingPath("file.name", "content", {Overwrite: true});
+const item = await file.file.getItem();
+await item.update({
+ Title: "A Title",
+ OtherField: "My Other Value"
+});
+
+You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file.
+ +import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+await sp.web.getFileByServerRelativePath("/sites/dev/documents/test.txt").setContent("New string content for the file.");
+
+await sp.web.getFileByServerRelativePath("/sites/dev/documents/test.mp4").setContentChunked(file);
+
+The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.
+Check in takes two optional arguments, comment and check in type.
+import { spfi } from "@pnp/sp";
+import { CheckinType } from "@pnp/sp/files";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// default options with empty comment and CheckinType.Major
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin();
+console.log("File checked in!");
+
+// supply a comment (< 1024 chars) and using default check in type CheckinType.Major
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin("A comment");
+console.log("File checked in!");
+
+// Supply both comment and check in type
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite);
+console.log("File checked in!");
+
+Check out takes no arguments.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkout();
+console.log("File checked out!");
+
+You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").approve("Approval Comment");
+console.log("File approved!");
+
+// deny with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").deny();
+console.log("File denied!");
+
+// deny with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").deny("Deny comment");
+console.log("File denied!");
+
+You can both publish and unpublish a file using the library. Both methods take an optional comment argument.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// publish with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").publish();
+console.log("File published!");
+
+// publish with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").publish("Publish comment");
+console.log("File published!");
+
+// unpublish with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").unpublish();
+console.log("File unpublished!");
+
+// unpublish with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment");
+console.log("File unpublished!");
+
+Both the addChunked and setContentChunked methods support options beyond just supplying the file content.
+ +A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:
+(data: ChunkedFileUploadProgressData) => void
The data interface is:
+export interface ChunkedFileUploadProgressData {
+ stage: "starting" | "continue" | "finishing";
+ blockNumber: number;
+ totalBlocks: number;
+ chunkSize: number;
+ currentPointer: number;
+ fileSize: number;
+}
+
+This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.
+This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.
+import { spFI, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem();
+console.log(item);
+
+const item2 = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem("Title", "Modified");
+console.log(item2);
+
+// you can also chain directly off this item instance
+const perms = await item.getCurrentUserEffectivePermissions();
+console.log(perms);
+
+You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.
+import { spFI, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/items";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// also supports typing the objects so your type will be a union type
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem<{ Id: number, Title: string }>("Id", "Title");
+
+// You get intellisense and proper typing of the returned object
+console.log(`Id: ${item.Id} -- ${item.Title}`);
+
+// You can also chain directly off this item instance
+const perms = await item.getCurrentUserEffectivePermissions();
+console.log(perms);
+
+It's possible to move a file to a new destination within a site collection
+++If you change the filename during the move operation this is considered an "edit" and the file's modified information will be updated regardless of the "RetainEditorAndModifiedOnMove" setting.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").moveByPath(destinationUrl, false, true);
+
+Added in 3.7.0
+You can also supply a set of detailed options to better control the move process:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/new-file.docx").moveByPath(destinationUrl, false, {
+ KeepBoth: false,
+ RetainEditorAndModifiedOnMove: true,
+ ShouldBypassSharedLocks: false,
+});
+
+It's possible to copy a file to a new destination within a site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyTo(destinationUrl, false);
+
+It's possible to copy a file to a new destination within the same or a different site collection.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyByPath(destinationUrl, false, true);
+
+Added in 3.7.0
+You can also supply a set of detailed options to better control the copy process:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyByPath(destinationUrl, false, {
+ KeepBoth: false,
+ ResetAuthorAndCreatedOnCopy: true,
+ ShouldBypassSharedLocks: false,
+});
+
+You can get a file by Id from a web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import { IFile } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const file: IFile = sp.web.getFileById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+
+Deletes a file
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("filename.txt").delete();
+
+Deletes a file with options
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("filename.txt").deleteWithParams({
+ BypassSharedLock: true,
+});
+
+Checks to see if a file exists
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+const exists = await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("name.txt").exists();
+
+Gets the user who currently has this file locked for shared use
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+const user = await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("name.txt").getLockedByUser();
+
+
+
+
+
+
+
+ Folders serve as a container for your files and list items.
+Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// gets web's folders
+const webFolders = await sp.web.folders();
+
+// gets list's folders
+const listFolders = await sp.web.lists.getByTitle("My List").rootFolder.folders();
+
+// gets item's folders
+const itemFolders = await sp.web.lists.getByTitle("My List").items.getById(1).folder.folders();
+
+Added in 3.3.0
+Utility method allowing you to get an IFolder reference using any SPQueryable as a base and the server relative path to the folder. Helpful when you do not have convenient access to an IWeb to use getFolderByServerRelativePath
.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromServerRelativePath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "/sites/dev/documents/folder4";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromServerRelativePath(sp.web, url);
+
+Added in 3.8.0
+Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.
+++Works across site collections within the same tenant
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromAbsolutePath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromAbsolutePath(sp.web, url);
+
+// for example
+const folderInfo = await folder();
+
+Added in 3.8.0
+Utility method allowing you to get an IFolder reference using any SPQueryable as a base and an absolute OR server relative path to the file.
+++Works across site collections within the same tenant
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromPath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromPath(sp.web, url);
+
+// for example
+const folderInfo = await folder();
+
+const url2 = "/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder2 = folderFromPath(sp.web, url2);
+
+// for example
+const folderInfo2 = await folder2();
+
+Adds a new folder to collection of folders
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// creates a new folder for web with specified url
+const folderAddResult = await sp.web.folders.addUsingPath("folder url");
+
+Gets a folder instance from a collection by folder's name
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder = await sp.web.folders.getByUrl("folder name")();
+
+Represents an instance of a SharePoint folder.
+ +import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// web's folder
+const rootFolder = await sp.web.rootFolder();
+
+// list's folder
+const listRootFolder = await sp.web.lists.getByTitle("234").rootFolder();
+
+// item's folder
+const itemFolder = await sp.web.lists.getByTitle("234").items.getById(1).folder();
+
+Gets list item associated with a folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folderItem = await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").getItem();
+
+Added in 3.8.0
+Gets a set of metrics describing the total file size contained in the folder.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const metrics = await sp.web.getFolderByServerRelativePath("/sites/dev/shared documents/target").storageMetrics();
+
+// you can also select specific metrics if desired:
+const metrics2 = await sp.web.getFolderByServerRelativePath("/sites/dev/shared documents/target").storageMetrics.select("TotalSize")();
+
+It's possible to move a folder to a new destination within the same or a different site collection
+++If you change the filename during the move operation this is considered an "edit" and the file's modified information will be updated regardless of the "RetainEditorAndModifiedOnMove" setting.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new folder
+const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
+
+await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").moveByPath(destinationUrl, true);
+
+Added in 3.8.0
+You can also supply a set of detailed options to better control the move process:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/folder`;
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents/folder").moveByPath(destinationUrl, {
+ KeepBoth: false,
+ RetainEditorAndModifiedOnMove: true,
+ ShouldBypassSharedLocks: false,
+});
+
+It's possible to copy a folder to a new destination within the same or a different site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new folder
+const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
+
+await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").copyByPath(destinationUrl, true);
+
+Added in 3.8.0
+You can also supply a set of detailed options to better control the copy process:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/folder`;
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents/folder").copyByPath(destinationUrl, false, {
+ KeepBoth: false,
+ ResetAuthorAndCreatedOnCopy: true,
+ ShouldBypassSharedLocks: false,
+});
+
+Deletes a folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").delete();
+
+Deletes a folder with options
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").deleteWithParams({
+ BypassSharedLock: true,
+ DeleteIfEmpty: true,
+ });
+
+Recycles a folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").recycle();
+
+Gets folder's server relative url
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const relUrl = await sp.web.rootFolder.folders.getByUrl("SiteAssets").select('ServerRelativeUrl')();
+
+Updates folder's properties
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").update({
+ "Name": "New name",
+ });
+
+Gets content type order of a folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const order = await sp.web.getFolderByServerRelativePath("Shared Documents").select('contentTypeOrder')();
+
+Gets all child folders associated with the current folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folders = await sp.web.rootFolder.folders();
+
+Gets all files inside a folder
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/files/folder";
+
+const sp = spfi(...);
+
+const files = await sp.web.getFolderByServerRelativePath("Shared Documents").files();
+
+Gets this folder's list item field values
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const itemFields = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").listItemAllFields();
+
+Gets the parent folder, if available
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const parentFolder = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").parentFolder();
+
+Gets this folder's properties
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const properties = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").properties();
+
+Gets a value that specifies the content type order.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const contentTypeOrder = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").select('uniqueContentTypeOrder')();
+
+You can rename a folder by updating FileLeafRef
property:
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder = sp.web.getFolderByServerRelativePath("Shared Documents/My Folder");
+
+const item = await folder.getItem();
+const result = await item.update({ FileLeafRef: "Folder2" });
+
+Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const newFolderResult = await sp.web.rootFolder.folders.getByUrl("Shared Documents").folders.addUsingPath("My New Folder");
+const item = await newFolderResult.folder.listItemAllFields();
+
+await sp.web.lists.getByTitle("Documents").items.getById(item.ID).update({
+ ContentTypeId: "0x0120001E76ED75A3E3F3408811F0BF56C4CDDD",
+ MyFolderField: "field value",
+ Title: "My New Folder",
+});
+
+You can use the addSubFolderUsingPath method to add a folder with some special chars supported
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// add a folder to site assets
+const folder: IFolder = await sp.web.rootFolder.folders.getByUrl("SiteAssets").addSubFolderUsingPath("folder name");
+
+You can get a folder by Id from a web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+
+Gets information about folder, including details about the parent list, parent list root folder, and parent web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+await folder.getParentInfos();
+
+
+
+
+
+
+
+ Forms in SharePoint are the Display, New, and Edit forms associated with a list.
+Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/forms";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the field by Id for web
+const form = sp.web.lists.getByTitle("Documents").forms.getById("{c4486774-f1e2-4804-96f3-91edf3e22a19}")();
+
+
+
+
+
+
+
+ The @pnp/sp/groupsitemanager
package represents calls to _api/groupsitemanager
endpoint and is accessible from any site url.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/groupsitemanager";
+
+const sp = spfi(...);
+
+// call method to check if the current user can create Microsoft 365 groups
+const isUserAllowed = await sp.groupSiteManager.canUserCreateGroup();
+
+// call method to delete a group-connected site
+await sp.groupSiteManager.delete("https://contoso.sharepoint.com/sites/hrteam");
+
+//call method to gets labels configured for the tenant
+const orgLabels = await sp.groupSiteManager.getAllOrgLabels(0);
+
+//call method to get information regarding site groupification configuration for the current site context
+const groupCreationContext = await sp.groupSiteManager.getGroupCreationContext();
+
+//call method to get information regarding site groupification configuration for the current site context
+const siteData = await sp.groupSiteManager.getGroupSiteConversionData();
+
+// call method to get teams membership for a user
+const userTeams = await sp.groupSiteManager.getUserTeamConnectedMemberGroups("meganb@contoso.onmicrosoft.com");
+
+// call method to get shared channel memberhsip for user
+const sharedChannels = await sp.groupSiteManager.getUserSharedChannelMemberGroups("meganb@contoso.onmicrosoft.com");
+
+//call method to get valid site url from Alias
+const siteUrl = await sp.groupSiteManager.getValidSiteUrlFromAlias("contoso");
+
+//call method to check if teamify prompt is hidden
+const isTeamifyPromptHidden = await sp.groupSiteManager.isTeamifyPromptHidden("https://contoso.sharepoint.com/sites/hrteam");
+
+++ + + + + + +For more information on the methods available and how to use them, please review the code comments in the source.
+
This module helps you with working with hub sites in your tenant.
+import { spfi } from "@pnp/sp";
+import { IHubSiteInfo } from "@pnp/sp/hubsites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+// invoke the hub sites object
+const hubsites: IHubSiteInfo[] = await sp.hubSites();
+
+// you can also use select to only return certain fields:
+const hubsites2: IHubSiteInfo[] = await sp.hubSites.select("ID", "Title", "RelatedHubSiteIds")();
+
+Using the getById method on the hubsites module to get a hub site by site Id (guid).
+import { spfi } from "@pnp/sp";
+import { IHubSiteInfo } from "@pnp/sp/hubsites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+const hubsite: IHubSiteInfo = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb")();
+
+// log hub site title to console
+console.log(hubsite.Title);
+
+We provide a helper method to load the ISite instance from the HubSite
+import { spfi } from "@pnp/sp";
+import { ISite } from "@pnp/sp/sites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+const site: ISite = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb").getSite();
+
+const siteData = await site();
+
+console.log(siteData.Title);
+
+import { spfi } from "@pnp/sp";
+import { IHubSiteWebData } from "@pnp/sp/hubsites";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+
+const webData: Partial<IHubSiteWebData> = await sp.web.hubSiteData();
+
+// you can also force a refresh of the hub site data
+const webData2: Partial<IHubSiteWebData> = await sp.web.hubSiteData(true);
+
+Allows you to apply theme updates from the parent hub site collection.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+
+await sp.web.syncHubSiteTheme();
+
+You manage hub sites at the Site level.
+Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// join a site to a hub site
+await sp.site.joinHubSite("{parent hub site id}");
+
+// remove a site from a hub site
+await sp.site.joinHubSite("00000000-0000-0000-0000-000000000000");
+
+Registers the current site collection as hub site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// register current site as a hub site
+await sp.site.registerHubSite();
+
+Un-registers the current site collection as hub site collection.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// make a site no longer a hub
+await sp.site.unRegisterHubSite();
+
+
+
+
+
+
+
+ Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// get all the items from a list
+const items: any[] = await sp.web.lists.getByTitle("My List").items();
+console.log(items);
+
+// get a specific item by id.
+const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
+console.log(item);
+
+// use odata operators for more efficient queries
+const items2: any[] = await sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true)();
+console.log(items2);
+
+Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// basic case to get paged items form a list
+const items = await sp.web.lists.getByTitle("BigList").items.getPaged();
+
+// you can also provide a type for the returned values instead of any
+const items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>();
+
+// the query also works with select to choose certain fields and top to set the page size
+const items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>();
+
+// the results object will have two properties and one method:
+
+// the results property will be an array of the items returned
+if (items.results.length > 0) {
+ console.log("We got results!");
+
+ for (let i = 0; i < items.results.length; i++) {
+ // type checking works here if we specify the return type
+ console.log(items.results[i].Title);
+ }
+}
+
+// the hasNext property is used with the getNext method to handle paging
+// hasNext will be true so long as there are additional results
+if (items.hasNext) {
+
+ // this will carry over the type specified in the original query for the results array
+ items = await items.getNext();
+ console.log(items.results.length);
+}
+
+The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// Using RowLimit. Enables paging
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'});
+
+// Use QueryOptions to make a XML-style query.
+// Because it's XML we need to escape special characters
+// Instead of & we use & in the query
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext="Paged=TRUE&p_ID=5" />'});
+
+// Get everything. Using null with ChangeToken gets everything
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null});
+
+
+Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used.
+++In v3 there is a separate import for get-all to include the functionality. This is to remove the code from bundles for folks who do not need it.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/items/get-all";
+
+const sp = spfi(...);
+
+// basic usage
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll();
+console.log(allItems.length);
+
+// set page size
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll(4000);
+console.log(allItems.length);
+
+// use select and top. top will set page size and override the any value passed to getAll
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll();
+console.log(allItems.length);
+
+// we can also use filter as a supported odata operation, but this will likely fail on large lists
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll();
+console.log(allItems.length);
+
+When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const items = await sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
+console.log(items);
+
+const item = await sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
+console.log(item);
+
+To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+
+const r = await sp.web.lists.getByTitle("TaxonomyList").getItemsByCAMLQuery({
+ ViewXml: `<View><Query><Where><Eq><FieldRef Name="MetaData"/><Value Type="TaxonomyFieldType">Term 2</Value></Eq></Where></Query></View>`,
+});
+
+The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { Web } from "@pnp/sp/webs";
+
+try {
+ const sp = spfi("https://{publishing site url}").using(SPFx(this.context));
+
+ const r = await sp.web.lists.getByTitle("Pages").items
+ .select("Title", "FileRef", "FieldValuesAsText/MetaInfo")
+ .expand("FieldValuesAsText")
+ ();
+
+ // look through the returned items.
+ for (var i = 0; i < r.length; i++) {
+
+ // the title field value
+ console.log(r[i].Title);
+
+ // find the value in the MetaInfo string using regex
+ const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);
+ if (matches !== null && matches.length > 1) {
+
+ // this wil be the value of the PublishingPageImage field
+ console.log(matches[1]);
+ }
+ }
+}
+catch (e) {
+ console.error(e);
+}
+
+There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { IItemAddResult } from "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// add an item to the list
+const iar: IItemAddResult = await sp.web.lists.getByTitle("My List").items.add({
+ Title: "Title",
+ Description: "Description"
+});
+
+console.log(iar);
+
+You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+await sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({
+ Title: "Test 1",
+ ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF"
+});
+
+There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.
+Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an array. Examples for both are shown below.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { getGUID } from "@pnp/core";
+
+const sp = spfi(...);
+
+const i = await sp.web.lists.getByTitle("PeopleFields").items.add({
+ Title: getGUID(),
+ User1Id: 9, // allows a single user
+ User2Id: [16, 45] // allows multiple users
+});
+
+console.log(i);
+
+If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{
+ FieldName: "UserField",
+ FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]),
+},
+{
+ FieldName: "Title",
+ FieldValue: "Test - Updated",
+}]);
+
+What is said for User Fields is, in general, relevant to Lookup Fields:
+Id
suffix should be appended to the end of lookups EntityPropertyName
in payloadsimport { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { getGUID } from "@pnp/core";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("LookupFields").items.add({
+ Title: getGUID(),
+ LookupFieldId: 2, // allows a single lookup value
+ MultiLookupFieldId: [1, 56] // allows multiple lookup value
+});
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const list = batchedSP.web.lists.getByTitle("rapidadd");
+
+let res = [];
+
+list.items.add({ Title: "Batch 6" }).then(r => res.push(r));
+
+list.items.add({ Title: "Batch 7" }).then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+ ///Do something with the results
+}
+
+The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.
+++Note: For updating certain types of fields, see the Add examples above. The payload will be the same you will just need to replace the .add method with .getById({itemId}).update.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+const i = await list.items.getById(1).update({
+ Title: "My New Title",
+ Description: "Here is a new description"
+});
+
+console.log(i);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// you are getting back a collection here
+const items: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'")();
+
+// see if we got something
+if (items.length > 0) {
+ const updatedItem = await sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({
+ Title: "Updated Title",
+ });
+
+ console.log(JSON.stringify(updatedItem));
+}
+
+This approach avoids multiple calls for the same list's entity type name.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching"
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const list = batchedSP.web.lists.getByTitle("rapidupdate");
+
+list.items.getById(1).update({ Title: "Batch 6" }).then(b => {
+ console.log(b);
+});
+
+list.items.getById(2).update({ Title: "Batch 7" }).then(b => {
+ console.log(b);
+});
+
+// Executes the batched calls
+await execute();
+
+console.log("Done");
+
+Note: Updating Taxonomy field for a File item should be handled differently. Instead of using update(), use validateUpdateListItem(). Please see below
+List Item
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("Demo").items.getById(1).update({
+ MetaDataColumn: { Label: "Demo", TermGuid: '883e4c81-e8f9-4f19-b90b-6ab805c9f626', WssId: '-1' }
+});
+
+
+File List Item
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+await (await sp.web.getFileByServerRelativePath("/sites/demo/DemoLibrary/File.txt").getItem()).validateUpdateListItem([{
+ FieldName: "MetaDataColumn",
+ FieldValue:"Demo|883e4c81-e8f9-4f19-b90b-6ab805c9f626", //Label|TermGuid
+}]);
+
+Based on this excellent article from Beau Cameron.
+As he says you must update a hidden field to get this to work via REST. My meta data field accepting multiple values is called "MultiMetaData".
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// first we need to get the hidden field's internal name.
+// The Title of that hidden field is, in my case and in the linked article just the visible field name with "_0" appended.
+const fields = await sp.web.lists.getByTitle("TestList").fields.filter("Title eq 'MultiMetaData_0'").select("Title", "InternalName")();
+// get an item to update, here we just create one for testing
+const newItem = await sp.web.lists.getByTitle("TestList").items.add({
+ Title: "Testing",
+});
+// now we have to create an update object
+// to do that for each field value you need to serialize each as -1;#{field label}|{field id} joined by ";#"
+// update with the values you want, this also works in the add call directly to avoid a second call
+const updateVal = {};
+updateVal[fields[0].InternalName] = "-1;#New Term|bb046161-49cc-41bd-a459-5667175920d4;#-1;#New 2|0069972e-67f1-4c5e-99b6-24ac5c90b7c9";
+// execute the update call
+await newItem.item.update(updateVal);
+
+Please see the issue for full details.
+You will need to use validateUpdateListItem
to ensure hte BCS field is updated correctly.
const update = await sp.web.lists.getByTitle("Price").items.getById(7).select('*,External').validateUpdateListItem([
+ {FieldName:"External",FieldValue:"Fauntleroy Circus"},
+ {FieldName:"Customers_ID", FieldValue:"__bk410024003500240054006500"}
+ ]);
+
+To send an item to the recycle bin use recycle.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+const recycleBinIdentifier = await list.items.getById(1).recycle();
+
+Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+await list.items.getById(1).delete();
+
+Deletes the item object with options.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+await list.items.getById(1).deleteWithParams({
+ BypassSharedLock: true,
+ });
+
+++The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true
+
It's a very common mistake trying wrong field names in the requests.
+Field's EntityPropertyName
value should be used.
The easiest way to get know EntityPropertyName is to use the following snippet:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const response =
+ await sp.web.lists
+ .getByTitle('[Lists_Title]')
+ .fields
+ .select('Title, EntityPropertyName')
+ .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)
+ ();
+
+console.log(response.map(field => {
+ return {
+ Title: field.Title,
+ EntityPropertyName: field.EntityPropertyName
+ };
+}));
+
+Lookup fields' names should be ended with additional Id
suffix. E.g. for Editor
EntityPropertyName EditorId
should be used.
Gets information about an item, including details about the parent list, parent list root folder, and parent web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
+await item.getParentInfos();
+
+
+
+
+
+
+
+ Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.
+Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the list by Id
+const list = sp.web.lists.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
+
+// we can use this 'list' variable to execute more queries on the list:
+const r = await list.select("Title")();
+
+// show the response from the server
+console.log(r.Title);
+
+You can also get a list from the collection by title.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the default document library 'Documents'
+const list = sp.web.lists.getByTitle("Documents");
+
+// we can use this 'list' variable to run more queries on the list:
+const r = await list.select("Id")();
+
+// log the list Id to console
+console.log(r.Id);
+
+You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// create a new list, passing only the title
+const listAddResult = await sp.web.lists.add("My new list");
+
+// we can work with the list created using the IListAddResult.list property:
+const r = await listAddResult.list.select("Title")();
+
+// log newly created list title to console
+console.log(r.Title);
+});
+
+You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs.
+// this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings)
+const listAddResult = await sp.web.lists.add("My Doc Library", "This is a description of doc lib.", 101, true, { OnQuickLaunch: true });
+
+// get the Id of the newly added document library
+const r = await listAddResult.list.select("Id")();
+
+// log id to console
+console.log(r.Id);
+
+Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings.
+ +import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default):
+const listEnsureResult = await sp.web.lists.ensure("My List");
+
+// check if the list was created, or if it already existed:
+if (listEnsureResult.created) {
+ console.log("My List was created!");
+} else {
+ console.log("My List already existed!");
+}
+
+// work on the created/updated list
+const r = await listEnsureResult.list.select("Id")();
+
+// log the Id
+console.log(r.Id);
+
+If the list already exists, the other settings you provide will be used to update the existing list.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// add a new list to the lists collection of the web
+sp.web.lists.add("My List 2").then(async () => {
+
+// then call ensure on the created list with an updated description
+const listEnsureResult = await sp.web.lists.ensure("My List 2", "Updated description");
+
+// get the updated description
+const r = await listEnsureResult.list.select("Description")();
+
+// log the updated description
+console.log(r.Description);
+});
+
+Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// get Site Assets library
+const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary();
+
+// get the Title
+const r = await siteAssetsList.select("Title")();
+
+// log Title
+console.log(r.Title);
+
+Gets a list that is the default location for wiki pages.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// get Site Pages library
+const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary();
+
+// get the Title
+const r = await siteAssetsList.select("Title")();
+
+// log Title
+console.log(r.Title);
+
+Scenario | +Import Statement | +
---|---|
Selective 1 | +import { List, IList } from "@pnp/sp/lists"; | +
Selective 2 | +import "@pnp/sp/lists"; | +
Preset: All | +import { sp, List, IList } from "@pnp/sp/presets/all"; | +
Preset: Core | +import { sp, List, IList } from "@pnp/sp/presets/core"; | +
Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is "*")
+import { IListUpdateResult } from "@pnp/sp/lists";
+
+// create a TypedHash object with the properties to update
+const updateProperties = {
+ Description: "This list title and description has been updated using PnPjs.",
+ Title: "Updated title",
+};
+
+// update the list with the properties above
+list.update(updateProperties).then(async (l: IListUpdateResult) => {
+
+ // get the updated title and description
+ const r = await l.list.select("Title", "Description")();
+
+ // log the updated properties to the console
+ console.log(r.Title);
+ console.log(r.Description);
+});
+
+From the change log, you can get a collection of changes that have occurred within the list based on the specified query.
+import { IChangeQuery } from "@pnp/sp";
+
+// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore
+const changeQuery: IChangeQuery = {
+ Add: true,
+ ChangeTokenEnd: null,
+ ChangeTokenStart: null,
+ DeleteObject: true,
+ Rename: true,
+ Restore: true,
+};
+
+// get list changes
+const r = await list.getChanges(changeQuery);
+
+// log changes to console
+console.log(r);
+
+To get changes from a specific time range you can use the ChangeTokenStart or a combination of ChangeTokenStart and ChangeTokenEnd.
+import { IChangeQuery } from "@pnp/sp";
+
+//Resource is the list Id (as Guid)
+const resource = list.Id;
+const changeStart = new Date("2022-02-22").getTime();
+const changeTokenStart = `1;3;${resource};${changeStart};-1`;
+
+// build the changeQuery object, here we look at changes regarding Add and Update for Items.
+const changeQuery: IChangeQuery = {
+ Add: true,
+ Update: true,
+ Item: true,
+ ChangeTokenEnd: null,
+ ChangeTokenStart: { StringValue: changeTokenStart },
+};
+
+// get list changes
+const r = await list.getChanges(changeQuery);
+
+// log changes to console
+console.log(r);
+
+You can get items from SharePoint using a CAML Query.
+import { ICamlQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const caml: ICamlQuery = {
+ ViewXml: "<View><ViewFields><FieldRef Name='Title' /></ViewFields><RowLimit>5</RowLimit></View>",
+};
+
+// get list items
+const r = await list.getItemsByCAMLQuery(caml);
+
+// log resulting array to console
+console.log(r);
+
+If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment)
+import { ICamlQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const caml: ICamlQuery = {
+ ViewXml: "<View><ViewFields><FieldRef Name='Title' /><FieldRef Name='RoleAssignments' /></ViewFields><RowLimit>5</RowLimit></View>",
+};
+
+// get list items
+const r = await list.getItemsByCAMLQuery(caml, "RoleAssignments");
+
+// log resulting item array to console
+console.log(r);
+
+import { IChangeLogItemQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const changeLogItemQuery: IChangeLogItemQuery = {
+ Contains: `<Contains><FieldRef Name="Title"/><Value Type="Text">Item16</Value></Contains>`,
+ QueryOptions: `<QueryOptions>
+ <IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns>
+ <DateInUtc>False</DateInUtc>
+ <IncludePermissions>TRUE</IncludePermissions>
+ <IncludeAttachmentUrls>FALSE</IncludeAttachmentUrls>
+ <Folder>My List</Folder></QueryOptions>`,
+};
+
+// get list items
+const r = await list.getListItemChangesSinceToken(changeLogItemQuery);
+
+// log resulting XML to console
+console.log(r);
+
+Removes the list from the web's list collection and puts it in the recycle bin.
+await list.recycle();
+
+import { IRenderListData } from "@pnp/sp/lists";
+
+// render list data, top 5 items
+const r: IRenderListData = await list.renderListData("<View><RowLimit>5</RowLimit></View>");
+
+// log array of items in response
+console.log(r.Row);
+
+import { IRenderListDataParameters } from "@pnp/sp/lists";
+// setup parameters object
+const renderListDataParams: IRenderListDataParameters = {
+ ViewXml: "<View><RowLimit>5</RowLimit></View>",
+};
+// render list data as stream
+const r = await list.renderListDataAsStream(renderListDataParams);
+// log array of items in response
+console.log(r.Row);
+
+You can also supply other options to renderListDataAsStream including override parameters and query params. This can be helpful when looking to apply sorting to the returned data.
+import { IRenderListDataParameters } from "@pnp/sp/lists";
+// setup parameters object
+const renderListDataParams: IRenderListDataParameters = {
+ ViewXml: "<View><RowLimit>5</RowLimit></View>",
+};
+const overrideParams = {
+ ViewId = "{view guid}"
+};
+// OR if you don't want to supply override params use null
+// overrideParams = null;
+// Set the query params using a map
+const query = new Map<string, string>();
+query.set("SortField", "{AField}");
+query.set("SortDir", "Desc");
+// render list data as stream
+const r = await list.renderListDataAsStream(renderListDataParams, overrideParams, query);
+// log array of items in response
+console.log(r.Row);
+
+const listItemId = await list.reserveListItemId();
+
+// log id to console
+console.log(listItemId);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const list = await sp.webs.lists.getByTitle("MyList").select("Title", "ParentWebUrl")();
+const formValues: IListItemFormUpdateValue[] = [
+ {
+ FieldName: "Title",
+ FieldValue: title,
+ },
+ ];
+
+list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)
+
+
+Get all content types for a list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+import "@pnp/sp/content-types/list";
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.contentTypes();
+
+Scenario | +Import Statement | +
---|---|
Selective 1 | +import "@pnp/sp/fields"; | +
Selective 2 | +import "@pnp/sp/fields/list"; | +
Preset: All | +import { sp } from "@pnp/sp/presets/all"; | +
Get all the fields for a list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+import "@pnp/sp/fields/list";
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.fields();
+
+Add a field to the site, then add the site field to a list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+const fld = await sp.site.rootWeb.fields.addText("MyField");
+await sp.web.lists.getByTitle("MyList").fields.createFieldAsXml(fld.data.SchemaXml);
+
+Get the root folder of a list.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/folders/list";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.rootFolder();
+
+import "@pnp/sp/forms/list";
+
+const r = await list.forms();
+
+Get a collection of list items.
+import "@pnp/sp/items/list";
+
+const r = await list.items();
+
+Get the default view of the list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views/list";
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const views = await list.views();
+const defaultView = await list.defaultView();
+
+Get a list view by Id
+const view = await list.getView(defaultView.Id).select("Title")();
+
+To work with list security, you can import the list methods as follows:
+import "@pnp/sp/security/list";
+
+For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.
+Get all subscriptions on the list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions/list";
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const subscriptions = await list.subscriptions();
+
+Get a collection of the list's user custom actions.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/user-custom-actions/web"
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.userCustomActions();
+
+Gets information about an list, including details about the parent list root folder, and parent web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("Documents");
+await list.getParentInfos();
+
+
+
+
+
+
+
+ The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.
+The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\,containingcomma
+NOTE: the , separator can be escaped using the \ as escape character as done in the example above. The string above would split like:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.
+const state = await sp.navigation.getMenuState();
+
+// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5
+const state2 = await sp.navigation.getMenuState("1002", 5);
+
+// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5
+const state3 = await sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode");
+
+Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+const key = await sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx");
+
+Scenario | +Import Statement | +
---|---|
Selective 1 | +import "@pnp/sp/webs"; import "@pnp/sp/navigation"; |
+
The navigation object contains two properties "quicklaunch" and "topnavigationbar". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+const top = await sp.web.navigation.topNavigationBar();
+const quick = await sp.web.navigation.quicklaunch();
+
+For the following examples we will refer to a variable named "nav" that is understood to be one of topNavigationBar or quicklaunch:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+// note we are just getting a ref to the nav object, not executing a request
+const nav = sp.web.navigation.topNavigationBar;
+// -- OR --
+// note we are just getting a ref to the nav object, not executing a request
+const nav = sp.web.navigation.quicklaunch;
+
+import "@pnp/sp/navigation";
+
+const node = await nav.getById(3)();
+
+import "@pnp/sp/navigation";
+
+const result = await nav.add("Node Title", "/sites/dev/pages/mypage.aspx", true);
+
+const nodeDataRaw = result.data;
+
+// request the data from the created node
+const nodeData = result.node();
+
+Places a navigation node after another node in the tree
+import "@pnp/sp/navigation";
+
+const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true);
+const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true);
+const node1 = await node1result.node();
+const node2 = await node2result.node();
+
+await nav.moveAfter(node1.Id, node2.Id);
+
+Deletes a given node
+import "@pnp/sp/navigation";
+
+const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true);
+let nodes = await nav();
+// check we added a node
+let index = nodes.findIndex(n => n.Id === node1result.data.Id)
+// index >= 0
+
+// delete a node
+await nav.getById(node1result.data.Id).delete();
+
+nodes = await nav();
+index = nodes.findIndex(n => n.Id === node1result.data.Id)
+// index = -1
+
+You are able to update various properties of a given node, such as the the Title, Url, IsVisible.
+You may update the Audience Targeting value for the node by passing in Microsoft Group IDs in the AudienceIds array. Be aware, Audience Targeting must already be enabled on the navigation.
+import "@pnp/sp/navigation";
+
+
+await nav.getById(4).update({
+ Title: "A new title",
+ AudienceIds:["d50f9511-b811-4d76-b20a-0d6e1c8095f7"],
+ Url:"/sites/dev/SitePages/home.aspx",
+ IsVisible:false
+});
+
+The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch.
+import "@pnp/sp/navigation";
+
+const childrenData = await nav.getById(1).children();
+
+// add a child
+await nav.getById(1).children.add("Title", "Url", true);
+
+
+
+
+
+
+
+ A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.
+Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.
+This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const roles = await sp.web.roleAssignments();
+
+This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const obj = await sp.web.firstUniqueAncestorSecurableObject();
+
+This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com");
+
+const perms2 = await sp.web.getCurrentUserEffectivePermissions();
+
+Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems);
+
+const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems);
+
+If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.getCurrentUserEffectivePermissions();
+if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {
+ // ...
+}
+
+
+
+
+
+
+
+ The profile services allows you to work with the SharePoint User Profile Store.
+Profiles is accessed directly from the root sp object.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/profiles";
+
+getEditProfileLink(): Promise<string>
+
+const sp = spfi(...);
+const editProfileLink = await sp.profiles.getEditProfileLink();
+
+Provides a boolean that indicates if the current users "People I'm Following" list is public or not
+getIsMyPeopleListPublic(): Promise<boolean>
+
+const sp = spfi(...);
+const isPublic = await sp.profiles.getIsMyPeopleListPublic();
+
+Provides a boolean that indicates if the current users is followed by a specific user.
+amIFollowedBy(loginName: string): Promise<boolean>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const isFollowedBy = await sp.profiles.amIFollowedBy(loginName);
+
+Provides a boolean that indicates if the current users is followed by a specific user.
+amIFollowing(loginName: string): Promise<boolean>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const following = await sp.profiles.amIFollowing(loginName);
+
+Gets the tags the current user is following. Accepts max count, default is 20.
+getFollowedTags(maxCount = 20): Promise<string[]>
+
+const sp = spfi(...);
+const tags = await sp.profiles.getFollowedTags();
+
+Gets the people who are following the specified user.
+getFollowersFor(loginName: string): Promise<any[]>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const followers = await sp.profiles.getFollowersFor(loginName);
+followers.forEach((value) => {
+ ...
+});
+
+Gets the people who are following the current user.
+myFollowers(): ISPCollection
+
+const sp = spfi(...);
+const folowers = await sp.profiles.myFollowers();
+
+Gets user properties for the current user.
+myProperties(): ISPInstance
+
+const sp = spfi(...);
+const profile = await sp.profiles.myProperties();
+console.log(profile.DisplayName);
+console.log(profile.Email);
+console.log(profile.Title);
+console.log(profile.UserProfileProperties.length);
+
+// Properties are stored in Key/Value pairs,
+// so parse into an object called userProperties
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+ props[prop.Key] = prop.Value;
+});
+profile.userProperties = props;
+console.log("Account Name: " + profile.userProperties.AccountName);
+
+// you can also select properties to return before
+const sp = spfi(...);
+const profile = await sp.profiles.myProperties.select("Title", "Email")();
+console.log(profile.Email);
+console.log(profile.Title);
+
+getPeopleFollowedBy(loginName: string): Promise<any[]>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const folowers = await sp.profiles.getFollowersFor(loginName);
+followers.forEach((value) => {
+ ...
+});
+
+getPropertiesFor(loginName: string): Promise<any>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const profile = await sp.profiles.getPropertiesFor(loginName);
+console.log(profile.DisplayName);
+console.log(profile.Email);
+console.log(profile.Title);
+console.log(profile.UserProfileProperties.length);
+
+// Properties are stored in inconvenient Key/Value pairs,
+// so parse into an object called userProperties
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+ props[prop.Key] = prop.Value;
+});
+
+profile.userProperties = props;
+console.log("Account Name: " + profile.userProperties.AccountName);
+
+Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first
+trendingTags(): Promise<IHashTagCollection>
+
+const sp = spfi(...);
+const tags = await sp.profiles.trendingTags();
+tags.Items.forEach((tag) => {
+ ...
+});
+
+getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const propertyName = "AccountName";
+const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);
+
+Removes the specified user from the user's list of suggested people to follow.
+hideSuggestion(loginName: string): Promise<void>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+await sp.profiles.hideSuggestion(loginName);
+
+Indicates whether the first user is following the second user. +First parameter is the account name of the user who might be following the followee. +Second parameter is the account name of the user who might be followed by the follower.
+isFollowing(follower: string, followee: string): Promise<boolean>
+
+const sp = spfi(...);
+const follower = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const followee = "i:0#.f|membership|testuser2@mytenant.onmicrosoft.com";
+const isFollowing = await sp.profiles.isFollowing(follower, followee);
+
+Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. +Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB.
+ +setMyProfilePic(profilePicSource: Blob): Promise<void>
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/profiles";
+import "@pnp/sp/folders";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// get the blob object through a request or from a file input
+const blob = await sp.web.lists.getByTitle("Documents").rootFolder.files.getByName("profile.jpg").getBlob();
+
+await sp.profiles.setMyProfilePic(blob);
+
+accountName The account name of the user +propertyName Property name +propertyValue Property value
+setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise<void>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+await sp.profiles.setSingleValueProfileProperty(loginName, "CellPhone", "(123) 555-1212");
+
+accountName The account name of the user +propertyName Property name +propertyValues Property values
+setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>
+
+const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const propertyName = "SPS-Skills";
+const propertyValues = ["SharePoint", "Office 365", "Architecture", "Azure"];
+await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues);
+const profile = await sp.profiles.getPropertiesFor(loginName);
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+ props[prop.Key] = prop.Value;
+});
+profile.userProperties = props;
+console.log(profile.userProperties[propertyName]);
+
+Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) +Emails The email addresses of the users to provision sites for
+createPersonalSiteEnqueueBulk(...emails: string[]): Promise<void>
+
+const sp = spfi(...);
+let userEmails: string[] = ["testuser1@mytenant.onmicrosoft.com", "testuser2@mytenant.onmicrosoft.com"];
+await sp.profiles.createPersonalSiteEnqueueBulk(userEmails);
+
+ownerUserProfile(): Promise<IUserProfile>
+
+const sp = spfi(...);
+const profile = await sp.profiles.ownerUserProfile();
+
+userProfile(): Promise<any>
+
+const sp = spfi(...);
+const profile = await sp.profiles.userProfile();
+
+createPersonalSite(interactiveRequest = false): Promise<void>
+
+const sp = spfi(...);
+await sp.profiles.createPersonalSite();
+
+Set the privacy settings for all social data.
+shareAllSocialData(share: boolean): Promise<void>
+
+const sp = spfi(...);
+await sp.profiles.shareAllSocialData(true);
+
+Resolves user or group using specified query parameters
+clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity>
+
+const sp = spfi(...);
+const result = await sp.profiles.clientPeoplePickerResolveUser({
+ AllowEmailAddresses: true,
+ AllowMultipleEntities: false,
+ MaximumEntitySuggestions: 25,
+ QueryString: 'mbowen@contoso.com'
+});
+
+Searches for users or groups using specified query parameters
+clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity[]>
+
+const sp = spfi(...);
+const result = await sp.profiles.clientPeoplePickerSearchUser({
+ AllowEmailAddresses: true,
+ AllowMultipleEntities: false,
+ MaximumEntitySuggestions: 25,
+ QueryString: 'John'
+});
+
+
+
+
+
+
+
+ Through the REST api you are able to call a SP.Publishing.SitePageService method GetCurrentUserMemberships. This method allows you to fetch identifiers of unified groups to which current user belongs. It's an alternative for using graph.me.transitiveMemberOf() method from graph package. Note, method only works with the context of a logged in user, and not with app-only permissions.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/publishing-sitepageservice";
+
+const sp = spfi(...);
+
+const groupIdentifiers = await sp.publishingSitePageService.getCurrentUserMemberships();
+
+
+
+
+
+
+
+ The contents of the recycle bin.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/recycle-bin";
+
+const sp = spfi(...);
+
+// gets contents of the web's recycle bin
+const bin = await sp.web.recycleBin();
+
+// gets a specific item from the recycle bin
+const rbItem = await sp.web.recycleBin.getById(bin[0].id);
+
+// delete the item from the recycle bin
+await rbItem.delete();
+
+// restore the item from the recycle bin
+await rbItem.restore();
+
+// move the item to the second-stage (site) recycle bin.
+await rbItem.moveToSecondStage();
+
+// deletes everything in the recycle bin
+await sp.web.recycleBin.deleteAll();
+
+// restores everything in the recycle bin
+await sp.web.recycleBin.restoreAll();
+
+// moves contents of recycle bin to second-stage (site) recycle bin.
+await sp.web.recycleBin.moveAllToSecondStage();
+
+// deletes contents of the second-stage (site) recycle bin.
+await sp.web.recycleBin.deleteAllSecondStageItems();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/recycle-bin";
+
+const sp = spfi(...);
+
+// gets contents of the second-stage recycle bin
+const ssBin = await sp.site.recycleBin();
+
+// gets a specific item from the second-stage recycle bin
+const rbItem = await sp.site.recycleBin.getById(ssBin[0].id);
+
+// delete the item from the second-stage recycle bin
+await rbItem.delete();
+
+// restore the item from the second-stage recycle bin
+await rbItem.restore();
+
+// deletes everything in the second-stage recycle bin
+await sp.site.recycleBin.deleteAll();
+
+// restores everything in the second-stage recycle bin
+await sp.site.recycleBin.restoreAll();
+
+
+
+
+
+
+
+ The regional settings module helps with managing dates and times across various timezones.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+// get all the web's regional settings
+const s = await sp.web.regionalSettings();
+
+// select only some settings to return
+const s2 = await sp.web.regionalSettings.select("DecimalSeparator", "ListSeparator", "IsUIRightToLeft")();
+
+You can get a list of the installed languages in the web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+const s = await sp.web.regionalSettings.getInstalledLanguages();
+
+++The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.
+
You can also get information about the selected timezone in the web and all of the defined timezones.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+// get the web's configured timezone
+const s = await sp.web.regionalSettings.timeZone();
+
+// select just the Description and Id
+const s2 = await sp.web.regionalSettings.timeZone.select("Description", "Id")();
+
+// get all the timezones
+const s3 = await sp.web.regionalSettings.timeZones();
+
+// get a specific timezone by id
+// list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx
+const s4 = await sp.web.regionalSettings.timeZones.getById(23);
+const s5 = await s.localTimeToUTC(new Date());
+
+// convert a given date from web's local time to UTC time
+const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date());
+
+// convert a given date from UTC time to web's local time
+const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date())
+const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))
+
+Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings";
+
+const sp = spfi(...);
+
+//
+// The below methods appears on
+// - Web
+// - List
+// - Field
+// - ContentType
+// - User Custom Action
+//
+// after you import @pnp/sp/regional-settings
+//
+// you can also import just parts of the regional settings:
+// import "@pnp/sp/regional-settings/web";
+// import "@pnp/sp/regional-settings/list";
+// import "@pnp/sp/regional-settings/content-type";
+// import "@pnp/sp/regional-settings/field";
+// import "@pnp/sp/regional-settings/user-custom-actions";
+
+
+const title = await sp.web.titleResource("en-us");
+const title2 = await sp.web.titleResource("de-de");
+
+const description = await sp.web.descriptionResource("en-us");
+const description2 = await sp.web.descriptionResource("de-de");
+
+++ + + + + + +You can only read the values through the REST API, not set the value.
+
The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.
+Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work.
+import { spfi, SPFx, extractWebUrl } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/related-items/web";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+import "@pnp/sp/files/list";
+import { IList } from "@pnp/sp/lists";
+import { getRandomString } from "@pnp/core";
+
+const sp = spfi(...);
+
+// setup some lists (or just use existing ones this is just to show the complete process)
+// we need two lists to use for creating related items, they need to use template 107 (task list)
+const ler1 = await sp.web.lists.ensure("RelatedItemsSourceList", "", 107);
+const ler2 = await sp.web.lists.ensure("RelatedItemsTargetList", "", 107);
+
+const sourceList = ler1.list;
+const targetList = ler2.list;
+
+const sourceListName = await sourceList.select("Id")().then(r => r.Id);
+const targetListName = await targetList.select("Id")().then(r => r.Id);
+
+// or whatever you need to get the web url, both our example lists are in the same web.
+const webUrl = sp.web.toUrl();
+
+// ...individual samples start here
+
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document.
+// get a file's server relative url in some manner, here we add one
+const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, "Content", true).then(r => r.data);
+// add an item or get an item from the task list
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);
+
+This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.
+This method allows you to delete a link previously created.
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add the link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+// delete the link
+await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+Gets the related items for an item
+import { IRelatedItem } from "@pnp/sp/related-items";
+
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
+
+const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id);
+
+// items.length === 2
+
+Related items are defined by the IRelatedItem interface
+export interface IRelatedItem {
+ ListId: string;
+ ItemId: number;
+ Url: string;
+ Title: string;
+ WebId: string;
+ IconUrl: string;
+}
+
+Gets an abbreviated set of related items
+import { IRelatedItem } from "@pnp/sp/related-items";
+
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
+
+const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id);
+
+// items.length === 2
+
+
+
+
+
+
+
+ Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.
+Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// text search using SharePoint default values for other parameters
+const results: SearchResults = await sp.search("test");
+
+console.log(results.ElapsedTime);
+console.log(results.RowCount);
+console.log(results.PrimarySearchResults);
+
+
+// define a search query object matching the ISearchQuery interface
+const results2: SearchResults = await sp.search(<ISearchQuery>{
+ Querytext: "test",
+ RowLimit: 10,
+ EnableInterleaving: true,
+});
+
+console.log(results2.ElapsedTime);
+console.log(results2.RowCount);
+console.log(results2.PrimarySearchResults);
+
+// define a query using a builder
+const builder = SearchQueryBuilder("test").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites;
+const results3 = await sp.search(builder);
+
+console.log(results3.ElapsedTime);
+console.log(results3.RowCount);
+console.log(results3.PrimarySearchResults);
+
+Starting with v3 you can use any of the caching behaviors with search and the results will be cached. Please see here for more details on caching options.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...).using(Caching());
+
+sp.search({/* ... query */}).then((r: SearchResults) => {
+
+ console.log(r.ElapsedTime);
+ console.log(r.RowCount);
+ console.log(r.PrimarySearchResults);
+});
+
+// use a query builder
+const builder = SearchQueryBuilder("test").rowLimit(3);
+
+// supply a search query builder and caching options
+const results2 = await sp.search(builder);
+
+console.log(results2.TotalRows);
+
+Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// this will hold our current results
+let currentResults: SearchResults = null;
+let page = 1;
+
+// triggered on page load or through some other means
+function onStart() {
+
+ // construct our query that will be used throughout the paging process, likely from user input
+ const q = SearchQueryBuilder("test").rowLimit(5);
+ const results = await sp.search(q);
+ currentResults = results; // set the current results
+ page = 1; // reset page counter
+ // update UI...
+}
+
+// triggered by an event
+async function next() {
+
+ currentResults = await currentResults.getPage(++page);
+ // update UI...
+}
+
+// triggered by an event
+async function prev() {
+
+ currentResults = await currentResults.getPage(--page);
+ // update UI...
+}
+
+The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { SearchQueryBuilder, SearchResults, ISearchQuery } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// basic usage
+let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic;
+
+sp.search(q).then(h => { /* ... */ });
+
+// provide a default query text at creation
+let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic;
+
+const results: SearchResults = await sp.search(q2);
+
+// provide query text and a template for
+// shared settings across queries that can
+// be overwritten by individual builders
+const appSearchSettings: ISearchQuery = {
+ EnablePhonetic: true,
+ HiddenConstraints: "reports"
+};
+
+let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules;
+let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming;
+const results2 = await sp.search(q3);
+const results3 = sp.search(q4);
+
+Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISuggestQuery, ISuggestResult } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+const results = await sp.searchSuggest("test");
+
+const results2 = await sp.searchSuggest({
+ querytext: "test",
+ count: 5,
+} as ISuggestQuery);
+
+You can also configure a search or suggest query against any valid SP url using the factory methods.
+++In this case you'll need to ensure you add observers, or use the tuple constructor to inherit
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/search";
+import { Search, Suggest } from "@pnp/sp/search";
+import { SPDefault } from "@pnp/nodejs";
+
+const sp = spfi(...);
+
+// set the url for search
+const searcher = Search([sp.web, "https://mytenant.sharepoint.com/sites/dev"]);
+
+// this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder)
+const results = await searcher("test");
+
+// you can reuse the ISearch instance
+const results2 = await searcher("another query");
+
+// same process works for Suggest
+const suggester = Suggest([sp.web, "https://mytenant.sharepoint.com/sites/dev"]);
+
+const suggestions = await suggester({ querytext: "test" });
+
+// resetting the observers on the instance
+const searcher2 = Search("https://mytenant.sharepoint.com/sites/dev").using(SPDefault({
+ msal: {
+ config: {...},
+ scopes: [...],
+ },
+}));
+
+const results3 = await searcher2("test");
+
+
+
+
+
+
+
+ There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below.
+++Site permissions are managed on the root web of the site collection.
+
Because the method are shared you can opt to import only the methods for one of the instances.
+import "@pnp/sp/security/web";
+import "@pnp/sp/security/list";
+import "@pnp/sp/security/item";
+
+Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module:
+import "@pnp/sp/security";
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/security/list";
+import "@pnp/sp/site-users/web";
+import { IList } from "@pnp/sp/lists";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// ensure we have a list
+const ler = await sp.web.lists.ensure("SecurityTestingList");
+const list: IList = ler.list;
+
+// role assignments (see section below)
+await list.roleAssignments();
+
+// data will represent one of the possible parents Site, Web, or List
+const data = await list.firstUniqueAncestorSecurableObject();
+
+// getUserEffectivePermissions
+const users = await sp.web.siteUsers.top(1).select("LoginName")();
+const perms = await list.getUserEffectivePermissions(users[0].LoginName);
+
+// getCurrentUserEffectivePermissions
+const perms2 = list.getCurrentUserEffectivePermissions();
+
+// userHasPermissions
+const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems)
+
+// currentUserHasPermissions
+const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems)
+
+// breakRoleInheritance
+await list.breakRoleInheritance();
+// copy existing permissions
+await list.breakRoleInheritance(true);
+// copy existing permissions and reset all child securables to the new permissions
+await list.breakRoleInheritance(true, true);
+
+// resetRoleInheritance
+await list.resetRoleInheritance();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/security/web";
+
+const sp = spfi(...);
+
+// role definitions (see section below)
+const defs = await sp.web.roleDefinitions();
+
+Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/security/web";
+import "@pnp/sp/site-users/web";
+import { IList } from "@pnp/sp/lists";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// ensure we have a list
+const ler = await sp.web.lists.ensure("SecurityTestingList");
+const list: IList = ler.list;
+
+// list role assignments
+const assignments = await list.roleAssignments();
+
+// add a role assignment
+const defs = await sp.web.roleDefinitions();
+const user = await sp.web.currentUser();
+const r = await list.roleAssignments.add(user.Id, defs[0].Id);
+
+// remove a role assignment
+const { Id: fullRoleDefId } = await sp.web.roleDefinitions.getByName('Full Control')();
+const ras = await list.roleAssignments();
+// filter/find the role assignment you want to remove
+// here we just grab the first
+const ra = ras.find(v => true);
+const r = await list.roleAssignments.remove(ra.PrincipalId, fullRoleDefId);
+
+// read role assignment info
+const info = await list.roleAssignments.getById(ra.Id)();
+
+// get the groups
+const info2 = await list.roleAssignments.getById(ra.Id).groups();
+
+// get the bindings
+const info3 = await list.roleAssignments.getById(ra.Id).bindings();
+
+// delete a role assignment (same as remove)
+const ras = await list.roleAssignments();
+// filter/find the role assignment you want to remove
+// here we just grab the first
+const ra = ras.find(v => true);
+
+// delete it
+await list.roleAssignments.getById(ra.Id).delete();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/security/web";
+
+const sp = spfi(...);
+
+// read role definitions
+const defs = await sp.web.roleDefinitions();
+
+// get by id
+const def = await sp.web.roleDefinitions.getById(5)();
+const def = await sp.web.roleDefinitions.getById(5).select("Name", "Order")();
+
+// get by name
+const def = await sp.web.roleDefinitions.getByName("Full Control")();
+const def = await sp.web.roleDefinitions.getByName("Full Control").select("Name", "Order")();
+
+// get by type
+const def = await sp.web.roleDefinitions.getByType(5)();
+const def = await sp.web.roleDefinitions.getByType(5).select("Name", "Order")();
+
+// add
+// name The new role definition's name
+// description The new role definition's description
+// order The order in which the role definition appears
+// basePermissions The permissions mask for this role definition
+const rdar = await sp.web.roleDefinitions.add("title", "description", 99, { High: 1, Low: 2 });
+
+
+
+// the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example
+
+// delete
+await sp.web.roleDefinitions.getById(5).delete();
+
+// update
+const res = sp.web.roleDefinitions.getById(5).update({ Name: "New Name" });
+
+In order to get a list of items that have unique permissions you have to specifically select the '' field and then filter on the client.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/security/items";
+
+const sp = spfi(...);
+
+const listItems = await sp.web.lists.getByTitle("pnplist").items.select("Id, HasUniqueRoleAssignments")();
+
+//Loop over list items filtering for HasUniqueRoleAssignments value
+
+
+
+
+
+
+
+
+ ++Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.
+
One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.
+In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.
+To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module:
+import "@pnp/sp/sharing";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+const user = await sp.web.siteUsers.getByEmail("user@site.com")();
+const r = await sp.web.shareWith(user.LoginName);
+
+Import only the web's sharing methods into the library
+import "@pnp/sp/sharing/web";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+const user = await sp.web.siteUsers.getByEmail("user@site.com")();
+const r = await sp.web.shareWith(user.LoginName);
+
+Applies to: Item, Folder, File
+Creates a sharing link for the given resource with an optional expiration.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { SharingLinkKind, IShareLinkResponse } from "@pnp/sp/sharing";
+import { dateAdd } from "@pnp/core";
+
+const sp = spfi(...);
+
+const result = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView);
+
+console.log(JSON.stringify(result, null, 2));
+
+
+const result2 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5));
+
+console.log(JSON.stringify(result2, null, 2));
+
+Applies to: Item, Folder, File, Web
+Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames
parameter or an array of loginnames
. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/files/web";
+import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result = await sp.web.shareWith("i:0#.f|membership|user@site.com");
+
+console.log(JSON.stringify(result, null, 2));
+
+// Share and allow editing
+const result2 = await sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
+
+console.log(JSON.stringify(result2, null, 2));
+
+
+// share folder
+const result3 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com");
+
+// Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children)
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true);
+
+// Share a file
+await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com");
+
+// Share a file with edit permissions
+await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
+
+Applies to: Web
+Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Share an object in this web
+const result = await sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View);
+
+// Share an object with all settings available
+await sp.web.shareObjectRaw({
+ url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt",
+ peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }],
+ roleValue: "role: 1973741327",
+ groupId: 0,
+ propagateAcl: false,
+ sendEmail: true,
+ includeAnonymousLinkInEmail: false,
+ emailSubject: "subject",
+ emailBody: "body",
+ useSimplifiedRoles: true,
+});
+
+Applies to: Web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { ISharingResult } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result = await sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt");
+
+Applies to: Item, Folder, File
+Checks Permissions on the list of Users and returns back role the users have on the Item.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing/folders";
+import "@pnp/sp/folders/web";
+import { SharingEntityPermission } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// check the sharing permissions for a folder
+const perms = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]);
+
+Applies to: Item, Folder, File
+Get Sharing Information.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingInformation } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Get the sharing information for a folder
+const info = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation();
+
+// get sharing informaiton with a request object
+const info2 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation({
+ maxPrincipalsToReturn: 10,
+ populateInheritedLinks: true,
+});
+
+// get sharing informaiton using select and expand, NOTE expand comes first in the API signature
+const info3 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation({}, ["permissionsInformation"], ["permissionsInformation","anyoneLinkTrackUsers"]);
+
+Applies to: Item, Folder, File
+Gets the sharing settings
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { IObjectSharingSettings } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Gets the sharing object settings
+const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings();
+
+Applies to: Item, Folder, File
+Unshares a given resource
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingResult } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare();
+
+Applies to: Item, Folder, File
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingResult, SharingLinkKind } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);
+
+Applies to: Item, Folder, File
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { SharingLinkKind } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit);
+
+// specify the sharing link id if available
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345");
+
+
+
+
+
+
+
+ You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out SharePoint site design and site script overview for more information.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// WebTemplate: 64 Team site template, 68 Communication site template
+const siteDesign = await sp.siteDesigns.createSiteDesign({
+ SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"],
+ Title: "SiteDesign001",
+ WebTemplate: "64",
+});
+
+console.log(siteDesign.Title);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Limited to 30 actions in a site script, but runs synchronously
+await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001");
+
+// Better use the following method for 300 actions in a site script
+const task = await sp.web.addSiteDesignTask("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Retrieving all site designs
+const allSiteDesigns = await sp.siteDesigns.getSiteDesigns();
+console.log(`Total site designs: ${allSiteDesigns.length}`);
+
+// Retrieving a single site design by Id
+const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(siteDesign.Title);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Update
+const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" });
+
+// Delete
+await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Get
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(rights.length > 0 ? rights[0].PrincipalName : "");
+
+// Grant
+await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Revoke
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Reset all view rights
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName));
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+const runs = await sp.web.getSiteDesignRuns();
+const runs2 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite");
+
+// Get runs specific to a site design
+const runs3 = await sp.web.getSiteDesignRuns("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+const runs4 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite", "75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+// For more information about the site script actions
+const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);
+const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus("https://TENANT.sharepoint.com/sites/mysite", runs[0].ID);
+
+
+
+
+
+
+
+
+ The site groups module provides methods to manage groups for a sharepoint site.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// gets all site groups of the web
+const groups = await sp.web.siteGroups();
+
+You can get the associated Owner, Member and Visitor groups of a web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Gets the associated visitors group of a web
+const visitorGroup = await sp.web.associatedVisitorGroup();
+
+// Gets the associated members group of a web
+const memberGroup = await sp.web.associatedMemberGroup();
+
+// Gets the associated owners group of a web
+const ownerGroup = await sp.web.associatedOwnerGroup();
+
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Breaks permission inheritance and creates the default associated groups for the web
+
+// Login name of the owner
+const owner1 = "owner@example.onmicrosoft.com";
+
+// Specify true, the permissions should be copied from the current parent scope, else false
+const copyRoleAssignments = false;
+
+// Specify true to make all child securable objects inherit role assignments from the current object
+const clearSubScopes = true;
+
+await sp.web.createDefaultAssociatedGroups("PnP Site", owner1, copyRoleAssignments, clearSubScopes);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Creates a new site group with the specified title
+await sp.web.siteGroups.add({"Title":"new group name"});
+
+Scenario | +Import Statement | +
---|---|
Selective 2 | +import "@pnp/sp/webs"; import "@pnp/sp/site-groups"; |
+
Selective 3 | +import "@pnp/sp/webs"; import "@pnp/sp/site-groups/web"; |
+
Preset: All | +import {sp, SiteGroups, SiteGroup } from "@pnp/sp/presets/all"; | +
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// get the group using a group id
+const groupID = 33;
+let grp = await sp.web.siteGroups.getById(groupID)();
+
+// get the group using the group's name
+const groupName = "ClassicTeam Visitors";
+grp = await sp.web.siteGroups.getByName(groupName)();
+
+// update a group
+await sp.web.siteGroups.getById(groupID).update({"Title": "New Group Title"});
+
+// delete a group from the site using group id
+await sp.web.siteGroups.removeById(groupID);
+
+// delete a group from the site using group name
+await sp.web.siteGroups.removeByLoginName(groupName);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// get all users of group
+const groupID = 7;
+const users = await sp.web.siteGroups.getById(groupID).users();
+
+Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// Update the owner with a user id
+await sp.web.siteGroups.getById(7).setUserAsOwner(4);
+
+
+
+
+
+
+
+ import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+const sitescriptContent = {
+ "$schema": "schema.json",
+ "actions": [
+ {
+ "themeName": "Theme Name 123",
+ "verb": "applyTheme",
+ },
+ ],
+ "bindata": {},
+ "version": 1,
+};
+
+const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent);
+
+console.log(siteScript.Title);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Retrieving all site scripts
+const allSiteScripts = await sp.siteScripts.getSiteScripts();
+console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : "");
+
+// Retrieving a single site script by Id
+const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+console.log(siteScript.Title);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Update
+const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" });
+console.log(updatedSiteScript.Title);
+
+// Delete
+await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Using the absolute URL of the list
+const ss = await sp.siteScripts.getSiteScriptFromList("https://TENANT.sharepoint.com/Lists/mylist");
+
+// Using the PnPjs web object to fetch the site script from a specific list
+const ss2 = await sp.web.lists.getByTitle("mylist").getSiteScript();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const extractInfo = {
+ IncludeBranding: true,
+ IncludeLinksToExportedItems: true,
+ IncludeRegionalSettings: true,
+ IncludeSiteExternalSharingCapability: true,
+ IncludeTheme: true,
+ IncludedLists: ["Lists/MyList"]
+};
+
+const ss = await sp.siteScripts.getSiteScriptFromWeb("https://TENANT.sharepoint.com/sites/mysite", extractInfo);
+
+// Using the PnPjs web object to fetch the site script from a specific web
+const ss2 = await sp.web.getSiteScript(extractInfo);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+const siteScript = "your site script action...";
+
+const ss = await sp.siteScripts.executeSiteScriptAction(siteScript);
+
+import { spfi } from "@pnp/sp";
+import { SiteScripts } "@pnp/sp/site-scripts";
+
+const siteScript = "your site script action...";
+
+const scriptService = SiteScripts("https://absolute/url/to/web");
+
+const ss = await scriptService.executeSiteScriptAction(siteScript);
+
+
+
+
+
+
+
+ The site users module provides methods to manage users for a sharepoint site.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const users = await sp.web.siteUsers();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let user = await sp.web.currentUser();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const id = 6;
+user = await sp.web.getUserById(id)();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const username = "usernames@microsoft.com";
+result = await sp.web.ensureUser(username);
+
+Scenario | +Import Statement | +
---|---|
Selective 2 | +import "@pnp/sp/webs"; import "@pnp/sp/site-users"; |
+
Selective 3 | +import "@pnp/sp/webs"; import "@pnp/sp/site-users/web"; |
+
Preset: All | +import {sp, SiteUsers, SiteUser } from "@pnp/sp/presets/all"; | +
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let groups = await sp.web.currentUser.groups();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const user = await sp.web.ensureUser("userLoginname")
+const users = await sp.web.siteUsers;
+
+await users.add(user.data.LoginName);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+// get user object by id
+const user = await sp.web.siteUsers.getById(6)();
+
+//get user object by Email
+const user = await sp.web.siteUsers.getByEmail("user@mail.com")();
+
+//get user object by LoginName
+const user = await sp.web.siteUsers.getByLoginName("userLoginName")();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let userProps = await sp.web.currentUser();
+userProps.Title = "New title";
+await sp.web.currentUser.update(userProps);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+// remove user by id
+await sp.web.siteUsers.removeById(6);
+
+// remove user by LoginName
+await sp.web.siteUsers.removeByLoginName(6);
+
+User properties:
+Property Name | +Type | +Description | +
---|---|---|
string | +Contains Site user email | +|
Id | +Number | +Contains Site user Id | +
IsHiddenInUI | +Boolean | +Site user IsHiddenInUI | +
IsShareByEmailGuestUser | +boolean | +Site user is external user | +
IsSiteAdmin | +Boolean | +Describes if Site user Is Site Admin | +
LoginName | +string | +Site user LoginName | +
PrincipalType | +number | +Site user Principal type | +
Title | +string | +Site user Title | +
interface ISiteUserProps {
+
+ /**
+ * Contains Site user email
+ *
+ */
+ Email: string;
+
+ /**
+ * Contains Site user Id
+ *
+ */
+ Id: number;
+
+ /**
+ * Site user IsHiddenInUI
+ *
+ */
+ IsHiddenInUI: boolean;
+
+ /**
+ * Site user IsShareByEmailGuestUser
+ *
+ */
+ IsShareByEmailGuestUser: boolean;
+
+ /**
+ * Describes if Site user Is Site Admin
+ *
+ */
+ IsSiteAdmin: boolean;
+
+ /**
+ * Site user LoginName
+ *
+ */
+ LoginName: string;
+
+ /**
+ * Site user Principal type
+ *
+ */
+ PrincipalType: number | PrincipalType;
+
+ /**
+ * Site user Title
+ *
+ */
+ Title: string;
+}
+
+
+
+
+
+
+
+ Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.
+Using the library, you can get the context information of the current site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { IContextInfo } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const oContext: IContextInfo = await sp.site.getContextInfo();
+console.log(oContext.FormDigestValue);
+
+Using the library, you can get a list of the document libraries present in the a given web.
+Note: Works only in SharePoint online
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { IDocumentLibraryInformation } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite");
+
+//we got the array of document library information
+docLibs.forEach((docLib: IDocumentLibraryInformation) => {
+ // do something with each library
+});
+
+Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const w = await sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e");
+
+//we got all the data from the web as well
+console.log(w.data);
+
+// we can chain
+const w2 = await w.web.select("Title")();
+
+Using the library, you can get the absolute web url by providing a page url
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const d: string = await sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx");
+
+console.log(d); //https://tenant.sharepoint.com/sites/test
+
+There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. "_api/sites/rootweb" which does not work for all operations.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// use for rootweb information access
+const rootwebData = await sp.site.rootWeb();
+
+// use for chaining
+const rootweb = await sp.site.getRootWeb();
+const listData = await rootWeb.lists.getByTitle("MyList")();
+
+Note: Works only in SharePoint online
+Creates a modern communication site.
+Property | +Type | +Required | +Description | +
---|---|---|---|
Title | +string | +yes | +The title of the site to create. | +
lcid | +number | +yes | +The default language to use for the site. | +
shareByEmailEnabled | +boolean | +yes | +If set to true, it will enable sharing files via Email. By default it is set to false | +
url | +string | +yes | +The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection ) of the site. |
+
description | +string | +no | +The description of the communication site. | +
classification | +string | +no | +The Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information | +
siteDesignId | +string | +no | +The Guid of the site design to be used. | +
+ | + | + | You can use the below default OOTB GUIDs: | +
+ | + | + | Topic: null | +
+ | + | + | Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 | +
+ | + | + | Blank: f6cc5403-0d63-442e-96c0-285923709ffc | +
hubSiteId | +string | +no | +The Guid of the already existing Hub site | +
Owner | +string | +no | +Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com | +
+ | + | + | + |
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const result = await sp.site.createCommunicationSite(
+ "Title",
+ 1033,
+ true,
+ "https://tenant.sharepoint.com/sites/commSite",
+ "Description",
+ "HBI",
+ "f6cc5403-0d63-442e-96c0-285923709ffc",
+ "a00ec589-ea9f-4dba-a34e-67e78d41e509",
+ "user@TENANT.onmicrosoft.com");
+
+
+You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps
method.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// in this case you supply a single struct deinfing the creation props
+const result = await sp.site.createCommunicationSiteFromProps({
+ Owner: "patrick@three18studios.com",
+ Title: "A Test Site",
+ Url: "https://{tenant}.sharepoint.com/sites/commsite2",
+ WebTemplate: "STS#3",
+});
+
+Note: Works only in SharePoint online. It wont work with App only tokens
+Creates a modern team site backed by O365 group.
+Property | +Type | +Required | +Description | +
---|---|---|---|
displayName | +string | +yes | +The title/displayName of the site to be created. | +
alias | +string | +yes | +Alias of the underlying Office 365 Group. | +
isPublic | +boolean | +yes | +Defines whether the Office 365 Group will be public (default), or private. | +
lcid | +number | +yes | +The language to use for the site. If not specified will default to English (1033). | +
description | +string | +no | +The description of the modern team site. | +
classification | +string | +no | +The Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information | +
owners | +string array (string[]) | +no | +The Owners of the site to be created | +
hubSiteId | +string | +no | +The Guid of the already existing Hub site | +
siteDesignId | +string | +no | +The Guid of the site design to be used. | +
+ | + | + | You can use the below default OOTB GUIDs: | +
+ | + | + | Topic: null | +
+ | + | + | Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 | +
+ | + | + | Blank: f6cc5403-0d63-442e-96c0-285923709ffc | +
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const result = await sp.site.createModernTeamSite(
+ "displayName",
+ "alias",
+ true,
+ 1033,
+ "description",
+ "HBI",
+ ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"],
+ "a00ec589-ea9f-4dba-a34e-67e78d41e509",
+ "f6cc5403-0d63-442e-96c0-285923709ffc"
+ );
+
+console.log(d);
+
+You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps
method.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// in this case you supply a single struct deinfing the creation props
+const result = await sp.site.createModernTeamSiteFromProps({
+ alias: "JenniferGarner",
+ displayName: "A Test Site",
+ owners: ["patrick@three18studios.com"],
+});
+
+Using the library, you can delete a specific site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { Site } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// Delete the current site
+await sp.site.delete();
+
+// Specify which site to delete
+const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
+const site2 = Site(siteUrl);
+await site2.delete();
+
+Using the library, you can check if a specific site collection exist or not on your tenant
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// Specify which site to verify
+const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
+const exists = await sp.site.exists(siteUrl);
+console.log(exists);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import {ISiteLogoProperties, SiteLogoAspect, SiteLogoType} from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+//set the web's site logo
+const logoProperties: ISiteLogoProperties = {
+ relativeLogoUrl: "/sites/mySite/SiteAssets/site_logo.png",
+ aspect: SiteLogoAspect.Rectangular,
+ type: SiteLogoType.WebLogo
+};
+await sp.site.setSiteLogo(logoProperties);
+
+
+
+
+
+
+
+ The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions.
+Gets a URI to a site that lists the current user's followed sites.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const uri = await sp.social.getFollowedSitesUri();
+
+Gets a URI to a site that lists the current user's followed documents.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const uri = await sp.social.getFollowedDocumentsUri();
+
+Makes the current user start following a user, document, site, or tag
+import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// follow a site
+const r1 = await sp.social.follow({
+ ActorType: SocialActorType.Site,
+ ContentUri: "htts://tenant.sharepoint.com/sites/site",
+});
+
+// follow a person
+const r2 = await sp.social.follow({
+ AccountName: "i:0#.f|membership|person@tenant.com",
+ ActorType: SocialActorType.User,
+});
+
+// follow a doc
+const r3 = await sp.social.follow({
+ ActorType: SocialActorType.Document,
+ ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx",
+});
+
+// follow a tag
+// You need the tag GUID to start following a tag.
+// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.
+// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.
+// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid
+const r4 = await sp.social.follow({
+ ActorType: SocialActorType.Tag,
+ TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928",
+});
+
+Indicates whether the current user is following a specified user, document, site, or tag
+import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.isFollowed({
+ AccountName: "i:0#.f|membership|person@tenant.com",
+ ActorType: SocialActorType.User,
+});
+
+Makes the current user stop following a user, document, site, or tag
+import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.stopFollowing({
+ AccountName: "i:0#.f|membership|person@tenant.com",
+ ActorType: SocialActorType.User,
+});
+
+Gets this user's social information
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const r = await sp.social.my();
+
+Gets users, documents, sites, and tags that the current user is following based on the supplied flags.
+import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get all the followed documents
+const r1 = await sp.social.my.followed(SocialActorTypes.Document);
+
+// get all the followed documents and sites
+const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);
+
+// get all the followed sites updated in the last 24 hours
+const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);
+
+Works as followed but returns on the count of actors specified by the query
+import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.followedCount(SocialActorTypes.Document);
+
+Gets the users who are following the current user.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.followers();
+
+Gets users who the current user might want to follow.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.suggestions();
+
+
+
+
+
+
+
+ Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.
+This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).
+export interface TypedHash<T> {
+ [key: string]: T;
+}
+
+export interface EmailProperties {
+
+ To: string[];
+ CC?: string[];
+ BCC?: string[];
+ Subject: string;
+ Body: string;
+ AdditionalHeaders?: TypedHash<string>;
+ From?: string;
+}
+
+You must define the To, Subject, and Body values - the remaining are optional.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+import { IEmailProperties } from "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+const emailProps: IEmailProperties = {
+ To: ["user@site.com"],
+ CC: ["user2@site.com", "user3@site.com"],
+ BCC: ["user4@site.com", "user5@site.com"],
+ Subject: "This email is about...",
+ Body: "Here is the body. <b>It supports html</b>",
+ AdditionalHeaders: {
+ "content-type": "text/html"
+ }
+};
+
+await sp.utility.sendEmail(emailProps);
+console.log("Email Sent!");
+
+This method returns the current user's email addresses known to SharePoint.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let addressString: string = await sp.utility.getCurrentUserEmailAddresses();
+
+// and use it with sendEmail
+await sp.utility.sendEmail({
+ To: [addressString],
+ Subject: "This email is about...",
+ Body: "Here is the body. <b>It supports html</b>",
+ AdditionalHeaders: {
+ "content-type": "text/html"
+ },
+});
+
+Gets information about a principal that matches the specified Search criteria
+import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principal : IPrincipalInfo = await sp.utility.resolvePrincipal("user@site.com", PrincipalType.User, PrincipalSource.All, true, false, true);
+
+console.log(principal);
+
+Gets information about the principals that match the specified Search criteria.
+import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals("john", PrincipalType.User, PrincipalSource.All,"", 10);
+
+console.log(principals);
+
+Gets the external (outside the firewall) URL to a document or resource in a site.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let url : string = await sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx");
+console.log(url);
+
+Resolves the principals contained within the supplied groups
+import { spfi, SPFx, IPrincipalInfo } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]);
+console.log(principals);
+
+// optionally supply a max results count. Default is 30.
+let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10);
+console.log(principals);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+import { ICreateWikiPageResult } from "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({
+ ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx",
+ WikiHtmlContent: "This is my <b>page</b> content. It supports rich html.",
+});
+
+// newPage contains the raw data returned by the service
+console.log(newPage.data);
+
+// newPage contains a File instance you can use to further update the new page
+let file = await newPage.file();
+console.log(file);
+
+
+
+
+
+
+
+ Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.
+Using this library, you can add a webhook to a specified list within the SharePoint site.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+import { Subscriptions, ISubscriptions} from "@pnp/sp/subscriptions";
+import "@pnp/sp/subscriptions/list";
+
+const sp = spfi(...);
+
+// This is the URL which will be called by SharePoint when there is a change in the list
+const notificationUrl = "<notification-url>";
+
+// Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date.
+const expiryDate = dateAdd(new Date(), "day" , 180).toISOString();
+
+// Adds a webhook to the Documents library
+var res = await sp.web.lists.getByTitle("Documents").subscriptions.add(notificationUrl,expiryDate);
+
+Read all the webhooks' details which are associated to the list
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions";
+
+const sp = spfi(...);
+
+const res = await sp.web.lists.getByTitle("Documents").subscriptions();
+
+This interface provides the methods for managing a particular webhook.
+ +Scenario | +Import Statement | +
---|---|
Selective | +import "@pnp/sp/webs"; import "@pnp/sp/lists"; import { Subscriptions, ISubscriptions, Subscription, ISubscription} from "@pnp/sp/subscriptions"; import "@pnp/sp/subscriptions/list" |
+
Preset: All | +import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from "@pnp/sp/presets/all"; | +
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions";
+
+const sp = spfi(...);
+
+// Get details of a webhook based on its ID
+const webhookId = "1f029e5c-16e4-4941-b46f-67895118763f";
+const webhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId)();
+
+// Update a webhook
+const newDate = dateAdd(new Date(), "day" , 150).toISOString();
+const updatedWebhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).update(newDate);
+
+// Delete a webhook
+await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).delete();
+
+
+
+
+
+
+
+ Provides access to the v2.1 api term store
+++ + +NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabilize this note will be removed.
+
Access term store data from the root sp object as shown below.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermStoreInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term store data
+const info: ITermStoreInfo = await sp.termStore();
+
+Added in 3.3.0
+Search for terms starting with provided label under entire termStore or a termSet or a parent term.
+The following properties are valid for the supplied query: label: string
, setId?: string
, parentTermId?: string
, languageTag?: string
, stringMatchOption?: "ExactMatch" | "StartsWith"
.
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// minimally requires the label
+const results1 = await sp.termStore.searchTerm({
+ label: "test",
+});
+
+// other properties can be included as needed
+const results2 = await sp.termStore.searchTerm({
+ label: "test",
+ setId: "{guid}",
+});
+
+// other properties can be included as needed
+const results3 = await sp.termStore.searchTerm({
+ label: "test",
+ setId: "{guid}",
+ stringMatchOption: "ExactMatch",
+});
+
+Added in 3.10.0
+Allows you to update language setttings for the store
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+await sp.termStore.update({
+ defaultLanguageTag: "en-US",
+ languageTags: ["en-US", "en-IE", "de-DE"],
+});
+
+Access term group information
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term groups
+const info: ITermGroupInfo[] = await sp.termStore.groups();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term groups data
+const info: ITermGroupInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+Added in 3.10.0
+Allows you to add a term group to a store.
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+const groupInfo: ITermGroupInfo = await sp.termStore.groups.add({
+ displayName: "Accounting",
+ description: "Term Group for Accounting",
+ name: "accounting1",
+ scope: "global",
+});
+
+Added in 3.10.0
+Allows you to add a term group to a store.
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+Access term set information
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermSetInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get set info
+const info: ITermSetInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermSetInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term set data by group id then by term set id
+const info: ITermSetInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+// get term set data by term set id
+const infoByTermSetId: ITermSetInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+Added in 3.10.0
+Allows you to add a term set.
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+// when adding a set directly from the root .sets property, you must include the "parentGroup" property
+const setInfo = await sp.termStore.sets.add({
+ parentGroup: {
+ id: "338666a8-1111-2222-3333-f72471314e72"
+ },
+ contact: "steve",
+ description: "description",
+ isAvailableForTagging: true,
+ isOpen: true,
+ localizedNames: [{
+ name: "MySet",
+ languageTag: "en-US",
+ }],
+ properties: [{
+ key: "key1",
+ value: "value1",
+ }]
+});
+
+// when adding a termset through a group's sets property you do not specify the "parentGroup" property
+const setInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.add({
+ contact: "steve",
+ description: "description",
+ isAvailableForTagging: true,
+ isOpen: true,
+ localizedNames: [{
+ name: "MySet2",
+ languageTag: "en-US",
+ }],
+ properties: [{
+ key: "key1",
+ value: "value1",
+ }]
+});
+
+This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change.
+++Starting with version 2.6.0 you can now include an optional param to retrieve all of the term's properties and localProperties in the tree. Default is false.
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+import { dateAdd, PnPClientStorage } from "@pnp/core";
+
+const sp = spfi(...);
+
+// here we get all the children of a given set
+const childTree = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
+
+// here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available
+const store = new PnPClientStorage();
+
+// our tree likely doesn't change much in 30 minutes for most applications
+// adjust to be longer or shorter as needed
+const cachedTree = await store.local.getOrPut("myKey", () => {
+ return sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
+}, dateAdd(new Date(), "minute", 30));
+
+// you can also get all the properties and localProperties
+const set = sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72");
+const childTree = await set.getAllChildrenAsOrderedTree({ retrieveProperties: true });
+
+Access term set information
+Added in 3.10.0
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termSetInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").update({
+ properties: [{
+ key: "MyKey2",
+ value: "MyValue2",
+ }],
+});
+
+const termSetInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").update({
+ properties: [{
+ key: "MyKey3",
+ value: "MyValue3",
+ }],
+});
+
+Added in 3.10.0
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+Access term set information
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// list all the terms that are direct children of this set
+const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children();
+
+You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// list all the terms available in this term set by group id then by term set id
+const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").terms();
+
+// list all the terms available in this term set by term set id
+const infosByTermSetId: ITermInfo[] = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").terms();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term set data
+const info: ITermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72")();
+
+Added in 3.10.0
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const newTermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children.add({
+ labels: [
+ {
+ isDefault: true,
+ languageTag: "en-us",
+ name: "New Term",
+ }
+ ]
+});
+
+const newTermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children.add({
+ labels: [
+ {
+ isDefault: true,
+ languageTag: "en-us",
+ name: "New Term 2",
+ }
+ ]
+});
+
+++Note that when updating a Term if you update the
+properties
it replaces the collection, so a merge of existing + new needs to be handled by your application.
Added in 3.10.0
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").update({
+ properties: [{
+ key: "something",
+ value: "a value 2",
+ }],
+});
+
+const termInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").update({
+ properties: [{
+ key: "something",
+ value: "a value",
+ }],
+});
+
+Added in 3.10.0
+import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+const termInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+Behavior Change in 2.1.0
+The server API changed again, resulting in the removal of the "parent" property from ITerm as it is not longer supported as a path property. You now must use "expand" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get a ref to the set
+const set = sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72");
+
+// get a term's information and expand parent to get the parent info as well
+const w = await set.getTermById("338666a8-1111-2222-3333-f72471314e72").expand("parent")();
+
+// get a ref to the parent term
+const parent = set.getTermById(w.parent.id);
+
+// make a request for the parent term's info - this data currently match the results in the expand call above, but this
+// is to demonstrate how to gain a ref to the parent and select its data
+const parentInfo = await parent.select("Id", "Descriptions")();
+
+
+
+
+
+
+
+ You can set, read, and remove tenant properties using the methods shown below:
+This method MUST be called in the context of the app catalog web or you will get an access denied message.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const w = await sp.getTenantAppCatalogWeb();
+
+// specify required key and value
+await w.setStorageEntity("Test1", "Value 1");
+
+// specify optional description and comments
+await w.setStorageEntity("Test2", "Value 2", "description", "comments");
+
+This method can be used from any web to retrieve values previously set.
+import { spfi, SPFx } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+import { IStorageEntity } from "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const prop: IStorageEntity = await sp.web.getStorageEntity("Test1");
+
+console.log(prop.Value);
+
+This method MUST be called in the context of the app catalog web or you will get an access denied message.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const w = await sp.getTenantAppCatalogWeb();
+
+await w.removeStorageEntity("Test1");
+
+
+
+
+
+
+
+ Represents a custom action associated with a SharePoint list, web or site collection.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const userCustomActions = sp.web.userCustomActions();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions';
+
+const sp = spfi(...);
+
+const newValues: TypedHash<string> = {
+ "Title": "New Title",
+ "Description": "New Description",
+ "Location": "ScriptLink",
+ "ScriptSrc": "https://..."
+};
+
+const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const uca: IUserCustomAction = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
+
+const ucaData = await uca();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+// Site collection level
+await sp.site.userCustomActions.clear();
+
+// Site (web) level
+await sp.web.userCustomActions.clear();
+
+// List level
+await sp.web.lists.getByTitle("Documents").userCustomActions.clear();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions';
+
+const sp = spfi(...);
+
+const uca = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
+
+const newValues: TypedHash<string> = {
+ "Title": "New Title",
+ "Description": "New Description",
+ "ScriptSrc": "https://..."
+};
+
+const response: IUserCustomActionUpdateResult = uca.update(newValues);
+
+
+
+
+
+
+
+ Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+// get all the views and their properties
+const views1 = await list.views();
+
+// you can use odata select operations to get just a set a fields
+const views2 = await list.views.select("Id", "Title")();
+
+// get the top three views
+const views3 = await list.views.top(3)();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+// create a new view with default fields and properties
+const result = await list.views.add("My New View");
+
+// create a new view with specific properties
+const result2 = await list.views.add("My New View 2", false, {
+ RowLimit: 10,
+ ViewQuery: "<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>",
+});
+
+// manipulate the view's fields
+await result2.view.fields.removeAll();
+
+await Promise.all([
+ result2.view.fields.add("Title"),
+ result2.view.fields.add("Modified"),
+]);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}")();
+
+const result2 = await list.views.getByTitle("My View")();
+
+const result3 = await list.views.getByTitle("My View").select("Id", "Title")();
+
+const result4 = await list.defaultView();
+
+const result5 = await list.getView("{GUID view id}")();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}").fields();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}").update({
+ RowLimit: 20,
+});
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const result = await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").renderAsHtml();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const viewXml = "...";
+
+await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").setViewXml(viewXml);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const viewXml = "...";
+
+await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").delete();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const xml = await sp.web.lists.getByTitle("My List").defaultView.fields.getSchemaXml();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.add("Created");
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.move("Created", 0);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.remove("Created");
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.removeAll();
+
+
+
+
+
+
+
+ Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.
+Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions.
+import { spfi } from "@pnp/sp";
+import { IWebAddResult } from "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const result = await sp.web.webs.add("title", "subweb1");
+
+// show the response from the server when adding the web
+console.log(result.data);
+
+// we can immediately operate on the new web
+result.web.select("Title")().then((w: IWebInfo) => {
+
+ // show our title
+ console.log(w.Title);
+});
+
+import { spfi } from "@pnp/sp";
+import { IWebAddResult } from "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// create a German language wiki site with title, url, description, which does not inherit permissions
+sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, false).then((w: IWebAddResult) => {
+
+ // ...
+});
+
+There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named "web" which represents an IWeb instance - regardless of how it was initially accessed.
+Access the web from the imported "spfi" object using selective import:
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const r = await sp.web();
+
+Access the web from the imported "spfi" object using the 'all' preset
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/presets/all";
+
+const sp = spfi(...);
+
+const r = await sp.web();
+
+Access the web using any SPQueryable as a base
+In this scenario you might be deep in your code without access to the original start of the fluid chain (i.e. the instance produced from spfi). You can pass any queryable to the Web or Site factory and get back a valid IWeb instance. In this case all of the observers registered to the supplied instance will be referenced by the IWeb, and the url will be rebased to ensure a valid path.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// we have a ref to the IItems instance
+const items = await sp.web.lists.getByTitle("Generic").items;
+
+// we create a new IWeb instance using the items as a base
+const web = Web(items);
+
+// gets the web info
+const webInfo = await web();
+
+// get a reference to a different list
+const list = web.lists.getByTitle("DifferentList");
+
+Access a web using the Web factory method
+There are several ways to use the Web
factory directly and have some special considerations unique to creating IWeb
instances from Web
. The easiest is to supply the absolute URL of the web you wish to target, as seen in the first example below. When supplying a path parameter to Web
you need to include the _api/web
part in the appropriate location as the library can't from strings determine how to append the path. Example 2 below shows a wrong usage of the Web factory as we cannot determine how the path part should be appended. Examples 3 and 4 show how to include the _api/web
part for both subwebs or queries within the given web.
++When in doubt, supply the absolute url to the web as the first parameter as shown in example 1 below
+
import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+// creates a web:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/_api/web"
+// - has no registered observers
+const web1 = Web("https://tenant.sharepoint.com/sites/myweb");
+
+// creates a web that will not work due to missing the _api/web portion
+// this is because we don't know that the extra path should come before/after the _api/web portion
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - has no registered observers
+const web2-WRONG = Web("https://tenant.sharepoint.com/sites/myweb", "some sub path");
+
+// creates a web:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/some sub web/_api/web"
+// including the _api/web ensures the path you are providing is correct and can be parsed by the library
+// - has no registered observers
+const web3 = Web("https://tenant.sharepoint.com/sites/myweb", "some sub web/_api/web");
+
+// creates a web that actually points to the lists endpoint:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/_api/web/lists"
+// including the _api/web ensures the path you are providing is correct and can be parsed by the library
+// - has no registered observers
+const web4 = Web("https://tenant.sharepoint.com/sites/myweb", "_api/web/lists");
+
+The above examples show you how to use the constructor to create the base url for the Web
although none of them are usable as is until you add observers. You can do so by either adding them explicitly with a using...
import { spfi, SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+const web1 = Web("https://tenant.sharepoint.com/sites/myweb").using(SPFx(this.context));
+
+or by copying them from another SPQueryable instance...
+import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+//sp.web is of type SPQueryable; using tuple pattern pass SPQueryable and the web's url
+const web = Web([sp.web, "https://tenant.sharepoint.com/sites/otherweb"]);
+
+Access the child webs collection of this web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const webs = await web.webs();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// basic get of the webs properties
+const props = await sp.web();
+
+// use odata operators to get specific fields
+const props2 = await sp.web.select("Title")();
+
+// type the result to match what you are requesting
+const props3 = await sp.web.select("Title")<{ Title: string }>();
+
+Get the data and IWeb instance for the parent web for the given web instance
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = web.getParentWeb();
+
+Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const subWebs = web.getSubwebsFilteredForCurrentUser()();
+
+// apply odata operations to the collection
+const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select("Title", "Language").orderBy("Created", true)();
+
+++Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.
+
Allows access to the web's all properties collection. This is readonly in REST.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const props = await web.allProperties();
+
+// select certain props
+const props2 = await web.allProperties.select("prop1", "prop2")();
+
+Gets a collection of WebInfos for this web's subwebs
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+
+const infos = await web.webinfos();
+
+// or select certain fields
+const infos2 = await web.webinfos.select("Title", "Description")();
+
+// or filter
+const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")();
+
+// or both
+const infos4 = await web.webinfos.select("Title", "Description").filter("Title eq 'MyWebTitle'")();
+
+// get the top 4 ordered by Title
+const infos5 = await web.webinfos.top(4).orderBy("Title")();
+
+++Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.
+
Updates this web instance with the supplied properties
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+// update the web's title and description
+const result = await web.update({
+ Title: "New Title",
+ Description: "My new description",
+});
+
+// a project implementation could wrap the update to provide type information for your expected fields:
+
+interface IWebUpdateProps {
+ Title: string;
+ Description: string;
+}
+
+function updateWeb(props: IWebUpdateProps): Promise<void> {
+ web.update(props);
+}
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+
+await web.delete();
+
+Applies the theme specified by the contents of each of the files specified in the arguments to the site
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { combine } from "@pnp/core";
+
+const sp = spfi("https://{tenant}.sharepoint.com/sites/dev/subweb").using(SPFx(this.context));
+const web = sp.web;
+
+// the urls to the color and font need to both be from the catalog at the root
+// these urls can be constants or calculated from existing urls
+const colorUrl = combine("/", "sites/dev", "_catalogs/theme/15/palette011.spcolor");
+// this gives us the same result
+const fontUrl = "/sites/dev/_catalogs/theme/15/fontscheme007.spfont";
+
+// apply the font and color, no background image, and don't share this theme
+await web.applyTheme(colorUrl, fontUrl, "", false);
+
+Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+const templates = (await web.availableWebTemplates().select("Name")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name));
+
+// apply the wiki template
+const template = templates.length > 0 ? templates[0].Name : "STS#0";
+
+await web.applyWebTemplate(template);
+
+Returns the collection of changes from the change log that have occurred within the web, based on the specified query.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+// get the web changes including add, update, and delete
+const changes = await web.getChanges({
+ Add: true,
+ ChangeTokenEnd: undefined,
+ ChangeTokenStart: undefined,
+ DeleteObject: true,
+ Update: true,
+ Web: true,
+ });
+
+Returns the name of the image file for the icon that is used to represent the specified file
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { combine } from "@pnp/core";
+
+const iconFileName = await web.mapToIcon("test.docx");
+// iconPath === "icdocx.png"
+// which you can need to map to a real url
+const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`;
+
+// OR dynamically
+const sp = spfi(...);
+const webData = await sp.web.select("Url")();
+const iconFullPath2 = combine(webData.Url, "_layouts", "images", iconFileName);
+
+// OR within SPFx using the context
+const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, "_layouts", "images", iconFileName);
+
+// You can also set size
+// 16x16 pixels = 0, 32x32 pixels = 1
+const icon32FileName = await web.mapToIcon("test.docx", 1);
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/appcatalog";
+import { IStorageEntity } from "@pnp/sp/webs";
+
+// needs to be unique, GUIDs are great
+const key = "my-storage-key";
+
+const sp = spfi(...);
+
+// read an existing entity
+const entity: IStorageEntity = await sp.web.getStorageEntity(key);
+
+// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site
+// you can get the tenant app catalog using the getTenantAppCatalogWeb
+const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();
+
+tenantAppCatalogWeb.setStorageEntity(key, "new value");
+
+// set other properties
+tenantAppCatalogWeb.setStorageEntity(key, "another value", "description", "comments");
+
+const entity2: IStorageEntity = await sp.web.getStorageEntity(key);
+/*
+entity2 === {
+ Value: "another value",
+ Comment: "comments";
+ Description: "description",
+};
+*/
+
+// you can also remove a storage entity
+await tenantAppCatalogWeb.removeStorageEntity(key);
+
+Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IApp } from "@pnp/sp/appcatalog";
+
+const sp = spfi(...);
+
+const appWeb = sp.web.appcatalog;
+const app: IApp = appWeb.getAppById("{your app id}");
+// appWeb url === web url
+
+You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+
+const sp = spfi(...);
+
+// simplest add a page example
+const page = await sp.web.addClientsidePage("mypage1");
+
+// simplest load a page example
+const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+Allows access to the collection of content types in this web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/content-types/web";
+
+const sp = spfi(...);
+
+const cts = await sp.web.contentTypes();
+
+// you can also select fields and use other odata operators
+const cts2 = await sp.web.contentTypes.select("Name")();
+
+Allows access to the collection of content types in this web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/features/web";
+
+const sp = spfi(...);
+
+const features = await sp.web.features();
+
+Allows access to the collection of fields in this web.
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/fields/web";
+
+const sp = spfi(...);
+const fields = await sp.web.fields();
+
+Gets a file by server relative url if your file name contains # and % characters
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+import { IFile } from "@pnp/sp/files/types";
+
+const sp = spfi(...);
+const file: IFile = web.getFileByServerRelativePath("/sites/dev/library/my # file%.docx");
+
+Gets the collection of folders in this web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+
+const sp = spfi(...);
+
+const folders = await sp.web.folders();
+
+// you can also filter and select as with any collection
+const folders2 = await sp.web.folders.select("ServerRelativeUrl", "TimeLastModified").filter("ItemCount gt 0")();
+
+// or get the most recently modified folder
+const folders2 = await sp.web.folders.orderBy("TimeLastModified").top(1)();
+
+Gets the root folder of the web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+
+const sp = spfi(...);
+
+const folder = await sp.web.rootFolder();
+
+Gets a folder by server relative url if your folder name contains # and % characters
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = web.getFolderByServerRelativePath("/sites/dev/library/my # folder%/");
+
+Gets hub site data for the current web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+// get the data and force a refresh
+const data = await sp.web.hubSiteData(true);
+
+Applies theme updates from the parent hub site collection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+await sp.web.syncHubSiteTheme();
+
+Gets the collection of all lists that are contained in the Web site
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { ILists } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const lists: ILists = sp.web.lists;
+
+// you can always order the lists and select properties
+const data = await lists.select("Title").orderBy("Title")();
+
+// and use other odata operators as well
+const data2 = await sp.web.lists.top(3).orderBy("LastItemModifiedDate")();
+
+Gets the UserInfo list of the site collection that contains the Web site
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const list: IList = sp.web.siteUserInfoList;
+
+const data = await list();
+
+// or chain off that list to get additional details
+const items = await list.items.top(2)();
+
+Get a reference to the default document library of a web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+const list: IList = sp.web.defaultDocumentLibrary;
+
+Gets the collection of all list definitions and list templates that are available
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const templates = await sp.web.customListTemplates();
+
+// odata operators chain off the collection as expected
+const templates2 = await sp.web.customListTemplates.select("Title")();
+
+Gets a list by server relative url (list's root folder)
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+const list: IList = sp.web.getList("/sites/dev/lists/test");
+
+const listData = await list();
+
+Returns the list gallery on the site
+Name | +Value | +
---|---|
WebTemplateCatalog | +111 | +
WebPartCatalog | +113 | +
ListTemplateCatalog | +114 | +
MasterPageCatalog | +116 | +
SolutionCatalog | +121 | +
ThemeCatalog | +123 | +
DesignCatalog | +124 | +
AppDataCatalog | +125 | +
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const templateCatalog: IList = await sp.web.getCatalog(111);
+
+const themeCatalog: IList = await sp.web.getCatalog(123);
+
+Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation/web";
+import { INavigation } from "@pnp/sp/navigation";
+
+const sp = spfi(...);
+const nav: INavigation = sp.web.navigation;
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation/web";
+import { IRegionalSettings } from "@pnp/sp/navigation";
+
+const sp = spfi(...);
+const settings: IRegionalSettings = sp.web.regionalSettings;
+
+const settingsData = await settings();
+
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/related-items/web";
+import { IRelatedItemManager, IRelatedItem } from "@pnp/sp/related-items";
+
+const sp = spfi(...);
+const manager: IRelatedItemManager = sp.web.relatedItems;
+
+const data: IRelatedItem[] = await manager.getRelatedItems("{list name}", 4);
+
+Please see information around the available security methods in the security article.
+Please see information around the available sharing methods in the sharing article.
+The site groups
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+const groups = await sp.web.siteGroups();
+
+const groups2 = await sp.web.siteGroups.top(2)();
+
+The web's owner group
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedOwnerGroup();
+
+const users = await sp.web.associatedOwnerGroup.users();
+
+The web's member group
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedMemberGroup();
+
+const users = await sp.web.associatedMemberGroup.users();
+
+The web's visitor group
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedVisitorGroup();
+
+const users = await sp.web.associatedVisitorGroup.users();
+
+Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}");
+
+// copy the role assignments
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", true);
+
+// don't clear sub assignments
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, false);
+
+// specify secondary owner, don't copy permissions, clear sub scopes
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, true, "{second owner login}");
+
+The site users
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const users = await sp.web.siteUsers();
+
+const users2 = await sp.web.siteUsers.top(5)();
+
+const users3 = await sp.web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent("i:0#.f|m")}')`)();
+
+Information on the current user
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const user = await sp.web.currentUser();
+
+// check the login name of the current user
+const user2 = await sp.web.currentUser.select("LoginName")();
+
+Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { IWebEnsureUserResult } from "@pnp/sp/site-users/";
+
+const sp = spfi(...);
+
+const result: IWebEnsureUserResult = await sp.web.ensureUser("i:0#.f|membership|user@domain.onmicrosoft.com");
+
+Returns the user corresponding to the specified member identifier for the current web
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { ISiteUser } from "@pnp/sp/site-users/";
+
+const sp = spfi(...);
+
+const user: ISiteUser = sp.web.getUserById(23);
+
+const userData = await user();
+
+const userData2 = await user.select("LoginName")();
+
+Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions/web";
+import { IUserCustomActions } from "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const actions: IUserCustomActions = sp.web.userCustomActions;
+
+const actionsData = await actions();
+
+Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations.
+interface IWebInfosData {
+ Configuration: number;
+ Created: string;
+ Description: string;
+ Id: string;
+ Language: number;
+ LastItemModifiedDate: string;
+ LastItemUserModifiedDate: string;
+ ServerRelativeUrl: string;
+ Title: string;
+ WebTemplate: string;
+ WebTemplateId: number;
+}
+
+
+
+
+
+
+
+ It is our hope that the transition from version 2.* to 3.* will be as painless as possible, however given the transition we have made from a global sp object to an instance based object some architectural and inital setup changes will need to be addressed. In the following sections we endevor to provide an overview of what changes will be required. If we missed something, please let us know in the issues list so we can update the guide. Thanks!
+For a full, detailed list of what's been added, updated, and removed please see our CHANGELOG
+For a full sample project, utilizing SPFx 1.14 and V3 that showcases some of the more dramatic changes to the library check out this sample.
+For version 2 the core themes were selective imports, a model based on factory functions & interfaces, and improving the docs. This foundation gave us the opportunity to re-write the entire request pipeline internals with minimal external library changes - showing a bit of long-term planning 🙂. With version 3 your required updates are likely to only affect the initial configuration of the library, a huge accomplishment when updating the entire internals.
+Our request pipeline remained largely unchanged since it was first written ~5 years ago, hard to change something so central to the library. The advantage of this update it is now easy for developers to inject their own logic into the request process. As always, this work was based on feedback over the years and understanding how we can be a better library. The new observer design allows you to customize every aspect of the request, in a much clearer way than was previously possible. In addition this work greatly reduced internal boilerplate code and optimized for library size. We reduced the size of sp & graph libraries by almost 2/3. As well we embraced a fully async design built around the new Timeline. Check out the new model around authoring observers and understand how they relate to moments. We feel this new architecture will allow far greater flexibility for consumers of the library to customize the behavior to exactly meet their needs.
+We also used this as an opportunity to remove duplicate methods, clean up and improve our typings & method signatures, and drop legacy methods. Be sure to review the changelog. As always we do our best to minimize breaking changes but major versions are breaking versions.
+We thank you for using the library. Your continued feedback drives these improvements, and we look forward to seeing what you build!
+The biggest change in version 3 of the library is the movement away from the globally defined sp and graph objects. Starting in version 2.1.0 we added the concept of Isolated Runtime
which allowed you to create a separate instance of the global object that would have a separate configuration. We found that the implementation was finicky and prone to issues, so we have rebuilt the internals of the library from the ground up to better address this need. In doing so, we decided not to offer a global object at all.
Because of this change, any architecture that relies on the sp
or graph
objects being configured during initialization and then reused throughout the solution will need to be rethought. Essentially you have three options:
spfi
/graphfi
object wherever it's required.In other words, the sp
and graph
objects have been deprecated and will need to be replaced.
For more information on getting started with these new setup methods please see the Getting Started docs for a deeper look into the Queryable interface see Queryable.
+With the new Querable instance architecture we have provided a way to branch from one instance to another. To do this we provide two methods: AssignFrom and CopyFrom. These methods can be helpful when you want to establish a new instance to which you might apply other behaviors but want to reuse the configuration from a source. To learn more about them check out the Core/Behaviors documentation.
+If you are still using the queryableInstance.get()
method of queryable you must replace it with a direct invoke call queryableInstance()
.
Another benefit of the new updated internals is a significantly streamlined and simplified process for batching requests. Essentially, the interface for SP and Graph now function the same.
+A new module called "batching" will need to be imported which then provides the batched interface which will return a tuple with a new Querable instance and an execute function. To see more details check out Batching.
+In V2, to connect to a different web you would use the function
+const web = Web({Other Web URL});
+
+In V3 you would create a new instance of queryable connecting to the web of your choice. This new method provides you significantly more flexibility by not only allowing you to easily connect to other webs in the same tenant but also to webs in other tenants.
+We are seeing a significant number of people report an error when using this method:
+No observers registered for this request.
which results when it hasn't been updated to use the version 3 convention. Please see the examples below to pick the one that most suits your codebase.
+import { spfi, SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+const spWebA = spfi().using(SPFx(this.context));
+
+// Easiest transition is to use the tuple pattern and the Web constructor which will copy all the observers from the object but set the url to the one provided
+const spWebE = Web([spWebA.web, "{Absolute URL of Other Web}"]);
+
+// Create a new instance of Queryable
+const spWebB = spfi("{Other Web URL}").using(SPFx(this.context));
+
+// Copy/Assign a new instance of Queryable using the existing
+const spWebC = spfi("{Other Web URL}").using(AssignFrom(sp.web));
+
+// Create a new instance of Queryable using other credentials?
+const spWebD = spfi("{Other Web URL}").using(SPFx(this.context));
+
+
+Please see the documentation for more information on the updated Web constructor.
+Starting with v3 we are dropping the commonjs versions of all packages. Previously we released these as we worked to transition to esm and the current node didn't yet support esm. With esm now a supported module type, and having done the work to ensure they work in node we feel it is a good time to drop the -commonjs variants. Please see documentation on Getting started with NodeJS Project using TypeScript producing CommonJS modules
+ + + + + + +Added in 1.0.4
+This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with +SharePoint Framework's permissions.
+Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes. This method only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined above using the constructor to specify the values for an AAD Application you have setup.
+By providing the context in the onInit we can create the adal client from known information.
+import { graph } from "@pnp/graph"; +import { getRandomString } from "@pnp/core"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + graph.setup({ + spfxContext: this.context + }); + }); +} + +public render(): void { + + // here we are creating a team with a random name, required Group ReadWrite All permissions + const teamName = `ATeam.${getRandomString(4)}`; + + this.domElement.innerHTML = `Hello, I am creating a team named "${teamName}" for you...`; + + graph.teams.create(teamName, "This is a description").then(t => { + + this.domElement.innerHTML += "done!"; + + }).catch(e => { + + this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; + }); +} +
This example shows how to use the ADALClient with the @pnp/sp library to call
+import { sp } from "@pnp/sp"; +import { AdalClient } from "@pnp/core"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + sp.setup({ + spfxContext: this.context, + sp: { + fetchClientFactory: () => , + }, + }); + + }); +} + +public render(): void { + + sp.web.get().then(t => { + this.domElement.innerHTML = JSON.stringify(t); + }).catch(e => { + this.domElement.innerHTML = JSON.stringify(e); + }); +} +
You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example.
+import { AdalClient, FetchOptions } from "@pnp/core"; +import { ODataDefaultParser } from "@pnp/queryable"; + +// ... + +public render(): void { + + // create an ADAL Client + const client = AdalClient.fromSPFxContext(this.context); + + // setup the request options + const opts: FetchOptions = { + method: "GET", + headers: { + "Accept": "application/json", + }, + }; + + // execute the request + client.fetch("https://tenant.sharepoint.com/_api/web", opts).then(response => { + + // create a parser to convert the response into JSON. + // You can create your own, at this point you have a fetch Response to work with + const parser = new ODataDefaultParser(); + + parser.parse(response).then(json => { + this.domElement.innerHTML = JSON.stringify(json); + }); + + }).catch(e => { + this.domElement.innerHTML = JSON.stringify(e); + }); + +} +
This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.
+This sample uses a custom AzureAd app you have created and granted the appropriate permissions.
+import { AdalClient } from "@pnp/core"; +import { graph } from "@pnp/graph"; + +// configure the graph client +// parameters are: +// client id - the id of the application you created in azure ad +// tenant - can be id or URL (shown) +// redirect url - absolute url of a page to which your application and Azure AD app allows replies +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalClient( + "e3e9048e-ea28-423b-aca9-3ea931cc7972", + "{tenant}.onmicrosoft.com", + "https://myapp/singlesignon.aspx"); + }, + }, +}); + +try { + + // call the graph API + const groups = await graph.groups.get(); + + console.log(JSON.stringify(groups, null, 4)); + +} catch (e) { + console.error(e); +} +
The collections module provides typings and classes related to working with dictionaries.
+Interface used to described an object with string keys corresponding to values of type T
+export interface TypedHash<T> { + [key: string]: T; +} +
Converts a plain object to a Map instance
+const map = objectToMap({ a: "b", c: "d"}); +
Merges two or more maps, overwriting values with the same key. Last value in wins.
+const m1 = new Map(); +const m2 = new Map(); +const m3 = new Map(); +const m4 = new Map(); + +const m = mergeMaps(m1, m2, m3, m4); +
This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation.
+It is possible you may need complete control over the sending and receiving of requests.
+Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation.
+The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl.
+export interface HttpClientImpl { + fetch(url: string, options: FetchOptions): Promise<Response>; +} +
There is a single method "fetch" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface.
+export interface FetchOptions { + method?: string; + headers?: HeadersInit | { [index: string]: string }; + body?: BodyInit; + mode?: string | RequestMode; + credentials?: string | RequestCredentials; + cache?: string | RequestCache; +} +
So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read 👍.
+Once you have written your implementation using it on your requests is done by setting it in the global library configuration:
+import { setup } from "@pnp/core"; +import { sp, Web } from "@pnp/sp"; +import { MyAwesomeClient } from "./awesomeclient"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new MyAwesomeClient(); + } + } +}); + +let w = new Web("{site url}"); + +// this request will use your client. +w.select("Title").get().then(w => { + console.log(w); +}); +
You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.
+Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.
+ + + + + + + + + +The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.
+Install the library and required dependencies
+npm install @pnp/core --save
Import and use functionality, see details on modules below.
+import { getGUID } from "@pnp/core"; + +console.log(getGUID()); +
Graphical UML diagram of @pnp/core. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core +configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and +contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.
+Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below
+export interface LibraryConfiguration { + + /** + * Allows caching to be global disabled, default: false + */ + globalCacheDisable?: boolean; + + /** + * Defines the default store used by the usingCaching method, default: session + */ + defaultCachingStore?: "session" | "local"; + + /** + * Defines the default timeout in seconds used by the usingCaching method, default 30 + */ + defaultCachingTimeoutSeconds?: number; + + /** + * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval + */ + enableCacheExpiration?: boolean; + + /** + * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) + */ + cacheExpirationIntervalMilliseconds?: number; + + /** + * Used to supply the current context from an SPFx webpart to the library + */ + spfxContext?: any; +} +
The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary +used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.
+The extend method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any +existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please +see the section below "Using RuntimeConfig within your application". Note there are no methods to remove/clear the global config as it should be considered fairly static +as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application.
+import { RuntimeConfig } from "@pnp/core"; + +// add your custom keys to the global configuration +// note you can use object hashes as values +RuntimeConfig.extend({ + "myKey1": "value 1", + "myKey2": { + "subKey": "sub value 1", + "subKey2": "sub value 2", + }, +}); + +// read your custom values +const v = RuntimeConfig.get("myKey1"); // "value 1" +
If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To +do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties
+import { LibraryConfiguration, RuntimeConfig } from "@pnp/core"; + +// first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because +// TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. + +// create the interface of your properties +// by creating this separately you allows others to compose your parts into their own config +interface MyConfigurationPart { + + // you can create a grouped definition and access your settings as an object + // keys can be optional or required as defined by your interface + my?: { + prop1?: string; + prop2?: string; + } + + // and/or define multiple top level properties (beware key collision) + // it is good practice to use a unique prefix + myProp1: string; + myProp2: number; +} + +// now create a combined interface +interface MyConfiguration extends LibraryConfiguration, MyConfigurationPart { } + + +// now create a wrapper object and expose your properties +class MyRuntimeConfigImpl { + + // exposing a nested property + public get prop1(): TypedHash<string> { + + const myPart = RuntimeConfig.get("my"); + if (myPart !== null && typeof myPart !== "undefined" && typeof myPart.prop1 !== "undefined") { + return myPart.prop1; + } + + return {}; + } + + // exposing a root level property + public get myProp1(): string | null { + + let myProp1 = RuntimeConfig.get("myProp1"); + + if (myProp1 === null) { + myProp1 = "some default value"; + } + + return myProp1; + } + + setup(config: MyConfiguration): void { + RuntimeConfig.extend(config); + } +} + +// create a single static instance of your impl class +export let MyRuntimeConfig = new MyRuntimeConfigImpl(); +
Now in other files you can use and set your configuration with a typed interface and properties
+import { MyRuntimeConfig } from "{location of module}"; + + +MyRuntimeConfig.setup({ + my: { + prop1: "hello", + }, +}); + +const value = MyRuntimeConfig.prop1; // "hello" +
This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces +are described below (many have no use outside the library) as well as several classes.
+Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method "fetch" take a URL and
+options and returning a Promise
An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The +difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the +underlying HttpClientImpl fetch method.
+This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.
+Basic implementation that calls the global (window) fetch method with no additional processing.
+import { FetchClient } from "@pnp/core"; + +const client = new FetchClient(); + +client.fetch("{url}", {}); +
A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and +the token is treated as a static string.
+import { BearerTokenFetchClient } from "@pnp/core"; + +const client = new BearerTokenFetchClient("{authentication token}"); + +client.fetch("{url}", {}); +
This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with +a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.
+The main export of this module, contains properties representing local and session storage.
+import { PnPClientStorage } from "@pnp/core"; + +const storage = new PnPClientStorage(); +const myvalue = storage.local.get("mykey"); +
Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.
+import { PnPClientStorage } from "@pnp/core"; + +const storage = new PnPClientStorage(); + +// get a value from storage +const value = storage.local.get("mykey"); + +// put a value into storage +storage.local.put("mykey2", "my value"); + +// put a value into storage with an expiration +storage.local.put("mykey2", "my value", new Date()); + +// put a simple object into storage +// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects +storage.local.put("mykey3", { + key: "value", + key2: "value2", +}); + +// remove a value from storage +storage.local.delete("mykey3"); + +// get an item or add it if it does not exist +// returns a promise in case you need time to get the value for storage +// optionally takes a third parameter specifying the expiration +storage.local.getOrPut("mykey4", () => { + return Promise.resolve("value"); +}); + +// delete expired items +storage.local.deleteExpired(); +
The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.
+import { PnPClientStorage } from "@pnp/core"; + +const storage = new PnPClientStorage(); + +// session storage +storage.session.deleteExpired(); + +// local storage +storage.local.deleteExpired(); + +// this returns a promise, so you can perform some activity after the expired items are removed: +storage.local.deleteExpired().then(_ => { + // init my application +}); +
The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout.
+import { setup } from "@pnp/core"; + +setup({ + enableCacheExpiration: true, + cacheExpirationIntervalMilliseconds: 1000, // optional +}); +
This module contains utility methods that you can import individually from the common library.
+import { + getRandomString, +} from "@pnp/core"; + +// use from individual;y imported method +console.log(getRandomString(10)); +
Gets a callback function which will maintain context across async calls.
+import { getCtxCallback } from "@pnp/core"; + +const contextThis = { + myProp: 6, +}; + +function theFunction() { + // "this" within this function will be the context object supplied + // in this case the variable contextThis, so myProp will exist + return this.myProp; +} + +const callback = getCtxCallback(contextThis, theFunction); + +callback(); // returns 6 + +// You can also supply additional parameters if needed + +function theFunction2(g: number) { + // "this" within this function will be the context object supplied + // in this case the variable contextThis, so myProp will exist + return this.myProp + g; +} + +const callback2 = getCtxCallback(contextThis, theFunction, 4); + +callback2(); // returns 10 (6 + 4) +
Manipulates a date, please see the Stackoverflow discussion from where this method was taken.
+Combines any number of paths, normalizing the slashes as required
+import { combine } from "@pnp/core"; + +// "https://microsoft.com/something/more" +const paths = combine("https://microsoft.com", "something", "more"); + +// "also/works/with/relative" +const paths2 = combine("/also/", "/works", "with/", "/relative\\"); +
Gets a random string consisting of the number of characters requested.
+import { getRandomString } from "@pnp/core"; + +const randomString = getRandomString(10); +
Creates a random guid, please see the Stackoverflow discussion from where this method was taken.
+Determines if a supplied variable represents a function.
+Determines if an object is defined and not null.
+Determines if a supplied variable represents an array.
+Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing +properties.
+import { extend } from "@pnp/core"; + +let obj1 = { + prop: 1, + prop2: 2, +}; + +const obj2 = { + prop: 4, + prop3: 9, +}; + +const example1 = extend(obj1, obj2); +// example1 = { prop: 4, prop2: 2, prop3: 9 } + +const example2 = extend(obj1, obj2, true); +// example2 = { prop: 1, prop2: 2, prop3: 9 } +
Determines if a supplied url is absolute and returns true; otherwise returns false.
+Determines if a supplied string is null or empty
+Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods +below for use in your projects should you require.
+/** + * Loads a stylesheet into the current page + * + * @param path The url to the stylesheet + * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues + */ +public static loadStylesheet(path: string, avoidCache: boolean): void { + if (avoidCache) { + path += "?" + encodeURIComponent((new Date()).getTime().toString()); + } + const head = document.getElementsByTagName("head"); + if (head.length > 0) { + const e = document.createElement("link"); + head[0].appendChild(e); + e.setAttribute("type", "text/css"); + e.setAttribute("rel", "stylesheet"); + e.setAttribute("href", path); + } +} + +/** + * Tests if a url param exists + * + * @param name The name of the url parameter to check + */ +public static urlParamExists(name: string): boolean { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + return regex.test(location.search); +} + +/** + * Gets a url param value by name + * + * @param name The name of the parameter for which we want the value + */ +public static getUrlParamByName(name: string): string { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"); + const results = regex.exec(location.search); + return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +} + +/** + * Gets a url param by name and attempts to parse a bool value + * + * @param name The name of the parameter for which we want the boolean value + */ +public static getUrlParamBoolByName(name: string): boolean { + const p = this.getUrlParamByName(name); + const isFalse = (p === "" || /false|0/i.test(p)); + return !isFalse; +} + +/** + * Inserts the string s into the string target as the index specified by index + * + * @param target The string into which we will insert s + * @param index The location in target to insert s (zero based) + * @param s The string to insert into target at position index + */ +public static stringInsert(target: string, index: number, s: string): string { + if (index > 0) { + return target.substring(0, index) + s + target.substring(index, target.length); + } + return s + target; +} +
The main class exported from the config-store package is Settings. This is the class through which you will load and access your +settings via providers.
+import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create an instance of the settings class, could be static and shared across your application +// or built as needed. +const settings = new Settings(); + +// you can add/update a single value using add +settings.add("mykey", "myvalue"); + +// you can also add/update a JSON value which will be stringified for you as a shorthand +settings.addJSON("mykey2", { + field: 1, + field2: 2, + field3: 3, +}); + +// and you can apply a plain object of keys/values that will be written as single values +// this results in each enumerable property of the supplied object being added to the settings collection +settings.apply({ + field: 1, + field2: 2, + field3: 3, +}); + +// and finally you can load values from a configuration provider +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +// this will load values from the supplied list +// by default the key will be from the Title field and the value from a column named Value +await settings.load(provider); + +// once we have loaded values we can then read them +const value = settings.get("mykey"); + +// or read JSON that will be parsed for you from the store +const value2 = settings.getJSON("mykey2"); +
This module providers a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.
+Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/config-store --save
See the topics below for usage:
+ +Graphical UML diagram of @pnp/config-store. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +Currently there is a single provider included in the library, but contributions of additional providers are welcome.
+This provider is based on a SharePoint list and read all of the rows and makes them available as a TypedHash
import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create a new provider instance +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +const settings = new Settings(); + +// load our values from the list +await settings.load(provider); +
Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a +provider and caches the configuration in local or session storage.
+import { Web } from "@pnp/sp"; +import { Settings, SPListConfigurationProvider } from "@pnp/config-store"; + +// create a new provider instance +const w = new Web("https://mytenant.sharepoint.com/sites/dev"); +const provider = new SPListConfigurationProvider(w, "myconfiglistname"); + +// get an instance of the provider wrapped +// you can optionally provide a key that will be used in the cache to the asCaching method +const wrappedProvider = provider.asCaching(); + +// use that wrapped provider to populate the settings +await settings.load(wrappedProvider); +
Note this article applies to version 1.4.1 SharePoint Framework projects targetting on-premesis only.
+When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premesis it installs TypeScript version 2.2.2. Unfortunately this library relies on 2.4.2 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article.
+"typescript": "2.2.2"
"typescript": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", + "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", + "dev": true +} +
rm -rf node_modules/
npm install
This can be checked with:
+npm list typescript +
+-- @microsoft/sp-build-web@1.1.0 +| `-- @microsoft/gulp-core-build-typescript@3.1.1 +| +-- @microsoft/api-extractor@2.3.8 +| | `-- typescript@2.4.2 +| `-- typescript@2.4.2 +
To help folks try out new features early and provide feedback prior to releases we publish beta versions of the packages. Released as a set with matching version numbers, just like when we do a normal release. Generally every Friday a new set of beta libraries will be released. While not ready for production use we encourage you to try out these pre-release packages and provide us feedback.
+To install the beta packages in your project you use the @beta version number on the packages. This applies to all packages, not just the ones +shown in the example below.
+npm install @pnp/logging@beta @pnp/core@beta @pnp/queryable@beta @pnp/sp@beta --save +
Please remember that it is possible something may not work in a beta version, so be aware and if you find something please report an +issue.
+ + + + + + + + + +The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses the launch.json file to build and run the library using ./debug/launch/main.ts as the program entry. You can add any number of files to this directory and they will be ignored by git, however the debug.ts file is not, so please ensure you don't commit any login information.
+If you have not already you need to create a settings.js files by copying settings.example.js and renaming it to settings.js. Then update the clientId, clientSecret, and siteUrl fields in the testing section. (See below for guidance on registering a client id and secret)
+If you hit F5 now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.
+Using ./debug/launch/example.ts as a reference create a debugging file in the debug folder, let's call it mydebug.ts and add this content:
+// note we can use the actual package names for our imports +import { sp, ListEnsureResult } from "@pnp/sp"; +import { Logger, LogLevel, ConsoleListener } from "@pnp/logging"; + +declare var process: { exit(code?: number): void }; + +export function MyDebug() { + + // run some debugging + sp.web.lists.ensure("MyFirstList").then((list: ListEnsureResult) => { + + Logger.log({ + data: list.created, + message: "Was list created?", + level: LogLevel.Verbose + }); + + if (list.created) { + + Logger.log({ + data: list.data, + message: "Raw data from list creation.", + level: LogLevel.Verbose + }); + + } else { + + Logger.log({ + data: null, + message: "List already existed!", + level: LogLevel.Verbose + }); + } + + process.exit(0); + }).catch(e => { + + Logger.error(e); + process.exit(1); + }); +} +
First comment out the import for the default example and then add the import and function call for yours, the updated main.ts should look like this:
+// ... + +// comment out the example +// import { Example } from "./example"; +// Example(); + +import { MyDebug } from "./mydebug" +MyDebug(); + +// ... +
Place a break point within the promise resolution in your debug file and hit F5. Your module should be run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.
+Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally.
+You can also serve files locally to debug in a browser through two methods. The first will serve code using ./debug/serve/main.ts as the entry. Meaning you can easily +write code and test it in the browser. The second method allows you to serve a single package (bundled with all dependencies) for in browser testing. Both methods serve +the file from https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing.
+This will serve a package with ./debug/serve/main.ts as the entry.
+gulp serve
Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
+<script src="https://localhost:8080/assets/pnp.js"></script> +<div id="pnptestdiv"></div> +
You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but note that any changes +included as part of a PR to this file will not be allowed.
+For example if you wanted to serve the @pnp/sp package for testing you would use:
+gulp serve --p sp
This will serve a bundle of the sp functionality along with all dependencies and place a global variable named "pnp.{packagename}", in this case pnp.sp. This will be +true for each package, if you served just the graph package the global would be pnp.graph. This mirrors how the umd modules are built in the distributed npm packages +to allow testing with matching packages.
+You can make changes to the library and immediately see them reflected in the browser. All files are watched regardless of which serve method you choose.
+Before you can begin debugging you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly.
+Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site.
+<AppPermissionRequests AllowAppOnlyPolicy="true"> + <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" /> + <AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" /> + <AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal" /> + </AppPermissionRequests> +
Note these are very broad permissions to ensure you can test any feature of the library, for production you should tailor the permissions to only those required
+There are two recommended ways to consume the library in a production deployment: bundle the code into your solution (such as with webpack), or reference the code from a CDN. These methods are outlined here but this is not meant to be an exhaustive guide on all the ways to package and deploy solutions.
+If you have installed the library via NPM into your application solution bundlers such as webpack can bundle the PnPjs libraries along with your solution. This can make deployment easier, but will increase the size of your application by the size of the included libraries. The PnPjs libraries are setup to support tree shaking which can help with the bundle size.
+If you have public internet access you can reference the library from cdnjs or unpkg which maintains copies of all versions. This is ideal as you do not need to host the file yourself, and it is easy to update to a newer release by updating the URL in your solution. Below lists all of the library locations within cdnjs, you will need to ensure you have the full url to the file you need, such as: "https://cdnjs.cloudflare.com/ajax/libs/pnp-common/1.1.1/common.es5.umd.min.js". To use the libraries with a script tag in a page it is recommended to use the *.es5.umd.min.js versions. This will add a global pnp value with each library added as pnp.{lib name} such as pnp.sp, pnp.common, etc.
+If you are developing in SPFx and install and import the PnPjs libraries the default behavior will be to bundle the library into your solution. You have a couple of choices on how best to work with CDNs and SPFx. Because SPFx doesn't currently respect peer dependencies it is easier to reference the pnpjs rollup package for your project. In this case you would install the package, reference it in your code, and update your config.js file externals as follows:
+npm install @pnp/pnpjs --save
import { sp } from "@pnp/pnpjs"; + +sp.web.lists.getByTitle("BigList").get().then(r => { + + this.domElement.innerHTML += r.Title; +}); +
"externals": { + "@pnp/pnpjs": { + "path": "https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.1.4/pnpjs.es5.umd.bundle.min.js", + "globalName": "pnp" + } + }, +
You can still work with the individual packages from a cdn, but you have a bit more work to do. First install the modules you need, update the config with the JSON externals below, and add some blind require statements into your code. These are needed because peer dependencies are not processed by SPFx so you have to "trigger" the SPFx manifest creator to include those packages.
+++Note this approach requires using version 1.1.5 (specifically beta 1.1.5-2) or later of the libraries as we had make a few updates to how things are packaged to make this a little easier.
+
npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save
// blind require statements +require("tslib"); +require("@pnp/logging"); +require("@pnp/core"); +require("@pnp/queryable"); +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("BigList").get().then(r => { + + this.domElement.innerHTML += r.Title; +}); +
"externals": { + "@pnp/sp": { + "path": "https://unpkg.com/@pnp/sp@1.1.5-2/dist/sp.es5.umd.min.js", + "globalName": "pnp.sp", + "globalDependencies": [ + "@pnp/logging", + "@pnp/core", + "@pnp/queryable", + "tslib" + ] + }, + "@pnp/queryable": { + "path": "https://unpkg.com/@pnp/queryable@1.1.5-2/dist/odata.es5.umd.min.js", + "globalName": "pnp.odata", + "globalDependencies": [ + "@pnp/core", + "@pnp/logging", + "tslib" + ] + }, + "@pnp/core": { + "path": "https://unpkg.com/@pnp/core@1.1.5-2/dist/common.es5.umd.bundle.min.js", + "globalName": "pnp.common" + }, + "@pnp/logging": { + "path": "https://unpkg.com/@pnp/logging@1.1.5-2/dist/logging.es5.umd.min.js", + "globalName": "pnp.logging", + "globalDependencies": [ + "tslib" + ] + }, + "tslib": { + "path": "https://cdnjs.cloudflare.com/ajax/libs/tslib/1.9.3/tslib.min.js", + "globalName": "tslib" + } +} +
Don't forget to update the version number in the url to match the version you want to use. This will stop the library from being bundled directly into the solution and instead use the copy from the CDN. When a new version of the PnPjs libraries are released and you are ready to update just update this url in your SPFX project's config.js file.
+ + + + + + + + + +Building the documentation locally can help you visualize change you are making to the docs. What you see locally should be what you see online.
+Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.
+When executing the pip module on Windows you can prefix it with python -m. +For example:
+python -m pip install mkdocs-material +
Thank you for your interest in contributing to our work. This guide should help you get started, please let us know if you have any questions.
+gulp test
gulp lint
These steps will help you get your environment setup for contributing to the core library.
+Install Visual Studio Code - this is the development environment we will use. It is similar to a light-weight Visual Studio designed for each editing of client file types such as .ts and .js. (Note that if you prefer you can use Visual Studio).
+Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).
+On Windows: Install Python v2.7.10 - this is used by some of the plug-ins and build tools inside Node JS - (Python v3.x.x is not supported by those modules). If Visual Studio is not installed on the client in addition to this C++ runtime is required. Please see node-gyp Readme
+Install a console emulator of your choice, for Windows Cmder is popular. If installing Cmder choosing the full option will allow you to use git for windows. Whatever option you choose we will refer in the rest of the guide to "console" as the thing you installed in this step.
+Install the tslint extension in VS Code:
+Install the gulp command line globally by typing the following code in your console npm install -g gulp-cli
Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.
+Once you have the code locally, navigate to the root of the project in your console. Type the following command:
+npm install
- installs all of the npm package dependencies (may take awhile the first time)
Copy settings.example.js in the root of your project to settings.js. Edit settings.js to reflect your personal environment (usename, password, siteUrl, etc.).
+Then you can follow the guidance in the debugging article to get started testing right away!
+These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install +the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install +more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.
+If you need to support older browsers please review the article on polyfills for required functionality.
+First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any +environment or project.
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/graph --save
Next we can import and use the functionality within our application. The below is a very simple example, please see the individual package documentation +for more details.
+import { getRandomString } from "@pnp/core"; + +(function() { + + // get and log a random string + console.log(getRandomString(20)); + +})() +
The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 on-premises please read this note on a workaround for the included TypeScript version. If you are targetting SharePoint online you do not need to take any additional steps.
+Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request +urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the spfx context to the library. Either through the setup method +imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if +you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports.
+The setup is always done in the onInit method to ensure it runs before your other lifecycle code. You can also set any other settings at this time.
+import { setup as pnpSetup } from "@pnp/core"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + + pnpSetup({ + spfxContext: this.context + }); + }); +} + +// ... +
import { sp } from "@pnp/sp"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + + sp.setup({ + spfxContext: this.context + }); + }); +} + +// ... +
import { graph } from "@pnp/graph"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + + graph.setup({ + spfxContext: this.context + }); + }); +} + +// ... +
Because you do not have access to the full context object within a service you need to setup things slightly differently. This works for the sp library, but not the graph library as we don't have access to the AAD token provider from the full context.
+import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library"; +import { PageContext } from "@microsoft/sp-page-context"; +import { AadTokenProviderFactory } from "@microsoft/sp-http"; +import { sp } from "@pnp/sp"; + +export interface ISampleService { + getLists(): Promise<any[]>; +} + +export class SampleService { + + public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService); + + constructor(serviceScope: ServiceScope) { + + serviceScope.whenFinished(() => { + + const pageContext = serviceScope.consume(PageContext.serviceKey); + const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey); + + // we need to "spoof" the context object with the parts we need for PnPjs + sp.setup({ + spfxContext: { + aadTokenProviderFactory: tokenProviderFactory, + pageContext: pageContext, + } + }); + + // This approach also works if you do not require AAD tokens + // you don't need to do both + // sp.setup({ + // sp : { + // baseUrl : pageContext.web.absoluteUrl + // } + // }); + }); + } + public getLists(): Promise<any[]> { + return sp.web.lists.get(); + } +} +
Because peer dependencies are not installed automatically you will need to list out each package to install. Don't worry if you forget one you will get a message +on the command line that a peer dependency is missing. Let's for example look at installing the required libraries to connect to SharePoint from nodejs. You can see +./debug/launch/sp.ts for a live example.
+npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs +
This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. +Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:
+import { sp } from "@pnp/sp"; +import { SPFetchClient } from "@pnp/nodejs"; +
Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint.
+// configure your node options (only once in your application) +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}"); + }, + }, +}); + +// make a call to SharePoint and log it in the console +sp.web.select("Title", "Description").get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +
Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see +./debug/launch/graph.ts for a live example.
+npm i @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs +
Now we need to import what we'll need to call graph
+import { graph } from "@pnp/graph"; +import { AdalFetchClient } from "@pnp/nodejs"; +
Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions.
+graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{mytenant}.onmicrosoft.com", "{application id}", "{application secret}"); + }, + }, +}); + +// make a call to Graph and get all the groups +graph.v1.groups.get().then(g => { + console.log(JSON.stringify(g, null, 4)); +}); +
In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.
+Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when +working against unpatched versions of SharePoint 2013 as discussed here. +This is optional for 2016 or SharePoint Online.
+import { sp } from "@pnp/sp"; + +sp.setup({ + sp: { + headers: { + Accept: "application/json;odata=verbose", + }, + baseUrl: "{Absolute SharePoint Web URL}" + }, +}); + +const w = await sp.web.get(); +
Using this method you create the web directly with the url you want to use as the base.
+import { Web } from "@pnp/sp"; + +const web = new Web("{Absolute SharePoint Web URL}"); +const w = await web.get(); +
This library uses Gulp to orchestrate various tasks. The tasks described below are available for your use. Please review the +getting started for development to ensure you've setup your environment correctly. The source for the gulp commands can be found in +the tools\gulptasks folder at the root of the project.
+All gulp commands are run on the command line in the fashion shown below.
+gulp <command> [optional pararms] +
The build command transpiles the solution from TypeScript into JavaScript using our custom build system. It is controlled by the pnp-build.js file at +the project root.
+gulp build +
Note when building a single package none of the dependencies are currently built, so you need to specify in order those packages to build which are dependencies.
+# fails +gulp build --p sp + +# works as all the dependencies are built in order +gulp build --p logging,common,odata,sp +
You can also build the packages and then not clean using the nc flag. So for example if you are working on the sp package you can build all the packages once, then +use the "nc" flag to leave those that aren't changing.
+# run once +gulp build --p logging,common,odata,sp + +# run on subsequent builds +gulp build --p sp --nc +
The clean command removes all of the generated folders from the project and is generally used automatically before other commands to ensure there is a clean workspace.
+gulp clean +
To clean the build folder. This build folder is no longer included in automatic cleaning after the move to use the TypeScript project references feature that compares previous output and doesn't rebuild unchanged files. This command will erase the entire build folder ensuring you can conduct a clean build/test/etc.
+gulp clean-build +
Runs the project linting based on the tslint.json rules defined at the project root. This should be done before any PR submissions as linting failures will block merging.
+gulp lint +
Used to create the packages in the ./dist folder as they would exist for a release.
+gulp package +
You can also package individual packages, but as with build you must also package any dependencies at the same time.
+gulp package --p logging,common,odata,sp +
This command is only for use by package authors to publish a version to npm and is not for developer use.
+The serve command allows you to serve either code from the ./debug/serve folder OR an individual package for testing in the browser. The file will always be served as +https://localhost:8080/assets/pnp.js so can create a static page in your tenant for easy testing of a variety of scenarios. NOTE that in most browsers this file will +be flagged as unsafe so you will need to trust it for it to execute on the page.
+When running the command with no parameters you will generate a package with the entry being based on the tsconfig.json file in ./debug/serve. By default this will +use serve.ts. This allows you to write any code you want to test to easily run it in the browser with all changes being watched and triggering a rebuild.
+gulp serve +
If instead you want to test how a particular package will work in the browser you can serve just that package. In this case you do not need to specify the dependencies +and specifying multiple packages will throw an error. Packages will be injected into the global namespace on a variable named pnp.
+gulp serve --p sp +
Runs the tests specified in each package's tests folder
+gulp test +
The test command will switch to the "spec" mocha reporter if you supply the verbose flag. Doing so will list out each test's description and sucess instead of the "dot" used by default. This flag works with all other test options.
+gulp test --verbose +
You can test individual packages as needed, and there is no need to include dependencies in this case
+# test the logging and sp packages +gulp test --p logging,sp +
If you are working on a specific set of tests for a single module you can also use the single or s parameter to select just +a single module of tests. You specify the filename without the ".test.ts" suffix. It must be within the specified package and +this option can only be used with a single package for --p
+# will test only the client-side pages module within the sp package +gulp test --p sp --s clientsidepages +
If you want you can test within the same site and avoid creating a new one, though for some tests this might cause conflicts. +This flag can be helpful if you are rapidly testing things with no conflict as you can avoid creating a site each time. Works +with both of the above options --p and --s as well as individually. The url must be absolute.
+#testing using the specified site. +gulp test --site https://{tenant}.sharepoint.com/sites/testing + +# with other options +gulp test --p logging,sp --site https://{tenant}.sharepoint.com/sites/testing + +gulp test --p sp --s clientsidepages --site https://{tenant}.sharepoint.com/sites/testing +
Each of the packages is published with the same structure, so this article applies to all of the packages. We will use @pnp/core as an example for discussion.
+In addition to the files in the root each package has three folders dist, docs, and src.
+These files are found at the root of each package.
+File | +Description | +
---|---|
index.d.ts | +Referenced in package.json typings property and provides the TypeScript type information for consumers | +
LICENSE | +Package license | +
package.json | +npm package definition | +
readme.md | +Basic readme referencing the docs site | +
The dist folder contains the transpiled files bundled in various ways. You can choose the best file for your usage as needed. Below the {package} will be +replaced with the name of the package - in our examples case this would be "common" making the file name "{package}.es5.js" = "common.es5.js". All of the *.map +files are the debug mapping files related to the .js file of the same name.
+File | +Description | +
---|---|
{package}.es5.js | +Library packaged in es5 format not wrapped as a module | +
{package}.es5.umd.bundle.js | +The library bundled with all dependencies into a single UMD module. Global variable will be "pnp.{package}". Referenced in the main property of package.json | +
{package}.es5.umd.bundle.min.js | +Minified version of the bundled umd module | +
{package}.es5.umd.js | +The library in es5 bundled as a UMD modules with no included dependencies. They are designed to work with the other *.es5.umd.js files. Referenced in the module property of package.json | +
{package}.es5.umd.min.js | +Minified version of the es5 umd module | +
{package}.js | +es6 format file of the library. Referenced by es2015 property of package.json | +
This folder contains markdown documentation for the library. All packages will include an index.md which serves as the root of the docs. These files are also used +to build the public site. To edit these files they can be found in the packages/{package}/docs folder.
+Contains the TypeScript definition files refrenced by the index.d.ts in the package root. These files serve to provide typing information about the library to +consumers who can process typing information.
+ + + + + + + + + +The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within +the @pnp scope.
+The latest published version is ****.
++ | + | + |
---|---|---|
@pnp/ | ++ | + |
+ | common | +Provides shared functionality across all pnp libraries | +
+ | config-store | +Provides a way to manage configuration within your application | +
+ | graph | +Provides a fluent api for working with Microsoft Graph | +
+ | logging | +Light-weight, subscribable logging framework | +
+ | nodejs | +Provides functionality enabling the @pnp libraries within nodejs | +
+ | odata | +Provides shared odata functionality and base classes | +
+ | pnpjs | +Rollup library of core functionality (mimics sp-pnp-js) | +
+ | sp | +Provides a fluent api for working with SharePoint REST | +
+ | sp-addinhelpers | +Provides functionality for working within SharePoint add-ins | +
+ | sp-clientsvc | +Provides base classes for working with the legacy SharePoint | +
+ | sp-taxonomy | +Provides a fluent api for working with SharePoint Managed Metadata | +
These libraries may make use of some features not found in older browsers, mainly fetch, Map, and Proxy. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. There are several ways to include this missing functionality.
+We created a package you can use to include the needed functionality without having to determine what polyfills are required. Also, this package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you need to support IE 11.
+npm install --save @pnp/polyfill-ie11
import "@pnp/polyfill-ie11"; +import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("BigList").items.filter(`ID gt 6000`).get().then(r => { + this.domElement.innerHTML += r.map(l => `${l.Title}<br />`); +}); +
Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version for IE 11 as shown below.
+import "@pnp/polyfill-ie11"; +import { SearchQueryBuilder } from "@pnp/polyfill-ie11/dist/searchquerybuilder"; +import { sp, ISearchQueryBuilder } from "@pnp/sp"; + +// works in IE11 and other browsers +const builder: ISearchQueryBuilder = SearchQueryBuilder().text("test"); + +sp.search(builder).then(r => { + this.domElement.innerHTML = JSON.stringify(r); +}); +
If acceptable to your design and security requirements you can use a service to provide missing functionality. This loads scripts from a service outside of your and our +control, so please ensure you understand any associated risks.
+To use this option you need to wrap the code in a function, here called "stuffisloaded". Then you need to add another script tag as shown below that will load what you need from the polyfill service. Note the parameter "callback" takes our function name.
+<script src="https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.2.1/pnpjs.es5.umd.bundle.min.js" type="text/javascript"></script> +<script> +// this function will be executed once the polyfill is loaded. +function stuffisloaded() { + + pnp.sp.web.select("Title").get() + .then(function(data){ + document.getElementById("main").innerText=data.Title; + }) + .catch(function(err){ + document.getElementById("main").innerText=err; + }); +} +</script> +<!-- This script tag loads the required polyfills from the service --> +<script src="https://cdn.polyfill.io/v2/polyfill.min.js?callback=stuffisloaded&features=es6,fetch,Map&flags=always,gated"></script> +
If you are using a module loader you need to load the following two files as well. You can do this form a CDN or your style library.
+One issue you still may see is that you get errors that certain libraries are undefined when you try to run your code. This is because your code is running before +these libraries are loaded. You need to ensure that all dependencies are loaded before making use of the pnp libraries.
+ + + + + + + + + +These libraries are based on the sp-pnp-js library and our goal was to make transition as easy as possible. The most +obvious difference is the splitting of the library into multiple packages. We have however created a rollup library to help folks make the move - though our +recommendation is to switch to the separate packages. This article outlines transitioning your existing projects from sp-pnp-js to the new libraries, please provide +feedback on how we can improve out guidance.
+With the separation of the packages we needed a way to indicate how they are related, while making things easy for folks to track and update and we have used peer +dependencies between the packages to do this. With each release we will release all packages so that the version numbers move in lock-step, making it easy to ensure +you are working with compatible versions. One thing to keep in mind with peer dependencies is that they are not automatically installed. The advantage is you +will only have one copy of each library in your project.
+Installing peer dependencies is easy, you can specify each of the packages in a single line, here we are installing everything required to use the @pnp/sp package.
+npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp +
If you do not install all of the peer dependencies you will get a message specifying which ones are missing along with the version expected.
+With the separation of packages we have also simplified the imports, and allowed you more control over what you are importing. Compare these two examples showing +the same set of imports, but one is done via sp-pnp-js and the other using the @pnp libraries.
+import pnp, { + Web, + Util, + Logger, + FunctionListener, + LogLevel, +} from "sp-pnp-js"; +
import { + Logger, + LogLevel, + FunctionListener +} from "@pnp/logging"; + +import * as Util from "@pnp/core"; + +import { + sp, + Web +} from "@pnp/sp"; +
In the above example the "sp" import replaces "pnp" and is the root of your method chains. Once we have updated our imports we have a few small code changes to make, +depending on how you have used the library in your applications. Watch this short video discussing the most common updates:
+ + +If you are doing local debugging or testing you have likely created a settings.js from the supplied settings.example.js. Please note the format of that file has changed, +the new format is shown below.
+var settings = { + + spsave: { + username: "develina.devsson@mydevtenant.onmicrosoft.com", + password: "pass@word1", + siteUrl: "https://mydevtenant.sharepoint.com/" + }, + testing: { + enableWebTests: true, + sp: { + id: "{ client id }", + secret: "{ client secret }", + url: "{ site collection url }", + notificationUrl: "{ notification url }", + }, + graph: { + tenant: "{tenant.onmicrosoft.com}", + id: "{your app id}", + secret: "{your secret}" + }, + } +} +
If you used HttpClient from sp-pnp-js it was renamed to SPHttpClient. A transition to @pnp/sp assumes replacement of:
+import { HttpClient } from 'sp-pnp-js'; +
to the following import statement:
+import { SPHttpClient } from '@pnp/sp'; +
The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described +you can add and edit both contacts and folders in a users Outlook.
+Using the contacts() you can get the users contacts from Outlook
+import { graph } from "@pnp/graph"; + +const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.get(); + +const contacts = await graph.me.contacts.get(); +
Using the contacts.add() you can a add Contact to the users Outlook
+import { graph } from "@pnp/graph"; + +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +const addedContact = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); +
Using the contacts.getById() you can get one of the users Contacts in Outlook
+import { graph } from "@pnp/graph"; + +const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId'); + +const contact = await graph.me.contacts.getById('userId'); +
Using the delete you can remove one of the users Contacts in Outlook
+import { graph } from "@pnp/graph"; + +const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').delete(); + +const delContact = await graph.me.contacts.getById('userId').delete(); +
Using the update you can update one of the users Contacts in Outlook
+import { graph } from "@pnp/graph"; + +const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').update({birthday: "1986-05-30" }); + +const updContact = await graph.me.contacts.getById('userId').update({birthday: "1986-05-30" }); +
Using the contactFolders() you can get the users Contact Folders from Outlook
+import { graph } from "@pnp/graph"; + +const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.get(); + +const contactFolders = await graph.me.contactFolders.get(); +
Using the contactFolders.add() you can a add Contact Folder to the users Outlook
+import { graph } from "@pnp/graph"; + +const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add('displayName', '<ParentFolderId>'); + +const addedContactFolder = await graph.me.contactFolders.contactFolders.add('displayName', '<ParentFolderId>'); +
Using the contactFolders.getById() you can get one of the users Contact Folders in Outlook
+import { graph } from "@pnp/graph"; + +const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId'); + +const contactFolder = await graph.me.contactFolders.getById('folderId'); +
Using the delete you can remove one of the users Contact Folders in Outlook
+import { graph } from "@pnp/graph"; + +const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').delete(); + +const delContactFolder = await graph.me.contactFolders.getById('folderId').delete(); +
Using the update you can update one of the users Contact Folders in Outlook
+import { graph } from "@pnp/graph"; + +const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('userId').update({displayName: "value" }); + +const updContactFolder = await graph.me.contactFolders.getById('userId').update({displayName: "value" }); +
Using the contacts() in the Contact Folder gets the users Contact from the folder.
+import { graph } from "@pnp/graph"; + +const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').contacts.get(); + +const contactsInContactFolder = await graph.me.contactFolders.getById('folderId').contacts.get(); +
Using the childFolders() you can get the Child Folders of the current Contact Folder from Outlook
+import { graph } from "@pnp/graph"; + +const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.get(); + +const childFolders = await graph.me.contactFolders.getById('<id>').childFolders.get(); +
Using the childFolders.add() you can a add Child Folder in a Contact Folder
+import { graph } from "@pnp/graph"; + +const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.add('displayName', '<ParentFolderId>'); + +const addedChildFolder = await graph.me.contactFolders.getById('<id>').childFolders.add('displayName', '<ParentFolderId>'); +
Using the childFolders.getById() you can get one of the users Child Folders in Outlook
+import { graph } from "@pnp/graph"; + +const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.getById('folderId'); + +const childFolder = await graph.me.contactFolders.getById('<id>').childFolders.getById('folderId'); +
Using contacts.add in the Child Folder of a Contact Folder, adds a new Contact to that folder
+import { graph } from "@pnp/graph"; + +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); + +const addedContact = await graph.me.contactFolders.getById('<id>').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); +
import { graph } from "@pnp/graph"; + +const memberOf = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').memberOf.get(); + +const memberOf = await graph.me.memberOf.get(); +
import { graph } from "@pnp/graph"; + +const memberGroups = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); + +const memberGroups = await graph.me.getMemberGroups(); + +const memberGroups = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); + +const memberGroups = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups(); +
import { graph } from "@pnp/graph"; + +const memberObjects = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); + +const memberObjects = await graph.me.getMemberObjects(); + +const memberObjects = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); + +const memberObjects = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); +
And returns from that list those groups of which the specified user, group, or directory object is a member
+import { graph } from "@pnp/graph"; + +const checkedMembers = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); + +const checkedMembers = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]); +
import { graph } from "@pnp/graph"; + +const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').get(); +
import { graph } from "@pnp/graph"; + +const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete() +
This package contains the fluent api used to call the graph rest services.
+Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save
Import the library into your application and access the root sp object
+import { graph } from "@pnp/graph"; + +(function main() { + + // here we will load the current web's properties + graph.groups.get().then(g => { + + console.log(`Groups: ${JSON.stringify(g, null, 4)}`); + }); +})() +
Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save
Import the library into your application, update OnInit, and access the root sp object in render
+import { graph } from "@pnp/graph"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + + graph.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +public render(): void { + + // A simple loading message + this.domElement.innerHTML = `Loading...`; + + // here we will load the current web's properties + graph.groups.get().then(groups => { + + this.domElement.innerHTML = `Groups: <ul>${groups.map(g => `<li>${g.displayName}</li>`).join("")}</ul>`; + }); +} +
Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save
Import the library into your application, setup the node client, make a request
+import { graph } from "@pnp/graph"; +import { AdalFetchClient } from "@pnp/nodejs"; + +// do this once per page load +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{tenant}.onmicrosoft.com", "AAD Application Id", "AAD Application Secret"); + }, + }, +}); + +// here we will load the groups information +graph.groups.get().then(g => { + + console.log(`Groups: ${JSON.stringify(g, null, 4)}`); +}); +
Graphical UML diagram of @pnp/graph. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +Insights are relationships calculated using advanced analytics and machine learning techniques. You can, for example, identify OneDrive documents trending around users.
+Using the trending() returns documents from OneDrive and from SharePoint sites trending around a user.
+import { graph } from "@pnp/graph"; + +const trending = await graph.users.getById('user@tenant.onmicrosoft.com').insights.trending.get(); + +const trending = await graph.me.insights.trending.get(); +
Using the used() returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.
+import { graph } from "@pnp/graph"; + +const used = await graph.users.getById('user@tenant.onmicrosoft.com').insights.used.get(); + +const used = await graph.me.insights.used.get(); +
Using the shared() returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.
+import { graph } from "@pnp/graph"; + +const shared = await graph.users.getById('user@tenant.onmicrosoft.com').insights.shared.get(); + +const shared = await graph.me.insights.shared.get(); +
The ability invite an external user via the invitation manager
+Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).
+import { graph } from "@pnp/graph"; + +const invitationResult = await graph.invitations.create('external.user@emailadress.com', 'https://tenant.sharepoint.com/sites/redirecturi'); +
The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive.
+Using the drive() you can get the default drive from Onedrive
+import { graph } from "@pnp/graph"; + +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get(); + +const drives = await graph.me.drives.get(); +
Using the drives() you can get the users available drives from Onedrive
+import { graph } from "@pnp/graph"; + +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get(); + +const drives = await graph.me.drives.get(); +
Using the drives.getById() you can get one of the available drives in Outlook
+import { graph } from "@pnp/graph"; + +const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); + +const drive = await graph.me.drives.getById('driveId'); +
Using the list() you get the associated list
+import { graph } from "@pnp/graph"; + +const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list.get(); + +const list = await graph.me.drives.getById('driveId').list.get(); +
Using the recent() you get the recent files
+import { graph } from "@pnp/graph"; + +const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent.get(); + +const files = await graph.me.drives.getById('driveId').recent.get(); +
Using the sharedWithMe() you get the files shared with the user
+import { graph } from "@pnp/graph"; + +const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe.get(); + +const shared = await graph.me.drives.getById('driveId').sharedWithMe.get(); +
Using the root() you get the root folder
+import { graph } from "@pnp/graph"; + +const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.get(); + +const root = await graph.me.drives.getById('driveId').root.get(); +
Using the children() you get the children
+import { graph } from "@pnp/graph"; + +const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.get(); + +const rootChildren = await graph.me.drives.getById('driveId').root.children.get(); + +const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children.get(); + +const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children.get(); +
Using the add you can add a folder or an item
+import { graph } from "@pnp/graph"; +import { DriveItem as IDriveItem } from "@microsoft/microsoft-graph-types"; + +const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}}); + +const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}}); +
Using the search() you can search for items, and optionally select properties
+import { graph } from "@pnp/graph"; + +const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText').get(); + +const search = await graph.me.drives.getById('driveId')root.search('queryText').get(); +
Using the items.getById() you can get a specific item from the current drive
+import { graph } from "@pnp/graph"; + +const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); + +const item = await graph.me.drives.getById('driveId').items.getById('itemId'); +
Using the thumbnails() you get the thumbnails
+import { graph } from "@pnp/graph"; + +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails.get(); + +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails.get(); +
Using the delete() you delete the current item
+import { graph } from "@pnp/graph"; + +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); + +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete(); +
Using the update() you update the current item
+import { graph } from "@pnp/graph"; + +const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: "New Name"}); + +const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: "New Name"}); +
Using the move() you move the current item, and optionally update it
+import { graph } from "@pnp/graph"; + +// Requires a parentReference to the new folder location +const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"}); + +const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"}); +
The ability to retrieve a list of person objects ordered by their relevance to the user, which is determined by the user's communication and collaboration patterns, and business relationships.
+Using the people() you can retrieve a list of person objects ordered by their relevance to the user.
+import { graph } from "@pnp/graph"; + +const people = await graph.users.getById('user@tenant.onmicrosoft.com').people.get(); + +const people = await graph.me.people.get(); +
The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner.
+Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan.
+import { graph } from "@pnp/graph"; + +const plan = await graph.planner.plans.getById('planId'); +
Using the planner.plans.add() you can create a new Plan.
+import { graph } from "@pnp/graph"; + +const newPlan = await graph.planner.plans.add('groupObjectId', 'title'); +
Using the tasks() you can get the Tasks in a Plan.
+import { graph } from "@pnp/graph"; + +const planTasks = await graph.planner.plans.getById('planId').tasks.get(); +
Using the buckets() you can get the Buckets in a Plan.
+import { graph } from "@pnp/graph"; + +const planBuckets = await graph.planner.plans.getById('planId').buckets.get(); +
Using the details() you can get the details in a Plan.
+import { graph } from "@pnp/graph"; + +const planDetails = await graph.planner.plans.getById('planId').details.get(); +
Using the delete() you can get delete a Plan.
+import { graph } from "@pnp/graph"; + +const delPlan = await graph.planner.plans.getById('planId').delete(); +
Using the update() you can get update a Plan.
+import { graph } from "@pnp/graph"; + +const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title'}); +
Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task.
+import { graph } from "@pnp/graph"; + +const task = await graph.planner.tasks.getById('taskId'); +
Using the planner.tasks.add() you can create a new Task.
+import { graph } from "@pnp/graph"; + +const newTask = await graph.planner.tasks.add('planId', 'title'); +
Using the details() you can get the details in a Task.
+import { graph } from "@pnp/graph"; + +const taskDetails = await graph.planner.tasks.getById('taskId').details.get(); +
Using the delete() you can get delete a Task.
+import { graph } from "@pnp/graph"; + +const delTask = await graph.planner.tasks.getById('taskId').delete(); +
Using the update() you can get update a Task.
+import { graph } from "@pnp/graph"; + +const updTask = await graph.planner.tasks.getById('taskId').update({properties}); +
Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket.
+import { graph } from "@pnp/graph"; + +const bucket = await graph.planner.buckets.getById('bucketId'); +
Using the planner.buckets.add() you can create a new Bucket.
+import { graph } from "@pnp/graph"; + +const newBucket = await graph.planner.buckets.add('name', 'planId'); +
Using the update() you can get update a Bucket.
+import { graph } from "@pnp/graph"; + +const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name"}); +
Using the delete() you can get delete a Bucket.
+import { graph } from "@pnp/graph"; + +const delBucket = await graph.planner.buckets.getById('bucketId').delete(); +
Using the tasks() you can get Tasks in a Bucket.
+import { graph } from "@pnp/graph"; + +const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks.get(); +
The Microsoft Graph Security API can be used as a federated security aggregation service to submit queries to all onboarded security providers to get aggregated responses.
+Using the alerts() to retrieve a list of Alert objects
+import { graph } from "@pnp/graph"; + +const alerts = await graph.security.alerts.get(); +
Using the alerts.getById() to retrieve a specific Alert object
+import { graph } from "@pnp/graph"; + +const alert = await graph.security.alerts.getById('alertId').get(); +
Using the alerts.getById().update() to retrieve a specific Alert object
+import { graph } from "@pnp/graph"; + +const updAlert = await graph.security.alerts.getById('alertId').update({status: 'Status' }); +
The ability to manage sites, lists and listitems in SharePoint is a capability introduced in version 1.3.0 of @pnp/graph.
+Using the sites.root()() you can get the tenant root site
+import { graph } from "@pnp/graph"; + +const tenantRootSite = await graph.sites.root.get() +
Using the sites.getById()() you can get the root site as well
+import { graph } from "@pnp/graph"; + +const tenantRootSite = await graph.sites.getById('contoso.sharepoint.com').get() +
Using the sites.getById()() you can get a specific site. With the combination of the base URL and a relative URL.
+We are using an internal method for combining the URL in the right combination, with :
ex: contoso.sharepoint.com:/sites/site1:
Here are a few url combinations that works:
+import { graph } from "@pnp/graph"; + +// No / in the URLs +const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com', 'sites/site1').get() + +// Both trailing / in the base URL and starting / in the relative URL +const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com/', '/sites/site1').get() + +// Both trailing / in the base URL and starting and trailing / in the relative URL +const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com/', '/sites/site1/').get() +
Using the sites()() you can get the sub sites of a site. As this is returned as Sites, you could use getById() for a specific site and use the operations.
+import { graph } from "@pnp/graph"; + +const subsites = await graph.sites.getById('contoso.sharepoint.com').sites.get(); +
Using the contentTypes()() you can get the Content Types from a Site or from a List
+import { graph } from "@pnp/graph"; + +const contentTypesFromSite = await graph.sites.getById('contoso.sharepoint.com').contentTypes.get(); + +const contentTypesFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').contentTypes.get(); +
Using the getById() you can get a specific Content Type from a Site or from a List
+import { graph } from "@pnp/graph"; + +const contentTypeFromSite = await graph.sites.getById('contoso.sharepoint.com').contentTypes.getById('contentTypeId').get(); + +const contentTypeFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').contentTypes.getById('contentTypeId').get(); +
Using the lists() you can get the lists of a site.
+import { graph } from "@pnp/graph"; + +const lists = await graph.sites.getById('contoso.sharepoint.com').lists.get(); +
Using the lists.getById() you can get the lists of a site.
+import { graph } from "@pnp/graph"; + +const list = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').get(); +
Using the lists.create() you can create a list in a site.
+import { graph } from "@pnp/graph"; + +const newLists = await graph.sites.getById('contoso.sharepoint.com').lists.create('DisplayName', {contentTypesEnabled: true, hidden: false, template: "genericList"}) +
Using the drive() you can get the default drive from a Site or a List
+import { graph } from "@pnp/graph"; + +const drive = await graph.sites.getById('contoso.sharepoint.com').drive.get(); + +const drive = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').drive.get(); +
Using the drives() you can get the drives from the Site
+import { graph } from "@pnp/graph"; + +const drives = await graph.sites.getById('contoso.sharepoint.com').drives.get(); +
Using the drives.getById() you can get one specific Drive. For more operations make sure to have a look in the onedrive
documentation.
import { graph } from "@pnp/graph"; + +const drive = await raph.sites.getById('contoso.sharepoint.com').lists.getById('listId').drives.getById('driveId').get(); +
Using the columns() you can get the columns from a Site or from a List
+import { graph } from "@pnp/graph"; + +const columnsFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.get(); + +const columnsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.get(); +
Using the columns.getById() you can get a specific column from a Site or from a List
+import { graph } from "@pnp/graph"; + +const columnFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').get(); + +const columnsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').get(); +
Using the column.columnLinks() you can get the column links for a specific column, from a Site or from a List
+import { graph } from "@pnp/graph"; + +const columnLinksFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').columnLinks.get(); + +const columnLinksFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').columnLinks.get(); +
Using the column.columnLinks().getById() you can get a specific column link for a specific column, from a Site or from a List
+import { graph } from "@pnp/graph"; + +const columnLinkFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').columnLinks.getById('columnLinkId').get(); + +const columnLinkFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').columnLinks.getById('columnLinkId').get(); +
Using the items() you can get the Items from a List
+import { graph } from "@pnp/graph"; + +const itemsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.get(); +
Using the getById()() you can get a specific Item from a List
+import { graph } from "@pnp/graph"; + +const itemFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').get(); +
Using the items.create() you can create an Item in a List.
+import { graph } from "@pnp/graph"; + +const newItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.create({ + "Title": "Widget", + "Color": "Purple", + "Weight": 32 +}); +
Using the update() you can update an Item in a List.
+import { graph } from "@pnp/graph"; + +const Item = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').update({ +{ + "Color": "Fuchsia" +} +}) +
Using the delete() you can delete an Item in a List.
+import { graph } from "@pnp/graph"; + +const Item = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').delete() +
Using the fields() you can the Fields in an Item
+import { graph } from "@pnp/graph"; + +const fieldsFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').fields.get(); +
Using the versions() you can the Versions of an Item
+import { graph } from "@pnp/graph"; + +const versionsFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').versions.get(); +
Using the versions.getById()() you can the Versions of an Item
+import { graph } from "@pnp/graph"; + +const versionFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').versions.getById('versionId').get(); +
The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: + Mail, events, and contacts from Outlook. + Conversations from Office Groups. + Drive root items from OneDrive. + Users and Groups from Azure Active Directory. +* Alerts from the Microsoft Graph Security API.
+Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.
+import { graph } from "@pnp/graph"; + +const subscriptions = await graph.subscriptions.get(); +
Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. +To learn more about the scopes visit this url.
+import { graph } from "@pnp/graph"; + +const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z"); +
Using the subscriptions.getById() you can get one of the subscriptions
+import { graph } from "@pnp/graph"; + +const subscription = await graph.subscriptions.getById('subscriptionId'); +
Using the subscriptions.getById().delete() you can remove one of the Subscriptions
+import { graph } from "@pnp/graph"; + +const delSubscription = await graph.subscription.getById('subscriptionId').delete(); +
Using the subscriptions.getById().update() you can update one of the Subscriptions
+import { graph } from "@pnp/graph"; + +const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" }); +
The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams.
+import { graph } from "@pnp/graph"; + +const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams.get(); + +const myJoinedTeams = await graph.me.joinedTeams.get(); +
Using the teams.getById() you can get a specific Team.
+import { graph } from "@pnp/graph"; + +const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').get(); +
When you create a new group and add a Team, the group needs to have an Owner. Or else we get an error. +So the owner Id is important, and you could just get the users Ids from
+import { graph } from "@pnp/graph"; + +const users = await graph.users.get(); +
Then create
+import { graph } from "@pnp/graph"; + +const createdGroupTeam = await graph.teams.create('Groupname', 'mailNickname', 'description', 'OwnerId',{ +"memberSettings": { + "allowCreateUpdateChannels": true +}, +"messagingSettings": { + "allowUserEditMessages": true, +"allowUserDeleteMessages": true +}, +"funSettings": { + "allowGiphy": true, + "giphyContentRating": "strict" +}}); +
Here we get the group via id and use createTeam
import { graph } from "@pnp/graph"; + +const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ +"memberSettings": { + "allowCreateUpdateChannels": true +}, +"messagingSettings": { + "allowUserEditMessages": true, +"allowUserDeleteMessages": true +}, +"funSettings": { + "allowGiphy": true, + "giphyContentRating": "strict" +}}); +
import { graph } from "@pnp/graph"; + +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive(); +
import { graph } from "@pnp/graph"; + +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive(); +
import { graph } from "@pnp/graph"; + +const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( +'Cloned','mailNickname','description','apps,tabs,settings,channels,members','public'); +
import { graph } from "@pnp/graph"; + +const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.get(); +
import { graph } from "@pnp/graph"; + +const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').get(); +
import { graph } from "@pnp/graph"; + +const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description'); +
import { graph } from "@pnp/graph"; + +const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.get(); +
import { graph } from "@pnp/graph"; + +const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a'); +
import { graph } from "@pnp/graph"; + +const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove(); +
import { graph } from "@pnp/graph"; + +const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs +.get(); +
import { graph } from "@pnp/graph"; + +const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs +.getById('Id'); +
import { graph } from "@pnp/graph"; + +const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{}); +
PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.
++Animation of the library in use, note intellisense help in building your queries
+These articles provide general guidance for working with the libraries. If you are migrating from sp-pnp-js please review the transition guide.
+Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.
++ | + | + |
---|---|---|
@pnp/ | ++ | + |
+ | common | +Provides shared functionality across all pnp libraries | +
+ | config-store | +Provides a way to manage configuration within your application | +
+ | graph | +Provides a fluent api for working with Microsoft Graph | +
+ | logging | +Light-weight, subscribable logging framework | +
+ | nodejs | +Provides functionality enabling the @pnp libraries within nodejs | +
+ | odata | +Provides shared odata functionality and base classes | +
+ | pnpjs | +Rollup library of core functionality (mimics sp-pnp-js) | +
+ | sp | +Provides a fluent api for working with SharePoint REST | +
+ | sp-addinhelpers | +Provides functionality for working within SharePoint add-ins | +
+ | sp-clientsvc | +Provides based classes used to create a fluent api for working with SharePoint Managed Metadata | +
+ | sp-taxonomy | +Provides a fluent api for working with SharePoint Managed Metadata | +
Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.
+This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
+Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program.
+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.
+ + + + + + + + + +The logging module provides light weight subscribable and extensiable logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.
+Install the logging module, it has no other dependencies
+npm install @pnp/logging --save
The logging framework is based on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the LogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface.
+/** + * Interface that defines a log listener + * + */ +export interface LogListener { + /** + * Any associated data that a given logging listener may choose to log or ignore + * + * @param entry The information to be logged + */ + log(entry: LogEntry): void; +} + +/** + * Interface that defines a log entry + * + */ +export interface LogEntry { + /** + * The main message to be logged + */ + message: string; + /** + * The level of information this message represents + */ + level: LogLevel; + /** + * Any associated data that a given logging listener may choose to log or ignore + */ + data?: any; +} +
export const enum LogLevel { + Verbose = 0, + Info = 1, + Warning = 2, + Error = 3, + Off = 99, +} +
To write information to a logger you can use either write, writeJSON, or log.
+import { + Logger, + LogLevel +} from "@pnp/logging"; + +// write logs a simple string as the message value of the LogEntry +Logger.write("This is logging a simple string"); + +// optionally passing a level, default level is Verbose +Logger.write("This is logging a simple string", LogLevel.Error); + +// this will convert the object to a string using JSON.stringify and set the message with the result +Logger.writeJSON({ name: "value", name2: "value2"}); + +// optionally passing a level, default level is Verbose +Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warn); + +// specify the entire LogEntry interface using log +Logger.log({ + data: { name: "value", name2: "value2"}, + level: LogLevel.Warning, + message: "This is my message" +}); +
There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance pased in, the level will be Error, and the message will be the Error instance message.
+const e = new Error("An Error"); + +Logger.error(e); +
By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.
+import { + Logger, + ConsoleListener, + LogLevel +} from "@pnp/logging"; + +// subscribe a listener +Logger.subscribe(new ConsoleListener()); + +// set the active log level +Logger.activeLogLevel = LogLevel.Info; +
There are two listeners included in the library, ConsoleListener and FunctionListener.
+This listener outputs information to the console and works in Node as well as within browsers. It takes no settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Usage is shown in the example above.
+The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.
+import { + Logger, + FunctionListener, + LogEntry +} from "@pnp/logging"; + +let listener = new FunctionListener((entry: LogEntry) => { + + // pass all logging data to an existing framework + MyExistingCompanyLoggingFramework.log(entry.message); +}); + +Logger.subscribe(listener); +
If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the LogListener interface.
+import { + Logger, + LogListener, + LogEntry +} from "@pnp/logging"; + +class MyListener implements LogListener { + + log(entry: LogEntry): void { + // here you would do something with the entry + } +} + +Logger.subscribe(new MyListener()); +
Graphical UML diagram of @pnp/logging. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +The AdalCertificateFetchClient class depends on the adal-node package to authenticate against Azure AD using the client credentials with a client certificate flow. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.
+import { AdalCertificateFetchClient } from "@pnp/nodejs"; +import { graph } from "@pnp/graph"; +import * as fs from "fs"; +import * as path from "path"; + +// Get the private key from a file (Assuming it's a .pem file) +const keyPemFile = "/path/to/privatekey.pem"; +const privateKey = fs.readFileSync( + path.resolve(__dirname, keyPemFile), + { encoding : 'utf8'} +); + +// setup the client using graph setup function +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalCertificateFetchClient( + "{tenant id}", + "{app id}", + "{certificate thumbprint}", + privateKey); + }, + }, +}); + +// execute a library request as normal +graph.groups.get().then(g => { + + console.log(JSON.stringify(g, null, 4)); + +}).catch(e => { + + console.error(e); +}); +
The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.
+import { AdalFetchClient } from "@pnp/nodejs"; +import { graph } from "@pnp/graph"; + +// setup the client using graph setup function +graph.setup({ + graph: { + fetchClientFactory: () => { + return new AdalFetchClient("{tenant}", "{app id}", "{app secret}"); + }, + }, +}); + +// execute a library request as normal +graph.groups.get().then(g => { + + console.log(JSON.stringify(g, null, 4)); + +}).catch(e => { + + console.error(e); +}); +
The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you.
+import { BearerTokenFetchClient } from "@pnp/nodejs"; +import { graph } from "@pnp/graph"; + +// setup the client using graph setup function +graph.setup({ + graph: { + fetchClientFactory: () => { + return new BearerTokenFetchClient("{Bearer Token}"); + }, + }, +}); + +// execute a library request as normal +graph.groups.get().then(g => { + + console.log(JSON.stringify(g, null, 4)); + +}).catch(e => { + + console.error(e); +}); +
This package supplies helper code when using the @pnp libraries within the context of nodejs. This removes the node specific functionality from any of the packages. +Primarily these consist of clients to enable use of the libraries in nodejs.
+Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the +exported functionality.
+npm install @pnp/logging @pnp/core @pnp/nodejs --save
Graphical UML diagram of @pnp/nodejs. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +Added in 1.2.7
+The ProviderHostedRequestcontext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user.
+The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI.
+Note: To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient
class. Instead, use the more generic NodeFetchClient
class. The downside is that you have to manually configure each request to use the desired user/app context.
import { sp, SPRest } from "@pnp/sp"; +import { NodeFetchClient, ProviderHostedRequestContext } from "@pnp/nodejs"; + +// configure your node options +sp.setup({ + sp: { + fetchClientFactory: () => { + return new NodeFetchClient(); + }, + }, +}); + +// get request data generated by /_layouts/15/AppRedirect.aspx +const spAppToken = request.body.SPAppToken; +const spSiteUrl = request.body.SPSiteUrl; + +// create a context based on the add-in details and SPAppToken +const ctx = await ProviderHostedRequestContext.create(spSiteUrl, "{client id}", "{client secret}", spAppToken); + +// create an SPRest object configured to use our context +// this is used in place of the global sp object +const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); +const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); + +// make a request on behalf of the user +const user = await userSP.web.currentUser.get(); +console.log(`Hello ${user.Title}`); + +// make an add-in only request +const app = await addinSP.web.currentUser.get(); +console.log(`Add-in principal: ${app.Title}`); +
Added in 1.3.2
+In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. In the 1.3.2 relesae we introduced the ability to use a proxy with the @pnp/nodejs library.
+You need to import the new setProxyUrl
function from the library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library.
import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + + // call the set proxy url function and it will be used for all requests regardless of client + setProxyUrl("{your proxy url}"); + return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); + }, + }, +}); +
To get Fiddler to work you may need to set an environment variable. This should only be done for testing!
+import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + + // ignore certificate errors: ONLY FOR TESTING!! + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + + // this is my fiddler url locally + setProxyUrl("http://127.0.0.1:8888"); + return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); + }, + }, +}); +
The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively.
+import { SPFetchClient } from "@pnp/nodejs"; +import { sp } from "@pnp/sp"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}"); + }, + }, +}); + +// execute a library request as normal +sp.web.get().then(w => { + + console.log(JSON.stringify(w, null, 4)); + +}).catch(e => { + + console.error(e); +}); +
Added in 1.1.2
+For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK.
+import { sp } from "@pnp/sp"; +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.China); + }, + }, +}); +
In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to "https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx" and copying the GUID value that appears after the "@" - this is the realm id.
+As of version 1.1.2 the realm parameter is now the 5th parameter in the constructor.
+import { sp } from "@pnp/sp"; +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs"; + +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.SPO, "{realm}"); + }, + }, +}); +
This section outlines how to register for a client id and secret for use in the above code.
+Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly.
+Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site.
+<AppPermissionRequests AllowAppOnlyPolicy="true"> + <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" /> + <AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" /> + <AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal" /> + </AppPermissionRequests> +
Note that the above XML will grant full tenant control, you should grant only those permissions necessary for your application
+ + + + + + + + + +Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests.
+The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.
+You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The below code will get the items from the list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() should always be the last method in the chain before the get() (OR if you are using [[batching]] these methods can be transposed, more details below).
+import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("Tasks").items.usingCaching().get().then(r => { + console.log(r) +}); + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => { + console.log(r) +}); +
If you would like to not use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application.
+import { sp } from "@pnp/sp"; + +sp.setup({ + defaultCachingStore: "session", // or "local" + defaultCachingTimeoutSeconds: 30, + globalCacheDisable: false // or true to disable caching in case of debugging/testing +}); + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => { + console.log(r) +}); +
If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key.
+export interface ICachingOptions { + expiration?: Date; + storeName?: "session" | "local"; + key: string; +} +
import { sp } from "@pnp/sp"; +import { dateAdd } from "@pnp/core"; + +sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching({ + expiration: dateAdd(new Date(), "minute", 20), + key: "My Key", + storeName: "local" +}).get().then(r => { + console.log(r) +}); +
You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid.
+import { sp } from "@pnp/sp"; + +let batch = sp.createBatch(); + +sp.web.lists.inBatch(batch).usingCaching().get().then(r => { + console.log(r) +}); + +sp.web.lists.getByTitle("Tasks").items.usingCaching().inBatch(batch).get().then(r => { + console.log(r) +}); + +batch.execute().then(() => console.log("All done!")); +
You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here.
+We create a map to act as our cache storage and a function to wrap the request caching logic
+const map = new Map<string, any>(); + +async function staleWhileRevalidate<T>(key: string, p: Promise<T>): Promise<T> { + + if (map.has(key)) { + + // In Cache + p.then(u => { + // Update Cache once we have a result + map.set(key, u); + }); + + // Return from Cache + return map.get(key); + } + + // Not In Cache so we need to wait for the value + const r = await p; + + // Set Cache + map.set(key, r); + + // Return from Promise + return r; +} +
++Don't call usingCaching just apply the helper method
+
// this one will wait for the request to finish +const r1 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); + +console.log(JSON.stringify(r1, null, 2)); + +// this one will return the result from cache and then update the cache in the background +const r2 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); + +console.log(JSON.stringify(r2, null, 2)); +
You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well.
+interface WebData { + Title: string; + Description: string; +} + +function getWebData(): Promise<WebData> { + + return staleWhileRevalidate("test1", sp.web.select("Title", "Description").get()); +} + + +// this one will wait for the request to finish +const r1 = await getWebData(); + +console.log(JSON.stringify(r1, null, 2)); + +// this one will return the result from cache and then update the cache in the background +const r2 = await getWebData(); + +console.log(JSON.stringify(r2, null, 2)); +
This modules contains shared interfaces and abstract classes used within, and by inheritors of, the @pnp/queryable package.
+The exception thrown when a response is returned and cannot be processed.
+Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the +value to be returned. It has two methods, one is optional:
+The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper +methods.
+You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most +cases.
+class MyParser extends ODataParserBase<any> { + + // we need to override the parse method to do our custom stuff + public parse(r: Response): Promise<T> { + + // we wrap everything in a promise + return new Promise((resolve, reject) => { + + // lets use the default error handling which returns true for no error + // and will call reject with an error if one exists + if (this.handleError(r, reject)) { + + // now we add our custom parsing here + r.text().then(txt => { + // here we call a madeup function to parse the result + // this is where we would do our parsing as required + myCustomerUnencode(txt).then(v => { + resolve(v); + }); + }); + } + }); + } +} +
This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata +library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure +the core code is solid and well tested, with any updates benefitting all inheriting libraries.
+Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable --save
Graphical UML diagram of @pnp/queryable. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +This module contains an abstract class used as a base when inheriting libraries support batching.
+This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will +be responsible for processing that info by implementing the abstract executeImpl method.
+Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp +and @pnp/graph modules.
+ + + + + + + + + +This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.
+The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request.
+import { sp } from "@pnp/sp"; +import { JSONParser } from "@pnp/queryable"; + +try { + + const parser = new JSONParser(); + + // this always throws a 404 error + await sp.web.getList("doesn't exist").get(parser); + +} catch (e) { + + // we can check for the property "isHttpRequestError" to see if this is an instance of our class + // this gets by all the many limitations of subclassing Error and type detection in JavaScript + if (e.hasOwnProperty("isHttpRequestError")) { + + console.log("e is HttpRequestError"); + + // now we can access the various properties and make use of the response object. + // at this point the body is unread + console.log(`status: ${e.status}`); + console.log(`statusText: ${e.statusText}`); + + const json = await e.response.clone().json(); + console.log(JSON.stringify(json)); + const text = await e.response.clone().text(); + console.log(text); + const headers = e.response.headers; + } + + console.error(e); +} +
Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.
+Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.
+Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.
+Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.
+Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type.
+import { LambdaParser } from "@pnp/queryable"; +import { sp } from "@pnp/sp"; + +// here a simple parser duplicating the functionality of the JSONParser +const parser = new LambdaParser((r: Response) => r.json()); + +const webDataJson = await sp.web.get(parser); + +console.log(webDataJson); +
All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by
+the RequestContext
The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This +allows full control over the methods called during a request, and allows for the insertion of any custom methods required.
+interface RequestContext<T> { + batch: ODataBatch; + batchDependency: () => void; + cachingOptions: ICachingOptions; + hasResult?: boolean; + isBatched: boolean; + isCached: boolean; + options: FetchOptions; + parser: ODataParser<T>; + pipeline: Array<(c: RequestContext<T>) => Promise<RequestContext<T>>>; + requestAbsoluteUrl: string; + requestId: string; + result?: T; + verb: string; + clientFactory: () => RequestClient; +} +
The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you +would like your method to always run regardless of the existance of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument +of the current RequestContext and returns a promise resolving to the RequestContext updated as needed.
+@requestPipelineMethod(true) +public static myPipelineMethod<T>(context: RequestContext<T>): Promise<RequestContext<T>> { + + return new Promise<RequestContext<T>>(resolve => { + + // do something + + resolve(context); + }); +} +
The Queryable class is the base class for all of the libraries building fluent request apis.
+This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching +you can create a dummy class here and simply not use the batching calls.
+Provides access to the query string builder for this url
+Directly concatenates the supplied string to the current url, not normalizing "/" chars
+Sets custom options for current object and all derived objects accessible via chaining
+import { ConfigOptions } from "@pnp/queryable"; +import { sp } from "@pnp/sp"; + +const headers: ConfigOptions = { + Accept: 'application/json;odata=nometadata' +}; + +// here we use configure to set the headers value for all child requests of the list instance +const list = sp.web.lists.getByTitle("List1").configure({ headers }); + +// this will use the values set in configure +list.items.get().then(items => console.log(JSON.stringify(items, null, 2)); +
For reference the ConfigOptions interface is shown below:
+export interface ConfigOptions { + headers?: string[][] | { [key: string]: string } | Headers; + mode?: "navigate" | "same-origin" | "no-cors" | "cors"; + credentials?: "omit" | "same-origin" | "include"; + cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached"; +} +
Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.
+Enables caching for this request. See caching for more details.
+import { sp } from "@pnp/sp" + +sp.web.usingCaching().get().then(...); +
Adds this query to the supplied batch
+Gets the current url
+When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request
+Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.
+ + + + + + + + + +The pnpjs library is a rollup of the core libraries across the @pnp scope and is designed only as a bridge to help folks transition from sp-pnp-js, primarily +in scenarios where a single file is being imported via a script tag. It is recommended to not use this rollup library where possible and migrate to the +individual libraries.
+There are two approaches to using this library: the first is to import, the second is to manually extract the bundled file for use in your project.
+npm install @pnp/pnpjs --save
You can then make use of the pnpjs rollup library within your application. It's structure matches sp-pnp-js, though some things may have changed based on the rolled-up dependencies.
+import pnp from "@pnp/pnpjs"; + +pnp.sp.web.get().then(w => { + + console.log(JSON.stringify(w, null, 4)); +}); +
This method is useful if you are primarily working within a script editor web part or similar case where you are not using a build pipeline to bundle your application.
+Install only this library.
+npm install @pnp/pnpjs
Browse to ./node_modules/@pnp/pnpjs/dist and grab either pnpjs.es5.umd.bundle.js or pnpjs.es5.umd.bundle.min.js depending on your needs. You can then add a script tag referencing this file and you will have a global variable "pnp".
+For example you could paste the following into a script editor web part:
+<p>Script Editor is on page.</p> +<script src="https://mysite/site_assets/pnpjs.es5.umd.bundle.min.js" type="text/javascript"></script> +<script type="text/javascript"> + + pnp.Logger.subscribe(new pnp.ConsoleListener()); + pnp.Logger.activeLogLevel = pnp.LogLevel.Info; + + pnp.sp.web.get().then(w => { + + console.log(JSON.stringify(w, null, 4)); + }); +</script> +
Alternatively to serve the script from the project at "https://localhost:8080/assets/pnp.js" you can use:
+gulp serve --p pnpjs
This will allow you to test your changes to the entire bundle live while making updates.
+ + + + + + + + + +This module contains classes to allow use of the libraries within a SharePoint add-in.
+Install the library and all dependencies,
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-addinhelpers --save
Now you can make requests to the host web from your add-in using the crossDomainWeb method.
+// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +
Graphical UML diagram of @pnp/sp-addinhelpers. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on +the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.
+To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a +SharePoint add-in web.
+// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +
This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls
+// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers"; + +// this only needs to be done once within your application +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPRequestExecutorClient(); + } + } +}); + +// now we need to use the crossDomainWeb method to make our requests to the host web +const addInWenUrl = "{The add-in web url, likely from the query string}"; +const hostWebUrl = "{The host web url, likely from the query string}"; + +// make requests into the host web via the SP.RequestExecutor +sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => { + console.log(JSON.stringify(w, null, 4)); +}); +
This library provides base classes for working with the legacy SharePoint client.svc/ProcessQuery endpoint. The base classes support most of the possibilities for types of query calls, as well as supporting fluent batching and caching. They are based on the same @pnp/queryable foundation as the other libraries so should feel familiar when extending. You can see @pnp/sp-taxonomy for an example showing how to extend these base classes into a functional fluent model.
+Graphical UML diagram of @pnp/sp-clientsvc. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +This module provides a fluent interface for working with the SharePoint term store. It does not rely on SP.taxonomy.js or other dependencies outside the @pnp scope. It is designed to function in a similar manner and present a similar feel to the other data retrieval libraries. It works by calling the "/_vti_bin/client.svc/ProcessQuery" endpoint.
+You will need to install the @pnp/sp-taxonomy package as well as the packages it requires to run.
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-taxonomy @pnp/sp-clientsvc --save
All fluent taxonomy operations originate from the Taxonomy object. You can access it in several ways.
+This method will grab an existing instance of the Taxonomy class and allow you to immediately chain additional methods.
+import { taxonomy } from "@pnp/sp-taxonomy"; + +await taxonomy.termStores.get(); +
You can also import the Taxonomy class and create a new instance. This useful in those cases where you want to work with taxonomy in another web than the current web.
+import { Session } from "@pnp/sp-taxonomy"; + +const taxonomy = new Session("https://mytenant.sharepoint.com/sites/dev"); + +await taxonomy.termStores.get(); +
Because the sp-taxonomy library uses the same @pnp/queryable request pipeline as the other libraries you can call the setup method with the same options used for the @pnp/sp library. The setup method is provided as shorthand and avoids the need to import anything from @pnp/sp if you do not need to. A call to this setup method is equivilent to calling the sp.setup method and the configuration is shared between the libraries within your application.
+In the below example all requests for the @pnp/sp-taxonomy library and the @pnp/sp library will be routed through the specified SPFetchClient. Sharing the configuration like this handles the most common scenario of working on the same web easily. You can set other values here as well such as baseUrl and they will be respected by both libraries.
+import { taxonomy } from "@pnp/sp-taxonomy"; +import { SPFetchClient } from "@pnp/nodejs"; + +// example for setting up the node client using setup method +// we also set a custom header, as an example +taxonomy.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{url}", "{client id}", "{client secret}"); + }, + headers: { + "X-Custom-Header": "A Great Value", + }, + }, +}); +
Graphical UML diagram of @pnp/sp-taxonomy. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +You can load labels by accessing the labels property of a term.
+import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <see terms article for loading term> + +// load the terms merged with data +const labelsWithData: (ILabel & ILabelData)[] = await term.labels.get(); + + +// get a label by value +const label: ILabel = term.labels.getByValue("term value"); + +// get a label merged with data +const label2: ILabel & ILabelData = term.labels.getByValue("term value").get(); +
Sets this labels as the default for the language
+import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <see terms article for loading term> + +// get a label by value +await term.labels.getByValue("term value").setAsDefaultForLanguage(); +
Deletes this label
+import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <see terms article for loading term> + +// get a label by value +await term.labels.getByValue("term value").delete(); +
Term groups are used as a container for terms within a term store.
+Term groups are loaded from a term store
+import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Adds a contributor to the Group
+import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await group.addContributor("i:0#.f|membership|person@tenant.com"); +
Adds a group manager to the Group
+import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await group.addGroupManager("i:0#.f|membership|person@tenant.com"); +
Creates a new term set
+import { taxonomy, ITermStore, ITermGroup, ITermSet, ITermSetData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const set: ITermSet & ITermSetData = await group.createTermSet("name", 1031); + +// you can optionally supply the term set id, if you do not we create a new id for you +const set2: ITermSet & ITermSetData = await group.createTermSet("name", 1031, "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Gets this term group's data
+import { taxonomy, ITermStore, ITermGroupData, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup & ITermGroupData = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get(); +
Term sets contain terms within the taxonomy heirarchy.
+You load a term set directly from a term store.
+import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Or you can load a term set from a collection - though if you know the id it is more efficient to get the term set directly.
+import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set = store.getTermSetsByName("my set", 1031).getById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const setWithData = await store.getTermSetsByName("my set", 1031).getByName("my set").get(); +
Adds a stakeholder to the TermSet
+import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await set.addStakeholder("i:0#.f|membership|person@tenant.com"); +
Deletes a stakeholder to the TermSet
+import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +await set.deleteStakeholder("i:0#.f|membership|person@tenant.com"); +
Gets the data for this TermSet
+import { taxonomy, ITermStore, ITermSet, ITermSetData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const setWithData: ITermSet & ITermSetData = await set.get(); +
Provides access to the terms collection for this termset
+import { taxonomy, ITermStore, ITermSet, ITerms, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const terms: ITerms = set.terms; + +// load the data into the terms instances +const termsWithData: (ITermData & ITerm)[] = set.terms.get(); +
Gets a term by id from this set
+import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const term: ITerm = set.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +// load the data into the term instances +const termWithData: ITermData & ITerm = term.get(); +
Adds a term to a term set
+import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +const term: ITerm & ITermData = await set.addTerm("name", 1031, true); + +// you can optionally set the id when you create the term +const term2: ITerm & ITermData = await set.addTerm("name", 1031, true, "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Term stores contain term groups, term sets, and terms. This article describes how to work find, load, and use a term store to access the terms inside.
+You can access a list of all term stores via the termstores property of the Taxonomy class.
+// get a list of term stores and return all properties +const stores = await taxonomy.termStores.get(); + +// you can also select the fields to return for the term stores using the select operator. +const stores2 = await taxonomy.termStores.select("Name").get(); +
To load a specific term store you can use the getByName or getById methods. Using the get method executes the request to the server.
+const store = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get(); + +const store2 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").get(); + +// you can use select as well with either method to choose the fields to return +const store3 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").select("Name").get(); +
For term stores and all other objects data is returned as a merger of the data and a new instance of the representative class. Allowing you to immediately begin acting on the object. IF you do not need the data, skip the get call until you do.
+// no data loaded yet, store is an instance of TermStore class +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +// I can call subsequent methods on the same object and will now have an object with data +// I could have called get above as well - this is just an example +const store2: ITermStore & ITermStoreData = await store.get(); + +// log the Name property +console.log(store2.Name); + +// call another TermStore method on the same object +await store2.addLanguage(1031); +
Loads the data for this term store
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get(); +
Gets the collection of term sets with a matching name
+import { taxonomy, ITermSets } from "@pnp/sp-taxonomy"; + +const sets: ITermSets = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").getTermSetsByName("My Set", 1033); +
Gets the term set with a matching id
+import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy"; + +// note that you can also use instances if you wanted to conduct multiple operations on a single store +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); +const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +// we will handle normalizing guids for you as well :) +const set2: ITermSet = store.getTermSetById("{a63aefc9-359d-42b7-a0d2-cb1809acd260}"); +
Gets a term by id
+import { taxonomy, ITermStore, ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +const termWithData: ITerm & ITermData = await term.get(); +
Added in 1.2.6
+import { taxonomy, ITermStore, ITerms, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const terms: ITerms = store.getTermsById("0ba6845c-1468-4ec5-a5a8-718f1fb05431", "0ba6845c-1468-4ec5-a5a8-718f1fb05432"); +const termWithData: (ITerm & ITermData)[] = await term.get(); +
Gets a term group by id
+import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Gets terms that match the provided criteria. Please see this article for details on valid querys.
+import { taxonomy, ITermStore, ILabelMatchInfo, ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const terms: ITerms = store.getTerms({ + TermLabel: "test label", + TrimUnavailable: true, + }); + +// load the data based on the above query +const termsWithData: (ITerm & ITermData)[] = terms.get(); + +// select works here too :) +const termsWithData2: (ITerm & ITermData)[] = terms.select("Name").get(); +
Adds a language to the term store by LCID
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.addLanguage(1031); +
Adds a term group to the term store
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const group: ITermGroup & ITermGroupData = await store.addGroup("My Group Name"); + +// you can optionally specify the guid of the group, if you don't we just create a new guid for you +const groups: ITermGroup & ITermGroupData = await store.addGroup("My Group Name", "0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Commits all updates to the database that have occurred since the last commit or rollback.
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.commitAll(); +
Delete a working language from the TermStore
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.deleteLanguage(1031); +
Discards all updates that have occurred since the last commit or rollback. It is unlikely you will need to call this method through this library due to how things are structured.
+import { taxonomy, ITermStore } from "@pnp/sp-taxonomy"; + +const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +await store.rollbackAll(); +
Terms are the individual entries with a term set.
+You can load a collection of terms through a term set or term store.
+import { + taxonomy, + ITermStore, + ITerms, + ILabelMatchInfo, + ITerm, + ITermData +} from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +const labelMatchInfo: ILabelMatchInfo = { + TermLabel: "My Label", + TrimUnavailable: true, +}; + +const terms: ITerms = store.getTerms(labelMatchInfo); + +// get term instances merged with data +const terms2: (ITermData & ITerm)[] = await store.getTerms(labelMatchInfo).get(); + +const terms3: ITerms = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms; + +// get terms merged with data from a term set +const terms4: (ITerm & ITermData)[] = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms.get(); +
You can get a single term a variety of ways as shown below. The "best" way will be determined by what information is available to do the lookup but ultimately will result in the same end product.
+import { + taxonomy, + ITermStore, + ITerms, + ILabelMatchInfo, + ITerm, + ITermData +} from "@pnp/sp-taxonomy"; + +const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l=="); + +// get a single term by id +const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); + +// get single get merged with data +const term2: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get(); + +// use select to choose which fields to return +const term3: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").select("Name").get(); + +// get a term from a term set +const term4: ITerm = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431"); +
Accesses the labels collection for this term
+import { taxonomy, ITermStore, ITerm, ILabels } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +const labels: ILabels = term.labels; + +// labels merged with data +const labelsWithData = term.labels.get(); +
Creates a new label for this Term
+import { taxonomy, ITermStore, ITerm, ILabelData, ILabel } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +const label: ILabelData & ILabel = term.createLabel("label text", 1031); + +// optionally specify this is the default label +const label2: ILabelData & ILabel = term.createLabel("label text", 1031, true); +
Sets the deprecation flag on a term
+import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +await term.deprecate(true); +
Loads the term data
+import { ITerm, ITermData } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +// load term instance merged with data +const term2: ITerm & ITermData = await term.get(); +
Sets the description
+import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +// load term instance merged with data +const description = await term.getDescription(1031); +
Sets the description
+import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +// load term instance merged with data +await term.setDescription("the description", 1031); +
Sets a custom property on this term
+import { ITerm } from "@pnp/sp-taxonomy"; + +const term: ITerm = <from one of the above methods>; + +// load term instance merged with data +await term.setLocalCustomProperty("name", "value"); +
Added in 1.2.8
+Adds a child term to an existing term instance.
+import { ITerm } from "@pnp/sp-taxonomy"; + +const parentTerm: ITerm = <from one of the above methods>; + +await parentTerm.addTerm("child 1", 1033); + +await parentTerm.addTerm("child 2", 1033); +
These are a collection of helper methods you may find useful.
+Allows you to easily set the value of a metadata field in a list item.
+import { sp } from "@pnp/sp"; +import { taxonomy, setItemMetaDataField } from "@pnp/sp-taxonomy"; + +// create a new item, or load an existing +const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({ + Title: "My Title", +}); + +// get a term +const term = await taxonomy.getDefaultSiteCollectionTermStore() + .getTermById("99992696-1111-1111-1111-15e65b221111").get(); + +setItemMetaDataField(itemResult.item, "MetaDataFieldName", term); +
Allows you to easily set the value of a multi-value metadata field in a list item.
+import { sp } from "@pnp/sp"; +import { taxonomy, setItemMetaDataMultiField } from "@pnp/sp-taxonomy"; + +// create a new item, or load an existing +const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({ + Title: "My Title", +}); + +// get a term +const term = await taxonomy.getDefaultSiteCollectionTermStore() + .getTermById("99992696-1111-1111-1111-15e65b221111").get(); + +// get another term +const term2 = await taxonomy.getDefaultSiteCollectionTermStore() + .getTermById("99992696-1111-1111-1111-15e65b221112").get(); + +// get yet another term +const term3 = await taxonomy.getDefaultSiteCollectionTermStore() + .getTermById("99992696-1111-1111-1111-15e65b221113").get(); + +setItemMetaDataMultiField( + itemResult.item, + "MultiValueMetaDataFieldName", + term, + term2, + term3 +); +
Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the +url length limits when working with files and folders.
+To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query.
+Pattern: !@{label name}::{value}
+Example: "!@p1::\sites\dev" or "!@p2::\text.txt"
+import { sp } from "@pnp/sp"; +// still works as expected, no aliasing +const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 + +query.get().then(r => { + + console.log(r); +}); +
import { sp } from "@pnp/sp"; +// same query with aliasing +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 + +query.get().then(r => { + + console.log(r); +}); +
Aliasing is supported with batching as well:
+import { sp } from "@pnp/sp"; +// same query with aliasing and batching +const batch = sp.web.createBatch(); + +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3); + +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 + +query.inBatch(batch).get().then(r => { + + console.log(r); +}); + +batch.execute(); +
The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.
+Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.
+There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.
+import { sp } from "@pnp/sp"; +// get the curren't context web's app catalog +const catalog = sp.web.getAppCatalog(); + +// you can also chain off the app catalog +pnp.sp.web.getAppCatalog().get().then(console.log); +
import { sp } from "@pnp/sp"; +// you can get the tenant app catalog (or any app catalog) by passing in a url + +// get the tenant app catalog +const tenantCatalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/appcatalog"); + +// get a different app catalog +const catalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/anothersite"); +
// alternatively you can create a new app catalog instance directly by importing the AppCatalog class +import { AppCatalog } from "@pnp/sp"; + +const catalog = new AppCatalog("https://mytenant.sharepoint.com/sites/dev"); +
// and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web +import { Web, AppCatalog } from "@pnp/sp"; + +const web = new Web("https://mytenant.sharepoint.com/sites/dev"); +const catalog = new AppCatalog(web); +
The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.
+The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.
+// get available apps +catalog.get().then(console.log); + +// get available apps selecting two fields +catalog.select("Title", "Deployed").get().then(console.log); +
This action must be performed in the context of the tenant app catalog
+// this represents the file bytes of the app package file +const blob = new Blob(); + +// there is an optional third argument to control overwriting existing files +catalog.add("myapp.app", blob).then(r => { + + // this is at its core a file add operation so you have access to the response data as well + // as a File isntance representing the created file + + console.log(JSON.stringify(r.data, null, 4)); + + // all file operations are available + r.file.select("Name").get().then(console.log); +}); +
You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").get().then(console.log); +
Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success using then and catch.
+// deploy +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").deploy().then(console.log).catch(console.error); + +// retract +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").retract().then(console.log).catch(console.error); + +// install +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").install().then(console.log).catch(console.error); + +// uninstall +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").uninstall().then(console.log).catch(console.error); + +// upgrade +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").upgrade().then(console.log).catch(console.error); + +// remove +catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").remove().then(console.log).catch(console.error); +
The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.
+import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +// get all the attachments +item.attachmentFiles.get().then(v => { + + console.log(v); +}); + +// get a single file by file name +item.attachmentFiles.getByName("file.txt").get().then(v => { + + console.log(v); +}); + +// select specific properties using odata operators +item.attachmentFiles.select("ServerRelativeUrl").get().then(v => { + + console.log(v); +}); +
You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.
+import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.add("file2.txt", "Here is my content").then(v => { + + console.log(v); +}); +
This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining.
+const list = sp.web.lists.getByTitle("MyList"); + +var fileInfos: AttachmentFileInfo[] = []; + +fileInfos.push({ + name: "My file name 1", + content: "string, blob, or array" +}); + +fileInfos.push({ + name: "My file name 2", + content: "string, blob, or array" +}); + +list.items.getById(2).attachmentFiles.addMultiple(fileInfos).then(r => { + + console.log(r); +}); +
const list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(2).attachmentFiles.deleteMultiple("1.txt","2.txt").then(r => { + console.log(r); +}); +
You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.
+import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file.txt").getText().then(v => { + + console.log(v); +}); + +// use this in the browser, does not work in nodejs +item.attachmentFiles.getByName("file.mp4").getBlob().then(v => { + + console.log(v); +}); + +// use this in nodejs +item.attachmentFiles.getByName("file.mp4").getBuffer().then(v => { + + console.log(v); +}); + +// file must be valid json +item.attachmentFiles.getByName("file.json").getJSON().then(v => { + + console.log(v); +}); +
You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library.
+import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!").then(v => { + + console.log(v); +}); +
import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").delete().then(v => { + + console.log(v); +}); +
Added in 1.2.4
+Delete the attachment and send it to recycle bin
+import { sp } from "@pnp/sp"; + +let item = sp.web.lists.getByTitle("MyList").items.getById(1); + +item.attachmentFiles.getByName("file2.txt").recycle().then(v => { + + console.log(v); +}); +
Added in 1.2.4
+Delete multiple attachments and send them to recycle bin
+import { sp } from "@pnp/sp"; + +const list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(2).attachmentFiles.recycleMultiple("1.txt","2.txt").then(r => { + console.log(r); +}); +
The ability to manage client-side pages is a capability introduced in version 1.0.2 of @pnp/sp. Through the methods described +you can add and edit "modern" pages in SharePoint sites.
+Using the addClientSidePage you can add a new client side page to a site, specifying the filename.
+import { sp } from "@pnp/sp"; + +const page = await sp.web.addClientSidePage(`file-name`); + +// OR + +const page = await sp.web.addClientSidePage(`file-name`, `Page Display Title`); +
Added in 1.0.5 you can also add a client side page using the list path. This gets around potential language issues with list title. You must specify the list path when calling this method in addition to the new page's filename.
+import { sp } from "@pnp/sp"; + +const page = await sp.web.addClientSidePageByPath(`file-name`, "/sites/dev/SitePages"); +
You can also load an existing page based on the file representing that page. Note that the static fromFile returns a promise which +resolves so the loaded page. Here we are showing use of the getFileByServerRelativeUrl method to get the File instance, but any of the ways +of getting a File instance will work. Also note we are passing the File instance, not the file content.
+import { + sp, + ClientSidePage, +} from "@pnp/sp"; + +const page = await ClientSidePage.fromFile(sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/ExistingFile.aspx")); +
The remaining examples below reference a variable "page" which is assumed to be a ClientSidePage instance loaded through one of the above means.
+A client-side page is made up of sections, which have columns, which contain controls. A new page will have none of these and an existing page may have +any combination of these. There are a few rules to understand how sections and columns layout on a page for display. A section is a horizontal piece of +a page that extends 100% of the page width. A page with multiple sections will stack these sections based on the section's order property - a 1 based index.
+Within a section you can have one or more columns. Each column is ordered left to right based on the column's order property. The width of each column is +controlled by the factor property whose value is one of 0, 2, 4, 6, 8, 10, or 12. The columns in a section should have factors that add up to 12. Meaning +if you wanted to have two equal columns you can set a factor of 6 for each. A page can have empty columns.
+import { + sp, + ClientSideText, +} from "@pnp/sp"; + +// this code adds a section, and then adds a control to that section. The control is added to the section's defaultColumn, and if there are no columns a single +// column of factor 12 is created as a default. Here we add the ClientSideText part +page.addSection().addControl(new ClientSideText("@pnp/sp is a great library!")); + +// here we add a section, add two columns, and add a text control to the second section so it will appear on the right of the page +// add and get a reference to a new section +const section = page.addSection(); + +// add a column of factor 6 +section.addColumn(6); + +// add and get a reference to a new column of factor 6 +const column = section.addColumn(6); + +// add a text control to the second new column +column.addControl(new ClientSideText("Be sure to check out the @pnp docs at https://pnp.github.io/pnpjs/")); + +// we need to save our content changes +await page.save(); +
Beyond the text control above you can also add any of the available client-side web parts in a given site. To find out what web parts are available you +first call the web's getClientSideWebParts method. Once you have a list of parts you need to find the defintion you want to use, here we get the Embed web part +whose's id is "490d7c76-1824-45b2-9de3-676421c997fa" (at least in one farm, your mmv).
+import { + sp, + ClientSideWebpart, + ClientSideWebpartPropertyTypes, +} from "@pnp/sp"; + +// this will be a ClientSidePageComponent array +// this can be cached on the client in production scenarios +const partDefs = await sp.web.getClientSideWebParts(); + +// find the definition we want, here by id +const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa"); + +// optionally ensure you found the def +if (partDef.length < 1) { + // we didn't find it so we throw an error + throw new Error("Could not find the web part"); +} + +// create a ClientWebPart instance from the definition +const part = ClientSideWebpart.fromComponentDef(partDef[0]); + +// set the properties on the web part. Here we have imported the ClientSideWebpartPropertyTypes module and can use that to type +// the available settings object. You can use your own types or help us out and add some typings to the module :). +// here for the embed web part we only have to supply an embedCode - in this case a youtube video. +part.setProperties<ClientSideWebpartPropertyTypes.Embed>({ + embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg", +}); + +// we add that part to a new section +page.addSection().addControl(part); + +// save our content changes back to the server +await page.save(); +
Added in 1.0.3
+You can use the either of the two available method to locate controls within a page. These method search through all sections, columns, and controls returning the first instance that meets the supplied criteria.
+import { ClientSideWebPart } from "@pnp/sp"; + +// find a control by instance id +const control1 = page.findControlById("b99bfccc-164e-4d3d-9b96-da48db62eb78"); + +// type the returned control +const control2 = page.findControlById<ClientSideWebPart>("c99bfccc-164e-4d3d-9b96-da48db62eb78"); +const control3 = page.findControlById<ClientSideText>("a99bfccc-164e-4d3d-9b96-da48db62eb78"); + +// use any predicate to find a control +const control4 = page2.findControl<ClientSideWebpart>((c: CanvasControl) => { + + // any logic you wish can be used on the control here + // return true to return that control + return c.order > 3; +}); +
You can choose to enable or disable comments on a page using these methods
+// indicates if comments are disabled, not valid until the page is loaded (Added in _1.0.3_) +page.commentsDisabled + +// enable comments +await page.enableComments(); + +// disable comments +await page.disableComments(); +
Added in 1.2.4
+You can like or unlike a modern page. You can also get information about the likes (i.e like Count and which users liked the page)
+// Like a Client-side page (Added in _1.2.4_) +await page.like(); + +// Unlike a Client-side page +await page.unlike(); + +// Get liked by information such as like count and user's who liked the page +await page.getLikedByInformation(); +
The below sample shows the process to add a Yammer feed webpart to the page. The properties required as well as the data version are found by adding the part using the UI and reviewing the values. Some or all of these may be discoverable using Yammer APIs. An identical process can be used to add web parts of any type by adjusting the definition, data version, and properties appropriately.
+// get webpart defs +const defs = await sp.web.getClientSideWebParts(); + +// this is the id of the definition in my farm +const yammerPartDef = defs.filter(d => d.Id === "31e9537e-f9dc-40a4-8834-0e3b7df418bc")[0]; + +// page file +const file = sp.web.getFileByServerRelativePath("/sites/dev/SitePages/Testing_kVKF.aspx"); + +// create page instance +const page = await ClientSidePage.fromFile(file); + +// create part instance from definition +const part = ClientSideWebpart.fromComponentDef(yammerPartDef); + +// update data version +part.dataVersion = "1.5"; + +// set the properties required +part.setProperties({ + feedType: 0, + isSuiteConnected: false, + mode: 2, + networkId: 9999999, + yammerEmbedContainerHeight: 400, + yammerFeedURL: "", + yammerGroupId: -1, + yammerGroupMugshotUrl: "https://mug0.assets-yammer.com/mugshot/images/{width}x{height}/all_company.png", + yammerGroupName: "All Company", + yammerGroupUrl: "https://www.yammer.com/{tenant}/#/threads/company?type=general", +}); + +// add to the section/column you want +page.sections[0].addControl(part); + +// persist changes +page.save(); +
Likes and comments in the context of modern sites are based on list items, meaning the operations branch from the Item class. To load an item you can refer to the guidance in the items article. If you want to set the likes or comments on a modern page and don't know the item id but do know the url you can first load the file and then use the getItem method to get an item instance:
+These APIs are currently in BETA and are subject to change or may not work on all tenants.
+import { sp } from "@pnp/sp"; + +const item = await sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/Test_8q5L.aspx").getItem(); + +// as an example, or any of the below options +await item.like(); +
The below examples use a variable named "item" which is taken to represent an instance of the Item class.
+const comments = await item.comments.get(); +
You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:
+import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +// these will be Comment instances in the array +comments[0].replies.add({ text: "#PnPjs is pretty ok!" }); + +//load the top 20 replies and comments for an item including likedBy information +const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20).get(); +
// you can add a comment as a string +item.comments.add("string comment"); + +// or you can add it as an object to include mentions +item.comments.add({ text: "comment from object property" }); +
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +// these will be Comment instances in the array +comments[0].delete() +
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +// these will be Comment instances in the array +comments[0].like() +
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +comments[0].unlike() +
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +const comment: Comment & CommentData = await comments[0].replies.add({ text: "#PnPjs is pretty ok!" }); +
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp"; + +const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment)); + +const replies = await comments[0].replies.get(); +
You can like items and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.
+import { LikeData } from "@pnp/sp"; + +// like an item +await item.like(); + +// unlike an item +await item.unlike(); + +// get the liked by information +const likedByData: LikeData[] = await item.getLikedBy(); +
interface OrderData { + ContentTypeOrder: { StringValue: string }[]; + UniqueContentTypeOrder?: { StringValue: string }[]; +} + +const folder = sp.web.lists.getById("{list id guid}").rootFolder; + +// here you need to see if there are unique content type orders already or just the default +const existingOrders = await folder.select("ContentTypeOrder", "UniqueContentTypeOrder").get<OrderData>(); + +const activeOrder = existingOrders.UniqueContentTypeOrder ? existingOrders.UniqueContentTypeOrder : existingOrders.ContentTypeOrder; + +// manipulate the order here however you want (I am just reversing the array as an example) +const newOrder = activeOrder.reverse(); + +// update the content type order thusly: +await folder.update({ + UniqueContentTypeOrder: { + __metadata: { type: "Collection(SP.ContentTypeId)" }, + results: newOrder, + }, +}); +
Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its represending type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.
+If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query.
+import { sp, spODataEntity, Item } from "@pnp/sp"; + +// interface defining the returned properites +interface MyProps { + Id: number; +} + +try { + + // get a list item laoded with data and merged into an instance of Item + const item = await sp.web.lists.getByTitle("ListTitle").items.getById(1).get(spODataEntity<Item, MyProps>(Item)); + + // log the item id, all properties specified in MyProps will be type checked + Logger.write(`Item id: ${item.Id}`); + + // now we can call update because we have an instance of the Item type to work with as well + await item.update({ + Title: "New title.", + }); + +} catch (e) { + Logger.error(e); +} +
The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method.
+import { sp, spODataEntityArray, Item } from "@pnp/sp"; + +// interface defining the returned properites +interface MyProps { + Id: number; + Title: string; +} + +try { + + // get a list item laoded with data and merged into an instance of Item + const items = await sp.web.lists.getByTitle("ListTitle").items.select("Id", "Title").get(spODataEntityArray<Item, MyProps>(Item)); + + Logger.write(`Item id: ${items.length}`); + + Logger.write(`Item id: ${items[0].Title}`); + + // now we can call update because we have an instance of the Item type to work with as well + await items[0].update({ + Title: "New title.", + }); + +} catch (e) { + + Logger.error(e); +} +
Added in 1.3.4
+Starting with 1.3.4 you can now include entity merging in the getPaged command as shown below. This approach will work with any objects matching the required factory pattern.
+// create Item instances with the defined property Title +const items = await sp.web.lists.getByTitle("BigList").items.select("Title").getPaged(spODataEntityArray<Item, { Title: string }>(Item)); + +console.log(items.results.length); + +// now invoke methods on the Item object +const perms = await items.results[0].getCurrentUserEffectivePermissions(); + +console.log(JSON.stringify(perms, null, 2)); + +// you can also type the result slightly differently if you prefer this, but the results are the same functionally. +const items2 = await sp.web.lists.getByTitle("BigList").items.select("Title").getPaged<(Item & { Title: string })[]>(spODataEntityArray(Item)); +
Features are used by SharePoint to package a set of functionality and either enable (activate) or disable (deactivate) that functionality based on requirements for a specific site. You can manage feature activation using the library as shown below. Note that the features collection only contains active features.
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +// get all the active features +web.features.get().then(f => { + + console.log(f); +}); + +// select properties using odata operators +web.features.select("DisplayName", "DefinitionId").get().then(f => { + + console.log(f); +}); + +// get a particular feature by id +web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").select("DisplayName", "DefinitionId").get().then(f => { + + console.log(f); +}); + +// get features using odata operators +web.features.filter("DisplayName eq 'MDSFeature'").get().then(f => { + + console.log(f); +}); +
To activate a feature you must know the feature id. You can optionally force activation - if you aren't sure don't use force.
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +// activate the minimum download strategy feature +web.features.add("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => { + + console.log(f); +}); +
import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.features.remove("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => { + + console.log(f); +}); + +// you can also deactivate a feature but going through the collection's remove method is faster +web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").deactivate().then(f => { + + console.log(f); +}); +
Fields allow you to store typed information within a SharePoint list. There are many types of fields and the library seeks to simplify working with the most common types. Fields exist in both site collections (site columns) or lists (list columns) and you can add/modify/delete them at either of these levels.
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +// get all the fields in a web +web.fields.get().then(f => { + + console.log(f); +}); + +// you can use odata operators on the fields collection +web.fields.select("Title", "InternalName", "TypeAsString").top(10).orderBy("Id").get().then(f => { + + console.log(f); +}); + +// get all the available fields in a web (includes parent web's fields) +web.availablefields.get().then(f => { + + console.log(f); +}); + +// get the fields in a list +web.lists.getByTitle("MyList").fields.get().then(f => { + + console.log(f); +}); + +// you can also get individual fields using getById, getByTitle, or getByInternalNameOrTitle +web.fields.getById("dee9c205-2537-44d6-94e2-7c957e6ebe6e").get().then(f => { + + console.log(f); +}); + +web.fields.getByTitle("MyField4").get().then(f => { + + console.log(f); +}); + +web.fields.getByInternalNameOrTitle("MyField4").get().then(f => { + + console.log(f); +}); +
Sometimes you only want a subset of fields from the collection. Below are some examples of using the filter operator with the fields collection.
+import { sp } from '@pnp/sp'; + +const list = sp.web.lists.getByTitle('Custom'); + +// Fields which can be updated +const filter1 = `Hidden eq false and ReadOnlyField eq false`; +list.fields.select('InternalName').filter(filter1).get().then(fields => { + console.log(`Can be updated: ${fields.map(f => f.InternalName).join(', ')}`); + // Title, ...Custom, ContentType, Attachments +}); + +// Only custom field +const filter2 = `Hidden eq false and CanBeDeleted eq true`; +list.fields.select('InternalName').filter(filter2).get().then(fields => { + console.log(`Custom fields: ${fields.map(f => f.InternalName).join(', ')}`); + // ...Custom +}); + +// Application specific fields +const includeFields = [ 'Title', 'Author', 'Editor', 'Modified', 'Created' ]; +const filter3 = `Hidden eq false and (ReadOnlyField eq false or (${ + includeFields.map(field => `InternalName eq '${field}'`).join(' or ') +}))`; +list.fields.select('InternalName').filter(filter3).get().then(fields => { + console.log(`Application specific: ${fields.map(f => f.InternalName).join(', ')}`); + // Title, ...Custom, ContentType, Modified, Created, Author, Editor, Attachments +}); + +// Fields in a view +list.defaultView.fields.select('Items').get().then(f => { + const fields = (f as any).Items.results || (f as any).Items; + console.log(`Fields in a view: ${fields.join(', ')}`); +}); +
You can add fields using the add, createFieldAsXml, or one of the type specific methods. Functionally there is no difference, however one method may be easier given a certain scenario.
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +// if you use add you _must_ include the correct FieldTypeKind in the extended properties +web.fields.add("MyField1", "SP.FieldText", { + Group: "~Example", + FieldTypeKind: 2, + Filterable: true, + Hidden: false, + EnforceUniqueValues: true, +}).then(f => { + + console.log(f); +}); + +// you can also use the addText or any of the other type specific methods on the collection +web.fields.addText("MyField2", 75, { + Group: "~Example" +}).then(f => { + + console.log(f); +}); + +// if you have the field schema (for example from an old elements file) you can use createFieldAsXml +let xml = `<Field DisplayName="MyField4" Type="Text" Required="FALSE" StaticName="MyField4" Name="MyField4" MaxLength="125" Group="~Example" />`; + +web.fields.createFieldAsXml(xml).then(f => { + + console.log(f); +}); + +// the same operations work on a list's fields collection +web.lists.getByTitle("MyList").fields.addText("MyField5", 100).then(f => { + + console.log(f); +}); + +// Create a lookup field, and a dependent lookup field +web.lists.getByTitle("MyList").fields.addLookup("MyLookup", "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx", "MyLookupTargetField").then(f => { + console.log(f); + + // Create the dependent lookup field + return web.lists.getByTitle("MyList").fields.addDependentLookupField("MyLookup_ID", f.Id, "ID"); +}).then(fDep => { + console.log(fDep); +}); +
Because the RichTextMode property is not exposed to the clients we cannot set this value via the API directly. The work around is to use the createFieldAsXml method as shown below
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +const fieldAddResult = await web.fields.createFieldAsXml(`<Field Type="Note" Name="Content" DisplayName="Content" Required="{TRUE|FALSE}" RichText="TRUE" RichTextMode="FullHtml" />`); +
You can also update the properties of a field in both webs and lists, but not all properties are able to be updated after creation. You can review this list for details.
+import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.fields.getByTitle("MyField4").update({ + Description: "A new description", + }).then(f => { + + console.log(f); +}); +
When updating a URL or Picture field you need to include the __metadata descriptor as shown below.
+import { sp } from "@pnp/sp"; + +const data = { + "My_Field_Name": { + "__metadata": { "type": "SP.FieldUrlValue" }, + "Description": "A Pretty picture", + "Url": "https://tenant.sharepoint.com/sites/dev/Style%20Library/DSC_0024.JPG", + }, +}; + +await sp.web.lists.getByTitle("MyListTitle").items.getById(1).update(data); +
import { sp } from "@pnp/sp"; + +let web = sp.web; + +web.fields.getByTitle("MyField4").delete().then(f => { + + console.log(f); +}); +
One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.
+Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.
+import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBlob().then((blob: Blob) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBuffer().then((buffer: ArrayBuffer) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.json").getJSON().then((json: any) => {}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.txt").getText().then((text: string) => {}); + +// all of these also work from a file object no matter how you access it +sp.web.getFolderByServerRelativeUrl("/sites/dev/documents").files.getByName("file.txt").getText().then((text: string) => {}); +
Likewise you can add files using one of two methods, add or addChunked. The second is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.
+declare var require: (s: string) => any; + +import { ConsoleListener, Web, Logger, LogLevel, ODataRaw } from "@pnp/sp"; +import { auth } from "./auth"; +let $ = require("jquery"); + +let siteUrl = "https://mytenant.sharepoint.com/sites/dev"; + +// comment this out for non-node execution +// auth(siteUrl); + +Logger.subscribe(new ConsoleListener()); +Logger.activeLogLevel = LogLevel.Verbose; + +let web = new Web(siteUrl); + +$(() => { + $("#testingdiv").append("<button id='thebuttontodoit'>Do It</button>"); + + $("#thebuttontodoit").on('click', (e) => { + + e.preventDefault(); + + let input = <HTMLInputElement>document.getElementById("thefileinput"); + let file = input.files[0]; + + // you can adjust this number to control what size files are uploaded in chunks + if (file.size <= 10485760) { + + // small upload + web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(_ => Logger.write("done")); + } else { + + // large upload + web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.addChunked(file.name, file, data => { + + Logger.log({ data: data, level: LogLevel.Verbose, message: "progress" }); + + }, true).then(_ => Logger.write("done!")); + } + }); +}); +
You can also update the file properties of a newly uploaded file using code similar to the below snippet:
+import { sp } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(f => { + + f.file.getItem().then(item => { + + item.update({ + Title: "A Title", + OtherField: "My Other Value" + }); + }); +}); +
You can of course use similar methods to update existing files as shown below:
+import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.txt").setContent("New string content for the file."); + +sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.mp4").setContentChunked(file); +
The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.
+Check in takes two optional arguments, comment and check in type.
+import { sp, CheckinType } from "@pnp/sp"; + +// default options with empty comment and CheckinType.Major +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin().then(_ => { + + console.log("File checked in!"); +}); + +// supply a comment (< 1024 chars) and using default check in type CheckinType.Major +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment").then(_ => { + + console.log("File checked in!"); +}); + +// Supply both comment and check in type +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite).then(_ => { + + console.log("File checked in!"); +}); +
Check out takes no arguments.
+import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkout().then(_ => { + + console.log("File checked out!"); +}); +
You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.
+import { sp } from "@pnp/sp"; + +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").approve("Approval Comment").then(_ => { + + console.log("File approved!"); +}); + +// deny with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny().then(_ => { + + console.log("File denied!"); +}); + +// deny with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny("Deny comment").then(_ => { + + console.log("File denied!"); +}); +
You can both publish and unpublish a file using the library. Both methods take an optional comment argument.
+import { sp } from "@pnp/sp"; +// publish with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish().then(_ => { + + console.log("File published!"); +}); + +// publish with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish("Publish comment").then(_ => { + + console.log("File published!"); +}); + +// unpublish with no comment +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish().then(_ => { + + console.log("File unpublished!"); +}); + +// unpublish with a supplied comment. +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment").then(_ => { + + console.log("File unpublished!"); +}); +
Both the addChunked and setContentChunked methods support options beyond just supplying the file content.
+A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:
+(data: ChunkedFileUploadProgressData) => void
The data interface is:
+export interface ChunkedFileUploadProgressData { + stage: "starting" | "continue" | "finishing"; + blockNumber: number; + totalBlocks: number; + chunkSize: number; + currentPointer: number; + fileSize: number; +} +
This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.
+This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.
+import { sp } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => { + + console.log(item); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem("Title", "Modified").then(item => { + + console.log(item); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => { + + // you can also chain directly off this item instance + item.getCurrentUserEffectivePermissions().then(perms => { + + console.log(perms); + }); +}); +
You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.
+import { sp } from "@pnp/sp"; +// also supports typing the objects so your type will be a union type +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem<{ Id: number, Title: string }>("Id", "Title").then(item => { + + // You get intellisense and proper typing of the returned object + console.log(`Id: ${item.Id} -- ${item.Title}`); + + // You can also chain directly off this item instance + item.getCurrentUserEffectivePermissions().then(perms => { + + console.log(perms); + }); +}); +
This package contains the fluent api used to call the SharePoint rest services.
+Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save
Import the library into your application and access the root sp object
+import { sp } from "@pnp/sp"; + +(function main() { + + // here we will load the current web's title + sp.web.select("Title").get().then(w => { + + console.log(`Web Title: ${w.Title}`); + }); +})() +
Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save
Import the library into your application, update OnInit, and access the root sp object in render
+import { sp } from "@pnp/sp"; + +// ... + +public onInit(): Promise<void> { + + return super.onInit().then(_ => { + + // other init code may be present + + sp.setup({ + spfxContext: this.context + }); + }); +} + +// ... + +public render(): void { + + // A simple loading message + this.domElement.innerHTML = `Loading...`; + + sp.web.select("Title").get().then(w => { + + this.domElement.innerHTML = `Web Title: ${w.Title}`; + }); +} +
Install the library and required dependencies
+npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs --save
Import the library into your application, setup the node client, make a request
+import { sp } from "@pnp/sp"; +import { SPFetchClient } from "@pnp/nodejs"; + +// do this once per page load +sp.setup({ + sp: { + fetchClientFactory: () => { + return new SPFetchClient("{your site url}", "{your client id}", "{your client secret}"); + }, + }, +}); + +// now make any calls you need using the configured client +sp.web.select("Title").get().then(w => { + + console.log(`Web Title: ${w.Title}`); +}); +
Graphical UML diagram of @pnp/sp. Right-click the diagram and open in new tab if it is too small.
+ + + + + + + + + +Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.
+import { sp } from "@pnp/sp"; + +// get all the items from a list +sp.web.lists.getByTitle("My List").items.get().then((items: any[]) => { + console.log(items); +}); + +// get a specific item by id +sp.web.lists.getByTitle("My List").items.getById(1).get().then((item: any) => { + console.log(item); +}); + +// use odata operators for more efficient queries +sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true).get().then((items: any[]) => { + console.log(items); +}); +
Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.
+import { sp } from "@pnp/sp"; + +// basic case to get paged items form a list +let items = await sp.web.lists.getByTitle("BigList").items.getPaged(); + +// you can also provide a type for the returned values instead of any +let items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>(); + +// the query also works with select to choose certain fields and top to set the page size +let items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>(); + +// the results object will have two properties and one method: + +// the results property will be an array of the items returned +if (items.results.length > 0) { + console.log("We got results!"); + + for (let i = 0; i < items.results.length; i++) { + // type checking works here if we specify the return type + console.log(items.results[i].Title); + } +} + +// the hasNext property is used with the getNext method to handle paging +// hasNext will be true so long as there are additional results +if (items.hasNext) { + + // this will carry over the type specified in the original query for the results array + items = await items.getNext(); + console.log(items.results.length); +} +
The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.
+import { sp } from "@pnp/sp"; + +// Using RowLimit. Enables paging +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'}); + +// Use QueryOptions to make a XML-style query. +// Because it's XML we need to escape special characters +// Instead of & we use & in the query +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext="Paged=TRUE&p_ID=5" />'}); + +// Get everything. Using null with ChangeToken gets everything +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null}); +
Added in 1.0.2
+Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should +be used.
+import { sp } from "@pnp/sp"; +// basic usage +sp.web.lists.getByTitle("BigList").items.getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// set page size +sp.web.lists.getByTitle("BigList").items.getAll(4000).then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// use select and top. top will set page size and override the any value passed to getAll +sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); + +// we can also use filter as a supported odata operation, but this will likely fail on large lists +sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll().then((allItems: any[]) => { + + // how many did we get + console.log(allItems.length); +}); +
When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.
+import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((items: any[]) => { + console.log(items); +}); + +sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((item: any) => { + console.log(item); +}); +
The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.
+import { Web } from "@pnp/sp"; + +const w = new Web("https://{publishing site url}"); + +w.lists.getByTitle("Pages").items + .select("Title", "FileRef", "FieldValuesAsText/MetaInfo") + .expand("FieldValuesAsText") + .get().then(r => { + + // look through the returned items. + for (var i = 0; i < r.length; i++) { + + // the title field value + console.log(r[i].Title); + + // find the value in the MetaInfo string using regex + const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); + if (matches !== null && matches.length > 1) { + + // this wil be the value of the PublishingPageImage field + console.log(matches[1]); + } + } + }).catch(e => { console.error(e); }); +
There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.
+import { sp, ItemAddResult } from "@pnp/sp"; + +// add an item to the list +sp.web.lists.getByTitle("My List").items.add({ + Title: "Title", + Description: "Description" +}).then((iar: ItemAddResult) => { + console.log(iar); +}); +
You can also set the content type id when you create an item as shown in the example below:
+import { sp } from "@pnp/sp"; + +sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({ + Title: "Test 1", + ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF" +}); +
There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.
+Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a "results" property and an array. Examples for both are shown below.
+import { sp } from "@pnp/sp"; +import { getGUID } from "@pnp/core"; + +sp.web.lists.getByTitle("PeopleFields").items.add({ + Title: getGUID(), + User1Id: 9, // allows a single user + User2Id: { + results: [ 16, 45 ] // allows multiple users + } +}).then(i => { + console.log(i); +}); +
If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.
+import { sp } from "@pnp/sp"; + +const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{ + FieldName: "UserField", + FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]), +}, +{ + FieldName: "Title", + FieldValue: "Test - Updated", +}]); +
What is said for User Fields is, in general, relevant to Lookup Fields:
+- Lookup Field types:
+ - Single-valued lookup
+ - Multiple-valued lookup
+- Id
suffix should be appended to the end of lookup's EntityPropertyName
in payloads
+- Numeric Ids for lookups' items should be passed as values
import { sp } from "@pnp/sp"; +import { getGUID } from "@pnp/core"; + +sp.web.lists.getByTitle("LookupFields").items.add({ + Title: getGUID(), + LookupFieldId: 2, // allows a single lookup value + MuptiLookupFieldId: { + results: [ 1, 56 ] // allows multiple lookup value + } +}).then(console.log).catch(console.log); +
import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("rapidadd"); + +list.getListItemEntityTypeFullName().then(entityTypeFullName => { + + let batch = sp.web.createBatch(); + + list.items.inBatch(batch).add({ Title: "Batch 6" }, entityTypeFullName).then(b => { + console.log(b); + }); + + list.items.inBatch(batch).add({ Title: "Batch 7" }, entityTypeFullName).then(b => { + console.log(b); + }); + + batch.execute().then(d => console.log("Done")); +}); +
The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.
+import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(1).update({ + Title: "My New Title", + Description: "Here is a new description" +}).then(i => { + console.log(i); +}); +
import { sp } from "@pnp/sp"; + +// you are getting back a collection here +sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'").get().then((items: any[]) => { + // see if we got something + if (items.length > 0) { + sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({ + Title: "Updated Title", + }).then(result => { + // here you will have updated the item + console.log(JSON.stringify(result)); + }); + } +}); +
This approach avoids multiple calls for the same list's entity type name.
+import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("rapidupdate"); + +list.getListItemEntityTypeFullName().then(entityTypeFullName => { + + let batch = sp.web.createBatch(); + + // note requirement of "*" eTag param - or use a specific eTag value as needed + list.items.getById(1).inBatch(batch).update({ Title: "Batch 6" }, "*", entityTypeFullName).then(b => { + console.log(b); + }); + + list.items.getById(2).inBatch(batch).update({ Title: "Batch 7" }, "*", entityTypeFullName).then(b => { + console.log(b); + }); + + batch.execute().then(d => console.log("Done")); +}); +
Sending an item to the Recycle Bin is as simple as calling the .recycle method.
+import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(1).recycle().then(_ => {}); +
Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.
+import { sp } from "@pnp/sp"; + +let list = sp.web.lists.getByTitle("MyList"); + +list.items.getById(1).delete().then(_ => {}); +
It's a very common mistake trying wrong field names in the requests.
+Field's EntityPropertyName
value should be used.
The easiest way to get know EntityPropertyName is to use the following snippet:
+import { sp } from "@pnp/sp"; + +sp.web.lists + .getByTitle('[Lists_Title]') + .fields + .select('Title, EntityPropertyName') + .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) + .get() + .then(response => { + console.log(response.map(field => { + return { + Title: field.Title, + EntityPropertyName: field.EntityPropertyName + }; + })); + }) + .catch(console.log); +
Lookup fields' names should be ended with additional Id
suffix. E.g. for Editor
EntityPropertyName EditorId
should be used.
The global navigation service located at "_api/navigation" provides access to the SiteMapProvider instances available in a given site collection.
+The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.
+The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma seperated string of property names like: property1,property2,property3\,containingcomma
+NOTE: the , seperator can be escaped using the \ as escape character as done in the example above. The string above would split like: + property1 + property2 +* property3,containingcomma
+import { sp } from "@pnp/sp"; + +// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. +sp.navigation.getMenuState().then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); + +// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 +sp.navigation.getMenuState("1002", 5).then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); + +// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 +sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode").then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); +
Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.
+import { sp } from "@pnp/sp"; + +sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx").then(r => { + + console.log(JSON.stringify(r, null, 4)); + +}).catch(console.error); +
A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.
+Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.
+This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.
+import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.roleAssignments.get().then(roles => { + + Logger.writeJSON(roles); +}); +
This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.
+import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.firstUniqueAncestorSecurableObject.get().then(obj => { + + Logger.writeJSON(obj); +}); +
This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.
+import { sp } from "@pnp/sp"; +import { Logger } from "@pnp/logging"; + +sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com").then(perms => { + + Logger.writeJSON(perms); +}); + +sp.web.getCurrentUserEffectivePermissions().then(perms => { + + Logger.writeJSON(perms); +}); +
Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.
+import { sp, PermissionKind } from "@pnp/sp"; + +sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems).then(perms => { + + console.log(perms); +}); + +sp.web.currentUserHasPermissions(PermissionKind.ApproveItems).then(perms => { + + console.log(perms); +}); +
If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.
+import { sp, PermissionKind } from "@pnp/sp"; + +sp.web.getCurrentUserEffectivePermissions().then(perms => { + + if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { + // ... + } +}); +
The profile services allows to to work with the SharePoint User Profile Store.
+Profiles is accessed directly from the root sp object.
+import { sp } from "@pnp/sp"; +
getPropertiesFor(loginName: string): Promise<any>;
sp + .profiles + .getPropertiesFor(loginName).then((profile: any) => { + + console.log(profile.DisplayName); + console.log(profile.Email); + console.log(profile.Title); + console.log(profile.UserProfileProperties.length); + + // Properties are stored in inconvenient Key/Value pairs, + // so parse into an object called userProperties + var properties = {}; + profile.UserProfileProperties.forEach(function(prop) { + properties[prop.Key] = prop.Value; + }); + profile.userProperties = properties; + +} +
getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>;
sp + .profiles + .getUserProfilePropertyFor(loginName, propName).then((prop: string) => { + console.log(prop); +}; +
isFollowing(follower: string, followee: string): Promise<boolean>;
sp + .profiles + .isFollowing(follower, followee).then((followed: boolean) => { + console.log(followed); +}; +
getPeopleFollowedBy(loginName: string): Promise<any[]>;
sp + .profiles + .getPeopleFollowedBy(loginName).then((followed: any[]) => { + console.log(followed.length); +}; +
amIFollowedBy(loginName: string): Promise<boolean>;
Returns a boolean indicating if the current user is followed by the user with loginName. +Get a specific property for the specified user.
+sp + .profiles + .amIFollowedBy(loginName).then((followed: boolean) => { + console.log(followed); +}; +
getFollowersFor(loginName: string): Promise<any[]>;
sp + .profiles + .getFollowersFor(loginName).then((followed: any) => { + console.log(followed.length); +}; +
setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string)
Set a user's user profile property.
+sp + .profiles + .setSingleValueProfileProperty(accountName, propertyName, propertyValue); +
setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>;
sp + .profiles + .setSingleValueProfileProperty(accountName, propertyName, propertyValues); +
Users can upload a picture to their own profile only). Not supported for batching. +Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB
+setMyProfilePic(profilePicSource: Blob): Promise<void>;
Related items are used in Task and Workflow lists (as well as others) to track items that have relationships similar to database relationships.
+All methods chain off the Web's relatedItems property as shown below:
+Expects the named library to exist within the contextual web.
+import { sp, RelatedItem } from "@pnp/sp"; + +sp.web.relatedItems.getRelatedItems("Documents", 1).then((result: RelatedItem[]) => { + + console.log(result); +}); +
Expects the named library to exist within the contextual web.
+import { sp, RelatedItem } from "@pnp/sp"; + +sp.web.relatedItems.getPageOneRelatedItems("Documents", 1).then((result: RelatedItem[]) => { + + console.log(result); +}); +
import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => { + + // ... return is void +}); +
Adds a related item link from an item specified by list name and item id, to an item specified by url
+import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", true).then(_ => { + // ... return is void +}); +
Adds a related item link from an item specified by url, to an item specified by list name and item id
+import { sp } from "@pnp/sp"; + +sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2).then(_ => { + // ... return is void +}); + +sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2, true).then(_ => { + + // ... return is void +}); +
import { sp } from "@pnp/sp"; + +sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => { + + // ... return is void +}); + +sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => { + + // ... return is void +}); +
Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and search suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.
+Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the SearchQuery interface, or a SearchQueryBuilder instance. The first two are shown below.
+import { sp, SearchQuery, SearchResults } from "@pnp/sp"; + +// text search using SharePoint default values for other parameters +sp.search("test").then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); + +// define a search query object matching the SearchQuery interface +sp.search(<SearchQuery>{ + Querytext: "test", + RowLimit: 10, + EnableInterleaving: true, +}).then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); +
Added in 1.1.5
+As of version 1.1.5 you can also use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace "search" with "searchWithCaching" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options
+import { sp, SearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp"; + +sp.searchWithCaching(<SearchQuery>{ + Querytext: "test", + RowLimit: 10, + EnableInterleaving: true, +}).then((r: SearchResults) => { + + console.log(r.ElapsedTime); + console.log(r.RowCount); + console.log(r.PrimarySearchResults); +}); + + +const builder = SearchQueryBuilder().text("test").rowLimit(3); + +// supply a search query builder and caching options +sp.searchWithCaching(builder, { key: "mykey", expiration: dateAdd(new Date(), "month", 1) }).then(r2 => { + + console.log(r2.TotalRows); +}); +
Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.
+import { sp, SearchQueryBuilder, SearchResults } from "@pnp/sp"; + +// this will hold our current results +let currentResults: SearchResults = null; +let page = 1; + +// triggered on page load through some means +function onStart() { + + // construct our query that will be throughout the paging process, likely from user input + const q = SearchQueryBuilder.create("test").rowLimit(5); + sp.search(q).then((r: SearchResults) => { + + currentResults = r; // update the current results + page = 1; // reset if needed + // update UI with data... + }); +} + +// triggered by an event +function next() { + currentResults.getPage(++page).then((r: SearchResults) => { + + currentResults = r; // update the current results + // update UI with data... + }); +} + +// triggered by an event +function prev() { + currentResults.getPage(--page).then((r: SearchResults) => { + + currentResults = r; // update the current results + // update UI with data... + }); +} +
The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.
+import { SearchQueryBuilder } from "@pnp/sp"; + +// basic usage +let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic; + +sp.search(q).then(h => { /* ... */ }); + +// provide a default query text in the create() +let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic; + +sp.search(q2).then(h => { /* ... */ }); + +// provide query text and a template + +// shared settings across queries +const appSearchSettings: SearchQuery = { + EnablePhonetic: true, + HiddenConstraints: "reports" +}; + +let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules; +let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming; +sp.search(q3).then(h => { /* ... */ }); +sp.search(q4).then(h => { /* ... */ }); +
Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches SearchSuggestQuery.
+import { sp, SearchSuggestQuery, SearchSuggestResult } from "@pnp/sp"; + +sp.searchSuggest("test").then((r: SearchSuggestResult) => { + + console.log(r); +}); + +sp.searchSuggest(<SearchSuggestQuery>{ + querytext: "test", + count: 5, +}).then((r: SearchSuggestResult) => { + + console.log(r); +}); +
Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.
+One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before submitting an issue.
+Applies to: Item, Folder, File
+Creates a sharing link for the given resource with an optional expiration.
+import { sp , SharingLinkKind, ShareLinkResponse } from "@pnp/sp"; +import { dateAdd } from "@pnp/core"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView).then(((result: ShareLinkResponse) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5)).then((result: ShareLinkResponse) => { + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File, Web
+Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.
+import { sp , SharingResult, SharingRole } from "@pnp/sp"; + +sp.web.shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Web
+Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.
+import { sp , SharingResult, SharingRole } from "@pnp/sp"; + +sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); + +sp.web.shareObjectRaw({ + url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt", + peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }], + roleValue: "role: 1973741327", + groupId: 0, + propagateAcl: false, + sendEmail: true, + includeAnonymousLinkInEmail: false, + emailSubject: "subject", + emailBody: "body", + useSimplifiedRoles: true, +}); +
Applies to: Web
+import { sp , SharingResult } from "@pnp/sp"; + +sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt").then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+Checks Permissions on the list of Users and returns back role the users have on the Item.
+import { sp , SharingEntityPermission } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]).then((result: SharingEntityPermission[]) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+Get Sharing Information.
+import { sp , SharingInformation } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation().then((result: SharingInformation) => { + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+Gets the sharing settings
+import { sp , ObjectSharingSettings } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings().then((result: ObjectSharingSettings) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+Unshares a given resource
+import { sp , SharingResult } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare().then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+import { sp , SharingLinkKind, SharingResult } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit).then((result: SharingResult) => { + + console.log(result); +}).catch(e => { + console.error(e); +}); +
Applies to: Item, Folder, File
+import { sp , SharingLinkKind } from "@pnp/sp"; + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit).then(_ => { + + console.log("done"); +}).catch(e => { + console.error(e); +}); + +sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345").then(_ => { + + console.log("done"); +}).catch(e => { + console.error(e); +}); +
You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out SharePoint site design and site script overview for more information.
+import { sp } from "@pnp/sp"; + +// WebTemplate: 64 Team site template, 68 Communication site template +const siteDesign = await sp.siteDesigns.createSiteDesign({ + SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"], + Title: "SiteDesign001", + WebTemplate: "64", +}); + +console.log(siteDesign.Title); +
import { sp } from "@pnp/sp"; + +// Limited to 30 actions in a site script, but runs synchronously +await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001"); + +// Better use the following method for 300 actions in a site script +const task = await sp.web.addSiteDesignTask("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +
import { sp } from "@pnp/sp"; + +// Retrieving all site designs +const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); +console.log(`Total site designs: ${allSiteDesigns.length}`); + +// Retrieving a single site design by Id +const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +console.log(siteDesign.Title); +
import { sp } from "@pnp/sp"; + +// Update +const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" }); + +// Delete +await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +
import { sp } from "@pnp/sp"; + +// Get +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +console.log(rights.length > 0 ? rights[0].PrincipalName : ""); + +// Grant +await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]); + +// Revoke +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]); + +// Reset all view rights +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName)); +
import { sp } from "@pnp/sp"; + +const runs = await sp.web.getSiteDesignRuns(); +const runs2 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite"); + +// Get runs specific to a site design +const runs3 = await sp.web.getSiteDesignRuns("75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); +const runs4 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite", "75b9d8fe-4381-45d9-88c6-b03f483ae6a8"); + +// For more information about the site script actions +const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID); +const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus("https://TENANT.sharepoint.com/sites/mysite", runs[0].ID); +
import { sp } from "@pnp/sp"; + +const sitescriptContent = { + "$schema": "schema.json", + "actions": [ + { + "themeName": "Theme Name 123", + "verb": "applyTheme", + }, + ], + "bindata": {}, + "version": 1, +}; + +const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent); + +console.log(siteScript.Title); +
import { sp } from "@pnp/sp"; + +// Retrieving all site scripts +const allSiteScripts = await sp.siteScripts.getSiteScripts(); +console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : ""); + +// Retrieving a single site script by Id +const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a"); +console.log(siteScript.Title); +
import { sp } from "@pnp/sp"; + +// Update +const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" }); +console.log(updatedSiteScript.Title); + +// Delete +await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a"); +
import { sp } from "@pnp/sp"; + +// Using the absolute URL of the list +const ss = await sp.siteScripts.getSiteScriptFromList("https://TENANT.sharepoint.com/Lists/mylist"); + +// Using the PnPjs web object to fetch the site script from a specific list +const ss2 = await sp.web.lists.getByTitle("mylist").getSiteScript(); +
import { sp } from "@pnp/sp"; + +const extractInfo = { + IncludeBranding: true, + IncludeLinksToExportedItems: true, + IncludeRegionalSettings: true, + IncludeSiteExternalSharingCapability: true, + IncludeTheme: true, + IncludedLists: ["Lists/MyList"] +}; + +const ss = await sp.siteScripts.getSiteScriptFromWeb("https://TENANT.sharepoint.com/sites/mysite", extractInfo); + +// Using the PnPjs web object to fetch the site script from a specific web +const ss2 = await sp.web.getSiteScript(extractInfo); +
Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.
+Using the library, you can get the context information of the current site collection
+import { sp } from "@pnp/sp"; + +sp.site.getContextInfo().then(d =>{ + console.log(d.FormDigestValue); +}); +
Using the library, you can get a list of the document libraries present in the a given web.
+Note: Works only in SharePoint online
+import { sp } from "@pnp/sp"; + +sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite").then((d:DocumentLibraryInformation[]) => { + // iterate over the array of doc lib +}); +
Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.
+sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e").then(w => { + + //we got all the data from the web as well + console.log(w.data); + + // we can chain + w.web.select("Title").get().then(w2 => { + // ... + }); +}); +
Using the library, you can get the site collection url by providing a page url
+import { sp } from "@pnp/sp"; + +sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx").then(d => { + console.log(d); +}); +
Added in 1.2.4
+Note: Works only in SharePoint online
+Join the current site collection to a hub site collection
+import { sp, Site } from "@pnp/sp"; + +var site = new Site("https://tenant.sharepoint.com/sites/HubSite/"); + +var hubSiteID = ""; + +site.select("ID").get().then(d => { + // get ID of the hub site collection + hubSiteID = d.Id; + + // associate the current site collection the hub site collection + sp.site.joinHubSite(hubSiteID).then(d => { + console.log(d); + }); + +}); +
Added in 1.2.4
+Note: Works only in SharePoint online
+import { sp } from "@pnp/sp"; + +sp.site.joinHubSite("00000000-0000-0000-0000-000000000000").then(d => { + console.log(d); +}); +
Added in 1.2.4
+Note: Works only in SharePoint online
+Registers the current site collection as a hub site collection
+import { sp } from "@pnp/sp"; + +sp.site.registerHubSite().then(d => { + console.log(d); +}); +
Added in 1.2.4
+Note: Works only in SharePoint online
+Un-Registers the current site collection as a hub site collection
+import { sp } from "@pnp/sp"; + +sp.site.unRegisterHubSite().then(d => { + console.log(d); +}); +
Added in 1.2.6
+Note: Works only in SharePoint online
+Creates a modern communication site.
+Property | +Type | +Required | +Description | +
---|---|---|---|
Title | +string | +yes | +The title of the site to create. | +
lcid | +number | +yes | +The default language to use for the site. | +
shareByEmailEnabled | +boolean | +yes | +If set to true, it will enable sharing files via Email. By default it is set to false | +
url | +string | +yes | +The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. | +
description | +string | +no | +The description of the communication site. | +
classification | +string | +no | +The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information | +
siteDesignId | +string | +no | +The Guid of the site design to be used. | +
+ | + | + | You can use the below default OOTB GUIDs: | +
+ | + | + | Topic: null | +
+ | + | + | Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 | +
+ | + | + | Blank: f6cc5403-0d63-442e-96c0-285923709ffc | +
+ | + | + | + |
hubSiteId | +string | +no | +The Guid of the already existing Hub site | +
owner | +string | +no | +Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com | +
import { sp } from "@pnp/sp"; + +const s = await sp.site.createCommunicationSite( + "Title", + 1033, + true, + "https://tenant.sharepoint.com/sites/commSite", + "Description", + "HBI", + "f6cc5403-0d63-442e-96c0-285923709ffc", + "a00ec589-ea9f-4dba-a34e-67e78d41e509", + "user@TENANT.onmicrosoft.com"); +
Added in 1.2.6
+Note: Works only in SharePoint online. It wont work with App only tokens
+Creates a modern team site backed by O365 group.
+Property | +Type | +Required | +Description | +
---|---|---|---|
displayName | +string | +yes | +The title/displayName of the site to be created. | +
alias | +string | +yes | +Alias of the underlying Office 365 Group. | +
isPublic | +boolean | +yes | +Defines whether the Office 365 Group will be public (default), or private. | +
lcid | +number | +yes | +The language to use for the site. If not specified will default to English (1033). | +
description | +string | +no | +The description of the modern team site. | +
classification | +string | +no | +The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information | +
owners | +string array (string[]) | +no | +The Owners of the site to be created | +
hubSiteId | +string | +no | +The Guid of the already existing Hub site | +
import { sp } from "@pnp/sp"; + +sp.site.createModernTeamSite( + "displayName", + "alias", + true, + 1033, + "description", + "HBI", + ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"], + "a00ec589-ea9f-4dba-a34e-67e78d41e509") + .then(d => { + console.log(d); + }); +
import { sp } from "@pnp/sp"; + +// Delete the current site +await sp.site.delete(); + +// Specify which site to delete +const siteUrl = "https://tenant.sharepoint.com/sites/tstpnpsitecoldelete5"; +const site2 = new Site(siteUrl); +await site2.delete(); +
The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions.
+Gets a URI to a site that lists the current user's followed sites.
+import { sp } from "@pnp/sp"; + +const uri = await sp.social.getFollowedSitesUri(); +
Gets a URI to a site that lists the current user's followed documents.
+import { sp } from "@pnp/sp"; + +const uri = await sp.social.getFollowedDocumentsUri(); +
Makes the current user start following a user, document, site, or tag
+import { sp, SocialActorType } from "@pnp/sp"; + +// follow a site +const r1 = await sp.social.follow({ + ActorType: SocialActorType.Site, + ContentUri: "htts://tenant.sharepoint.com/sites/site", +}); + +// follow a person +const r2 = await sp.social.follow({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); + +// follow a doc +const r3 = await sp.social.follow({ + ActorType: SocialActorType.Document, + ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx", +}); + +// follow a tag +// You need the tag GUID to start following a tag. +// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. +// See How to get a tag's GUID based on the tag's name by using the JavaScript object model. +// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid +const r4 = await sp.social.follow({ + ActorType: SocialActorType.Tag, + TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928", +}); +
Indicates whether the current user is following a specified user, document, site, or tag
+import { sp, SocialActorType } from "@pnp/sp"; + +// pass the same social actor struct as shown in follow example for each type +const r = await sp.social.isFollowed({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); +
Makes the current user stop following a user, document, site, or tag
+import { sp, SocialActorType } from "@pnp/sp"; + +// pass the same social actor struct as shown in follow example for each type +const r = await sp.social.stopFollowing({ + AccountName: "i:0#.f|membership|person@tenant.com", + ActorType: SocialActorType.User, +}); +
Gets this user's social information
+import { sp } from "@pnp/sp"; + +const r = await sp.social.my.get(); +
Gets users, documents, sites, and tags that the current user is following based on the supplied flags.
+import { sp, SocialActorTypes } from "@pnp/sp"; + +// get all the followed documents +const r1 = await sp.social.my.followed(SocialActorTypes.Document); + +// get all the followed documents and sites +const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); + +// get all the followed sites updated in the last 24 hours +const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours); +
Works as followed but returns on the count of actors specifed by the query
+import { sp, SocialActorTypes } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.followedCount(SocialActorTypes.Document); +
Gets the users who are following the current user.
+import { sp } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.followers(); +
Gets users who the current user might want to follow.
+import { sp } from "@pnp/sp"; + +// get the followed documents count +const r = await sp.social.my.suggestions(); +
Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.
+This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).
+export interface EmailProperties { + + To: string[]; + CC?: string[]; + BCC?: string[]; + Subject: string; + Body: string; + AdditionalHeaders?: TypedHash<string>; + From?: string; +} +
You must define the To, Subject, and Body values - the remaining are optional.
+import { sp, EmailProperties } from "@pnp/sp"; + +const emailProps: EmailProperties = { + To: ["user@site.com"], + CC: ["user2@site.com", "user3@site.com"], + Subject: "This email is about...", + Body: "Here is the body. <b>It supports html</b>", +}; + +sp.utility.sendEmail(emailProps).then(_ => { + + console.log("Email Sent!"); +}); +
This method returns the current user's email addresses known to SharePoint.
+import { sp } from "@pnp/sp"; + +sp.utility.getCurrentUserEmailAddresses().then((addressString: string) => { + + console.log(addressString); +}); +
Gets information about a principal that matches the specified Search criteria
+import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp"; + +sp.utility.resolvePrincipal("user@site.com", + PrincipalType.User, + PrincipalSource.All, + true, + false).then((principal: PrincipalInfo) => { + + + console.log(principal); + }); +
Gets information about the principals that match the specified Search criteria.
+import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp"; + +sp.utility.searchPrincipals("john", + PrincipalType.User, + PrincipalSource.All, + "", + 10).then((principals: PrincipalInfo[]) => { + + console.log(principals); + }); +
Gets the external (outside the firewall) URL to a document or resource in a site.
+import { sp } from "@pnp/sp"; + +sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx").then((r: string) => { + + console.log(r); +}); +
Resolves the principals contained within the supplied groups
+import { sp , PrincipalInfo } from "@pnp/sp"; + +sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]).then((principals: PrincipalInfo[]) => { + + console.log(principals); +}); + +// optionally supply a max results count. Default is 30. +sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10).then((principals: PrincipalInfo[]) => { + + console.log(principals); +}); +
import { sp , CreateWikiPageResult } from "@pnp/sp"; + +sp.utility.createWikiPage({ + ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx", + WikiHtmlContent: "This is my <b>page</b> content. It supports rich html.", +}).then((result: CreateWikiPageResult) => { + + // result contains the raw data returned by the service + console.log(result.data); + + // result contains a File instance you can use to further update the new page + result.file.get().then(f => { + + console.log(f); + }); +}); +
Checks if file or folder name contains invalid characters
+import { sp } from "@pnp/sp"; + +const isInvalid = sp.utility.containsInvalidFileFolderChars("Filename?.txt"); +console.log(isInvalid); // true +
Removes invalid characters from file or folder name
+import { sp } from "@pnp/sp"; + +const validName = sp.utility.stripInvalidFileFolderChars("Filename?.txt"); +console.log(validName); // Filename.txt +
Even if a method does not have an explicit implementation on the utility api you can still call it using the UtilityMethod class. In this example we will show calling the GetLowerCaseString method, but the technique works for any of the utility methods.
+import { UtilityMethod } from "@pnp/sp"; + +// the first parameter is the web url. You can use an empty string for the current web, +// or specify it to call other web's. The second parameter is the method name. +const method = new UtilityMethod("", "GetLowerCaseString"); + +// you must supply the correctly formatted parameters to the execute method which +// is generic and types the result as the supplied generic type parameter. +method.excute<string>({ + sourceValue: "HeRe IS my StrINg", + lcid: 1033, +}).then((s: string) => { + + console.log(s); +}); +
You can set, read, and remove tenant properties using the methods shown below:
+This method MUST be called in the context of the app catalog web or you will get an access denied message.
+import { Web } from "@pnp/sp"; + +const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/"); + +// specify required key and value +await w.setStorageEntity("Test1", "Value 1"); + +// specify optional description and comments +await w.setStorageEntity("Test2", "Value 2", "description", "comments"); +
This method can be used from any web to retrieve values previsouly set.
+import { sp, StorageEntity } from "@pnp/sp"; + +const prop: StorageEntity = await sp.web.getStorageEntity("Test1"); + +console.log(prop.Value); +
This method MUST be called in the context of the app catalog web or you will get an access denied message.
+import { Web } from "@pnp/sp"; + +const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/"); + +await w.removeStorageEntity("Test1"); +
Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.
+To get a views properties you need to know it's id or title. You can use the standard OData operators as expected to select properties. For a list of the properties, please see this article.
+import { sp } from "@pnp/sp"; +// know a view's GUID id +sp.web.lists.getByTitle("Documents").getView("2B382C69-DF64-49C4-85F1-70FB9CECACFE").select("Title").get().then(v => { + + console.log(v); +}); + +// get by the display title of the view +sp.web.lists.getByTitle("Documents").views.getByTitle("All Documents").select("Title").get().then(v => { + + console.log(v); +}); +
To add a view you use the add method of the views collection. You must supply a title and can supply other parameters as well.
+import { sp, ViewAddResult } from "@pnp/sp"; +// create a new view with default fields and properties +sp.web.lists.getByTitle("Documents").views.add("My New View").then(v => { + + console.log(v); +}); + +// create a new view with specific properties +sp.web.lists.getByTitle("Documents").views.add("My New View 2", false, { + + RowLimit: 10, + ViewQuery: "<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>", +}).then((v: ViewAddResult) => { + + // manipulate the view's fields + v.view.fields.removeAll().then(_ => { + + Promise.all([ + v.view.fields.add("Title"), + v.view.fields.add("Modified"), + ]).then(_ =>{ + + console.log("View created"); + }); + }); +}); +
import { sp, ViewUpdateResult } from "@pnp/sp"; + +sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").update({ + RowLimit: 20, +}).then((v: ViewUpdateResult) => { + + console.log(v); +}); +
Added in 1.2.6
+import { sp } from "@pnp/sp"; + +const viewXml: string = "..."; + +await sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").setViewXml(viewXml); +
import { sp } from "@pnp/sp"; + +sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").delete().then(_ => { + + console.log("View deleted"); +}); +
Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.
+Using the library you can add a web to another web's collection of subwebs. The basic usage requires only a title and url. This will result in a team site with all of the default settings.
+import { sp, WebAddResult } from "@pnp/sp"; + +sp.web.webs.add("title", "subweb1").then((w: WebAddResult) => { + + // show the response from the server when adding the web + console.log(w.data); + + w.web.select("Title").get().then(w => { + + // show our title + console.log(w.Title); + }); +}); +
You can also provide other settings such as description, template, language, and inherit permissions.
+import { sp, WebAddResult } from "@pnp/sp"; + +// create a German language wiki site with title, url, description, which inherits permissions +sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, true).then((w: WebAddResult) => { + + // show the response from the server when adding the web + console.log(w.data); + + w.web.select("Title").get().then(w => { + + // show our title + console.log(w.Title); + }); +}); +
If you create a web that doesn't inherit permissions from the parent web, you can create its default associated groups (Members, Owners, Visitors) with the default role assigments (Contribute, Full Control, Read)
+import { sp, WebAddResult } from "@pnp/sp"; + +sp.web.webs.add("title", "subweb1", "a wiki web", "WIKI#0", 1031, false).then((w: WebAddResult) => { + + w.web.createDefaultAssociatedGroups().then(() => { + + // ... + }); +}); +
import { sp } from "@pnp/sp"; + +// basic get of the webs properties +sp.web.get().then(w => { + + console.log(w.Title); +}); + +// use odata operators to get specific fields +sp.web.select("Title").get().then(w => { + + console.log(w.Title); +}); + +// use with get to give the result a type +sp.web.select("Title").get<{ Title: string }>().then(w => { + + console.log(w.Title); +}); +
Some properties, such as AllProperties, are not returned by default. You can still access them using the expand operator.
+import { sp } from "@pnp/sp"; + +sp.web.select("AllProperties").expand("AllProperties").get().then(w => { + + console.log(w.AllProperties); +}); +
You can also use the Web object directly to get any web, though of course the current user must have the necessary permissions. This is done by importing the web object.
+import { Web } from "@pnp/sp"; + +let web = new Web("https://my-tenant.sharepoint.com/sites/mysite"); + +web.get().then(w => { + + console.log(w); +}); +
Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.
+sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e").then(w => { + + //we got all the data from the web as well + console.log(w.data); + + // we can chain + w.web.select("Title").get().then(w2 => { + // ... + }); +}); +
You can update web properties using the update method. The properties available for update are listed in this table. Updating is a simple as passing a plain object with the properties you want to update.
+import { Web } from "@pnp/sp"; + +let web = new Web("https://my-tenant.sharepoint.com/sites/mysite"); + +web.update({ + Title: "New Title", + CustomMasterUrl: "{path to masterpage}", + Description: "My new description", +}).then(w => { + + console.log(w); +}); +
import { Web } from "@pnp/sp"; + +let web = new Web("https://my-tenant.sharepoint.com/sites/mysite"); + +web.delete().then(w => { + + console.log(w); +}); +
Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. Also we have had reports that after version 2.0.9 of hte library this workaround no longer works.
+When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article.
+npm i
+npm i -g rimraf # used to remove the node_modules folder (much better/faster)
+
+npm i @pnp/sp
rimraf node_modules
folder and execute npm install
"typescript"
or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016)Search for the next "typescript"
occurrence and replace the block with:
JSON
+"typescript": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
+ "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
+ "dev": true
+}
Remove node_modules folder rimraf node_modules
npm install
npm-force-resolutions
¶Install resolutions package and TypeScript providing considered version explicitly:
+bash
+npm i -D npm-force-resolutions typescript@3.6.4
Add a resolution for TypeScript and preinstall script into package.json
to a corresponding code blocks:
JSON
+{
+ "scripts": {
+ "preinstall": "npx npm-force-resolutions"
+ },
+ "resolutions": {
+ "typescript": "3.6.4"
+ }
+}
Run npm install
to trigger preinstall script and bumping TypeScript version into package-lock.json
npm run build
, should produce no errorsInstalling additional dependencies should be safe then.
+ + + + + + + +\n {translate(\"search.result.term.missing\")}: {...missing}\n
\n }\n