Skip to content

Instantly share code, notes, and snippets.

@iOnline247
Last active March 11, 2021 13:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iOnline247/f2dd4307fedc3661c8eae4705ac8717e to your computer and use it in GitHub Desktop.
Save iOnline247/f2dd4307fedc3661c8eae4705ac8717e to your computer and use it in GitHub Desktop.
Create a bulletproof configuration list for custom SharePoint apps.
/*!
* Created by Matthew Bramer
* Released under the MIT license
* Date: 2016-08-05
* Tested using SharePoint Online.
*/
// Get $sp.min.js from here.
// https://gist.github.com/iOnline247/cc8d68cb611b056695434224e6c2aa19
import '../vendor/$sp.min';
(async () => {
const rest = window.$sp();
const DEFAULT_EXPIRY_DAYS = 1;
const CONFIG_LIST_NAME = 'ConfigList';
// #region Utils
function debug(...args) {
console.error(...args);
// eslint-disable-next-line no-debugger
debugger;
}
function dateInFuture(daysInFuture) {
const date = new Date();
date.setDate(date.getDate() + daysInFuture);
return date;
}
function addItem(item) {
return rest.add({
url: `/_api/web/lists/getByTitle('${CONFIG_LIST_NAME}')/items`,
data: {
Expires: dateInFuture(DEFAULT_EXPIRY_DAYS).toISOString(),
Title: item.Title,
JSON: item.JSON,
},
});
}
function updateItem(item) {
return rest.update({
url: `/_api/web/lists/getByTitle('${CONFIG_LIST_NAME}')/items(${item.Id})`,
data: {
Expires: dateInFuture(DEFAULT_EXPIRY_DAYS).toISOString(),
JSON: item.JSON,
},
});
}
// #endregion
const dataConfigs = {
spaceX: async () => {
const output = {
// Gets the name of this function, so the key will be the same for the list.
// This is important later on in the logic.
Title: dataConfigs.spendingData.name,
};
const data = await someApiCall();
// Useful for debugging.
// output.JSON = JSON.stringify(data, null, '\t');
output.JSON = JSON.stringify(data);
return output;
},
test1: () => ({
Title: dataConfigs.test1.name,
JSON: JSON.stringify({ test1: true }),
}),
test2: () => ({
Title: dataConfigs.test2.name,
JSON: JSON.stringify({ test2: true }),
}),
};
function createConfigs(configs) {
// At this point, we have raw data from any missing config keys.
// format will be this.
// {
// Title: 'somekey',
// JSON: '{}'
// }
// We also have the existing data, that hasn't expired.
// This is stored at the end of `args`
// Find the unexpired config, so we can slice it off.
const spData = configs[configs.length - 1];
const newJsonConfigs = configs.slice(0, configs.length - 1);
// Add the missing JSON to the config list.
newJsonConfigs.forEach((v) => addItem(v).catch(debug));
// Join everything together in one array for promise handoff.
return newJsonConfigs.concat(spData.existingData);
}
async function getDataFromMissingConfigs(data) {
const { results } = data.d;
const configKeys = Object.keys(dataConfigs);
// We'll look at this object and compare with items that already exist.
const configData = configKeys
.filter(function findNonExistentConfigs(v) {
return !results.some((item) => item.Title === v);
})
// Now we have an array of the configs that *should* exist, but do not
// in SharePoint. Maybe they were deleted or never there...
// Let's fetch those from the API and return an array of promises.
.map((key) => dataConfigs[key]())
// concat list data.
// this will allow usage later on.
.concat({
existingData: results,
});
const configs = await Promise.all(configData);
return createConfigs(configs);
}
function separateByExpiryDate(configs) {
const now = new Date();
const expiredConfigs = configs.filter((v) => new Date(v.Expires) < now);
const nonExpiredConfigs = configs.filter(
(v) => new Date(v.Expires) >= now
);
return {
nonExpiredConfigs,
expiredConfigs,
};
}
async function refreshStaleDataFromDefinitions(staleData) {
// Convert all staleData configs to an array of promises.
const updateDataCalls = staleData.reduce(function refreshData(
output,
v
) {
const freshDataFunc = dataConfigs[v.Title];
const isFunction = typeof freshDataFunc === 'function';
if (isFunction) {
output.push(freshDataFunc());
} else {
console.warn(
`!!! ${v.Title} is not defined as a function in dataConfigs !!!`
);
}
return output;
},
[]);
const freshData = await Promise.all(updateDataCalls);
return freshData;
}
function updateSharePointConfigs(dataNeedsUpdating, existingData) {
dataNeedsUpdating.forEach((v) => {
const currConfig = existingData.find((j) => j.Title === v.Title);
// eslint-disable-next-line no-param-reassign
v.Id = currConfig.Id;
updateItem(v).catch(debug);
});
}
function signalEvent(data) {
const event = new CustomEvent('configList:ready', { detail: data });
// jQuery will have the data on the event.originalEvent.detail property.
document.dispatchEvent(event);
}
let allConfigData;
try {
const configListData = await rest.get(
`/_api/web/lists/getByTitle('${CONFIG_LIST_NAME}')/items?$select=Title,JSON,Expires,Id`
);
allConfigData = await getDataFromMissingConfigs(configListData);
const { nonExpiredConfigs, expiredConfigs } = separateByExpiryDate(
allConfigData
);
const dataNeedsUpdatingInConfigList = await refreshStaleDataFromDefinitions(
expiredConfigs
);
const freshData = nonExpiredConfigs.concat(
dataNeedsUpdatingInConfigList
);
signalEvent(freshData);
updateSharePointConfigs(dataNeedsUpdatingInConfigList, allConfigData);
} catch (err) {
// Fallback to what is in the configList at this moment.
signalEvent(allConfigData);
debug(err);
}
})();
/*
* Usage
*
*
// Bind to the ready event
document.addEventListener('configList:ready', (e) => console.log(e.detail));
// jQuery: Bind to the ready event
$(document).one('configList:ready', function (event) {
console.log(event.originalEvent.detail);
});
*/
@iOnline247
Copy link
Author

The list should have these columns:

  • JSON :: Multiple Lines of Text (Plain Text)
  • Expires :: Date and Time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment