Part 4. Create Custom Parser and Array Parser to generate query and property names in PnP JS Core

Post Series Index

This is a blog post in the series about working with Custom Business Objects, Parsers and Decorators in PnP JS Core:

  1. Introduction to Why do we should use Custom Business Objects (Models) in PnP JS Core
  2. Creating select and expand TypeScript Property Decorators to be used in PnP JS Core
  3. Creating MyDocument and MyDocumentCollection models extending Item and Items PnP JS Core classes
  4. Create Custom Parser and Array Parser to unify select and property names (this article)
  5. How to consume our decorators, models and parsers from SPFx, the winning combination
  6. Github project! Please remember to “star” if you liked it!

Introduction

In the previous posts of this series we explained why we should use Custom Business Objects in PnP JS Core and we implemented TypeScript decorators and Custom Business Object inheriting from Item and Items PnP JS Core classes to help us to have more generic and maintainable code . In this article, we will see how to solve some specific issue to unify query and business object properties by creating custom Parser and Array Parser in PnP JS Core.

What is a custom parsers in PnP JS Core?

PnP JS Core makes use of parser classes to handle converting the response object returned by fetch into the result. The default parser implemented, ODataDefaultParser is used for all requests by default (except a few special cases). But, we can create a custom parser by extending ODataParserBase class.

What is the difference between Parsers and Array Parsers?

Parsers process returned single item. Array Parsers process returned Item Collections as an array.

Parser and Array Parser implementation

Here we have both implementations for SelectDecoratorArrayParser and SelectDecoratorParser which is using the decorator metadata in order to combine the results and provide our real business object. For example, in the previous post, we did map @select(“File/Length”) with the property called Size, then the parser will be actually provide the information in the proper Size property.

import { ODataParserBase, QueryableConstructor, Util, Logger, LogLevel } from "sp-pnp-js";
import { getEntityUrl } from "sp-pnp-js/lib/sharepoint/odata";
import { getSymbol } from "../utils/symbol";
/**
* Custom Response Array Parser to be integrated with @select and @expand decorators
* It can be used on PnP Core JS get() method as a parameter
*/
export class SelectDecoratorsArrayParser<T> extends ODataParserBase<T[]> {
private _returnOnlySelectedWithDecorators = false;
constructor(protected factory: QueryableConstructor<T>, returnOnlySelectedWithDecorators?: boolean) {
super();
this._returnOnlySelectedWithDecorators = returnOnlySelectedWithDecorators;
}
public parse(r: Response): Promise<T[]> {
return super.parse(r).then((d: any[]) => {
if ("length" in d) {
return d.map(v => {
const o = <T>new this.factory(getEntityUrl(v), null);
const combinedWithResults: any = Util.extend(o, v);
const sym: string = getSymbol("select");
if (this._returnOnlySelectedWithDecorators === true) {
return SelectDecoratorsUtils.ProcessSingle(combinedWithResults, sym);
} else {
return Util.extend(combinedWithResults, SelectDecoratorsUtils.ProcessSingle(combinedWithResults, sym));
}
});
} else {
Logger.log({
data: {
d
},
level: LogLevel.Error,
message: "[SelectDecoratorsArrayParser] - response isn't a collection."
});
return null;
}
});
}
}
/**
* Custom Response Parser to be integrated with @select and @expand decorators
* It can be used on PnP Core JS get() method as a parameter
*/
export class SelectDecoratorsParser<T> extends ODataParserBase<T> {
private _returnOnlySelectedWithDecorators = false;
constructor(protected factory: QueryableConstructor<T>, returnOnlySelectedWithDecorators?: boolean) {
super();
this._returnOnlySelectedWithDecorators = returnOnlySelectedWithDecorators;
}
public parse(r: Response): Promise<T> {
// we don't need to handleError inside as we are calling directly
// to super.parse(r) and it's already handled there
return super.parse(r).then(d => {
const classDefaults: T = <T>new this.factory(getEntityUrl(d), null);
const combinedWithResults: any = Util.extend(classDefaults, d);
const sym: string = getSymbol("select");
if ("length" in combinedWithResults) {
Logger.log({
level: LogLevel.Warning,
message: "[SelectDecoratorsParser] - response is a collection. Consider using Array Parser (SelectDecoratorsArrayParser)."
});
// return SelectDecoratorsUtils.ProcessCollection(combinedWithResults, sym);
return combinedWithResults;
} else {
if (this._returnOnlySelectedWithDecorators === true) {
return SelectDecoratorsUtils.ProcessSingle(combinedWithResults, sym);
} else {
return Util.extend(combinedWithResults, SelectDecoratorsUtils.ProcessSingle(combinedWithResults, sym));
}
}
});
}
}
// utils class
class SelectDecoratorsUtils {
// get only custom model properties with @select decorator and return single item
public static ProcessSingle(combinedWithResults: any, symbolKey: string): any {
const arrayprops: { propName: string, queryName: string }[] = combinedWithResults[symbolKey];
let newObj = {};
arrayprops.forEach((item) => {
newObj[item.propName] = SelectDecoratorsUtils.GetDescendantProp(combinedWithResults, item.queryName);
});
return newObj;
}
// get only custom model properties with @select decorator and return item collection
public static ProcessCollection(combinedWithResults: any[], symbolKey: string): any[] {
let newArray: any[] = [];
const arrayprops: { propName: string, queryName: string }[] = combinedWithResults[symbolKey];
for (let i: number = 0; i < combinedWithResults.length; i++) {
const r: any = combinedWithResults[i];
let newObj = {};
arrayprops.forEach((item) => {
newObj[item.propName] = SelectDecoratorsUtils.GetDescendantProp(r, item.queryName);
});
newArray = newArray.concat(newObj);
}
return newArray;
}
private static GetDescendantProp(obj, objectString: string) {
var arr: string[] = objectString.split("/");
if (arr.length > 1 && arr[0] !== "") {
while (arr.length) {
var name: string = arr.shift();
if (name in obj) {
obj = obj[name];
} else {
Logger.log({
data: {
name
},
level: LogLevel.Warning,
message: "[getDescendantProp] - " + name + " property does not exists."
});
return null;
}
}
return obj;
}
if (objectString !== undefined && objectString !== "") {
return obj[objectString];
}
return null;
}
}

How to use Custom Parsers?

Here is an example of consuming information from a SP library by using custom objects and custom parsers.

Sample 1. Query one single document using MyDocument custom object and SelectDecoratorParsers (single)

console.log("*************************************************************");
console.log("*** One document using select, expand and get() with MyDocument Custom Parser");
console.log("*************************************************************");
const myDocumentWithSelectExpandGetParser: any = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
.getById(1)
.select("Title", "FileLeafRef", "File/Length")
.expand("File/Length")
.get(new SelectDecoratorsParser<MyDocument>(MyDocument));
// query only selected properties, but ideally should
// get the props from our custom object
console.log(myDocumentWithSelectExpandGetParser);

Sample 1

Sample 2. Query multiple documents using MyDocument and MyDocumentCollection custom object classes and SelectDecoratorsArrayParser (returning just the Custom Object properties):

console.log("*************************************************************");
console.log("*** Document Collection using as(MyDocumentCollection) and get() with Custom Array Parser returning only properties with @select");
console.log("*************************************************************");
const myDocumentsWithCustomObjectAsDocumentsGetParserJustSelect: any[] = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
// using as("Model") overrides select and expand queries
.as(MyDocumentCollection)
.get(new SelectDecoratorsArrayParser<MyDocument>(MyDocument, true));
// query only selected properties, using our Custom Model properties
// but only those that have the proper @select and @expand decorators
console.log(myDocumentsWithCustomObjectAsDocumentsGetParserJustSelect);

Sample 2

Sample 3. Query multiple documents using MyDocument and MyDocumentCollection custom object classes and SelectDecoratorsArrayParser (returning the full PnP JS Core object):

console.log("*************************************************************");
console.log("*** Document Collection using as(MyDocumentCollection) and get() with Custom Array Parser");
console.log("*************************************************************");
const myDocumentsWithCustomObjectAsDocumentsGetParser: any[] = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
// using as("Model") overrides select and expand queries
.as(MyDocumentCollection)
.skip(1)
// this renderer mix the properties and do the match between the props names and the selected if they have /
.get(new SelectDecoratorsArrayParser<MyDocument>(MyDocument));
// query only selected properties, using our Custom Model properties
// but only those that have the proper @select and @expand decorators
console.log(myDocumentsWithCustomObjectAsDocumentsGetParser);

Sample 3

Conclusion

The ideal scenario is using the sample 3 because will allows us continue with the pnp js core method chain if needed.

 

Author: José Quinto
Link: https://blog.josequinto.com/2017/06/28/create-custom-parser-and-array-parser-to-generate-query-and-property-names-in-pnp-js-core/
Copyright Notice: All articles in this blog are licensed under CC BY-SA 4.0 unless stating additionally.