5
(1)

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:

files-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!

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.