First Commit

This commit is contained in:
Fabian
2019-09-18 21:54:28 +02:00
commit 429ba7e291
27 changed files with 5815 additions and 0 deletions

84
src/database/database.ts Normal file
View File

@ -0,0 +1,84 @@
import { Rules } from "./rules";
import Settings from "../settings";
import getLevelDB from "../storage";
import PathLock from "./lock";
import Query from "./query";
export class DatabaseManager {
static databases = new Map<string, Database>();
static async init() {
let databases = await Settings.getDatabases();
databases.forEach(dbconfig => {
let db = new Database(dbconfig.name, dbconfig.accesskey, dbconfig.rules, dbconfig.publickey);
this.databases.set(dbconfig.name, db);
})
}
static addDatabase(name: string) {
if (this.databases.has(name))
throw new Error("Database already exists!");
let database = new Database(name);
this.databases.set(name, database);
return database;
}
static getDatabase(name: string) {
return this.databases.get(name);
}
static async deleteDatabase(name: string) {
let db = this.databases.get(name)
if (db) {
await Settings.deleteDatabase(name);
await db.stop();
}
}
}
export class Database {
public level = getLevelDB(this.name);
private rules: Rules;
public locks = new PathLock()
toJSON() {
return {
name: this.name,
accesskey: this.accesskey,
publickey: this.publickey,
rules: this.rules
}
}
constructor(public name: string, public accesskey?: string, rawRules?: string, public publickey?: string) {
if (rawRules)
this.rules = new Rules(rawRules);
}
async setRules(rawRules: string) {
let rules = new Rules(rawRules);
await Settings.setDatabaseRules(this.name, rawRules);
this.rules = rules;
}
async setAccessKey(key: string) {
await Settings.setDatabaseAccessKey(this.name, key);
this.accesskey = key;
}
async setPublicKey(key: string) {
await Settings.setDatabasePublicKey(this.name, key);
this.publickey = key;
}
getQuery(path: string[]) {
return new Query(this, path);
}
async stop() {
await this.level.close();
}
}

43
src/database/lock.ts Normal file
View File

@ -0,0 +1,43 @@
export type Release = { release: () => void };
export default class PathLock {
locks: {
path: string[],
next: (() => void)[]
}[] = [];
constructor() { }
async lock(path: string[]) {
let locks = this.locks.filter(lock => {
let idxs = Math.min(lock.path.length, path.length);
if (idxs === 0) return true;
for (let i = 0; i < idxs; i++) {
if (lock.path[i] !== path[i])
return false;
}
return true;
})
if (locks.length > 0) { // await till release
await Promise.all(locks.map(l => new Promise(res => l.next.push(res))))
} else {
let lock = {
path: path,
next: []
}
this.locks.push(lock);
locks = [lock];
}
return () => {
locks.forEach(lock => {
if (lock.next.length > 0) {
setImmediate(() => lock.next.shift()());
} else {
this.locks.splice(this.locks.indexOf(lock), 1);
}
})
}
}
}

211
src/database/query.ts Normal file
View File

