@markkauffman/bbrest
Version:
Make all REST API calls to Blackboard Learn Server. Based off similar work by Matthew Deakyne for Python.
180 lines (149 loc) • 7.94 kB
JavaScript
;
import fs from 'fs';
import path from 'path';
import axios from 'axios';
function add(a, b) {
return a + b;
}
// https://www.activestate.com/blog/javascript-refindall-workalike/
function findAll(regex, val) {
let rx = new RegExp(regex, "g");
let matches = new Array();
let match = "";
while ((match = rx.exec(val)) !== null){
matches.push(match);
}
return matches;
}
function isSupported(versions){
return true;
}
// https://bytearcher.com/articles/asynchronous-call-in-constructor/
/* For BbRestClient we use and asynchronous factory function as described above
This is similar to using initializer method, but we're not exposing the constructor ever to be called on its own.
Instead, we'd provide an asynchronous function that both instantiates the object and calls the initializer. This could
be called a factory function.
*/
// https://stackoverflow.com/questions/54784608/how-to-import-an-es-module-in-node-js-repl
// > import("./index.js").then(module => { myModule = module });
// > aclient = new myModule.BbRestClient('key', 'secret', 'url')
// https://stackoverflow.com/questions/56963708/how-to-use-import-in-node-repl
// if you start Node.JS with the --experimental-repl-await flag, you can use async directly
// let someModule = await import("some-module")
// Or - let response = await aclient.client.get('/learn/api/public/v1/system/version');
class BbRestClient {
constructor(key, secret, url, headers = null, code = "", redirect_uri = "https://localhost/") {
// Construct a client that talks to the REST integration using key/secret at url using 2LO auth for a token.
// url the url of the host and must be https, ex: https://bd-partner-a-ultra.blackboard.com
// If called with code and redirect_uri then we are doing 3LO.
this.ent_map = {};
let ent_map_string = "";
let buffer = '';
try {
buffer = fs.readFileSync(path.join('ent_map.json')); // ent_map is json representing entitlements to privilege mapping.
} catch (e) {
buffer = Buffer.from('{}'); // our json is that for an empty object.
}
ent_map_string = buffer.toString();
this.ent_map = JSON.parse(ent_map_string);
this._key = key;
this._secret = secret;
this._basicauth = 'Basic ' + new Buffer.from(this._key + ':' + this._secret).toString('base64');
this._url = url;
this._headers = headers;
this.version='';
this._all_functions = [];
this.functions = {};
this.client = axios.create({
baseURL: this._url
});
} // end BbRest.constructor
func;
// supported_functions() take everything we have in this._all_functions which came from the Swagger
// doc for all functions and build only the supported ones in this.functions.
// Also do a workaround for functions with similar names and clean the post parameters.
supported_functions() {
let dict = new Object();
for (const index in this._all_functions) {
let func = this._all_functions[index];
dict[func["summary"]] = func;
} // end for
this.functions = dict;
/*
for (const key in this.functions) {
console.log(`KEY ${key} FUNC ${JSON.stringify(this.functions[key])}`);
}
*/
} // end supported_functions()
async initialize(key, secret, url, headers = null, code = "", redirect_uri = "https://localhost/") {
// Get the version of the host via a REST call
let response = await this.client.get('/learn/api/public/v1/system/version');
let learn = response.data.learn;
this.version = `${learn.major}.${learn.minor}.0`; // Ignore incremental patches
response = await this.client.get(`https://developer.blackboard.com/portal/docs/apis/learn-swagger.json`);
let swagger_json = response.data;
let functions = [];
// The next slew of code is to replace the entitlements in the description we got from the Swagger doc with the
// privilege you see in Learn, if we can do that.
// Ex: "system.announcements.create": "Administrator Panel (Tools and Utilities) > Create Announcement"
// We store the description along with the Function we create for the call.
let meta = '';
let p = String.raw`\d+.\d+.\d+`; // using this Regex to match version #s in the version string
let q = String.raw`[A-Za-z]+\.[A-Za-z]+\.[A-Za-z]+\.?[A-Za-z]*\.?[A-Za-z]*`;
for (const path in swagger_json["paths"]) {
for (const call in swagger_json["paths"][path]) {
meta = swagger_json["paths"][path][call];
console.log(`PATH ${path}`);
// The first parts in the loop are for convenience. They attempt to replace the entitlements in the
// description to the system role privileges in the Learn UI. The map is in ent_map.json
// We could leave this out without affecting the functionality of the code. You can
// use any Learn system and a JS bookmarklet to show the entitlements that are associated with
// the GUI privileges.
let perms = findAll(q, meta["description"]);
let description = meta["description"]; // what we are doing here is downcasing the permissions in the description.
console.log(`DESCRIPTION`);
// console.log(`PERMS ${perms}`);
/* 2021.01.14 - For now we are going to leave the entitlements in the description and in our
function as entitlements(perms). We will not be switching them to the GUI privileges.
let perm = "";
for (const permIndex in perms) {
let permA = perms[permIndex];
perm = permA[0];
if (perm.toLowerCase() in this.ent_map) {
description = description.replace(perm, this.ent_map[perm.toLowerCase()]);
}
}
console.log(`NEW DESCRIPTION`);
*/
let versions = findAll(p, meta["description"]);
console.log(`VERSIONS ${versions}`);
// Now for every call on the path we build a function using the description, etc
// This will create all functions we got from the swagger document.
let summary = meta["summary"].replace(" ","");
functions.push(
{
"summary" : summary,
"description" : description,
"parameters" : meta["parameters"],
"method" : call,
"path" : path,
"version" : versions,
"permissions" : perms
}
)
} // end for (const path in swagger_json["paths][path]
} // end for path in swagger_json["paths"]
// Store all functions in a class variable list
this._all_functions = functions;
this.supported_functions(); // build the supported functions, this.functions from _all_functions
} // end initialize
} // end class BbRestClient
// asynchronous factory function
async function createBbRestClient(key, secret, url, headers = null, code = "", redirect_uri = "https://localhost/") {
const bbRestClient = await new BbRestClient(key, secret, url, headers = null, code = "", redirect_uri = "https://localhost/");
await bbRestClient.initialize(key, secret, url, headers = null, code = "", redirect_uri = "https://localhost/");
return bbRestClient;
}
// We only export the async factory function to be used to create the client. The consumer won't get the client until
// creation is complete.
export{add, createBbRestClient}