gitlab-acebase
Version:
AceBase realtime database server (webserver endpoint to allow remote connections)
438 lines (390 loc) • 16.6 kB
JavaScript
var connection = {
dbname: 'none',
username: 'anonymous',
db: null,
user: null,
auth_token: '',
};
async function connect(dbname, username, password) {
connection.db?.disconnect();
const host = location.hostname, port = location.port, https = location.protocol === 'https:', rootPath = location.pathname.match(/^\/?(.*?)\/webmanager/)[1];
connection.db = new AceBaseClient({ dbname, host, port, https, autoConnect: false, rootPath });
await connection.db.connect(false);
await connection.db.ready();
connection.dbname = dbname;
if (username) {
// Only sign in when credentials are passed
const signInDetails = await connection.db.auth.signIn(username, password);
connection.user = signInDetails.user;
connection.auth_token = signInDetails.accessToken;
connection.username = (signInDetails && signInDetails.user.displayName) || username;
}
}
function getChildPath (path, childKey) {
const isIndex = typeof childKey === 'number';
const trailPath = isIndex ? '[' + childKey + ']' : '/' + childKey;
const childPath = path ? path + trailPath : childKey;
return childPath;
}
function getPathKeys(path) {
if (typeof path === 'undefined' || path.length === 0) { return []; }
let keys = path.replace(/\[/g, '/[').split('/');
keys.forEach((key, index) => {
if (key.startsWith('[')) {
keys[index] = parseInt(key.substr(1, key.length - 2));
}
});
return keys;
}
/**
*
* @param {string} path
* @param {(event: 'changed'|'removed'|'added', childRef) => any} callback
*/
function subscribeToChildEvents(path, callback) {
// Create 3 subscriptions, combine them into 1
path = path || '';
const ref = connection.db.ref(path);
// Subscribe to events
const changes = ref.on('notify_child_changed').subscribe(ref => callback('changed', ref));
const adds = ref.on('notify_child_added').subscribe(ref => callback('added', ref));
const removes = ref.on('notify_child_removed').subscribe(ref => callback('removed', ref));
function stop() {
// Unsubscribe
changes.stop();
adds.stop();
removes.stop();
}
return { stop };
}
let currentPath = null;
let currentPathSubscription = null;
let currentPathIsArray = false;
let lastMoreClickListener;
function updateBrowsePath(path = '') {
if (currentPath !== path) {
if (currentPathSubscription) {
// unsubscribe from previous subscription
currentPathSubscription.stop();
}
currentPathSubscription = subscribeToChildEvents(path, (event, ref) => {
// M.toast({ html: `Child "${ref.key}" ${event}` });
clearErrors();
if (event === 'removed') {
removeNode({ key: ref.key });
}
else {
ref.reflect('info', { child_limit: 0, child_skip: 0, impersonate: impersonatedUid })
.then(event === 'changed' ? updateNode : addNode);
}
});
}
currentPath = path;
const impersonatedUid = document.getElementById('impersonate_uid').value;
const container = document.getElementById('browse_container');
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const breadcrumbContainer = document.getElementById('browse_breadcrumb_container');
while (breadcrumbContainer.firstChild) {
breadcrumbContainer.removeChild(breadcrumbContainer.firstChild);
}
const pathKeys = getPathKeys(path);
let parentPath = '';
pathKeys.forEach(key => {
const breadcrumbNode = document.createElement('a');
breadcrumbNode.className = 'browse_breadcrumb_node';
breadcrumbNode.textContent = key;
let childPath = getChildPath(parentPath, key);
breadcrumbNode.addEventListener('click', updateBrowsePath.bind(this, childPath));
breadcrumbContainer.appendChild(breadcrumbNode);
parentPath = childPath;
});
function createNode(nodeInfo) {
const childElem = document.createElement('div');
childElem.id = 'child_' + nodeInfo.key;
childElem.classList.add('db_node');
const isObjectOrArray = ['object', 'array'].includes(nodeInfo.type);
const hasInlineValue = typeof nodeInfo.value !== 'undefined';
if (isObjectOrArray && !hasInlineValue) {
childElem.classList.add('db_node_expand');
// add_box
// indeterminate_check_box?
const childPath = getChildPath(path, nodeInfo.key);
childElem.addEventListener('click', updateBrowsePath.bind(this, childPath));
}
const nodeNameElem = document.createElement('span');
nodeNameElem.className = 'db_node_name';
nodeNameElem.textContent = nodeInfo.key;
if (typeof nodeInfo.access === 'object') {
nodeNameElem.classList.add('access_mode');
nodeNameElem.classList.add(nodeInfo.access.read ? 'allow_read' : 'deny_read');
nodeNameElem.classList.add(nodeInfo.access.write ? 'allow_write' : 'deny_write');
}
childElem.appendChild(nodeNameElem);
if (hasInlineValue) {
const nodeValueElem = document.createElement('span');
nodeValueElem.className = 'db_node_inlinevalue';
let displayValue = nodeInfo.value;
if (nodeInfo.type === 'string') {
displayValue = `"${displayValue}"`;
}
else if (nodeInfo.type === 'reference') {
displayValue = `(ref to:) "${displayValue.path}"`;
}
else if (nodeInfo.type === 'date') {
displayValue = new Date(displayValue).toString();
}
else if (nodeInfo.type === 'object') {
displayValue = '{}';
}
else if (nodeInfo.type === 'array') {
displayValue = '[]';
}
else if (nodeInfo.type === 'binary') {
nodeValueElem.className = 'db_node_type';
displayValue = nodeInfo.type; //'(binary value)';
}
nodeValueElem.textContent = displayValue;
childElem.appendChild(nodeValueElem);
}
else if (!isObjectOrArray) {
const canLoadValue = ['string','reference'].includes(nodeInfo.type);
const nodeTypeElem = document.createElement(canLoadValue ? 'a' : 'span');
nodeTypeElem.className = 'db_node_type';
if (canLoadValue) { nodeTypeElem.classList.add('clickable'); }
nodeTypeElem.textContent = nodeInfo.type;
canLoadValue && nodeTypeElem.addEventListener('click', () => {
// load and display value
const childPath = getChildPath(path, nodeInfo.key);
connection.db.ref(childPath).get(snap => {
let val = snap.val();
if (val === null) { val = 'null'; }
else if (nodeInfo.type === 'string') { val = `"${val}"`; }
else if (nodeInfo.type === 'reference') { val = `${val.path}`; }
const nodeValueElem = document.createElement('span');
nodeValueElem.className = 'db_node_inlinevalue';
nodeValueElem.textContent = val;
// replace node
nodeTypeElem.parentElement.replaceChild(nodeValueElem, nodeTypeElem);
});
});
childElem.appendChild(nodeTypeElem);
}
return childElem;
}
function addNode(nodeInfo, noFlash = false) {
console.log(`Adding node ${nodeInfo.key}:`, nodeInfo);
const childElem = createNode(nodeInfo);
if (!noFlash) { childElem.classList.add('flash'); }
container.appendChild(childElem);
}
function updateNode(nodeInfo) {
const id = 'child_' + nodeInfo.key;
const currentElem = document.getElementById(id);
if (currentElem) { currentElem.id = 'prev_' + id; }
const newElem = createNode(nodeInfo);
newElem.classList.add('flash');
if (currentElem) {
container.replaceChild(newElem, currentElem);
}
else {
container.appendChild(newElem);
}
}
function removeNode(nodeInfo) {
const id = 'child_' + nodeInfo.key;
const currentElem = document.getElementById(id);
currentElem && currentElem.remove();
if (container.children.length === 0) {
// Node is either gone or has no more children. Refresh info
updateBrowsePath(path);
}
}
function showError(message) {
const errorElem = document.createElement('div');
errorElem.className = 'error';
errorElem.textContent = message;
container.appendChild(errorElem);
document.getElementById('browse_impersonate_access').className = '';
document.getElementById('more_children').className = '';
}
function clearErrors() {
container.querySelectorAll('div.error').forEach(elem => elem.remove());
}
let ref = path ? connection.db.ref(path) : connection.db.root;
const limit = 100;
async function getChildren(skip = 0) {
try {
// Load path children with reflect API
const info = await ref.reflect('info', { child_limit: limit, child_skip: skip, impersonate: impersonatedUid });
currentPathIsArray = info.type === 'array';
if (info.impersonation) {
const elem = document.getElementById('browse_impersonate_access');
if (info.impersonation.read.allow && info.impersonation.write.allow) {
elem.className = 'access_allow_read_write';
}
else if (info.impersonation.read.allow) {
elem.className = 'access_allow_read';
}
else if (info.impersonation.write.allow) {
elem.className = 'access_allow_write';
}
else {
elem.className = 'access_deny';
}
}
else {
document.getElementById('browse_impersonate_access').className = '';
}
if (!info.exists) {
return showError('node does not exist');
}
else if (info.children.list.length === 0) {
showError('node has no children');
}
if (skip === 0 && !info.children.more) {
// we've got all children, sort the list
info.children.list.sort((a,b) => a.key < b.key ? -1 : 1);
}
// Add children
info.children.list.forEach(childInfo => {
addNode(childInfo, true);
});
// Is there more data?
const moreElem = document.getElementById('more_children');
moreElem.className = info.children.more ? 'visible' : '';
const loadMoreElem = document.getElementById('load_more_children');
lastMoreClickListener && loadMoreElem.removeEventListener('click', lastMoreClickListener);
lastMoreClickListener = () => {
getChildren(skip + limit);
};
loadMoreElem.addEventListener('click', lastMoreClickListener);
// Enable export?
const exportAvailable = ['object','array'].includes(info.type);
const exportNode = document.getElementById('export_node');
if (exportAvailable) { exportNode.classList.remove('hide'); }
else { exportNode.classList.add('hide'); }
}
catch (err) {
// Server error?
showError('Error: ' + err.message);
}
}
getChildren(0);
}
function setSpans(className, value) {
const elems = document.getElementsByClassName(className);
for (let i = 0; i < elems.length; i++) {
elems.item(i).textContent = value;
}
}
function connectionChanged(success) {
if (!success) {
return;
}
setSpans('connected_user', connection.username);
setSpans('connected_db', connection.dbname);
let tabs = M.Tabs.getInstance(document.getElementById('main_tabs'));
tabs.select('browse');
updateBrowsePath();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function impersonateUser() {
// Called from index.html
const impersonateElem = document.getElementById('impersonate_uid');
let uid = window.prompt('Enter the user id (uid) to impersonate:', impersonateElem.value);
let impersonating = false;
if (uid === '') { uid = 'anonymous'; }
if (uid) {
impersonating = true;
}
impersonateElem.value = uid || '';
document.getElementById('impersonate_disabled').style.display = impersonating ? 'none' : 'inline';
document.getElementById('impersonate_enabled').style.display = impersonating ? 'inline' : 'none';
setSpans('impersonate_uid_label', impersonateElem.value);
updateBrowsePath(currentPath);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function specifyChildKey() {
// Called from index.html
let key = window.prompt('Enter the child key to browse to:', '');
if (typeof key !== 'string') { return; }
if (currentPathIsArray && /^[0-9]+$/.test(key)) {
// index
key = parseInt(key);
}
const targetPath = getChildPath(currentPath, key);
updateBrowsePath(targetPath);
}
const connectButton = document.getElementById('connect_button');
connectButton.addEventListener('click', async () => {
const connectingLabel = document.getElementById('connecting');
const successLabel = document.getElementById('connect_success');
const failLabel = document.getElementById('connect_fail');
const failReasonLabel = document.getElementById('fail_reason');
successLabel.classList.add('hide');
failLabel.classList.add('hide');
const dbname = document.getElementById('dbname').value;
const username = document.getElementById('username').value; //'admin'
const password = document.getElementById('password').value;
if (username !== 'admin') {
M.toast({html: 'Sorry! Only the admin user can currently use this interface'});
return;
}
try {
connectButton.classList.add('disabled');
connectingLabel.classList.remove('hide');
await connect(dbname, username, password);
successLabel.classList.remove('hide');
connectionChanged(true);
}
catch (err) {
failLabel.classList.remove('hide');
failReasonLabel.textContent = err.message;
connectionChanged(false);
}
finally {
connectButton.classList.remove('disabled');
connectingLabel.classList.add('hide');
}
});
document.getElementById('browse_breadcrumb_root').addEventListener('click', () => {
updateBrowsePath('');
});
document.getElementById('export_json').addEventListener('click', () => {
// Will initiate a download:
const url = `/export/${connection.dbname}/${currentPath}?format=json&auth_token=${connection.auth_token}`;
window.location.href = url;
});
document.getElementById('edit_node').addEventListener('click', () => {
document.getElementById('edit_form').classList.remove('hide');
});
document.getElementById('update_button').addEventListener('click', async () => {
const textarea = document.getElementById('update_json');
let json = textarea.value;
try {
// Check update value
if (!json.startsWith('{') || !json.endsWith('}')) { throw new Error('Value must be json'); }
// Parse object
let updates = JSON.parse(json);
// Allow passed data to be serialized by Transport.serialize2:
// this allows dates etc to be used: { "created": { ".type": "date", ".val": "2022-05-31T16:52:51Z" } }
updates = acebaseclient.Transport.deserialize2(updates);
console.log(updates);
if (typeof updates !== 'object') { throw new Error('value must be an object'); }
// Update
await connection.db.ref(currentPath).update(updates);
}
catch (err) {
M.toast({ html: `Error: ${err.message}` });
}
});
// TODO: create a stand-alone PWA (Ionic?) that has more functionality, such as:
// - connecting to multiple servers
// - edit data
// - create (and store) queries on data
// - monitoring
// - editing rules
// - managing users
// - etc!