@ -0,0 +1,211 @@
import { Database } from "./database";
import Encoder, { DataTypes } from "@hibas123/binary-encoder";
import { resNull } from "../storage";
import { Bytes } from "leveldown";
import { LevelUpChain } from "levelup";
import shortid = require("shortid");
import Logging from "@hibas123/nodelogging";
enum FieldTypes {
OBJECT,
VALUE
}
interface IField {
type: FieldTypes;
// fields?: string[];
value?: any
}
const FieldEncoder = new Encoder<IField>({
type: {
index: 1,
type: DataTypes.UINT8
},
// fields: {
// index: 2,
// type: DataTypes.STRING,
// array: true
// },
value: {
index: 3,
type: DataTypes.AUTO,
allowNull: true
}
})
export default class Query {
constructor(private database: Database, private path: string[]) {
if (path.length > 10) {
throw new Error("Path is to long. Path is only allowed to be 10 Layers deep!");
}
if (path.find(segment => segment.indexOf("/") >= 0)) {
throw new Error("Path cannot contain '/'!");
}
}
private pathToKey(path?: string[]) {
return "/" + (path || this.path).join("/");
}
private getField(path: string[]): Promise<IField | null> {
return this.database.level.get(this.pathToKey(path), { asBuffer: true }).then((res: Buffer) => FieldEncoder.decode(res)).catch(resNull);
}
private getFields(path: string[]) {
let p = this.pathToKey(path);
if (!p.endsWith("/"))
p += "/";
let t = Buffer.from(p);
let gt = Buffer.alloc(t.length + 1);
gt.set(t);
gt[t.length] = 0;
let lt = Buffer.alloc(t.length + 1);
lt.set(t);
lt[t.length] = 0xFF;
return new Promise<string[]>((yes, no) => {
let keys = [];
const stream = this.database.level.createKeyStream({
gt: Buffer.from(p),
lt: Buffer.from(lt)
})
stream.on("data", key => keys.push(key.toString()));
stream.on("end", () => yes(keys));
stream.on("error", no);
});
}
async get() {
const lock = await this.database.locks.lock(this.path);
try {
const getData = async (path: string[]) => {
let obj = await this.getField(path);
if (!obj)
return null;
else {
if (obj.type === FieldTypes.VALUE) {
return obj.value;
} else {
let res = {};
let fields = await this.getFields(this.path);
let a = fields.map(field => field.split("/").filter(e => e !== "")).sort((a, b) => a.length - b.length).map(async path => {
let field = await this.getField(path);
Logging.debug("Path:", path, "Field:", field);
let shortened = path.slice(this.path.length);
let t = res;
for (let section of shortened.slice(0, -1)) {
t = t[section];
}
if (field.type === FieldTypes.OBJECT) {
t[path[path.length - 1]] = {};
} else {
t[path[path.length - 1]] = field.value;
}
})
await Promise.all(a);
return res;
}
}
}
return await getData(this.path);
} finally {
lock();
}
}
async push(value: any) {
let id = shortid.generate();
let q = new Query(this.database, [...this.path, id]);
await q.set(value);
return id;
}
async set(value: any) {
const lock = await this.database.locks.lock(this.path);
let batch = this.database.level.batch();
try {
let field = await this.getField(this.path);
if (field) {
await this.delete(batch);
} else {
for (let i = 0; i < this.path.length; i++) {
let subpath = this.path.slice(0, i);
let field = await this.getField(subpath);
if (!field) {
batch.put(this.pathToKey(subpath), FieldEncoder.encode({
type: FieldTypes.OBJECT
}));
} else if (field.type !== FieldTypes.OBJECT) {
throw new Error("Parent elements not all Object. Cannot set value!");
}
}
}
const saveValue = (path: string[], value: any) => {
Logging.debug("Save Value:", path, value);
if (typeof value === "object") {
//TODO: Handle case array!
// Field type array?
batch.put(this.pathToKey(path), FieldEncoder.encode({
type: FieldTypes.OBJECT
}))
for (let field in value) {
saveValue([...path, field], value[field]);
}
} else {
batch.put(this.pathToKey(path), FieldEncoder.encode({
type: FieldTypes.VALUE,
value
}));
}
}
saveValue(this.path, value);
await batch.write();
} catch (err) {
if (batch.length > 0)
batch.clear();
throw err;
} finally {
lock();
}
}
async delete(batch?: LevelUpChain) {
let lock = batch ? undefined : await this.database.locks.lock(this.path);
const commit = batch ? false : true;
if (!batch)
batch = this.database.level.batch();
try {
let field = await this.getField(this.path);
if (field) {
let fields = await this.getFields(this.path)
fields.forEach(field => batch.del(field));
batch.del(this.pathToKey(this.path));
}
if (commit)
await batch.write();
} catch (err) {
if (batch.length > 0)
batch.clear()
} finally {
if (lock)
lock()
}
}
async subscribe() { }
async unsubscribe() { }
}

90
src/database/rules.ts Normal file
View File

@ -0,0 +1,90 @@
import Session from "./session";
interface IRule<T> {
".write"?: T
".read"?: T
}
type IRuleConfig<T> = {
[segment: string]: IRuleConfig<T>;
} | IRule<T>;
type IRuleRaw = IRuleConfig<string>;
type IRuleParsed = IRuleConfig<boolean>;
const resolve = (value: any) => {
if (value === true) {
return true;
} else if (typeof value === "string") {
}
return undefined;
}
export class Rules {
rules: IRuleParsed;
constructor(private config: string) {
let parsed: IRuleRaw = JSON.parse(config);
const analyze = (raw: IRuleRaw) => {
let r: IRuleParsed = {};
if (raw[".read"]) {
let res = resolve(raw[".read"]);
if (res) {
r[".read"] = res;
}
delete raw[".read"];
}
if (raw[".write"]) {
let res = resolve(raw[".write"]);
if (res) {
r[".write"] = res;
}
delete raw[".write"];
}
for (let segment in raw) {
if (segment.startsWith("."))
continue;
r[segment] = analyze(raw[segment]);
}
return r;
}
this.rules = analyze(parsed);
}
hasPermission(path: string[], session: Session) {
let read = this.rules[".read"] || false;
let write = this.rules[".write"] || false;
let rules = this.rules;
for (let segment of path) {
rules = rules[segment];
if (rules[segment]) {
if (rules[".read"]) {
read = rules[".read"]
}
if (rules[".write"]) {
read = rules[".write"]
}
} else {
break;
}
}
return {
read,
write
}
}
toJSON() {
return this.config;
}
}

3
src/database/session.ts Normal file
View File

@ -0,0 +1,3 @@
export default class Session {
}