diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml index efb46c8ef..b3a2e8fec 100644 --- a/.github/ISSUE_TEMPLATE/1-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -1,6 +1,6 @@ name: 🐞 Bug or Error Report description: Submit a bug or error report. -labels: ["type: someting isn't working", "status: investigate"] +labels: ["type: something isn't working", "status: investigate"] body: - type: markdown @@ -16,10 +16,10 @@ body: label: Major Version options: - 4.x - - 3.x + - 3.x (No longer supported) - 2.x (No longer supported) - 1.x (No longer supported) - default: 1 + default: 0 validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/1-question.yml b/.github/ISSUE_TEMPLATE/1-question.yml index 433793d24..d474ee3b3 100644 --- a/.github/ISSUE_TEMPLATE/1-question.yml +++ b/.github/ISSUE_TEMPLATE/1-question.yml @@ -16,10 +16,10 @@ body: label: What version of PnPjs library you are using options: - 4.x - - 3.x + - 3.x (No longer supported) - 2.x (No longer supported) - 1.x (No longer supported) - default: 1 + default: 0 validations: required: true - type: input diff --git a/CHANGELOG.md b/CHANGELOG.md index db5f57115..a2e73812c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 4.6.0 - 2024-Oct-14 + +- Only documentation and package updates + ## 4.5.0 - 2024-Sept-16 - Only documentation and package updates diff --git a/docs/msaljsclient/index.md b/docs/msaljsclient/index.md index c4a510fdd..93ac551b8 100644 --- a/docs/msaljsclient/index.md +++ b/docs/msaljsclient/index.md @@ -22,7 +22,7 @@ import "@pnp/sp/site-users/web"; const options: MSALOptions = { configuration: { auth: { - authority: "https://login.microsoftonline.com/{tanent_id}/", + authority: "https://login.microsoftonline.com/{tenant_id}/", clientId: "{client id}", }, cache: { diff --git a/docs/sp/folders.md b/docs/sp/folders.md index a56987d49..f1f3c116d 100644 --- a/docs/sp/folders.md +++ b/docs/sp/folders.md @@ -532,7 +532,7 @@ const folder: IFolder = await sp.web.rootFolder.folders.getByUrl("SiteAssets").a ### getFolderById -You can get a folder by Id from a web. +You can get a folder by UniqueId from a web. ```TypeScript import { spfi } from "@pnp/sp"; @@ -542,7 +542,8 @@ import { IFolder } from "@pnp/sp/folders"; const sp = spfi(...); -const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0"); +const folderItem = sp.web.lists.getByTitle("My List").items.getById(1).select("UniqueId")() +const folder: IFolder = sp.web.getFolderById(folderItem.UniqueId); ``` ### getParentInfos diff --git a/docs/sp/items.md b/docs/sp/items.md index bb2b08cfc..b0784e5da 100644 --- a/docs/sp/items.md +++ b/docs/sp/items.md @@ -367,11 +367,13 @@ await execute(); console.log("Done"); ``` + ### Update Taxonomy field Note: Updating Taxonomy field for a File item should be handled differently. Instead of using update(), use validateUpdateListItem(). Please see below -List Item +#### List Item + ```TypeScript import { spfi } from "@pnp/sp"; import "@pnp/sp/webs"; @@ -385,7 +387,9 @@ await sp.web.lists.getByTitle("Demo").items.getById(1).update({ }); ``` -File List Item + +#### File List Item + ```TypeScript import { spfi } from "@pnp/sp"; import "@pnp/sp/webs"; @@ -407,6 +411,8 @@ _Based on [this excellent article](https://www.aerieconsulting.com/blog/update-u 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". +#### List Item + ```TypeScript import { spfi } from "@pnp/sp"; import "@pnp/sp/webs"; @@ -433,7 +439,9 @@ await sp.web.lists.getByTitle("TestList").items.getById(newItem.Id).update(updat ``` #### File List Item + To update a multi-value taxonomy field on a file item, a different serialization is needed. + ```TypeScript import { spfi } from "@pnp/sp"; import "@pnp/sp/webs"; @@ -482,6 +490,30 @@ const update = await sp.web.lists.getByTitle("Price").items.getById(7).select('* ]); ``` +### Update Location Field + +This code shows how to update a location field's coordinates. + +```TypeScript +import { spfi } from "@pnp/sp"; +import "@pnp/sp/webs"; +import "@pnp/sp/lists"; +import "@pnp/sp/items"; + +const sp = spfi(...); +const coordinates = { + Latitude: 47.672082, + Longitude: -122.1409983 +} + +const projectId = 1; +const project = sp.web.lists.getByTitle("My List").items.getById(projectId).select("Id, ProjectLocation")() +const projectLocation = JSON.parse(project.ProjectLocation); +projectLocation.Coordinates = coordinates; +const ProjectLocation = JSON.stringify(projectLocation); +const update = await sp.web.lists.getByTitle("My List").items.getById(projectId).update({ ProjectLocation }); +``` + ## Recycle To send an item to the recycle bin use recycle. diff --git a/docs/v1/documentation/css/extra.css b/docs/v1/documentation/css/extra.css index 6391b1ff7..4fe95a2c7 100644 --- a/docs/v1/documentation/css/extra.css +++ b/docs/v1/documentation/css/extra.css @@ -4,15 +4,15 @@ padding: 0 0.25 0.5 !important; } -.md-header{ +.md-header { height: 75px; } -.md-container{ +.md-container { padding-top: 70px; } -.md-sidebar[data-md-state="lock"]{ +.md-sidebar[data-md-state="lock"] { padding-top: 75px; } @@ -27,7 +27,38 @@ } @media only screen and (max-width: 76.1875em) { - .md-nav--primary .md-nav__title--site .md-nav__button { - width: 150px; - } + .md-nav--primary .md-nav__title--site .md-nav__button { + width: 150px; + } +} + +body { + min-height: 100vh; + min-width: 100vw; + margin: 0; + background-color: light-dark(whitesmoke, black); + + &:before { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: "V1 \A Deprecated"; + display: block; + box-sizing: border-box; + padding: 20vh; + font-size: clamp(3rem, 20vw, 8rem); + height: 100vh; + width: 100vw; + text-align: center; + z-index: -1; + color: silver; + font-weight: 900; + font-family: sans-serif; + text-transform: uppercase; + transform: rotate(-45deg); + transform-origin: center; + white-space: pre-wrap; + } } \ No newline at end of file diff --git a/docs/v2/css/extra.css b/docs/v2/css/extra.css index 6391b1ff7..ab9c76f93 100644 --- a/docs/v2/css/extra.css +++ b/docs/v2/css/extra.css @@ -30,4 +30,35 @@ .md-nav--primary .md-nav__title--site .md-nav__button { width: 150px; } +} + +body { + min-height: 100vh; + min-width: 100vw; + margin: 0; + background-color: light-dark(whitesmoke, black); + + &:before { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: "V2 \A Deprecated"; + display: block; + box-sizing: border-box; + padding: 20vh; + font-size: clamp(1rem, 10vw, 5rem); + height: 100vh; + width: 100vw; + text-align: center; + z-index: -1; + color: silver; + font-weight: 900; + font-family: sans-serif; + text-transform: uppercase; + transform: rotate(-45deg); + transform-origin: center; + white-space: pre-wrap; + } } \ No newline at end of file diff --git a/docs/v3/css/extra.css b/docs/v3/css/extra.css index 6391b1ff7..ad63ece2e 100644 --- a/docs/v3/css/extra.css +++ b/docs/v3/css/extra.css @@ -30,4 +30,36 @@ .md-nav--primary .md-nav__title--site .md-nav__button { width: 150px; } +} + +body { + min-height: 100vh; + min-width: 100vw; + margin: 0; + background-color: light-dark(whitesmoke, black); + + &:before { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: "V3 \A Deprecated"; + + display: block; + box-sizing: border-box; + padding: 20vh; + font-size: clamp(1rem, 10vw, 5rem); + height: 100vh; + width: 100vw; + text-align: center; + z-index: -1; + color: silver; + font-weight: 900; + font-family: sans-serif; + text-transform: uppercase; + transform: rotate(-45deg); + transform-origin: center; + white-space: pre-wrap; + } } \ No newline at end of file diff --git a/docs/v3/v1/404.html b/docs/v3/v1/404.html deleted file mode 100644 index ffa3a384e..000000000 --- a/docs/v3/v1/404.html +++ /dev/null @@ -1,1611 +0,0 @@ - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -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