@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
1,201 lines (1,031 loc) • 67.2 kB
text/typescript
import { jsonClone } from "../../exports-index";
import { PushNoDuplicate, firstOrNull, makeUniqueArray, toHash } from "../../helpers/collections.base";
import { jsonStringify } from "../../helpers/json";
import { NormalizeListName, SPBasePermissions, SchemaJsonToXml, SchemaXmlToJson, extendFieldInfos } from "../../helpers/sharepoint";
import { normalizeGuid } from "../../helpers/strings";
import { SafeIfElse, isBoolean, isNotEmptyArray, isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined, isNumber, isPromise, isString, isValidGuid } from "../../helpers/typecheckers";
import { makeServerRelativeUrl, normalizeUrl } from "../../helpers/url";
import { IDictionary } from "../../types/common.types";
import { IRestOptions, contentTypes, jsonTypes } from "../../types/rest.types";
import { BaseTypes, FieldTypeAsString, FieldTypes, IFieldInfo, IFieldInfoEX, IFieldInfoExHash, IFieldJsonSchema, IFieldLookupInfo, ISPEventReceiver, ListTemplateTypes, PageType, SPBasePermissionKind } from "../../types/sharepoint.types";
import { GeListItemsFoldersBehaviour, IListWorkflowAssociation, IRestItem, ListExperienceOptions, iContentType, iList, iListVersionSettings, iListView } from "../../types/sharepoint.utils.types";
import { ConsoleLogger } from "../consolelogger";
import { GetJson, GetJsonSync, longLocalCache, shortLocalCache } from "../rest";
import { GetRestBaseUrl, GetSiteUrl, LIST_EXPAND, LIST_SELECT } from "./common";
import { __fixGetListItemsResults } from "./listutils/common";
import { GetContentTypes, GetContentTypesSync, GetListsSync, IGetContentTypesOptions } from "./web";
const logger = ConsoleLogger.get("SharePoint.Rest.List");
/** returns /_api/web/lists/getById() or /_api/web/lists/getByTitle() */
export function GetListRestUrl(siteUrl: string, listIdOrTitle: string): string {
siteUrl = GetSiteUrl(siteUrl);
let listId = GetListId(siteUrl, listIdOrTitle);
let listPart = isValidGuid(listId) ? `getById('${normalizeGuid(listId)}')` : `getByTitle('${encodeURIComponent(listIdOrTitle)}')`;
return GetRestBaseUrl(siteUrl) + `/web/lists/${listPart}`;
}
export function GetListId(siteUrl: string, listIdOrTitle: string): string {
if (isNullOrEmptyString(listIdOrTitle)) return null;
if (isValidGuid(listIdOrTitle)) return listIdOrTitle;
//Issue 7508
//When translation is enabled, and user changes list title but he is not on the same language as the site
//he translates the list, but not changing its title
//so REST api /lists/getByTitle will not work
//instead, we need to get the list id from the web's lists collection.
let lists = GetListsSync(siteUrl);
var lower = listIdOrTitle.toLowerCase();
var list = firstOrNull(lists, l => l.Title.toLowerCase() === lower);
return list && list.Id || null;
}
/** get the list ID from a list page, such as a list view or an item form */
export function GetListIdFromPageSync(siteUrl: string, listPageUrl: string): string {
let url = `${GetRestBaseUrl(siteUrl)}/web/getlist('${makeServerRelativeUrl(listPageUrl.split('?')[0].split('#')[0])}')?$select=id`;
let response = GetJsonSync<{ Id: string; }>(url, null, {
...longLocalCache,
jsonMetadata: jsonTypes.nometadata
});
if (!isNullOrUndefined(response) && response.success) {
let listId = response.result.Id;
return normalizeGuid(listId);
}
return null;
}
interface IGetSiteAssetLibraryResult { Id: string, Name: string, ServerRelativeUrl: string }
interface IGetSiteAssetLibraryReturnValue {
value: {
Id: string;
RootFolder: {
Name: string;
ServerRelativeUrl: string;
Exists: boolean;
};
}[];
}
/** ensures the site assets library exists and return its info. on errors - it will return null. */
export function EnsureAssetLibrary(siteUrl: string): Promise<IGetSiteAssetLibraryResult> {
siteUrl = GetSiteUrl(siteUrl);
let url = GetRestBaseUrl(siteUrl) +
"/web/lists/EnsureSiteAssetsLibrary?$select=ID,RootFolder/Name,RootFolder/ServerRelativeUrl,RootFolder/Exists&$expand=RootFolder";
return GetJson<{
d: { Id: string; RootFolder: { Name: string; ServerRelativeUrl: string; Exists: boolean; }; };
}>(url, null, { method: "POST", spWebUrl: siteUrl, ...longLocalCache }).then(result => {
if (result && result.d) {
return {
Id: result.d.Id,
Name: result.d.RootFolder.Name,
ServerRelativeUrl: result.d.RootFolder.ServerRelativeUrl
};
} else return null;
}).catch<IGetSiteAssetLibraryResult>(() => null);
}
interface IGetSitePagesLibrarResult { Id: string, Name: string, ServerRelativeUrl: string }
/** ensures the site pages library exists and return its info. on errors - it will return null. */
export async function EnsureSitePagesLibrary(siteUrl: string): Promise<IGetSitePagesLibrarResult> {
let url = `${GetRestBaseUrl(siteUrl)}/web/lists/EnsureSitePagesLibrary`
+ `?$select=ID,RootFolder/Name,RootFolder/ServerRelativeUrl,RootFolder/Exists&$expand=RootFolder`;
let response = await GetJson<iList>(url, null, {
method: "POST",
jsonMetadata: jsonTypes.nometadata,
includeDigestInPost: true,
...longLocalCache
});
if (!isNullOrUndefined(response) && !isNullOrUndefined(response.RootFolder)) {
return {
Id: response.Id,
Name: response.RootFolder.Name,
ServerRelativeUrl: response.RootFolder.ServerRelativeUrl
};
}
return null;
}
export function GetSiteAssetLibrary(siteUrl: string, sync?: false): Promise<IGetSiteAssetLibraryResult>;
export function GetSiteAssetLibrary(siteUrl: string, sync: true): IGetSiteAssetLibraryResult;
export function GetSiteAssetLibrary(siteUrl: string, sync?: boolean): IGetSiteAssetLibraryResult | Promise<IGetSiteAssetLibraryResult> {
let reqUrl = `${GetRestBaseUrl(siteUrl)}/web/lists?`
//Issue 1492: isSiteAssetsLibrary eq true does not work for reader users.
//+ `$filter=isSiteAssetsLibrary eq true&$select=ID,RootFolder/Name,RootFolder/ServerRelativeUrl,RootFolder/Exists`
+ `$filter=EntityTypeName%20eq%20%27SiteAssets%27&$select=ID,RootFolder/Name,RootFolder/ServerRelativeUrl,RootFolder/Exists`
+ `&$expand=RootFolder`;
let caller = sync ? GetJsonSync : GetJson;
let result = caller<IGetSiteAssetLibraryReturnValue>(reqUrl, null, { ...longLocalCache, jsonMetadata: jsonTypes.nometadata });
let transform: (v: IGetSiteAssetLibraryReturnValue) => IGetSiteAssetLibraryResult = (v) => {
if (isNotEmptyArray(v && v.value)) {
let assetLibrary = v.value[0];
return {
Id: assetLibrary.Id,
Name: assetLibrary.RootFolder.Name,
ServerRelativeUrl: assetLibrary.RootFolder.ServerRelativeUrl
};
}
return null;
};
if (isPromise(result))
return result.then(r => transform(r), () => null);
else
return result.success ? transform(result.result) : null;
}
/** Return the list Title */
export function GetListTitle(siteUrl: string, listIdOrTitle: string): Promise<string> {
siteUrl = GetSiteUrl(siteUrl);
return GetJson<{ d: { Title: string; }; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `/Title`, null, { allowCache: true })
.then(r => {
return r.d.Title;
})
.catch<string>(() => null);
}
/** Return the list */
export function GetList(siteUrlOrId: string, listIdOrTitle: string, options?: {
includeViews?: boolean;
viewOptions?: IListViewOptions;
includeContentTypes?: boolean;
includeRootFolder?: boolean;
includeEventReceivers?: boolean;
}, refreshCache = false): Promise<iList> {
let siteUrl = GetSiteUrl(siteUrlOrId);
if (isNullOrEmptyString(listIdOrTitle)) {
return null;
}
return GetJson<{ d: iList; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `?$select=${LIST_SELECT}&$expand=${LIST_EXPAND}`, null, { allowCache: true })
.then(async r => {
let list = r.d;
if (options) {
let promises = [];
if (options.includeViews) {
promises.push(GetListViews(siteUrl, listIdOrTitle, options.viewOptions, refreshCache).then((r) => {
list.Views = r;
}))
}
if (options.includeContentTypes) {
promises.push(GetListContentTypes(siteUrl, listIdOrTitle, null, refreshCache).then((r) => {
list.ContentTypes = r;
}));
}
if (options.includeRootFolder) {
promises.push(GetListRootFolder(siteUrl, listIdOrTitle).then((r) => {
list.RootFolder = r;
}));
}
if (options.includeEventReceivers) {
promises.push(GetListEventReceivers(siteUrl, listIdOrTitle, refreshCache).then((r) => {
list.EventReceivers = r;
}));
}
if (promises.length > 0) {
await Promise.all(promises);
}
}
if (list.EffectiveBasePermissions
&& (isString(list.EffectiveBasePermissions.High) || isString(list.EffectiveBasePermissions.Low))) {
list.EffectiveBasePermissions = {
High: Number(list.EffectiveBasePermissions.High),
Low: Number(list.EffectiveBasePermissions.Low)
};
}
return list;
})
.catch<iList>(() => null);
}
/** Return the list */
export function GetListSync(siteUrl: string, listIdOrTitle: string): iList {
siteUrl = GetSiteUrl(siteUrl);
if (isNullOrEmptyString(listIdOrTitle)) return null;
let result = GetJsonSync<{ d: iList; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `?$select=${LIST_SELECT}&$expand=${LIST_EXPAND}`, null, shortLocalCache);
if (result && result.success) {
let list = result.result.d;
if (list.EffectiveBasePermissions
&& (isString(list.EffectiveBasePermissions.High)
|| isString(list.EffectiveBasePermissions.Low))) {
list.EffectiveBasePermissions = {
High: Number(list.EffectiveBasePermissions.High),
Low: Number(list.EffectiveBasePermissions.Low)
};
}
return list;
}
else return null;
}
export function GetListNameSync(webUrl: string, listIdOrTitle: string): string {
let list = GetListSync(webUrl, listIdOrTitle);
return NormalizeListName({ EntityTypeName: list.EntityTypeName, BaseType: list.BaseType });
}
export async function GetListName(webUrl: string, listIdOrTitle: string) {
let list = await GetList(webUrl, listIdOrTitle);
return NormalizeListName({ EntityTypeName: list.EntityTypeName, BaseType: list.BaseType });
}
export function GetListRootFolder(siteUrlOrId: string, listIdOrTitle: string): Promise<{ ServerRelativeUrl: string; Name: string; }> {
let siteUrl = GetSiteUrl(siteUrlOrId);
return GetJson<{
d: { ServerRelativeUrl: string; Name: string; };
}>(GetListRestUrl(siteUrl, listIdOrTitle) + `/RootFolder?$Select=Name,ServerRelativeUrl`,
null, longLocalCache)
.then(r => {
return r.d;
})
.catch<{ ServerRelativeUrl: string; Name: string; }>(() => null);
}
export function GetListRootFolderSync(siteUrlOrId: string, listIdOrTitle: string): { ServerRelativeUrl: string; Name: string; } {
let siteUrl = GetSiteUrl(siteUrlOrId);
let result = GetJsonSync<{
d: { ServerRelativeUrl: string; Name: string; };
}>(GetListRestUrl(siteUrl, listIdOrTitle) + `/RootFolder?$Select=Name,ServerRelativeUrl`,
null, longLocalCache);
return SafeIfElse(() => result.result.d, null);
}
export function GetListField(siteUrlOrId: string, listIdOrTitle: string, fieldIdOrName: string, refreshCache?: boolean): Promise<IFieldInfo> {
let siteUrl = GetSiteUrl(siteUrlOrId);
var url = GetListRestUrl(siteUrl, listIdOrTitle) + `/fields`;
if (isValidGuid(fieldIdOrName)) {
url += `('${normalizeGuid(fieldIdOrName)}')`;
} else {
url += `/getbyinternalnameortitle(@u)?@u='${encodeURIComponent(fieldIdOrName)}'`;
}
let result = GetJson<{ d: IFieldInfo; }>(url, null, { allowCache: refreshCache !== true })
.then(r => {
return r.d;
})
.catch<IFieldInfo>(() => null);
return result;
}
function _getListFieldsRequestUrl(siteUrl: string, listIdOrTitle: string) {
return GetListRestUrl(siteUrl, listIdOrTitle) + `/fields`;
}
/** Gets ID, Title, ContentType Author, Editor, Created and Modified fields */
export function GetStandardListFields(siteUrlOrId: string, listIdOrTitle: string, refreshCache?: boolean) {
let fieldNames = ["ID", "Title", "ContentType", "Author", "Editor", "Created", "Modified"];
return GetListFields(siteUrlOrId, listIdOrTitle, { refreshCache: refreshCache, fieldNames: fieldNames });
}
export interface IGetListFieldsOptions {
refreshCache?: boolean;
/** fieldNames that should be returned with the request */
fieldNames?: string[];
}
function _processGetListFields(fields: IFieldInfo[], fieldNames: string[]) {
if (isNullOrEmptyArray(fields)) {
return fields as IFieldInfoEX[];
}
let extendedFields = extendFieldInfos(fields);
if (!isNullOrEmptyArray(fieldNames)) {
return extendedFields.filter((extendedField) => {
return fieldNames.includes(extendedField.InternalName);
});
}
return extendedFields;
}
export function GetListFields(siteUrlOrId: string, listIdOrTitle: string, options: IGetListFieldsOptions = {}): Promise<IFieldInfoEX[]> {
let siteUrl = GetSiteUrl(siteUrlOrId);
let url = _getListFieldsRequestUrl(siteUrl, listIdOrTitle);
let restOptions: IRestOptions = {
allowCache: options.refreshCache !== true,
jsonMetadata: jsonTypes.nometadata
};
return GetJson<{ value: IFieldInfo[]; }>(url, null, restOptions)
.then((result) => {
return _processGetListFields(result.value, options.fieldNames);
}).catch<IFieldInfoEX[]>(() => {
return null;
});
}
export function GetListFieldsSync(siteUrlOrId: string, listIdOrTitle: string, options: IGetListFieldsOptions = {}): IFieldInfoEX[] {
let siteUrl = GetSiteUrl(siteUrlOrId);
let url = _getListFieldsRequestUrl(siteUrl, listIdOrTitle);
let restOptions: IRestOptions = {
allowCache: options.refreshCache !== true,
jsonMetadata: jsonTypes.nometadata
};
let result = GetJsonSync<{ value: IFieldInfo[]; }>(url, null, restOptions);
if (result.success && !isNullOrUndefined(result.result)) {
return _processGetListFields(result.result.value, options.fieldNames);
}
return null;
}
export async function GetListFieldsAsHash(siteUrlOrId: string, listIdOrTitle: string, refreshCache?: boolean): Promise<IFieldInfoExHash> {
let siteUrl = GetSiteUrl(siteUrlOrId);
let fields = await GetListFields(siteUrl, listIdOrTitle, { refreshCache: refreshCache });
let hash: IFieldInfoExHash = {};
if (isNotEmptyArray(fields)) {
hash = toHash(fields, f => f.InternalName);
}
return hash;
}
export function GetListFieldsAsHashSync(siteUrlOrId: string, listIdOrTitle: string, refreshCache?: boolean): IFieldInfoExHash {
let siteUrl = GetSiteUrl(siteUrlOrId);
let fields = GetListFieldsSync(siteUrl, listIdOrTitle, { refreshCache: refreshCache });
let hash: IFieldInfoExHash = {};
if (isNotEmptyArray(fields)) {
fields.forEach(f => { hash[f.InternalName] = f; });
}
return hash;
}
export function GetListWorkflows(siteUrl: string, listIdOrTitle: string, refreshCache?: boolean): Promise<IListWorkflowAssociation[]> {
siteUrl = GetSiteUrl(siteUrl);
return GetJson<{
d: { results: IListWorkflowAssociation[]; };
}>(GetListRestUrl(siteUrl, listIdOrTitle) + `/workflowAssociations`,
null, { allowCache: refreshCache !== true })
.then(r => {
if (r && r.d && isNotEmptyArray(r.d.results)) {
r.d.results.forEach(wf => {
wf.BaseId = normalizeGuid(wf.BaseId);
wf.Id = normalizeGuid(wf.Id);
wf.ListId = normalizeGuid(wf.ListId);
wf.WebId = normalizeGuid(wf.WebId);
});
return r.d.results;
}
else return [];
})
.catch<IListWorkflowAssociation[]>(() => []);
}
export function UserHasManagePermissions(siteUrl: string, listIdOrTitle: string): Promise<boolean> {
siteUrl = GetSiteUrl(siteUrl);
return GetJson<{ d: { EffectiveBasePermissions: { High: number; Low: number; }; }; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `/EffectiveBasePermissions`, null,
{ ...shortLocalCache })
.then(r => {
return new SPBasePermissions(r.d.EffectiveBasePermissions).has(SPBasePermissionKind.ManageLists);
})
.catch<boolean>(() => null);
}
export function UserHasEditPermissions(siteUrl: string, listIdOrTitle: string): Promise<boolean> {
return UserHasPermissions(siteUrl, listIdOrTitle, SPBasePermissionKind.EditListItems);
}
export function UserHasPermissions(siteUrlOrId: string, listIdOrTitle: string, permissionKind: SPBasePermissionKind): Promise<boolean> {
let siteUrl = GetSiteUrl(siteUrlOrId);
return GetJson<{ d: { EffectiveBasePermissions: { High: number; Low: number; }; }; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `/EffectiveBasePermissions`, null,
{ ...shortLocalCache })
.then(r => {
return new SPBasePermissions(r.d.EffectiveBasePermissions).has(permissionKind);
})
.catch<boolean>(() => null);
}
export function UserHasPermissionsSync(siteUrlOrId: string, listIdOrTitle: string, permissionKind: SPBasePermissionKind): boolean {
let siteUrl = GetSiteUrl(siteUrlOrId);
const res = GetJsonSync<{ d: { EffectiveBasePermissions: { High: number; Low: number; }; }; }>(GetListRestUrl(siteUrl, listIdOrTitle) + `/EffectiveBasePermissions`, null,
{ ...shortLocalCache });
return new SPBasePermissions(res.result.d.EffectiveBasePermissions).has(permissionKind);
}
/** create a new column and try to add it to default view. Send either Title and Type, or SchemaXml. Create with SchemaXml also adds to all content types */
export async function CreateField(siteUrl: string, listIdOrTitle: string, options: {
Title?: string;
Type?: FieldTypes;
Required?: boolean;
Indexed?: boolean;
SchemaXml?: string;
/** requies Name and StaticName for the internal name */
SchemaXmlSpecificInternalName?: boolean;
SkipAddToDefaultView?: boolean;
ClientSideComponentId?: string;
ClientSideComponentProperties?: string;
JSLink?: string;
}): Promise<IFieldInfoEX> {
siteUrl = GetSiteUrl(siteUrl);
let finish = async (result: IFieldInfo) => {
if (!result) {
return null;
}
let internalName = result.InternalName;
//we need to clear and reload the list fields cache, so call it and return our field from that collection.
let fields = await GetListFields(siteUrl, listIdOrTitle, { refreshCache: true });
try {
if (options.SkipAddToDefaultView !== true) {
//try to add it to default view, don't wait for it
GetListViews(siteUrl, listIdOrTitle).then(views => {
let defaultView = firstOrNull(views, v => v.DefaultView);
if (defaultView)
GetJson(GetListRestUrl(siteUrl, listIdOrTitle) + `/views('${defaultView.Id}')/ViewFields/addViewField('${internalName}')`, null, { method: "POST", spWebUrl: siteUrl });
});
}
}
catch (e) { }
return firstOrNull(fields, f => f.InternalName === internalName);
};
if (!isNullOrEmptyString(options.SchemaXml)) {
try {
let updateObject: IDictionary<any> = {
'parameters': {
'__metadata': { 'type': 'SP.XmlSchemaFieldCreationInformation' },
'SchemaXml': options.SchemaXml,
'Options': options.SchemaXmlSpecificInternalName !== true ?
4 ://SP.AddFieldOptions.addToAllContentTypes
4 | 8//SP.AddFieldOptions.addToAllContentTypes | addFieldInternalNameHint
}
};
let url = `${GetListRestUrl(siteUrl, listIdOrTitle)}/fields/createFieldAsXml`;
let newFieldResult = await GetJson<{ d: IFieldInfo; }>(url, JSON.stringify(updateObject));
if (!isNullOrUndefined(newFieldResult)
&& !isNullOrUndefined(newFieldResult.d)) {
if ((!isNullOrEmptyString(options.Title) && options.Title !== newFieldResult.d.Title)
|| (isBoolean(options.Indexed) && options.Indexed !== newFieldResult.d.Indexed)) {
let updatedField = await UpdateField(siteUrl, listIdOrTitle, newFieldResult.d.InternalName, {
Title: options.Title,
Indexed: options.Indexed === true
});
return finish(updatedField);
}
}
return finish(newFieldResult && newFieldResult.d);
} catch {
}
return null;
} else if (!isNullOrEmptyString(options.Title) && !isNullOrUndefined(options.Type)) {
let updateObject: IDictionary<any> = {
'__metadata': { 'type': 'SP.Field' },
'Title': options.Title,
'FieldTypeKind': options.Type,
'Required': options.Required === true,
'Indexed': options.Indexed === true
};
if (!isNullOrEmptyString(options.ClientSideComponentId)) {
updateObject.ClientSideComponentId = options.ClientSideComponentId;
}
if (!isNullOrEmptyString(options.ClientSideComponentProperties)) {
updateObject.ClientSideComponentProperties = options.ClientSideComponentProperties;
}
if (!isNullOrEmptyString(options.JSLink)) {
updateObject.JSLink = options.JSLink;
}
try {
let url = `${GetListRestUrl(siteUrl, listIdOrTitle)}/fields`;
let newFieldResult = await GetJson<{ d: IFieldInfo; }>(url, JSON.stringify(updateObject));
return finish(newFieldResult && newFieldResult.d);
} catch {
}
return null;
}
else {
console.error("You must send either SchemaXml or Title and Type");
return null;
}
}
/** Update field SchemaXml OR Title, only 1 update at a time supported. */
export async function UpdateField(siteUrlOrId: string, listIdOrTitle: string, fieldInternalName: string, options: {
Title?: string;
Indexed?: boolean;
/** Update 'Choices' propertry on 'Choice' and 'MultiChoice' field types. */
Choices?: string[];
SchemaXml?: string;
FieldType?: FieldTypeAsString;
Required?: boolean;
Hidden?: boolean;
JSLink?: string;
ClientSideComponentId?: string;
ClientSideComponentProperties?: string;
}): Promise<IFieldInfoEX> {
let siteUrl = GetSiteUrl(siteUrlOrId);
let finish = async () => {
//we need to clear and reload the list fields cache, so call it and return our field from that collection.
let fields = await GetListFields(siteUrl, listIdOrTitle, { refreshCache: true });
return firstOrNull(fields, f => f.InternalName === fieldInternalName);
};
let fields = await GetListFieldsAsHash(siteUrl, listIdOrTitle, true);
let thisField = fields[fieldInternalName];
//updates can either be SchemaXml, or others. Cannot be both.
let updates: IDictionary<any> = {
'__metadata': { 'type': 'SP.Field' }
};
if (!isNullOrEmptyString(options.SchemaXml)) {
updates.SchemaXml = options.SchemaXml;
}
else {
//cannot send schema updates with other updates.
if (!isNullOrEmptyString(options.Title)) {
updates.Title = options.Title;
}
if (!isNullOrEmptyString(options.FieldType)) {
updates.TypeAsString = options.FieldType;
}
if (isBoolean(options.Required)) {
updates.Required = options.Required === true;
}
if (isBoolean(options.Indexed)) {
updates.Indexed = options.Indexed === true;
}
if (!isNullOrEmptyArray(options.Choices)) {
let choiceType = options.FieldType || thisField.TypeAsString;
if (choiceType === "Choice" || choiceType === "MultiChoice") {
updates["__metadata"]["type"] = choiceType === "Choice" ? "SP.FieldChoice" : "SP.FieldMultiChoice"
updates.Choices = { "results": options.Choices };
} else {
logger.warn("Can only update 'Choices' property on 'Choice' and 'MultiChoice' field types.");
}
}
if (isBoolean(options.Hidden)) {
//this requries the CanToggleHidden to be in the schema... if not - we will need to add it before we can update this.
let fields = await GetListFieldsAsHash(siteUrl, listIdOrTitle, false);
let thisField = fields[fieldInternalName];
if (thisField.Hidden !== options.Hidden) {
if (thisField) {
if (thisField.SchemaJson.Attributes.CanToggleHidden !== "TRUE") {
await UpdateField(siteUrl, listIdOrTitle, fieldInternalName, {
SchemaXml:
thisField.SchemaXml.replace("<Field ", `<Field CanToggleHidden="TRUE" `)
});
}
}
updates.Hidden = options.Hidden === true;
}
}
if (!isNullOrEmptyString(options.ClientSideComponentId))
updates.ClientSideComponentId = options.ClientSideComponentId;
if (!isNullOrEmptyString(options.ClientSideComponentProperties))
updates.ClientSideComponentProperties = options.ClientSideComponentProperties;
if (!isNullOrEmptyString(options.JSLink))
updates.JSLink = options.JSLink;
}
if (Object.keys(updates).length > 1) {
return GetJson(GetListRestUrl(siteUrl, listIdOrTitle) + `/fields/getbyinternalnameortitle('${fieldInternalName}')`,
JSON.stringify(updates), { xHttpMethod: "MERGE" })
.then(r => {
return finish();
})
.catch<IFieldInfoEX>(() => null);
}
else {
console.error("You must send an option to update");
return null;
}
}
export async function ChangeTextFieldMode(
siteUrlOrId: string,
listIdOrTitle: string,
textMode: "singleline" | "multiline" | "html",
currentField: IFieldInfoEX
) {
const newSchema = jsonClone(currentField.SchemaJson);
const currentSchemaAttributes = newSchema.Attributes;
switch (textMode) {
case "singleline":
let shouldIntermediateUpdate = false;
if (currentSchemaAttributes.RichText === 'TRUE') {
currentSchemaAttributes.RichText = 'FALSE';
shouldIntermediateUpdate = true;
};
if (currentSchemaAttributes.RichTextMode === 'FullHTML') {
currentSchemaAttributes.RichTextMode = 'Compatible';
shouldIntermediateUpdate = true;
};
if (shouldIntermediateUpdate) {
const intermediateSchema = SchemaJsonToXml(newSchema);
const intermediateUpdatedField = await UpdateField(siteUrlOrId, listIdOrTitle, currentField.InternalName, {
SchemaXml: intermediateSchema
});
// Early exit if intermediate change failed.
if (isNullOrUndefined(intermediateUpdatedField))
return false;
};
// Actual type update.
currentSchemaAttributes.Type = 'Text';
delete currentSchemaAttributes.RichTextMode;
delete currentSchemaAttributes.RichText;
break;
case "multiline":
currentSchemaAttributes.Type = 'Note';
currentSchemaAttributes.RichText = 'FALSE';
currentSchemaAttributes.RichTextMode = 'Compatible';
break;
case "html":
currentSchemaAttributes.Type = 'Note';
currentSchemaAttributes.RichText = 'TRUE';
currentSchemaAttributes.RichTextMode = 'FullHTML';
break;
}
const updatedSchema = SchemaJsonToXml(newSchema);
const fieldUpdated = await UpdateField(siteUrlOrId, listIdOrTitle, currentField.InternalName, {
SchemaXml: updatedSchema
});
// If object is null or undefined then request has failed.
return !isNullOrUndefined(fieldUpdated);
}
export async function ChangeDatetimeFieldMode(
siteUrlOrId: string,
listIdOrTitle: string,
includeTime: boolean,
currentField: IFieldInfoEX
) {
const dateTimeFormat = 'DateTime';
const dateOnlyFormat = 'DateOnly';
const newSchema = jsonClone(currentField.SchemaJson);
const fieldAttributes = newSchema.Attributes;
let needUpdate = false;
if (includeTime && fieldAttributes.Format === dateOnlyFormat) {
needUpdate = true;
fieldAttributes.Format = dateTimeFormat;
}
else if (!includeTime && fieldAttributes.Format === dateTimeFormat) {
needUpdate = true;
fieldAttributes.Format = dateOnlyFormat;
}
if (needUpdate) {
const updatedSchema = SchemaJsonToXml(newSchema);
const updateResponse = await UpdateField(siteUrlOrId, listIdOrTitle, currentField.InternalName, {
SchemaXml: updatedSchema
});
return !isNullOrUndefined(updateResponse);
}
// If an already existing format was chosen.
return true;
}
export async function DeleteField(siteUrl: string, listIdOrTitle: string, fieldInternalName: string, options?: { DeleteHiddenField?: boolean; }): Promise<boolean> {
siteUrl = GetSiteUrl(siteUrl);
// let finish = async () => {
// //we need to clear and reload the list fields cache, so call it and return our field from that collection.
// let fields = await GetListFields(siteUrl, listIdOrTitle, { refreshCache: true });
// return firstOrNull(fields, f => f.InternalName === fieldInternalName);
// };
if (options && options.DeleteHiddenField)
await UpdateField(siteUrl, listIdOrTitle, fieldInternalName, { Hidden: false });
return GetJson(GetListRestUrl(siteUrl, listIdOrTitle) + `/fields/getbyinternalnameortitle('${fieldInternalName}')`, null, {
method: "POST",
xHttpMethod: "DELETE"
})
.then(r => true)
.catch<boolean>((e) => false);
}
export interface IListViewOptions { includeViewFields?: boolean; }
export function GetListViews(siteUrl: string, listIdOrTitle: string, options?: IListViewOptions, refreshCache = false): Promise<iListView[]> {
siteUrl = GetSiteUrl(siteUrl);
return GetJson<{
value: iListView[];
}>(GetListRestUrl(siteUrl, listIdOrTitle) + `/views?$select=Title,Id,ServerRelativeUrl,RowLimit,Paged,ViewQuery,ListViewXml,PersonalView,MobileView,MobileDefaultView,Hidden,DefaultView,ReadOnlyView${options && options.includeViewFields ? "&$expand=ViewFields" : ""}`,
null, { allowCache: refreshCache !== true, jsonMetadata: jsonTypes.nometadata })
.then(r => {
let views = r.value;
if (isNotEmptyArray(views)) {
views.forEach(v => {
v.Id = normalizeGuid(v.Id);
if (options && options.includeViewFields) {
v.ViewFields = v.ViewFields && v.ViewFields["Items"] && v.ViewFields["Items"] || [];
}
});
}
return views;
})
.catch<iListView[]>(() => null);
}
export function GetListViewsSync(siteUrl: string, listIdOrTitle: string, refreshCache = false): iListView[] {
siteUrl = GetSiteUrl(siteUrl);
let result = GetJsonSync<{
d: {
results: iListView[];
};
}>(GetListRestUrl(siteUrl, listIdOrTitle) + `/views`,
null, { allowCache: refreshCache !== true });
if (result.success) {
let views = result && result.result && result.result.d && result.result.d.results;
if (isNotEmptyArray(views)) {
views.forEach(v => { v.Id = normalizeGuid(v.Id); });
}
return views;
}
return null;
}
export async function AddViewFieldToListView(siteUrl: string, listIdOrTitle: string, viewId: string, viewField: string) {
return _addOrRemoveViewField(siteUrl, listIdOrTitle, viewId, viewField, "addviewfield");
}
export async function RemoveViewFieldFromListView(siteUrl: string, listIdOrTitle: string, viewId: string, viewField: string) {
return _addOrRemoveViewField(siteUrl, listIdOrTitle, viewId, viewField, "removeviewfield");
}
async function _addOrRemoveViewField(siteUrl: string, listIdOrTitle: string, viewId: string, viewField: string, action: "addviewfield" | "removeviewfield") {
siteUrl = GetSiteUrl(siteUrl);
if (isNullOrEmptyString(viewField) || !isValidGuid(viewId)) {
return false;
}
let views = await GetListViews(siteUrl, listIdOrTitle, { includeViewFields: true });
if (isNullOrEmptyArray(views)) {
return false;
}
let view = views.filter((view) => {
return normalizeGuid(view.Id) === normalizeGuid(viewId);
})[0];
if (isNullOrUndefined(view)) {
return false;
}
let hasField = view.ViewFields.includes(viewField);
if (action === "addviewfield" && hasField === true) {
return true;
}
if (action === "removeviewfield" && hasField === false) {
return true;
}
try {
let url = GetListRestUrl(siteUrl, listIdOrTitle) + `/views('${normalizeGuid(view.Id)}')/viewfields/${action}('${viewField}')`;
let result = await GetJson<{ "odata.null": boolean; }>(url, null, { method: "POST" });
if (result && result["odata.null"] === true) {
return true;
}
} catch { }
return false;
}
export function GetListContentTypes(siteUrl: string, listIdOrTitle: string,
options?: Omit<IGetContentTypesOptions, "listIdOrTitle" | "fromRooWeb">, refreshCache = false): Promise<iContentType[]> {
return GetContentTypes(siteUrl, { ...(options || {}), listIdOrTitle: listIdOrTitle }, refreshCache);
}
export function GetListContentTypesSync(siteUrl: string, listIdOrTitle: string,
options?: Omit<IGetContentTypesOptions, "listIdOrTitle" | "fromRooWeb">, refreshCache = false): iContentType[] {
return GetContentTypesSync(siteUrl, { ...(options || {}), listIdOrTitle: listIdOrTitle }, refreshCache);
}
/** generic version. for the KWIZ forms version that supports action id call GetListFormUrlAppsWeb instead */
export function GetListFormUrl(siteUrl: string, listId: string, pageType: PageType, params?: { contentTypeId?: string; itemId?: number | string; rootFolder?: string }) {
siteUrl = GetSiteUrl(siteUrl);
if (!isValidGuid(listId)) console.error('GetListFormUrl requires a list id');
let url = `${normalizeUrl(siteUrl)}/_layouts/15/listform.aspx?PageType=${pageType}&ListId=${encodeURIComponent(listId)}`;
if (params) {
if (!isNullOrEmptyString(params.contentTypeId))
url += `&ContentTypeId=${encodeURIComponent(params.contentTypeId)}`;
if (!isNullOrEmptyString(params.itemId))
url += `&ID=${encodeURIComponent(params.itemId as string)}`;
if (!isNullOrEmptyString(params.rootFolder))
url += `&RootFolder=${encodeURIComponent(params.rootFolder)}`;
}
return url;
}
export function GetFieldSchemaSync(siteUrl: string, listIdOrTitle: string, fieldInternalName: string, refreshCache?: boolean): IFieldJsonSchema {
siteUrl = GetSiteUrl(siteUrl);
//ISSUE: 1516 - The get schema request will fail if the field doesn't exist in the list, so we load the fields and ensure the field
//exists before requesting the schema.
let fields = GetListFieldsSync(siteUrl, listIdOrTitle, {
refreshCache: refreshCache,
fieldNames: [fieldInternalName]
});
if (isNullOrEmptyArray(fields)) {
return null;
}
let field = fields[0];
return SchemaXmlToJson(field.SchemaXml);
// let url = GetListRestUrl(siteUrl, listIdOrTitle) + `/fields/getByInternalNameOrTitle('${fieldInternalName}')?$select=SchemaXml`;
// let result = GetJsonSync<{ d: { SchemaXml: string; }; }>(
// url,
// null,
// {
// ...shortLocalCache,
// forceCacheUpdate: refreshCache === true
// });
// if (result && result.success) {
// return SchemaXmlToJson(result.result.d.SchemaXml);
// }
// return null;
//#endregion
}
export async function GetFieldSchema(siteUrl: string, listIdOrTitle: string, fieldInternalName: string, refreshCache?: boolean) {
siteUrl = GetSiteUrl(siteUrl);
//ISSUE: 1516 - The get schema request will fail if the field doesn't exist in the list, so we load the fields and ensure the field
//exists before requesting the schema
let fields = await GetListFields(siteUrl, listIdOrTitle, {
refreshCache: refreshCache,
fieldNames: [fieldInternalName]
});
if (isNullOrEmptyArray(fields)) {
return null;
}
let field = fields[0];
return SchemaXmlToJson(field.SchemaXml);
}
export async function GetListItems(siteUrl: string, listIdOrTitle: string, options: {
/** Optional, default: 1000. 0: get all items. */
rowLimit?: number;
/** Id, Title, Modified, FileLeafRef, FileDirRef, FileRef, FileSystemObjectType */
columns: (string | IFieldInfoEX)[];
foldersBehaviour?: GeListItemsFoldersBehaviour;
/** Optional, request to expand some columns. */
expand?: string[];
/** allow to change the jsonMetadata for this request, default: verbose */
jsonMetadata?: jsonTypes;
refreshCache?: boolean;
/** allow to send a filter statement */
$filter?: string;
}): Promise<IRestItem[]> {
let info = _GetListItemsInfo(siteUrl, listIdOrTitle, options);
let items: IRestItem[] = [];
do {
let resultItems: IRestItem[] = [];
let next: string = null;
if (info.noMetadata) {
let requestResult = (await GetJson<{
value: IRestItem[];
"odata.nextLink": string;
}>(info.requestUrl, null, {
allowCache: options.refreshCache !== true,
jsonMetadata: options.jsonMetadata
}));
resultItems = requestResult.value;
next = requestResult["odata.nextLink"];
}
else {
let requestResult = (await GetJson<{
d: {
results: IRestItem[];
__next?: string;
};
}>(info.requestUrl, null, {
allowCache: options.refreshCache !== true
}));
resultItems = requestResult.d.results;
next = requestResult.d.__next;
}
if (isNotEmptyArray(resultItems))
items.push(...resultItems);
if (info.totalNumberOfItemsToGet > items.length)
info.requestUrl = next;
else
info.requestUrl = null;
} while (!isNullOrEmptyString(info.requestUrl));
return __fixGetListItemsResults(siteUrl, listIdOrTitle, items, options.foldersBehaviour, info.expandedLookupFields);
}
export function GetListItemsSync(siteUrl: string, listIdOrTitle: string, options: {
/** Optional, default: 1000. 0: get all items. */
rowLimit?: number;
/** Id, Title, Modified, FileLeafRef, FileDirRef, FileRef, FileSystemObjectType */
columns: (string | IFieldInfoEX)[];
foldersBehaviour?: GeListItemsFoldersBehaviour;
/** Optional, request to expand some columns. */
expand?: string[];
/** allow to send a filter statement */
$filter?: string;
}): IRestItem[] {
let info = _GetListItemsInfo(siteUrl, listIdOrTitle, options);
let items: IRestItem[] = [];
do {
let resultItems: IRestItem[] = [];
let next: string = null;
if (info.noMetadata) {
let requestResult = GetJsonSync<{
value: IRestItem[];
"odata.nextLink": string;
}>(info.requestUrl, null, { allowCache: true });
if (requestResult.success) {
resultItems = requestResult.result.value;
next = requestResult.result["odata.nextLink"];
}
}
else {
let requestResult = GetJsonSync<{
d: { results: IRestItem[]; __next?: string; };
}>(info.requestUrl, null, { allowCache: true });
if (requestResult.success) {
resultItems = requestResult.result.d.results;
next = requestResult.result.d.__next;
}
}
if (isNotEmptyArray(resultItems))
items.push(...resultItems);
if (info.totalNumberOfItemsToGet > items.length)
info.requestUrl = next;
else
info.requestUrl = null;
} while (!isNullOrEmptyString(info.requestUrl));
return __fixGetListItemsResults(siteUrl, listIdOrTitle, items, options.foldersBehaviour, info.expandedLookupFields);
}
function _GetListItemsInfo(siteUrl: string, listIdOrTitle: string, options: {
/** Optional, default: 1000. 0: get all items. */
rowLimit?: number;
/** Id, Title, Modified, FileLeafRef, FileDirRef, FileRef, FileSystemObjectType */
columns: (string | IFieldInfoEX)[];
/** Optional, request to expand some columns. */
expand?: string[];
/** allow to change the jsonMetadata for this request, default: verbose */
jsonMetadata?: jsonTypes;
/** allow to send a filter statement */
$filter?: string;
}) {
siteUrl = GetSiteUrl(siteUrl);
let url = GetListRestUrl(siteUrl, listIdOrTitle) + `/items`;
let queryParams: string[] = [];
//Issue 8189 expand lookup fields
let columns: string[] = [];
let expand: string[] = [];
let expandedLookupFields: IFieldInfoEX[] = [];
options.columns.forEach(c => {
if (isString(c)) columns.push(c);
else {
let internalName = c.InternalName;
//Issue 828, 336
if (internalName.startsWith("_")) internalName = `OData_${internalName}`;
let isLookupField = c.TypeAsString === "Lookup" || c.TypeAsString === "LookupMulti";
let isUserField = c.TypeAsString === "User" || c.TypeAsString === "UserMulti";
if (isLookupField || isUserField) {
//ISSUE: 1519 - Added lookupField property to able to retrieve value of the additional lookup field key
let lookupField = (c as IFieldLookupInfo).LookupField;
if (!isNullOrEmptyString(lookupField) && isLookupField) {
columns.push(`${internalName}/${lookupField}`);
}
//we want to expand it
columns.push(`${internalName}/Title`);
columns.push(`${internalName}/Id`);
expand.push(internalName);
expandedLookupFields.push(c);
}
else columns.push(internalName);
}
});
if (isNotEmptyArray(options.expand)) {
expand.push(...options.expand);
}
//add the ones we need
PushNoDuplicate(columns, "Id");
PushNoDuplicate(columns, "FileRef");
PushNoDuplicate(columns, "FileSystemObjectType");
queryParams.push(`$select=${encodeURIComponent(makeUniqueArray(columns).join(','))}`);
if (isNotEmptyArray(expand))
queryParams.push(`$expand=${encodeURIComponent(makeUniqueArray(expand).join(','))}`);
let batchSize = 2000;
let limit = options.rowLimit >= 0 && options.rowLimit < batchSize ? options.rowLimit : batchSize;
let totalNumberOfItemsToGet = !isNumber(options.rowLimit) || options.rowLimit < 1 ? 99999 : options.rowLimit > batchSize ? options.rowLimit : limit;
if (!isNullOrEmptyString(options.$filter))
queryParams.push(`$filter=${options.$filter}`);
queryParams.push(`$top=${limit}`);
let requestUrl = url + (queryParams.length > 0 ? '?' + queryParams.join('&') : '');
let noMetadata = options.jsonMetadata === jsonTypes.nometadata;
return { requestUrl, noMetadata, totalNumberOfItemsToGet, expandedLookupFields };
}
/** Find an item by id, even if it is nested in a sub-folder */
export function FindListItemById(items: IRestItem[], itemId: number): IRestItem {
for (let i = 0; i < items.length; i++) {
let current = items[i];
if (current.Id === itemId) return current;
else if (isNotEmptyArray(current.__Items))//folder? look inside
{
let nestedResult = FindListItemById(current.__Items, itemId);
if (!isNullOrUndefined(nestedResult)) return nestedResult;
}
}
//not found
return null;
}
function _getListEventReceiversRequestUrl(siteUrl: string, listIdOrTitle: string) {
return GetListRestUrl(siteUrl, listIdOrTitle) + `/EventReceivers`
}
export async function GetListEventReceivers(siteUrl: string, listIdOrTitle: string, refreshCache?: boolean): Promise<ISPEventReceiver[]> {
try {
let url = _getListEventReceiversRequestUrl(siteUrl, listIdOrTitle);
let response = await GetJson<{
value: ISPEventReceiver[];
}>(url,
null, {
allowCache: refreshCache !== true,
jsonMetadata: jsonTypes.nometadata
});
return !isNullOrUndefined(response) ? response.value : null;
} catch {
}
return null;
}
export async function AddListEventReceiver(siteUrl: string, listIdOrTitle: string, eventReceiverDefinition: Pick<ISPEventReceiver, "EventType" | "ReceiverName" | "ReceiverUrl" | "SequenceNumber">): Promise<ISPEventReceiver> {
let newEventReceiver: Omit<ISPEventReceiver, "ReceiverId" | "Synchronization"> = {
ReceiverAssembly: "",
ReceiverClass: "",
...eventReceiverDefinition
};
try {
let url = _getListEventReceiversRequestUrl(siteUrl, listIdOrTitle);
let response = await GetJson<ISPEventReceiver>(url, JSON.stringify(newEventReceiver), {
method: "POST",
includeDigestInPost: true,
jsonMetadata: jsonTypes.nometadata,
headers: {
"content-type": contentTypes.json
}
});
return !isNullOrUndefined(response) && isValidGuid(response.ReceiverId) ? response : null;
} catch {
}
return null;
}
export async function DeleteListEventReceiver(siteUrl: string, listIdOrTitle: string, eventReceiverId: string): Promise<boolean> {
try {
let url = `${_getListEventReceiversRequestUrl(siteUrl, listIdOrTitle)}('${normalizeGuid(eventReceiverId)}')/deleteObject`;
let response = await GetJson<{ "odata.null": boolean }>(url, null, {
method: "POST",
includeDigestInPost: true,
jsonMetadata: jsonTypes.nometadata
});
return !isNullOrUndefined(response) && response["odata.null"] === true;
} catch {
}
return false;
}
/** timestamp of changes:
* - item updates
* - changes to columns
* - content types
* - list versioning settings
* - list title/description
* - content approval settings
* does not track:
* - Changes to views
* - changing list/items permissions
*/
export function GetListLastItemModifiedDate(siteUrl: string, listIdOrTitle: string, options: {
sync: true;
refreshCache?: boolean;
/** ignore system changes */
userChangesOnly?: boolean;
}): string;
/** timestamp of changes:
* - item updates
* - changes to columns
* - content types
* - list versioning settings
* - list title/description
* - content approval settings
* does not track:
* - Changes to views
* - changing list/items permissions
*/
export function GetListLastItemModifiedDate(siteUrl: string, listId