Extensions¶
introduced in 2.0.0
Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable. You can control any behavior of the library with extensions.
Extensions do not work in ie11 compatibility mode. This is by design.
Types of Extensions¶
There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.
Function Extensions¶
The first type is a simple function with a signature:
(op: "apply" | "get" | "has" | "set", target: T, ...rest: any[]): void
This function is passed the current operation as the first argument, currently one of "apply", "get", "has", or "set". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.
Named Extensions¶
Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.
import { extendFactory } from "@pnp/queryable";
import { sp, List, Lists, IWeb, ILists, List, IList, Web } from "@pnp/sp/presets/all";
import { escapeQueryStrValue } from "@pnp/sp/utils/escapeQueryStrValue";
// create a plain object with the props and methods we want to add/change
const myExtensions = {
// override the lists property
get lists(this: IWeb): ILists {
// we will always order our lists by title and select just the Title for ALL calls (just as an example)
return Lists(this).orderBy("Title").select("Title");
},
// override the getByTitle method
getByTitle: function (this: ILists, title: string): IList {
// in our example our list has moved, so we rewrite the request on the fly
if (title === "List1") {
return List(this, `getByTitle('List2')`);
} else {
// you can't at this point call the "base" method as you will end up in loop within the proxy
// so you need to ensure you patch/include any original functionality you need
return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
}
},
};
// register all the named Extensions
extendFactory(Web, myExtensions);
// this will use our extension to ensure the lists are ordered
const lists = await sp.web.lists();
console.log(JSON.stringify(lists, null, 2));
// we will get the items from List1 but within the extension it is rewritten as List2
const items = await sp.web.lists.getByTitle("List1").items();
console.log(JSON.stringify(items.length, null, 2));
ProxyHandler Extensions¶
You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.
import { extendFactory } from "@pnp/queryable";
import { sp, Lists, IWeb, ILists, Web } from "@pnp/sp/presets/all";
import { escapeQueryStrValue } from "@pnp/sp/utils/escapeSingleQuote";
const myExtensions = {
get: (target, p: string | number | symbol, _receiver: any) => {
switch (p) {
case "getByTitle":
return (title: string) => {
// in our example our list has moved, so we rewrite the request on the fly
if (title === "LookupList") {
return List(target, `getByTitle('OrderByList')`);
} else {
// you can't at this point call the "base" method as you will end up in loop within the proxy
// so you need to ensure you patch/include any original functionality you need
return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);
}
};
}
},
};
extendFactory(Web, myExtensions);
const lists = sp.web.lists;
const items = await lists.getByTitle("LookupList").items();
console.log(JSON.stringify(items.length, null, 2));
Registering Extensions¶
You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions.
Global Registration¶
Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly.
import { extendGlobal } from "@pnp/queryable";
// we can add a logging method to very verbosely track what things are called in our application
extendGlobal((op: string, _target: any, ...rest: any[]): void => {
switch (op) {
case "apply":
Logger.write(`${op} ::> ()`, LogLevel.Info);
break;
case "has":
case "get":
case "set":
Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info);
break;
default:
Logger.write(`unknown ${op}`, LogLevel.Info);
}
});
Factory Registration¶
The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.
import "@pnp/sp/webs";
import "@pnp/sp/lists/web";
import { IWeb, Web } from "@pnp/sp/webs";
import { ILists, Lists } from "@pnp/sp/lists";
import { extendFactory } from "@pnp/queryable";
import { sp } from "@pnp/sp";
// sets up the types correctly when importing across your application
declare module "@pnp/sp/webs/types" {
// we need to extend the interface
interface IWeb {
orderedLists: ILists;
}
}
// sets up the types correctly when importing across your application
declare module "@pnp/sp/lists/types" {
// we need to extend the interface
interface ILists {
getOrderedListsQuery: (this: ILists) => ILists;
}
}
extendFactory(Web, {
// add an ordered lists property
get orderedLists(this: IWeb): ILists {
return this.lists.getOrderedListsQuery();
},
});
extendFactory(Lists, {
// add an ordered lists property
getOrderedListsQuery(this: ILists): ILists {
return this.top(10).orderBy("Title").select("Title");
},
});
// regardless of how we access the web and lists collections our extensions remain with all new instance based on
const web = Web("https://tenant.sharepoint.com/sites/dev/");
const lists1 = await web.orderedLists();
console.log(JSON.stringify(lists1, null, 2));
const lists2 = await Web("https://tenant.sharepoint.com/sites/dev/").orderedLists();
console.log(JSON.stringify(lists2, null, 2));
const lists3 = await sp.web.orderedLists();
console.log(JSON.stringify(lists3, null, 2));
Instance Registration¶
You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.
Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.
Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.
import { extendObj } from "@pnp/queryable";
import { sp, List, ILists } from "@pnp/sp/presets/all";
const myExtensions = {
getByTitle: function (this: ILists, title: string) {
// in our example our list has moved, so we rewrite the request on the fly
if (title === "List1") {
return List(this, "getByTitle('List2')");
} else {
// you can't at this point call the "base" method as you will end up in loop within the proxy
// so you need to ensure you patch/include any original functionality you need
return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
}
},
};
const lists = extendObj(sp.web.lists, myExtensions);
const items = await lists.getByTitle("LookupList").items();
console.log(JSON.stringify(items.length, null, 2));
Enable & Disable Extensions and Clear Global Extensions¶
Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.
import { enableExtensions, disableExtensions, clearGlobalExtensions } from "@pnp/queryable";
// disable Extensions
disableExtensions();
// enable Extensions
enableExtensions();
// clear all the globally registered extensions
clearGlobalExtensions();
Order of Operations¶
It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined
that value is returned and no other extensions are processed.
*extensions applied via an extended factory are considered instance extensions