lively.storage
Version:
Database interface for lively.next
1,076 lines (914 loc) • 39.1 kB
JavaScript
/*global System,process,require,fetch*/
import Database from "./database.js";
import { resource } from "lively.resources";
import { obj } from "lively.lang";
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// sha1
// Author: creationix
// Repo: https://github.com/creationix/git-sha1
// License: MIT https://github.com/creationix/git-sha1/blob/b3474591e6834232df63b5cf9bb969185a54a04c/LICENSE
const sha1 = (function sha1_setup(){function r(r){if(void 0===r)return o(!1);var e=o(!0);return e.update(r),e.digest()}function e(){var r=f.createHash("sha1");return{update:function(e){return r.update(e)},digest:function(){return r.digest("hex")}}}function t(r){function e(r){if("string"==typeof r)return t(r);var e=r.length;h+=8*e;for(var n=0;n<e;n++)o(r[n])}function t(r){var e=r.length;h+=8*e;for(var t=0;t<e;t++)o(r.charCodeAt(t))}function o(r){a[y]|=(255&r)<<g,g?g-=8:(y++,g=24),16===y&&u()}function f(){o(128),(y>14||14===y&&g<24)&&u(),y=14,g=24,o(0),o(0),o(h>0xffffffffff?h/1099511627776:0),o(h>4294967295?h/4294967296:0);for(var r=24;r>=0;r-=8)o(h>>r);return i(s)+i(c)+i(v)+i(p)+i(d)}function u(){for(var r=16;r<80;r++){var e=a[r-3]^a[r-8]^a[r-14]^a[r-16];a[r]=e<<1|e>>>31}var t,n,o=s,f=c,u=v,i=p,g=d;for(r=0;r<80;r++){r<20?(t=i^f&(u^i),n=1518500249):r<40?(t=f^u^i,n=1859775393):r<60?(t=f&u|i&(f|u),n=2400959708):(t=f^u^i,n=3395469782);var h=(o<<5|o>>>27)+t+g+n+(0|a[r]);g=i,i=u,u=f<<30|f>>>2,f=o,o=h}for(s=s+o|0,c=c+f|0,v=v+u|0,p=p+i|0,d=d+g|0,y=0,r=0;r<16;r++)a[r]=0}function i(r){for(var e="",t=28;t>=0;t-=4)e+=(r>>t&15).toString(16);return e}var a,s=1732584193,c=4023233417,v=2562383102,p=271733878,d=3285377520,y=0,g=24,h=0;return a=r?n:new Uint32Array(80),{update:e,digest:f}}var n,o,f;return"object"==typeof process&&"object"==typeof process.versions&&process.versions.node&&"renderer"!==process.__atom_type?(f="undefined"!=typeof System?System._nodeRequire("crypto"):require("crypto"),o=e):(n=new Uint32Array(80),o=t),r})();
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// let db = await ObjectDB.find("test-object-db");
// db = objectDBs.get("lively.morphic/objectdb/morphicdb")
// await db.objectStats()
var objectDBs = objectDBs || new Map();
export default class ObjectDB {
static async find(name) {
let found = objectDBs.get(name);
if (found) return found;
let metaDB = Database.ensureDB("__internal__objectdb-meta"),
meta = await metaDB.get(name);
if (!meta) return;
return this.named(name, meta);
}
static named(name, options = {}) {
let existing = objectDBs.get(name);
if (existing) return existing;
if (!options || !options.snapshotLocation)
throw new Error("need snapshotLocation");
if (typeof options.snapshotLocation === "string") {
try { options.snapshotLocation = resource(options.snapshotLocation); }
catch (err) { options.snapshotLocation = resource(System.baseURL)
.join(options.snapshotLocation); }
}
let db = new this(name, options);
objectDBs.set(name, db);
let metaDB = Database.ensureDB("__internal__objectdb-meta");
metaDB.set(name, {...options, snapshotLocation: options.snapshotLocation.url})
.catch(err => console.error(`error writing objectdb meta:`, err));
return db;
}
constructor(name, options) {
this.name = name;
if (!options.snapshotLocation || !options.snapshotLocation.isResource)
throw new Error(`ObjectDB needs snapshotLocation!`);
this.snapshotLocation = options.snapshotLocation;
this.__commitDB = null;
this.__versionDB = null;
}
async destroy() {
let commitDB = Database.findDB(this.name + "-commits");
if (commitDB) await commitDB.destroy();
let versionDB = Database.findDB(this.name + "-version-graph");
if (versionDB) await versionDB.destroy();
objectDBs.delete(this.name);
let metaDB = Database.ensureDB("__internal__objectdb-meta");
await metaDB.remove(this.name);
// await this.snapshotLocation.remove()
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// storage
snapshotResourceFor(commit) {
// content is sha1 hash
let first = commit.content.slice(0, 2),
rest = commit.content.slice(2);
return this.snapshotLocation.join(`${first}/${rest}.json`);
}
async snapshotObject(type, name, object, snapshotOptions, commitSpec, preview, ref, expectedPrevVersion) {
snapshotOptions = snapshotOptions || {};
let serializeFn = x => x,
snapshot = await serializeFn(object, snapshotOptions);
return this.commit(type, name, snapshot, commitSpec, preview, ref, expectedPrevVersion);
}
async loadObject(type, name, loadOptions, commitIdOrCommit, ref) {
loadOptions = loadOptions || {}
let snapshot = await this.loadSnapshot(type, name, commitIdOrCommit, ref),
deserializeFn = x => x;
return deserializeFn(snapshot, loadOptions);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// data management
async has(type, name) { return !!(await this.objectStats(type, name)); }
async objects(type) {
let stats = await this.objectStats(type);
if (type) return Object.keys(stats || {});
let result = {};
for (let type in stats)
result[type] = Object.keys(stats[type]);
return result;
}
async objectStats(objectType, objectName) {
let statsByType = {},
commitDB = this.__commitDB || await this._commitDB(),
queryOpts = {reduce: true, group: true};
if (objectType && objectName) {
queryOpts.key = `${objectType}\u0000${objectName}`;
// queryOpts.endkey = `${objectType}\u0000${objectName}`;
} else if (objectType) {
// queryOpts.key = objectType;
queryOpts.startkey = `${objectType}\u0000`;
queryOpts.endkey = `${objectType}\ufff0`;
}
try {
let {rows} = await commitDB.pouchdb.query("nameWithMaxMinTimestamp_index", queryOpts);
for (let {key: objectTypeAndName, value: {count, max: newest, min: oldest}} of rows) {
let [type, objectName] = objectTypeAndName.split("\u0000"),
statsOfType = statsByType[type] || (statsByType[type] = {});
statsOfType[objectName] = {count, newest, oldest};
}
} catch (err) {
console.error(err);
return statsByType;
}
if (objectType && objectName) return (statsByType[objectType] || {})[objectName];
if (objectType) return statsByType[objectType];
return statsByType;
}
async getCommits(type, objectName, ref = "HEAD", limit = Infinity) {
let history = await this._log(type, objectName, ref, limit);
if (!history.length) return [];
let commitDB = this.__commitDB || await this._commitDB(),
commits = await commitDB.getDocuments(history.map(ea => ({id: ea})));
return commits;
}
async getCommit(commitId) {
let commitDB = this.__commitDB || await this._commitDB();
return commitDB.get(commitId);
}
async getCommitsWithIds(commitIds) {
if (!commitIds.length) return [];
let commitDB = this.__commitDB || await this._commitDB();
return commitDB.getDocuments(commitIds.map(id => ({id})));
}
async getLatestCommit(type, objectName, ref = "HEAD", includeDeleted = false) {
let [commitId] = await this._log(type, objectName, ref, 1);
if (!commitId) return null;
let commitDB = this.__commitDB || await this._commitDB();
let commit = await commitDB.get(commitId);
if (commit && commit.deleted && !includeDeleted) return null;
return commit;
}
async commit(type, name, snapshot, commitSpec, preview, ref = "HEAD", expectedPrevVersion) {
let {
user,
description = "no description",
tags = [],
message = "",
metadata
} = commitSpec;
if (!type) throw new Error("object needs a type");
if (!name) throw new Error("object needs a name");
if (!user) throw new Error(`Cannot commit ${type}/${name} without user`);
// Retrieve version graph for object. Check if the prev version requirement
// is met and get the ancestors
let versionDB = this.__versionDB || await this._versionDB(),
versionData = await this.versionGraph(type, name),
ancestor = versionData ? versionData.refs[ref] : null,
ancestors = ancestor ? [ancestor] : [];
if (expectedPrevVersion) {
if (!versionData) throw new Error(`Trying to store "${type}/${name}" on top of expected version ${expectedPrevVersion} but no version entry exists!`);
if (ancestor !== expectedPrevVersion) throw new Error(`Trying to store "${type}/${name}" on top of expected version ${expectedPrevVersion} but ref ${ref} is of version ${ancestor}!`);
}
// Snapshot object and create commit.
let snapshotJson = snapshot ? JSON.stringify(snapshot) : null,
commit = this._createCommit(
type, name, description, tags, metadata,
user, message, ancestors,
snapshot, snapshotJson, preview);
// update version graph
if (!versionData) versionData = {refs: {}, history: {}};
versionData.refs[ref] = commit._id;
versionData.history[commit._id] = ancestors;
await versionDB.set(type + "/" + name, versionData);
// store the commit
let commitDB = this.__commitDB || await this._commitDB();
commit = await commitDB.set(commit._id, commit);
// write snapshot to resource
if (snapshot) {
let res = this.snapshotResourceFor(commit);
await res.parent().ensureExistance();
if (res.canDealWithJSON) await res.writeJson(snapshot);
else await res.write(snapshotJson);
}
return commit;
}
async loadSnapshot(type, name, commitOrId, ref = "HEAD") {
let commit;
if (commitOrId && typeof commitOrId !== "string") {
commit = commitOrId;
} else if (commitOrId) {
let commitDB = this.__commitDB || await this._commitDB();
commit = await commitDB.get(commitOrId);
} else {
commit = await this.getLatestCommit(type, name, ref);
}
if (!commit)
throw new Error(`Cannot find commit to loadSnapshot for ${type}/${name} (using ${commitOrId})`)
return this.snapshotResourceFor(commit).readJson();
}
_createCommit(
type, name, description, tags, metadata, user,
message = "", ancestors = [],
snapshot, snapshotJson, preview
) {
if (!preview && snapshot && snapshot.preview) preview = snapshot.preview;
let commit = {
name, type, timestamp: Date.now(),
author: {
name: user.name,
email: user.email,
realm: user.realm
},
tags: [], description,
message,
preview,
content: snapshotJson ? sha1(snapshotJson) : null,
deleted: !snapshot,
metadata,
ancestors
}
let hashObj = obj.dissoc(commit, ["preview"]),
commitHash = sha1(JSON.stringify(hashObj));
return Object.assign(commit, {_id: commitHash});
}
async _commitDB() {
if (this.__commitDB) return this.__commitDB;
let dbName = this.name + "-commits",
db = Database.findDB(dbName);
if (db) return this.__commitDB = db;
db = Database.ensureDB(dbName);
// prepare indexes
let hasIndexes = await Promise.all([
db.has("_design/name_index"),
db.has("_design/nameAndTimestamp_index"),
db.has("_design/nameWithMaxMinTimestamp_index")
]);
if (!hasIndexes.every(Boolean)) {
console.log(`Preparing indexes for object storage DB ${dbName}`);
var nameIndex = {
_id: '_design/name_index',
views: {'name_index': {map: 'function (doc) { emit(`${doc.type}\u0000${doc.name}}`); }'}}},
nameAndTimestampIndex = {
_id: '_design/nameAndTimestamp_index',
views: {'nameAndTimestamp_index': {
map: "function (doc) { emit(`${doc.type}\u0000${doc.name}\u0000${doc.timestamp}}\u0000${doc._id}}`); }"}}},
nameWithMaxMinTimestamp = {
_id: '_design/nameWithMaxMinTimestamp_index',
views: {
'nameWithMaxMinTimestamp_index': {
map: 'doc => emit(`${doc.type}\u0000${doc.name}`, doc.timestamp)',
reduce: "_stats"}}};
await db.setDocuments([nameIndex, nameAndTimestampIndex, nameWithMaxMinTimestamp]);
await Promise.all([
db.pouchdb.query('name_index', {stale: 'update_after'}),
db.pouchdb.query('nameAndTimestamp_index', {stale: 'update_after'}),
db.pouchdb.query("nameWithMaxMinTimestamp_index", {stale: 'update_after'}),
]);
}
return this.__commitDB = db;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// versioning
async versionGraph(type, objectName) {
let versionDB = this.__versionDB || await this._versionDB(),
graph = await versionDB.get(type + "/" + objectName);
return !graph || graph.deleted ? null : graph;
}
async _log(type, objectName, ref = "HEAD", limit = Infinity) {
let data = await this.versionGraph(type, objectName);
if (!data || data.deleted) return [];
let version = data.refs.HEAD, history = [];
while (true) {
if (history.includes(version))
throw new Error("cyclic version graph???");
history.push(version);
// FIXME what about multiple ancestors?
[version] = data.history[version] || [];
if (!version || history.length >= limit) break;
}
return history;
}
async _findTimestampedVersionsOfObjectNamed(objectName, options = {}) {
// other opts: {limit, include_docs}
let {
include_docs = true,
descending = true,
startTime = "0".repeat(13),
endTime = "9".repeat(13),
} = options,
startkey = `${objectName}\u0000${descending ? endTime : startTime}`,
endkey = `${objectName}\u0000${descending ? startTime : endTime}`,
objectDB = this.__commitDB || await this._commitDB(),
{rows} = await objectDB.pouchdb.query("nameAndTimestamp_index", {
...options,
descending,
include_docs,
startkey,
endkey
});
return include_docs ? rows.map(ea => ea.doc) : rows.map(ea => ea.id);
}
async _versionDB() {
if (this.__versionDB) return this.__versionDB;
let dbName = this.name + "-version-graph",
db = Database.findDB(dbName);
if (db) return this.__versionDB = db;
db = Database.ensureDB(dbName);
// var typeAndNameIndex = {
// _id: '_design/type_name_index',
// views: {'name_index': {map: 'function (doc) { emit(`${doc.type}\u0000${doc.name}}`); }'}}};
// db.setDocuments([typeAndNameIndex]);
// await Promise.alll([db.pouchdb.query('type_name_index', {stale: 'update_after'})]);
return this.__versionDB = db;
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// export
async exportToDir(exportDir, nameAndTypes, copyResources = false, includeDeleted = false) {
let commitDB = this.__commitDB || await this._commitDB();;
for (let {name, type} of nameAndTypes) {
let currentExportDir = exportDir.join(type).join(name).asDirectory(),
{refs, history} = await this.versionGraph(type, name),
commitIds = Object.keys(history),
commits = await this.getCommitsWithIds(commitIds);
if (!includeDeleted)
commits = commits.filter(ea => !ea.deleted);
let resourcesForCopy = copyResources ? commits.map(commit => {
delete commit._rev;
let from = this.snapshotResourceFor(commit),
to = currentExportDir.join(from.parent().name() + "/" + from.name());
return {from, to};
}) : [];
if (!copyResources) commits.forEach(commit => { delete commit._rev; });
await currentExportDir.ensureExistance();
await currentExportDir.join("index.json").writeJson({name, type});
await currentExportDir.join("commits.json").writeJson(commits);
await currentExportDir.join("history.json").writeJson({refs, history});
for (let {from, to} of resourcesForCopy)
await from.copyTo(to);
}
}
async exportToSpecs(nameAndTypes, includeDeleted = false) {
// note: only version data, no snapshots!
let specs = [];
if (!nameAndTypes) { // = everything
nameAndTypes = [];
let stats = (await this.objectStats()) || {};
for (let type in stats)
for (let name in stats[type])
nameAndTypes.push({type, name})
}
for (let {name, type} of nameAndTypes) {
let {refs, history} = await this.versionGraph(type, name),
commitIds = Object.keys(history),
commits = await this.getCommitsWithIds(commitIds);
if (!includeDeleted)
commits = commits.filter(ea => !ea.deleted);
commits.forEach(commit => { delete commit._rev; });
specs.push({type, name, commits, history: {refs, history}})
}
return specs;
}
async importFromDir(importDir, overwrite = false, copyResources = false) {
// let commitDB = this.__commitDB || await this._commitDB();;
// 1. discover type/names;
// depth 1: type dirs, depth 2: object dirs, those include index.json, ...
let indexes = await importDir.dirList(3, {exclude: ea => !ea.isDirectory() && ea.name() !== "index.json"})
indexes = indexes.filter(ea => ea.name() === "index.json"); // FIXME!
let dirs = indexes.map(ea => ea.parent());
let {snapshotLocation} = this, importSpecs = [];
// 2. retrieve import data
for (let dir of dirs) importSpecs.push(await findImportDataIn(dir));
return this.importFromSpecs(importSpecs, overwrite, copyResources);
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
async function findImportDataIn(dir) {
// dir is directory with index.json etc.
let [{type, name}, commits, history] = await Promise.all([
dir.join("index.json").readJson(),
dir.join("commits.json").readJson(),
dir.join("history.json").readJson(),
]),
snapshotDirs = copyResources ?
await dir.dirList(1, {exclude: ea => !ea.isDirectory()}) : [];
return {dir, type, name, commits, history, snapshotDirs};
}
}
async importFromSpecs(specs, overwrite = false, copyResources = false) {
if (!overwrite) {
let versionDB = this.__versionDB || await this._versionDB();
for (let {type, name} of specs) {
if (await versionDB.get(`${type}/${name}`))
throw new Error(`Import failed: object ${type}/${name} already exists and overwrite is not allowed`);
}
}
for (let spec of specs)
await this.importFromSpec(spec, true, copyResources);
return specs;
}
async importFromSpec(spec, overwrite = false, copyResources = false) {
let versionDB = this.__versionDB || await this._versionDB(),
commitDB = this.__commitDB || await this._commitDB(),
{snapshotLocation} = this,
{type, name, commits, history, snapshotDirs} = spec;
if (!overwrite && await versionDB.get(`${type}/${name}`))
throw new Error(`Import failed: object ${type}/${name} already exists and overwrite is not allowed`);
await Promise.all([
commitDB.setDocuments(commits),
versionDB.set(`${type}/${name}`, history),
...(snapshotDirs && copyResources
? snapshotDirs.map(ea =>
ea.copyTo(snapshotLocation.join(ea.name()).asDirectory())) : [])
]);
return spec;
}
async importFromResource(type, name, resource, commitSpec, purgeHistory = false) {
let snap = await resource.readJson();
if (purgeHistory && await this.has(type, name))
await this.delete(type, name, false);
return this.commit(type, name, snap, commitSpec);
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// deletion
async delete(type, name, dryRun = true) {
let resources = [],
commitDeletions = [];
// 1. meta data to delete
let objectDB = this.__commitDB || await this._commitDB(),
opts = {
include_docs: true,
startkey: `${type}\u0000${name}\u0000`,
endkey: `${type}\u0000${name}\uffff`
},
{rows} = await objectDB.query("nameAndTimestamp_index", opts);
for (let {doc: commit} of rows) {
// 2. resources to delete
resources.push(this.snapshotResourceFor(commit));
commitDeletions.push({...commit, _deleted: true});
}
// 3. history to delete
let versionDB = this.__versionDB || await this._versionDB(),
{_id, _rev} = await versionDB.get(type + "/" + name),
deletedHist = {_id, _rev, deleted: true}
if (!dryRun) {
await objectDB.setDocuments(commitDeletions);
await versionDB.setDocuments([deletedHist]);
Promise.all(resources.map(ea => ea.remove()));
}
return {
commits: commitDeletions,
history: deletedHist,
resources
}
}
async deleteCommit(commitOrId, dryRun = true, ref = "HEAD") {
let commit;
if (commitOrId && typeof commitOrId !== "string") {
commit = commitOrId;
} else if (commitOrId) {
let commitDB = this.__commitDB || await this._commitDB();
commit = await commitDB.get(commitOrId);
}
if (!commit) throw new Error("commit needed!");
let versionDB = this.__versionDB || await this._versionDB(),
objectDB = this.__commitDB || await this._commitDB(),
{name, type, _id} = commit,
resources = [this.snapshotResourceFor(commit)],
commitDeletions = [{...commit, _deleted: true}],
hist = await versionDB.get(type + "/" + name);
if (!hist) throw new Error(`No history for ${type}/${name}@${commit._id}`);
if (!hist.refs[ref]) throw new Error(`Cannot delete commit ${type}/${name}@${commit._id} b/c it is not where ref ${ref} is pointing!`);
let [ancestor] = hist.history[commit._id] || [];
if (!ancestor && Object.keys(hist.history).length <= 1) {
hist.deleted = true;
} else if (!ancestor) {
throw new Error(`Cannot delete commit ${type}/${name}@${commit._id} b/c it has no ancestor but there are still other commits!`);
} else {
delete hist.history[commit._id];
hist.refs[ref] = ancestor;
}
if (!dryRun) {
await objectDB.setDocuments(commitDeletions);
await versionDB.set(type + "/" + name, hist);
Promise.all(resources.map(ea => ea.remove()));
}
return {
commits: commitDeletions,
history: hist,
resources
}
}
}
function checkArg(name, value, spec) {
if (typeof value === "undefined" && typeof spec === "string" && !spec.includes("undefined"))
throw new Error(`parameter ${name} is undefined`);
if (typeof spec === "string") {
let actualType = typeof value,
actualClass = value ? value.constructor.name : "",
types = spec.split("|"),
matches = types.some(t => actualType === t || actualClass === t)
if (!matches)
throw new Error(`parameter "${name}" expected to be of type ${spec} but is ${actualClass || actualType}`)
}
if (typeof spec === "function") {
let result = spec(value);
if (result && result.error)
throw new Error(`check of parameter "${name}" failed: ${result.error}`)
}
}
function checkArgs(args, specs, testFn) {
for (let key in specs)
checkArg(key, args[key], specs[key]);
if (typeof testFn === "function") {
let result = testFn(args);
if (result && result.error)
throw new Error(result.error);
}
return args;
}
export var ObjectDBInterface = {
async describe(method) {
// await ObjectDBInterface.describe(method = "importFromResource")
// await ObjectDBInterface.describe()
try {
if (!this._methodSpecs) {
let src = await lively.modules.module("lively.storage/objectdb.js").source(),
parsed = lively.ast.parse(src, {withComments: true}),
entities = lively.ast.categorizer.findDecls(parsed);
this._methodSpecs = entities.filter(ea => ea.parent && ea.parent.name === "ObjectDBInterface");
}
return method ? methodNameAndParametersAndDescription(this._methodSpecs, method) :
this._methodSpecs
.map(ea => methodNameAndParametersAndDescription(this._methodSpecs, ea.name))
.filter(Boolean)
} catch (err) { return `Error in describe ${err}`; }
function methodNameAndParametersAndDescription(methodSpecs, name) {
let methodSpec = methodSpecs.find(ea => ea.name === name),
body = methodSpec.node.value.body,
stmts = body.body || [],
comment = (body.comments || []).find(ea => ea.end < stmts[0].start),
doc = {name, parameters: [], sideEffect: false, returns: null, description: ""};
if (comment && comment.text.trim()) {
let text = lively.lang.string.changeIndent(comment.text, " ", 0),
commentLines = text.split("\n");
for (let line of commentLines) {
if (line.startsWith("ignore-in-doc")) { doc.description = ""; break; }
if (line.startsWith("side effect:")) {
doc.sideEffect = JSON.parse(line.split(":")[1]);
continue;
}
if (line.startsWith("returns:")) {
doc.returns = line.split(":")[1].trim();
continue;
}
doc.description += line + "\n";
}
}
for (let stmt of stmts) {
if ("checkArgs" !== lively.lang.Path("declarations.0.init.callee.name").get(stmt))
continue;
let props = lively.lang.Path("declarations.0.id.properties").get(stmt);
if (props) {
doc.parameters = props.map(ea => ea.key.name);
}
}
return doc
}
},
async ensureDB(args) {
// side effect: true
// returns: boolean
// Ensures that a database with the name `db` exists. If not, creates one
// and sets it up so that snapshots that get committed are stored into
// snapshotLocation. The return value indicates if a new DB was create
// (true) or if one already existed (false).
let {db: dbName, snapshotLocation} = checkArgs(args, {
db: "string",
snapshotLocation: "string|Resource"
}), db = await ObjectDB.find(dbName);
if (db) return false;
ObjectDB.named(dbName, {snapshotLocation});
return true;
},
async destroyDB(args) {
// side effect: true
// returns: boolean
// Removes the DB with the name `db`. Returns true if such a DB existed and
// if it was destroyed
let {db: dbName} = checkArgs(args, {db: "string"}),
db = await ObjectDB.find(dbName);
if (!db) return false;
await db.destroy();
return true;
},
async fetchCommits(args) {
// side effect: false
// returns: [commits]
// Gets the lates commits from all objects specified in `typesAndNames`.
let {
db: dbName, ref, type,
typesAndNames, knownCommitIds, includeDeleted
} = checkArgs(args, {
db: "string",
ref: "string|undefined",
type: "string|undefined",
typesAndNames: "Array|undefined",
knownCommitIds: "object|undefined",
includeDeleted: "boolean|undefined"
}), db = await ObjectDB.find(dbName)
if (!ref) ref = "HEAD";
if (!db) throw new Error(`db ${dbName} does not exist`);
let commitDB = db.__commitDB || await db._commitDB(),
versionDB = db.__versionDB || await db._versionDB();
let versionQueryOpts = {},
refsByTypeAndName = {};
if (typesAndNames) {
let keys = versionQueryOpts.keys = [];
for (let {type, name, ref} of typesAndNames) {
keys.push(`${type}/${name}`);
if (ref) refsByTypeAndName[`${type}/${name}`] = ref;
}
} else if (type) {
versionQueryOpts.startkey = `${type}/\u0000"`;
versionQueryOpts.endkey = `${type}/\uffff"`;
}
let versions = await versionDB.getAll(versionQueryOpts), commitIds = [];
for (let version of versions) {
if (version.deleted) continue;
let {_id, refs} = version;
ref = refsByTypeAndName[_id] || ref;
let commitId = refs[ref];
if (commitId && !knownCommitIds
|| !knownCommitIds.hasOwnProperty(commitId))
commitIds.push(commitId);
}
let commits = await db.getCommitsWithIds(commitIds);
if (!includeDeleted)
commits = commits.filter(ea => !ea.deleted);
return commits;
},
async fetchVersionGraph(args) {
// side effect: false
// returns: {refs: {refName => commitId}, history: [commitId]}
let {db: dbName, type, name} = checkArgs(args, {
db: "string",
type: "string",
name: "string"
}),
db = await ObjectDB.find(dbName);
if (!db) throw new Error(`db ${dbName} does not exist`);
let {refs, history} = await db.versionGraph(type, name);
return {refs, history};
},
async fetchLog(args) {
// side effect: false
// returns: [commitIds]|[commits]
let {
db: dbName, type, name, ref,
commit, limit, includeCommits,
knownCommitIds
} = checkArgs(args, {
db: "string",
type: "string|undefined",
name: "string|undefined",
ref: "string|undefined",
commit: "string|undefined",
limit: "number|undefined",
includeCommits: "boolean|undefined",
knownCommitIds: "object|undefined",
}, args => args.type && args.name || args.commit
? null : {error: `Eiter .type + .name or .commit needed!`}),
db = await ObjectDB.find(dbName),
defaultRef = ref || "HEAD";
if (!db) throw new Error(`db ${dbName} does not exist`);
if (!limit) limit = Infinity;
if (!commit && !ref) ref = defaultRef;
let startCommitId;
if (commit) {
startCommitId = commit;
if (!type || !name) ({type, name} = await db.getCommit(commit));
}
let versionGraph = await db.versionGraph(type, name);
if (!versionGraph) throw new Error(`Unknown object ${type}/${name}`);
let {refs, history} = versionGraph;
if (!startCommitId) startCommitId = refs[ref];
let currentCommit = startCommitId, result = [];
while (result.length < limit && !result.includes(currentCommit)) {
result.push(currentCommit);
let ancestors = history[currentCommit];
if (!ancestors || !ancestors.length) break;
[currentCommit] = ancestors;
}
if (includeCommits) {
if (knownCommitIds) result = result.filter(id => !knownCommitIds.hasOwnProperty(id));
result = await db.getCommitsWithIds(result);
}
return result;
},
async fetchSnapshot(args) {
// side effect: false
// returns: object
let {db: dbName, type, name, ref, commit: commitId} = checkArgs(args, {
db: "string",
type: "string|undefined",
name: "string|undefined",
ref: "string|undefined",
commit: "string|undefined"
}, args => args.type && args.name || args.commit
? null : {error: `Eiter .type + .name or .commit needed!`}),
db = await ObjectDB.find(dbName),
defaultRef = "HEAD";
ref = ref || defaultRef;
if (!db) throw new Error(`db ${dbName} does not exist`);
if (!commitId) {
let versionGraph = await db.versionGraph(type, name);
if (!versionGraph) throw new Error(`Unknown object ${type}/${name}`);
commitId = versionGraph.refs[ref];
if (!commitId) throw new Error(`Cannot find commit for ref ${ref} of ${type}/${name}`);
}
let commit = await db.getCommit(commitId);
if (!commit) throw new Error(`Cannot find commit ${commitId}`);
return db.loadSnapshot(undefined, undefined, commit);
},
async commit(args) {
// side effect: true
// returns: commit
let {
db: dbName, type, name, ref,
expectedParentCommit, commitSpec,
snapshot, preview
} = checkArgs(args, {
db: "string",
type: "string", name: "string",
ref: "string|undefined",
snapshot: "object",
preview: "string|undefined",
commitSpec: "object",
expectedParentCommit: "string|undefined"
}), db = await ObjectDB.find(dbName);
if (!ref) ref = "HEAD";
return db.commit(type, name, snapshot, commitSpec, preview, ref, expectedParentCommit);
},
async exportToSpecs(args) {
// side effect: false
// returns: {name: String, type: String, history: {}, commits: [commit]}
let {db: dbName, nameAndTypes} = checkArgs(args, {
db: "string",
nameAndTypes: "Array|undefined",
includeDeleted: "boolean|undefined"
}), db = await ObjectDB.find(dbName);
if (!db) throw new Error(`db ${dbName} does not exist`);
return db.exportToSpecs(nameAndTypes);
},
async exportToDir(args) {
// side effect: true
// returns: undefined
let {db: dbName, url, nameAndTypes, copyResources, includeDeleted} = checkArgs(args, {
db: "string",
url: "string",
nameAndTypes: "Array|undefined",
copyResources: "boolean|undefined",
includeDeleted: "boolean|undefined"
}), db = await ObjectDB.find(dbName), exportDir;
if (!db) throw new Error(`db ${dbName} does not exist`);
try { exportDir = resource(url); }
catch (err) { exportDir = resource(System.baseURL).join(url); }
return db.exportToDir(exportDir, nameAndTypes, copyResources, includeDeleted);
},
async importFromDir(args) {
// side effect: true
// returns: [{dir, type, name, commits, history, snapshotDirs}]
let {db: dbName, url, overwrite, copyResources} = checkArgs(args, {
db: "string", url: "string",
overwrite: "boolean|undefined",
copyResources: "boolean|undefined"
}), db = await ObjectDB.find(dbName), importDir;
if (!db) throw new Error(`db ${dbName} does not exist`);
try { importDir = resource(url); }
catch (err) { importDir = resource(System.baseURL).join(url); }
return db.importFromDir(importDir, overwrite, copyResources);
},
async importFromSpecs(args) {
// side effect: true
// returns: [{dir, type, name, commits, history, snapshotDirs}]
let {db: dbName, specs, overwrite, copyResources} = checkArgs(args, {
db: "string",
specs: "object",
overwrite: "boolean|undefined",
copyResources: "boolean|undefined"
}), db = await ObjectDB.find(dbName);
if (!db) throw new Error(`db ${dbName} does not exist`);
return db.importFromSpecs(specs, overwrite, copyResources);
},
async importFromResource(args) {
// side effect: true
// returns: [{dir, type, name, commits, history, snapshotDirs}]
// Example:
// let user = select($world.getCurrentUser(), ["name", "realm", "email"])
// let commitSpec = {user, description: "An empty world.", metadata: {belongsToCore: true}};
// let result = ObjectDBInterface.importFromResource({
// db: "test-object-db",
// url: "lively.morphic/worlds/default.json",
// type: "world", name: "default", commitSpec, purgeHistory: true
// })
let {db: dbName, type, name, url, commitSpec, purgeHistory} = checkArgs(args, {
db: "string",
type: "string", name: "string",
url: "string",
commitSpec: "object",
purgeHistory: "boolean|undefined"
}), db = await ObjectDB.find(dbName), res;
if (!db) throw new Error(`db ${dbName} does not exist`);
try { res = resource(url); } catch (err) { res = resource(System.baseURL).join(url); }
return db.importFromResource(type, name, res, commitSpec, purgeHistory);
}
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// let httpDB = new ObjectDBHTTPInterface()
// await httpDB.exportToSpecs({db: "test-object-db"})
export class ObjectDBHTTPInterface {
constructor(serverURL = document.origin + "/objectdb/") {
this.serverURL = serverURL;
}
async _processResponse(res) {
let contentType = res.headers.get("content-type"),
answer = await res.text(), json;
if (contentType === "application/json") {
try { json = JSON.parse(answer); } catch (err) {}
}
if (!res.ok || json.error) {
throw new Error((json && json.error) || answer || res.statusText);
}
return json || answer;
}
async _GET(action, opts = {}) {
let query = Object.keys(opts).map(key => {
let val = opts[key];
if (typeof val === "object") val = JSON.stringify(val);
return `${key}=${encodeURIComponent(val)}`;
}).join("&"),
url = this.serverURL + action + "?" + query;
return this._processResponse(await fetch(url));
}
async _POST(action, opts = {}) {
let url = this.serverURL + action;
return this._processResponse(await fetch(url, {
method: "POST", body: JSON.stringify(opts),
headers: {"content-type": "application/json"}
}));
}
async describe(args) {
// parameters:
// returns: null
return this._GET("describe", args);
}
async ensureDB(args) {
// parameters: db, snapshotLocation
// returns: boolean
return this._POST("ensureDB", args);
}
async destroyDB(args) {
// parameters: db
// returns: boolean
return this._POST("destroyDB", args);
}
async fetchCommits(args) {
// parameters: db, ref, type, typesAndNames, knownCommitIds, includeDeleted
// returns: [commits]
return this._GET("fetchCommits", args);
}
async fetchVersionGraph(args) {
// parameters: db, type, name
// returns: {refs, history}
return this._GET("fetchVersionGraph", args);
}
async fetchLog(args) {
// parameters: db, type, name, ref, commit, limit, includeCommits, knownCommitIds
// returns: [commitIds]|[commits]
return this._GET("fetchLog", args);
}
async fetchSnapshot(args) {
// parameters: db, type, name, ref, commit
// returns: object
return this._GET("fetchSnapshot", args);
}
async commit(args) {
// parameters: db, type, name, ref, expectedParentCommit, commitSpec, preview, snapshot
// returns: commit
return this._POST("commit", args);
}
async exportToSpecs(args) {
// parameters: db, nameAndTypes, includeDeleted
// returns: [{dir, type, name, commits, history, snapshotDirs}]
return this._GET("exportToSpecs", args);
}
async exportToDir(args) {
// parameters: db, url, nameAndTypes, copyResources
// returns: undefined
return this._POST("exportToDir", args);
}
async importFromDir(args) {
// parameters: db, url, overwrite, copyResources
// returns: [{dir, type, name, commits, history, snapshotDirs}]
return this._POST("importFromDir", args);
}
async importFromSpecs(args) {
// parameters: db, specs, overwrite, copyResources
// returns: [{dir, type, name, commits, history, snapshotDirs}]
return this._POST("importFromSpecs", args);
}
async importFromResource(args) {
// parameters: db, type, name, url, commitSpec, purgeHistory
// returns: [{dir, type, name, commits, history, snapshotDirs}]
return this._POST("importFromResource", args);
}
}