src/index.js
import is from 'is_js';
/**
* A Moodle Context Level number. These are the actual numbers used in the
* Moodle database tables to represent the different context levels.
*
* @typedef {string|number} ContextLevelNumber
* @example '10'
*/
/**
* A Moodle Context Level name. These are the names of the PHP constants defined
* in the Moodle code
* (`[lib/accesslib.php](https://github.com/moodle/moodle/blob/master/lib/accesslib.php)`).
*
* @typedef {string} ContextLevelName
* @example 'CONTEXT_SYSTEM'
*/
/**
* The base names for a Moodle Context Level. These are the names of the PHP
* constants with the `CONTEXT_` prefix removed and converted to lower case.
*
* @typedef {string} ContextLevelBaseName
* @example 'system'
* @see {@link ContextLevelName}
*/
/**
* An alias for the base name for the Moodle Context Level. These names consist
* of only camel-cased letters.
*
* @typedef {string} ContextLevelAlias
* @example 'courseCategory'
*/
/**
* A mapping from context level numbers to context level base names.
*
* @type {Map<ContextLevelNumber, ContextLevelBaseName>}
* @protected
*/
const NUM_BASENAME_MAP = {
'10': 'system',
'30': 'user',
'40': 'coursecat',
'50': 'course',
'70': 'module',
'80': 'block'
};
/**
* A mapping from context level numbers to context level names.
*
* @type {Map<ContextLevelNumber, ContextLevelName>}
* @protected
*/
const NUM_NAME_MAP = {};
for(const num of Object.keys(NUM_BASENAME_MAP)){
NUM_NAME_MAP[num] = `CONTEXT_${NUM_BASENAME_MAP[num].toUpperCase()}`;
}
/**
* A mapping of base names to an array of aliases.
*
* @type {Map<ContextLevelBaseName, ContextLevelAlias[]>}
* @protected
*/
const BASENAME_ALIASES_MAP = {
system: [],
user: [],
coursecat: ['courseCategory', 'category'],
course: [],
module: [],
block: []
};
/**
* A mapping form context level names to context level numbers.
*
* @type {Map<ContextLevelName, ContextLevelNumber>}
* @protected
*/
const NAME_NUM_MAP = {};
for(const num of Object.keys(NUM_NAME_MAP)){
NAME_NUM_MAP[NUM_NAME_MAP[num]] = parseInt(num);
}
/**
* A mapping from context level base names and aliases to context level numbers.
*
* @type {Map<ContextLevelBaseName|ContextLevelAlias, ContextLevelNumber>}
* @protected
*/
const BASENAME_NUM_MAP = {};
for(const num of Object.keys(NUM_BASENAME_MAP)){
BASENAME_NUM_MAP[NUM_BASENAME_MAP[num]] = parseInt(num);
}
for(const bn of Object.keys(BASENAME_ALIASES_MAP)){
const aliases = BASENAME_ALIASES_MAP[bn];
for(const bna of aliases){
BASENAME_NUM_MAP[bna] = BASENAME_NUM_MAP[bn];
}
}
/**
* A mapping from lowser-cased context level base names and aliases to context
* level numbers.
*
* @type @type {Map<string, ContextLevelNumber>}
* @protected
*/
const LC_BASENAME_NUM_MAP = {};
for(const bn of Object.keys(BASENAME_NUM_MAP)){
LC_BASENAME_NUM_MAP[bn.toLowerCase()] = BASENAME_NUM_MAP[bn];
}
/**
* A class for representing
* [Context Levels](https://docs.moodle.org/38/en/Assign_roles#Context_and_roles)
* within the [Moodle VLE](http://moodle.org/)'s permissions system.
*
* As well as the various functions and properties described in the documetation
* below there are also dynamically created properties with each valid context
* level name which get MoodleContextLevel instances for the matching level.
* In many instances these accessors will obviate the need to use a contructor.
*
* ```
* const sysCtx = MoodleContextLevel.system;
* const courseCtx = MoodleContextLevel.CONTEXT_COURSE;
* ```
*
* @see https://docs.moodle.org/38/en/Assign_roles#Context_and_roles
*/
class MoodleContextLevel {
/**
* The default context is `CONTEXT_SYSTEM`.
*
* @param {ContextLevelNumber|ContextLevelName|ContextLevelBaseName|ContextLevelAlias} context
* @throws TypeError
* @throws RangeError
*/
constructor(context){
// default to system context
let num = BASENAME_NUM_MAP.system;
// process args (if any)
if(is.not.undefined(context)){
num = MoodleContextLevel.parseToNumber(context); // could throw error
}
/**
* @type {ContextLevelNumber}
*/
this._number = num;
}
/**
* A list of all existing context level names as they appear in the
* Moodle source code sorted from lowest context level number to highest.
*
* @type {string[]}
*/
static get names(){
const ans = [];
for(const n of Object.keys(NUM_NAME_MAP).sort()){
ans.push(NUM_NAME_MAP[n]);
}
return ans;
}
/**
* An alphabetic list of all defined base names, including aliases.
*
* @type {string[]}
*/
static get baseNames(){
return Object.keys(BASENAME_NUM_MAP).sort();
}
/**
* An alphabetic list of all defined level names, be they full names as they
* appear in the Moodle source code, base names, or aliases.
*
* @type {string[]}
*/
static get allNames(){
return [
...MoodleContextLevel.names,
...MoodleContextLevel.baseNames
].sort();
}
/**
* A sorted list of all defined context level numbers.
*
* @type {number[]}
*/
static get levelNumbers(){
return Object.keys(NUM_BASENAME_MAP).map(n => parseInt(n)).sort();
}
/**
* A list of all context levels sorted by context level number.
*
* @type {MoodleContextLevel[]}
*/
static get levels(){
return MoodleContextLevel.names.map(n=>new MoodleContextLevel(n));
}
/**
* Test if a given value is a valid Moodle Context Level Number.
*
* @param {*} val - the value to test.
* @param {boolean} [strictTypeCheck=false] - whether or not to enable
* strict type checking. With strict type cheking enabled, string
* representation of otherwise valid values will return `false`.
* @return {boolean}
* @see {@link ContextLevelNumber}
*/
static isContextLevelNumber(val, strictTypeCheck){
if(is.not.number(val)){
if(strictTypeCheck) return false;
if(is.not.string(val)) return false;
}
return String(val).match(/^[134578]0$/) ? true : false;
}
/**
* Test if a given value is a valid Moodle Context Level Name.
*
* By default names, base names, and aliases are considered valid, but with
* strict checking only the full context level names as used in the Moodle
* source code will be accepted.
*
* @param {*} val - the value to test.
* @param {boolean} [strictCheck=false] - By default any name that can be
* resolved to a context level number, ignoring case, will be considered
* valid, but if a truthy value is passed only full context level names in
* the correct case exactly as used in the Moodle source code will be
* accepted.
* @return {boolean}
* @see {@link ContextLevelName}
* @see {@link ContextLevelBaseName}
* @see {@link ContextLevelAlias}
*/
static isContextLevelName(val, strictCheck){
// short-circuit non-strings
if(is.not.string(val)) return false;
// sort-circuit the passing strict check
if(NAME_NUM_MAP[val]) return true;
// we only strict is acceptable, return false
if(strictCheck) return false;
// a case-insensitive check of names
if(NAME_NUM_MAP[val.toUpperCase()]) return true;
// a case-insensitive check of base names and aliases
if(LC_BASENAME_NUM_MAP[val.toLowerCase()]) return true;
// if we got here the name is not valid
return false;
}
/**
* Convert any valid name to a context level number. Valid names are
* context level names as they appear in the Moodle code, context level
* base names, and context level aliases.
*
* @param {ContextLevelName, ContextLevelBaseName, ContextLevelAlias} name
* @return {ContextLevelNumber|NaN} If the passed value can't be converted
* to a context level number `NaN` is returned.
*/
static numberFromName(name){
if(is.not.string(name)) return NaN;
const ucName = name.toUpperCase();
if(NAME_NUM_MAP[ucName]) return NAME_NUM_MAP[ucName];
const lcName = name.toLowerCase();
if(LC_BASENAME_NUM_MAP[lcName]) return LC_BASENAME_NUM_MAP[lcName];
return NaN;
}
/**
* Compare two values to see if they represent the same context level, a
* greater context level, or a lesser context level.
*
* Context levels are compared based on their context level number.
*
* @param {*} val1
* @param {*} val2
* @return {number} Unless both values are context level objects, `NaN` is
* returned. If `val1` represents lower context level number than `val2`
* `-1` is returned, if `val1` and `val2` represent the same context level
* `0` is returned, and if `val1` represents a greater context level number
* version than `val2` `1` is returned.
*/
static compare(val1, val2){
// unless we get two Moodle context levels, return NaN
if(!((val1 instanceof MoodleContextLevel) && (val2 instanceof MoodleContextLevel))) return NaN;
// compare numeric representations
const l1 = val1.number;
const l2 = val2.number;
if(l1 < l2) return -1;
if(l1 > l2) return 1;
return 0;
}
/**
* A factory method to build a {@link MoodleContextLevel} object from any
* parsable value. The following are supported:
*
* * A valid context level number (as a number or string)
* * A valid context level name as used in the Moodle code base (in any case).
* * A valid context level base name (in any case).
* * A valid context level alias (in any case).
* * A context level object.
*
* @param {number|string|MoodleContextLevel} level - the context level value to parse.
* @return {MoodleContextLevel}
* @throws {TypeError}
* @throws {RangeError}
* @see {@link ContextLevelNumber}
* @see {@link ContextLevelName}
* @see {@link ContextLevelBaseName}
* @see {@link ContextLevelAlias}
*/
static parse(level){
return new MoodleContextLevel(MoodleContextLevel.parseToNumber(level));
}
/**
* Try to convert a value to a context level number. The following values
* are supported:
*
* * A valid context level number (as a number or string)
* * A valid context level name as used in the Moodle code base (in any case).
* * A valid context level base name (in any case).
* * A valid context level alias (in any case).
* * A context level object.
*
* @param {number|string|MoodleContextLevel} level - the context level value to parse.
* @return {MoodleContextLevelNumber}
* @throws {TypeError}
* @throws {RangeError}
* @see {@link ContextLevelNumber}
* @see {@link ContextLevelName}
* @see {@link ContextLevelBaseName}
* @see {@link ContextLevelAlias}
*/
static parseToNumber(level){
if(level instanceof MoodleContextLevel){
return level.number;
}
if(is.number(level) || is.string(level)){
const strLevel = String(level);
if(strLevel.match(/^\d{2}$/)){
// is integer, check if it's a valid key
if(NUM_BASENAME_MAP[level]){
return parseInt(level);
}else{
throw new RangeError(`unknown level '${level}'`);
}
}else{
// is not an integer, so check if it's a known name
const num = MoodleContextLevel.numberFromName(level);
if(num){
return num;
}else{
throw new RangeError(`unknown level '${level}'`);
}
}
}
throw new TypeError('invalid value - level must be a number, string, or MoodleContextLevel object');
}
/**
* Try to convert a value to a context level name as used in the Moodle
* source code. The following values are supported:
*
* * A valid context level number (as a number or string)
* * A valid context level name as used in the Moodle code base (in any case).
* * A valid context level base name (in any case).
* * A valid context level alias (in any case).
* * A context level object.
*
* @param {number|string|MoodleContextLevel} level - the context level value to parse.
* @return {MoodleContextLevelName}
* @throws {TypeError}
* @throws {RangeError}
* @see {@link ContextLevelNumber}
* @see {@link ContextLevelName}
* @see {@link ContextLevelBaseName}
* @see {@link ContextLevelAlias}
*/
static parseToName(level){
if(level instanceof MoodleContextLevel){
return level.name;
}
if(is.number(level) || is.string(level)){
const strLevel = String(level);
if(strLevel.match(/^\d{2}$/)){
// is integer, check if it's a valid key
if(NUM_BASENAME_MAP[level]){
return NUM_NAME_MAP[level];
}else{
throw new RangeError(`unknown level '${level}'`);
}
}else{
// is not an integer, so check if it's a known name
const num = MoodleContextLevel.parseToNumber(level);
if(num){
return NUM_NAME_MAP[num];
}else{
throw new RangeError(`unknown level '${level}'`);
}
}
}
throw new TypeError('invalid value - level must be a number, string, or MoodleContextLevel object');
}
/**
* The level's numeric value.
*
* @type {number}
*/
get number(){
return this._number;
}
/**
* The level's numeric value, must be one of the levels defined in
* `lib/accesslib.php` in the Moodle source code.
*
* @type {ContextLevelNumber}
* @throws {TypeError}
* @throws {RangeError}
*/
set number(n){
if(!String(n).match(/^-?\d+$/)){
throw new TypeError('must be a number');
}
if(!MoodleContextLevel.isContextLevelNumber(n)){
throw new RangeError(`unknown level '${n}'`);
}
this._number = parseInt(n); // force to a number
}
/**
* The level's name as it appears in the Moodle sourse code.
*
* @type {ContextLevelName}
*/
get name(){
return NUM_NAME_MAP[this._number];
}
/**
* The level's name in any valid form.
*
* Any name that can be parsed by the `nameFromNumber()` static function
* is acceptable.
*
* @type {(ContextLevelName|ContextLevelBaseName|ContextLevelAlias)}
* @throws {TypeError}
* @throws {RangeError}
* @see MoodleContextLevel.nameFromNumber
*/
set name(n){
if(is.not.string(n)) throw new TypeError('must be a string');
const num = MoodleContextLevel.numberFromName(n);
if(num){
this._number = num;
}else{
throw new RangeError(`unknown level '${n}'`);
}
}
/**
* The level's base name.
*
* @type {ContextLevelBaseName}
*/
get baseName(){
return NUM_BASENAME_MAP[this._number];
}
/**
* All the level's aliases.
*
* @type {ContextLevelAlias[]}
*/
get aliases(){
return [...BASENAME_ALIASES_MAP[NUM_BASENAME_MAP[this._number]]];
}
/**
* All the level's valid names in alphabetical order. This includes the
* level's name as used in the Moodle source code, the level's base name,
* and all the level's aliases.
*
* @type {string[]}
*/
get names(){
return [
this.name,
this.baseName,
...this.aliases
].sort();
}
/**
* Create a new Moodle context level object representing the same context
* level.
*
* @return {MoodleContextLevel}
*/
clone(){
return new MoodleContextLevel(this._number);
}
/**
* The context level as a string consisting of the name followed by a space
* then the level number in parentheses, e.g. `SYSTEM (10)`.
*
* @return {string}
*/
toString(){
return `${this.name} (${this.number})`;
}
/**
* The version as a plain object indexed by:
*
* * `name`
* * `number`
* * `baseName`
* * `aliases`
*
* @return {Object}
*/
toObject(){
return {
name: this.name,
number: this.number,
baseName: this.baseName,
aliases: this.aliases
};
}
/**
* Test if a given value is a Moodle context level object representing the
* same context level.
*
* @param {*} val
* @return {boolean}
*/
equals(val){
return MoodleContextLevel.compare(this, val) === 0 ? true : false;
}
/**
* Compare this context level to another.
*
* @param {MoodleContextLevel} mv
* @return {number} `-1` returned if passed context level is lesser, `0` if
* the passed context level is the same, and `1` if the passed context level
* is greater. If the passed value is not a Moodle context level object,
* `NaN` will be returned.
*/
compareTo(mv){
return MoodleContextLevel.compare(mv, this);
}
}
// add dynamically created properties for each context to class
for(const n of MoodleContextLevel.allNames){
Object.defineProperty(MoodleContextLevel, n, {
get: function(){
return new MoodleContextLevel(n);
}
});
}
export default MoodleContextLevel;