spws
Version:
SharePoint Web Services Wrapper
316 lines (277 loc) • 12 kB
text/typescript
// Libraries
import { defaults, getListViewThreshold, getLastItemID, getFirstItemID } from "../../..";
// TODO: Remove this library dependency
import CamlBuilder from "camljs";
// Classes
import { SpwsRequest, SpwsError } from "../../../classes";
// Enum
import { WebServices, Fields } from "../../../enum";
// Types
import { Item, KnownKeys, SpwsBatchResponse, SpwsResponse } from "../../../types";
// Utils
import { asyncForEach } from "../../../utils";
// Local
import sendRequest from "./sendRequest";
interface Operation<T> extends SpwsBatchResponse {
data: (KnownKeys<Item> & T)[];
}
// Create cache for any data that doesn't need to be repeated
const cache: {
listViewThreshold: {
[key: string]: number;
};
} = { listViewThreshold: {} };
export type GetListItemsOptions<T> = {
/** The SharePoint webURL */
webURL?: string;
/** A callback function that is used to the parse an item after it as been
* parsed by the default parser but before it is returned to the array of items.
*/
parseItem?: (item: Item & T) => Item & T;
/**
* If true, requests are batched to not exceed the list view threshold.
* Batch sizes are automatically assigned to match the list view threshold limit.
*/
batch?: boolean;
/** A callback function that is called before the batches are sent. */
onBatchStart?: (total: number) => void;
/** A callback function that is called after each batch is sent. */
onBatchStep?: (params: {
index: number;
res: SpwsResponse & { data: (Item & T)[] };
total: number;
}) => void;
// create onBatchComplete async type to handle async functions
/**
* A string that contains the GUID for the view surrounded by curly braces ({}),
* which determines the view to use for the default view attributes represented by the query, viewFields, and rowLimit parameters.
* If this parameter contains an empty string, the default view is used.
* If the view GUID is supplied, the value of the query, viewFields, or rowLimit parameter overrides the
* equivalent setting within the view.
* For example, if the view specified by the viewFields parameter has a row limit of 100 rows but the rowLimit
* parameter contains 1000, then 1,000 rows are returned in the response.
*/
viewName?: string;
/** A Query xml string containing the query that determines which records are returned and in what order */
query?: string;
/** An array of strings that specifies which fields to return in the query and in what order */
fields?: string[];
/** rowLimit */
rowLimit?: number;
/** An XML fragment in the following form that contains separate nodes for the various properties of the SPQuery object */
queryOptions?: {
/** Specifies the format in which dates are returned.
* If True, dates returned are in Coordinated Universal Time (UTC) format.
* If False, dates returned are in [ISO-8601] format. */
DatesInUtc?: boolean;
/** If set to True, specifies that fields in list items that are lookup fields
* to the user information list are returned as if they were multi-value lookups,
* including "Name", "EMail", "SipAddress", and "Title" fields from the user
* information list for the looked up item. These values are separated by ";#",
* and any commas in the lookup field name are encoded as ",,". */
ExpandUserField?: boolean;
/** If set to True, the server MUST remove all characters that have an
* ASCII valuein the range 0-31,other than 9, 10, or 13, in the field values
* of the list items returned. */
RemoveInvalidXmlCharacters?: boolean;
/** Specifies a URL used to filter document library items for items in the specified folder.*/
Folder?: string;
/** Specifies that required fields and fields used by specified calculated fields
* be returned in addition to the fields specified by the viewFields parameter, if set to True */
IncludeMandatoryColumns?: boolean;
/** If it is True, the EffectivePermMask attribute is returned when the permissions of a
* given row differ from the base permissions of the list */
IncludePermissions?: boolean;
/** If set to True, specifies that the value returned for the Attachments field is a list of full URLs
* separated by the delimiter ";#". */
IncludeAttachmentUrls?: boolean;
/** This MUST be used only in conjunction with IncludeAttachmentUrls.
* Specifying True causes the GUID and version number to be returned for attachments that
* can be used for conflict detection on update. */
IncludeAttachmentVersion?: boolean;
/** Specifies how to return lookup values when a list item that is the target of a lookup
* is in an incomplete or draft state.
* When False, the data returned will include the current lookup field values
* When True, the query returns published lookup field values, if present, regardless of
* whether the list item that is the target of the lookup is in an incomplete or draft state */
OptimizeLookups?: boolean;
};
};
/**
* Adds the user to the specified group
* @param listName A string that contains either the display name or the GUID for the list
* @link https://docs.microsoft.com/en-us/previous-versions/office/developer/sharepoint-services/ms772599(v=office.12)
* @link https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-listsws/fa650bde-c79b-4a2a-bb5d-c43e74166322?redirectedfrom=MSDN
* @example
* ```
* const res = await getListItems("Task Tracker")
* ```
*/
const getListItems = async <T extends object = {}>(
listName: string,
{
onBatchStart = undefined,
onBatchStep = undefined,
parseItem,
batch = false,
viewName = "",
fields = [],
query = `<Query/>`,
webURL = defaults.webURL,
queryOptions = { ...defaults.queryOptions },
rowLimit = 0,
}: GetListItemsOptions<T> = {}
): Promise<Operation<T>> => {
try {
// Validate type
if (typeof listName !== "string")
throw new SpwsError({
message: `Expected string for listName but received ${typeof listName}`,
});
// Validate truthy string
if (!listName)
throw new SpwsError({
message: "Expected listName to be a valid string but received an empty string",
});
// Validate fields
if (fields && !Array.isArray(fields))
throw new SpwsError({
message: `Expected fields to be an array but recieved ${typeof fields}`,
});
// Creat viewFields string
let viewFields = `<ViewFields Properties='True' />`;
// If true, only specified fields will be returned with data instead of all item attributes
const parseFields = Array.isArray(fields) && fields.length > 0;
// Array literal to store fields if parseFields is true
let fieldsClone: string[] = [];
// If fields is an array and has fields
if (parseFields) {
// Add to fields clone
fieldsClone = [...new Set([...fields, Fields.EncodedAbsUrl])];
// Create fieldRef string
const fieldRefs = fieldsClone.map((field) => `<FieldRef Name="${field}" />`).join("");
// Wrap fieldRefs with viewFields tag
viewFields = `<ViewFields>${fieldRefs}</ViewFields>`;
}
// Create queryOptions
let queryOpt = "<QueryOptions>";
// If queryOptions are defined
if (queryOptions) {
// Iterate through all queryOptions
Object.entries(queryOptions).forEach(([tagName, value]) => {
// If value is not undefined
if (typeof value !== "undefined") {
// Add to query opt string
queryOpt += `<${tagName}>${value}</${tagName}>`;
}
});
}
// Close Query Options Tag
queryOpt += "</QueryOptions>";
// Create query string and include <Query/> tags
const camlQuery = /<query>|<\/query>|<query\/>/i.test(query)
? query
: `<Query>${query}</Query>`;
// Create variables to handle batching
let lastItemID = 0;
let firstItemID = 0;
let batchCount = 1;
// If batch is true
if (batch) {
if (!cache.listViewThreshold[webURL]) {
// Get list view threshold
const { data: listViewThreshold } = await getListViewThreshold(listName, { webURL });
// Set cached list view threshold
cache.listViewThreshold[webURL] = listViewThreshold;
}
// Get last item ID
const { data: lastID } = await getLastItemID(listName, { webURL });
lastItemID = lastID;
// If the last ID is greater than 0 (e.g. the list has items)
if (lastID > 0) {
// Get first item ID
const { data: firstID } = await getFirstItemID(listName, { webURL });
firstItemID = firstID;
// Calculate batchCount
batchCount = Math.ceil((lastItemID - firstID) / cache.listViewThreshold[webURL]) || 1;
}
}
// Create batches array
const batches = Array.from(new Array(batchCount));
// If batch is true and onBatchStart is defined (batching is always false when getting the first and last item ID)
if (batch && onBatchStart) await onBatchStart(batches.length);
// Create response object
const response: Operation<T> = {
data: [],
responseText: [],
responseXML: [],
status: [],
statusText: [],
envelope: [],
};
// Iterate over all batches
await asyncForEach(batches, async (b, index) => {
// Create request payload
let payload = {
camlQuery,
fields: fieldsClone,
listName,
parseFields,
queryOpt,
req: new SpwsRequest({ webService: WebServices.Lists, webURL }),
rowLimit,
viewFields,
viewName,
parseItem,
};
// if (index > 0) return;
if (batch) {
// Get from the first ID (-1)
let fromID = firstItemID + cache.listViewThreshold[webURL] * index - 1;
// Add the list view threshold (-1)
// let toID = firstItemID + (cache.listViewThreshold[webURL] - 1) * (index + 1);
let toID = fromID + cache.listViewThreshold[webURL];
// Use lastItemID if the less than the toID
if (toID > lastItemID) toID = lastItemID;
// If the query has no children
if (camlQuery.length < 15) {
payload.camlQuery = `<Query>${new CamlBuilder()
.Where()
.CounterField("ID")
.GreaterThan(fromID)
.And()
.CounterField("ID")
.LessThanOrEqualTo(toID)
.ToString()}</Query>`;
} else {
// Append to the query if it had children
payload.camlQuery = CamlBuilder.FromXml(camlQuery)
.ModifyWhere()
.AppendAnd()
.CounterField("ID")
.GreaterThan(fromID)
.And()
.CounterField("ID")
.LessThanOrEqualTo(toID)
.ToString();
}
}
// Send Request (using local function)
const res = await sendRequest<T>(payload);
// If batch is true and onBatchStart is defined (batching is always false when getting the first and last item ID)
if (batch && onBatchStep) await onBatchStep({ index, total: batches.length, res });
// Push to responses
response.data.push(...res.data);
response.responseText.push(res.responseText);
response.responseXML.push(res.responseXML);
response.status.push(res.status);
response.statusText.push(res.statusText);
response.envelope!.push(res.envelope!);
});
// Return response
return response;
} catch (error: any) {
throw new SpwsError(error);
}
};
export default getListItems;