Implementing Repository Pattern with SharePoint Framework Library Component
SharePoint framework 1.9.1 is out and Library Components are now generally available, Library component gives you ability to share code between your components.
In this post I’m going to implement a basic version of repository pattern that allows you to do CRUD operations on SharePoint lists and libraries as well as tenant properties (global properties that can be shared between your components such as web parts, extensions and libraries).
If you don’t want to read the post, the source code is available here.
Upgrade to SPFx 1.9.1 Version
Before we go further, make sure you upgrade your packages to SPFx 1.9.1, for more details please click here.
Create Library Component Type
Open PowerShell or command prompt and run Yeoman SharePoint Generator to create the solution.
yo @microsoft/sharepoint
Solution Name: ts-repository-libaray
Target for component: SharePoint Online only (you can select other options based on your needs)
Place of files: We may choose to use the same folder or create a subfolder for our solution.
Deployment option: Y
Permissions to access web APIs: N (as our solution doesn’t require permissions to access web APIs)
Type of client-side component to create: Library
Creating Code Files
Open the solution with Visual Studio Code or any other editors, under repository folder create below structure:

Interfaces
We are going to create these interfaces under core folder:
IRead: defines the contacts for reading entities.
import IQueryOption from "./IQueryOption"; export interface IRead<T> { getAll(): Promise<T[]>; getOne(id: number | string,options?:IQueryOption): Promise<T>; }
IWrite: contracts for add,update or delete entities.
export interface IWrite<T> { add(item: T): Promise<any>; update(item: T): Promise<any>; delete(id: number | string): Promise<void>; }
IQuery: queries we are going to implement to get data.
import { CamlQuery } from "@pnp/sp/src/types"; import IQueryOption from "./IQueryOption"; export default interface IQuery<T>{ getItemsByCAMLQuery:(query: CamlQuery, ...expands: string[])=> Promise<T[]>; getItemsByQuery:(queryOptions: IQueryOption)=>Promise<T[]>; }
IQueryOption: provides options to filter, expand, select, number of items to return etc.
export default interface IQueryOption{ select?: string[]; filter?:string; expand?:string[]; top?:number; skip?:number; }
IListItem: describe the basic properties of SharePoint list items.
export default interface IListItem{ Id:number; }
ITenantProperty: tenant property keys.
export interface ITenantProperty { key: string; Comment?: string; Description?: string; Value: string; }
SharePoint repository
Under SharePoint folder we have two files:
ISharePointBaseRepository: this interface implements IRead, IWrite and IQuery, it’s always recommended to seperate the responsibilities, if you are not familiar with SOLID principles I suggest you this post.
// import all interfaces import { IWrite } from '../core/IWrite'; import { IRead } from '../core/IRead'; import IListItem from '../core/IListItem'; import IQuery from '../core/IQuery'; // that class only can be extended export interface ISharePointBaseRepository<T extends IListItem> extends IWrite<T>, IRead<T>,IQuery<T> { }
SharePointBaseRepository: concrete class that implements the ISharePointBaseRepository interface.
import { sp, ItemAddResult, ItemUpdateResult, List, SPRest, Web } from "@pnp/sp"; import { ISharePointBaseRepository } from "./ISharePointBaseRepository"; import IListItem from "../core/IListItem"; import IQueryOption from "../core/IQueryOption"; export default class SharePointRepository<T extends IListItem> implements ISharePointBaseRepository<T>{ protected _list: List; protected _web: Web; protected _sp: SPRest; constructor(listId: string, webUrl?: string) { this._web = webUrl ? new Web(webUrl) : this._web = sp.web; this._list = this._web.lists.getById(listId); this._sp = sp; } // Add new entity to collection public async add(item: Omit<T, "Id">): Promise<ItemAddResult> { return this._list.items.add(item); } // Update an existing entity public async update(item: T): Promise<ItemUpdateResult> { const updatingItem: Omit<T, "Id"> = item; return this._list.items.getById(item.Id).update(updatingItem); } // Remove an entity public async delete(id: number): Promise<void> { return this._list.items.getById(id).delete(); } // Get all items public async getAll(): Promise<T[]> { try { const items = await this._list.items.getAll(); return items; } catch (error) { return Promise.reject(error.message); } } // Get one by Id, optional query options public async getOne(id: number, queryOptions?: Omit<IQueryOption, "top" | "filter">): Promise<T> { let result = this._list.items.getById(id); if (queryOptions) { if (queryOptions.expand) result = result.expand(...queryOptions.expand); if (queryOptions.select) result = result.select(...queryOptions.select); } try { const item = await result.get(); return item; } catch (error) { return Promise.reject(error.message); } } // Get items using CAML query public async getItemsByCAMLQuery(query: import("@pnp/sp").CamlQuery, ...expands: string[]): Promise<T[]> { return this._list.getItemsByCAMLQuery(query, ...expands); } // Get items using query options public async getItemsByQuery(queryOptions: IQueryOption): Promise<T[]> { const { filter, select, expand, top, skip } = queryOptions; let result = this._list.items; if (filter) result = result.filter(filter); if (select) result = result.select(...select); if (expand) result = result.expand(...expand); if (top) result = result.top(top); if (skip) result = result.skip(skip); return result.get(); } }
- Omit helper type added to TypeScript 3.5, click here for more information.
- There are 3 protected members in this class that help you extend it without changing the class! (Open/closed principle)
Tenant repository
Like SharePoint repository, we have two files unders tenant folder.
ITenantBaseRepository: contracts for reading and writing of tenant properties.
// import all interfaces import { IWrite } from '../core/IWrite'; import { IRead } from '../core/IRead'; // that class only can be extended export interface ITenantBaseRepository<T> extends IWrite<T>, IRead<T> {}
TenantRepository: concerete class implements ITenantBaseRepository.
import { sp, SPRest, Web, StorageEntity } from "@pnp/sp"; import { ITenantBaseRepository } from "./ITenantBaseRepository"; import { WebPartContext } from "@microsoft/sp-webpart-base"; import { ExtensionContext } from '@microsoft/sp-extension-base'; import { ITenantProperty } from "../core/ITenantProperty"; import { SPHttpClientResponse, SPHttpClient } from "@microsoft/sp-http"; export default class TenantBaseRepository<T extends StorageEntity & ITenantProperty> implements ITenantBaseRepository<T>{ protected _sp: SPRest; protected _context: WebPartContext | ExtensionContext; private appCatalogUrl:string; // Constructor constructor(context: WebPartContext | ExtensionContext) { // Setuo Context to PnPjs sp.setup({ spfxContext: context }); this._sp = sp; this._context = context; } // Add tenant property public async add(property: ITenantProperty): Promise<void> { const appCatalogWeb: Web= await this.getAppCatalogWeb(); return appCatalogWeb.setStorageEntity(property.key, property.Value, property.Description, property.Comment); } // Update tenant property public async update(newProperty: ITenantProperty): Promise<void>{ return this.add(newProperty); } // Remove tenant property public async delete(key:string): Promise<void> { const appCatalogWeb: Web= await this.getAppCatalogWeb(); return appCatalogWeb.removeStorageEntity(key); } // Get all properties public async getAll(): Promise<T[]> { try { const catalogUrl =await this.getAppCatalogUrl(); const apiUrl = `${catalogUrl}/_api/web/AllProperties?$select=storageentitiesindex`; const data: SPHttpClientResponse = await this._context.spHttpClient.get(apiUrl, SPHttpClient.configurations.v1); if (data.ok) { const results = await data.json(); if (results && results.storageentitiesindex) { const parsedData:{ [key: string]: ITenantProperty } = JSON.parse(results.storageentitiesindex); const keys: string[] = Object.keys(parsedData); let properties : ITenantProperty[] = []; keys.map((key: string): any => { const property: ITenantProperty = parsedData[key]; properties.push( { key, Value: property.Value, Description: property.Description, Comment: property.Comment } ); }); return properties as T[]; } } return null; } catch (error) { return Promise.reject(error.message); } } // Get tenant property public async getOne(key:string): Promise<T> { const appCatalogWeb: Web= await this.getAppCatalogWeb(); try{ const property = await appCatalogWeb.getStorageEntity(key); return {...property,key} as T; } catch(error){ return Promise.reject(error.message); } } // Get App Catalog private async getAppCatalogUrl() { if(this.appCatalogUrl) return this.appCatalogUrl; try { const appCatalog: Web = await this._sp.getTenantAppCatalogWeb(); const appCatalogWeb = await appCatalog.get(); this.appCatalogUrl=appCatalogWeb.Url; return appCatalogWeb.Url; } catch (error) { console.dir(error); return Promise.reject(error.message); } } // Get App Catalog web private async getAppCatalogWeb():Promise<Web>{ this.appCatalogUrl = await this.getAppCatalogUrl(); return new Web(this.appCatalogUrl); } }
Now we need to add above concerete classes to the index file so they can be accessible from our components. Open index.ts file and update it to:
export { RepositoryLibrary } from './libraries/repository/RepositoryLibrary'; export {default as TenantRepository} from "./libraries/repository/repositories/tenant/TenantRepository"; export {default as SharePointRepository} from "./libraries/repository/repositories/sharepoint/SharePointRepository";
Run gulp build to make sure you don’t have any errors, then run npm link to create a local npm link to the library with the name which is provided in the package.json
.
Consume the library for local testing
- Create a web part or an extension solution in a separate folder.
- Run the command
npm link ts-repository-library
. - Add an import to refer to the library:
import * as Repository from "ts-repository-library";
Now the repositories available to be consumed:
interface ISampleItem { Id: number; Title: string; } const sampleListId = this.props.listId; const sampleList = new Repository.SharePointRepository<ISampleItem>(sampleListId); const tenantRepository = new Repository.TenantRepository(this.props.spfxContext);
Try some of methods by calling them:
// get all items in the list const items = await this.sampleList.getAll(); // get item by id const item = await this.sampleList.getOne(3); // add new item await this.sampleList.add({ Title: "New Item" }); // update an Item await this.sampleList.update({ Id: 3, Title: "Updated value" }); // remove an item await this.sampleList.delete(3); // get all tenant properties const properties = await this.tenantRepository.getAll(); // get Property by key const property = await this.tenantRepository.getOne("New Tenant Property"); // add new Property await this.tenantRepository.add({ key:"New Tenant Property", Value:"New Tenant Value" }); // update a Property await this.tenantRepository.update({ key:"New Tenant Property", Value:"Value has been updated" }); // remove a Property await this.tenantRepository.delete("New Tenant Property");

Deployment
To deploy the library to app catalog run the following commands:
gulp bundle --ship gulp package-solution --ship
Then deploy the spkg file to your tenant app catalog and make it available to all site collection.
To deploy the web part or extension that is consuming the library, you need to update the package.json and add your library component as a dependency:
"dependencies": { "ts-repository-library": "0.0.1", // here we added the reference to the library "@microsoft/sp-core-library": "1.9.1", ... },
Now you can bundle and package the solution and deploy it to your tenant app catalog.
Conclusion
Library Component is the new way of resuing and sharing code across our components and with the power of TypeScript you can have clean, high quality and strongly typed code helps you implementing SOLID design patterns into a language that doesn’t really support it!
Next step to make this library even better is to add more resuable methods or a new repository to handle Graph API requests.
Thank you for reading this post and hope you enjoyed!
Loved this article mate and it was funny when you said “If you don’t want to read the post, the source code is available here.” but yeah that’s true.. some people just want to jump straight into the source code.
Thanks Ramin.