Fixing several bugs and adding very basic rule support

This commit is contained in:
Fabian 2019-09-19 16:24:35 +02:00
parent 429ba7e291
commit a7f7edcd0b
8 changed files with 163 additions and 73 deletions

View File

@ -2,11 +2,14 @@ import * as io from "socket.io";
import { Server } from "http"; import { Server } from "http";
import { DatabaseManager } from "./database/database"; import { DatabaseManager } from "./database/database";
import Logging from "@hibas123/logging"; import Logging from "@hibas123/logging";
import Query from "./database/query";
import Session from "./database/session";
type QueryTypes = "get" | "set" | "push" | "subscribe" | "unsubscribe"; type QueryTypes = "get" | "set" | "push" | "subscribe" | "unsubscribe";
export class ConnectionManager { export class ConnectionManager {
static server: io.Server; static server: io.Server;
static bind(server: Server) { static bind(server: Server) {
this.server = io(server); this.server = io(server);
this.server.on("connection", this.onConnection.bind(this)); this.server.on("connection", this.onConnection.bind(this));
@ -14,9 +17,13 @@ export class ConnectionManager {
private static onConnection(socket: io.Socket) { private static onConnection(socket: io.Socket) {
const reqMap = new Map<string, [number, number]>(); const reqMap = new Map<string, [number, number]>();
const stored = new Map<string, Query>();
const session = new Session();
const answer = (id: string, data: any, err: boolean = false) => { const answer = (id: string, data: any, err: boolean = false) => {
let time = process.hrtime(reqMap.get(id)); let time = process.hrtime(reqMap.get(id));
Logging.debug(`Sending answer for ${id} with data`, data, err ? "as error" : "", "Took", time[1] / 1000, "us"); reqMap.delete(id);
// Logging.debug(`Sending answer for ${id} with data`, data, err ? "as error" : "", "Took", time[1] / 1000, "us");
socket.emit("message", id, err, data); socket.emit("message", id, err, data);
} }
@ -25,33 +32,48 @@ export class ConnectionManager {
}) })
socket.on("query", async (id: string, type: QueryTypes, database: string, path: string[], data: any) => { socket.on("query", async (id: string, type: QueryTypes, database: string, path: string[], data: any) => {
Logging.debug(`Request with id ${id} from type ${type} for database ${database} and path ${path} with data ${data}`) Logging.debug(`Request with id '${id}' from type '${type}' for database '${database}' and path '${path}' with data`, data)
reqMap.set(id, process.hrtime()); reqMap.set(id, process.hrtime());
try { try {
const db = DatabaseManager.getDatabase(database); const db = DatabaseManager.getDatabase(database);
const perms = db.rules.hasPermission(path, session);
const noperm = new Error("No permisison!");
if (!db) if (!db)
answer(id, "Database not found!", true); answer(id, "Database not found!", true);
else { else {
const query = db.getQuery(path); const query = stored.get(id) || db.getQuery(path);
switch (type) { switch (type) {
case "get": case "get":
if (!perms.read)
throw noperm;
answer(id, await query.get()); answer(id, await query.get());
break; return;
case "set": case "set":
if (!perms.write)
throw noperm;
answer(id, await query.set(data)); answer(id, await query.set(data));
break; return;
case "push": case "push":
if (!perms.write)
throw noperm;
answer(id, await query.push(data)); answer(id, await query.push(data));
break; return;
case "subscribe": case "subscribe":
answer(id, await query.subscribe()); if (!perms.read)
break; throw noperm;
query.subscribe(data, (data) => {
answer(id, data);
});
stored.set(id, query);
return;
case "unsubscribe": case "unsubscribe":
answer(id, await query.unsubscribe()); query.unsubscribe();
break; stored.delete(id);
return;
} }
answer(id, "Invalid request!", true);
} }
} catch (err) { } catch (err) {
Logging.error(err); Logging.error(err);
@ -61,7 +83,9 @@ export class ConnectionManager {
}) })
socket.on("disconnect", () => { socket.on("disconnect", () => {
reqMap.clear();
stored.clear();
socket.removeAllListeners();
}) })
} }
} }

View File

@ -3,6 +3,8 @@ import Settings from "../settings";
import getLevelDB from "../storage"; import getLevelDB from "../storage";
import PathLock from "./lock"; import PathLock from "./lock";
import Query from "./query"; import Query from "./query";
import { Observable } from "@hibas123/utils";
import Logging from "@hibas123/logging";
export class DatabaseManager { export class DatabaseManager {
static databases = new Map<string, Database>(); static databases = new Map<string, Database>();
@ -16,10 +18,11 @@ export class DatabaseManager {
}) })
} }
static addDatabase(name: string) { static async addDatabase(name: string) {
if (this.databases.has(name)) if (this.databases.has(name))
throw new Error("Database already exists!"); throw new Error("Database already exists!");
await Settings.addDatabase(name);
let database = new Database(name); let database = new Database(name);
this.databases.set(name, database); this.databases.set(name, database);
return database; return database;
@ -39,11 +42,23 @@ export class DatabaseManager {
} }
} }
export enum ChangeTypes {
SET,
PUSH
}
export type Change = {
type: ChangeTypes;
path: string[]
}
export class Database { export class Database {
public level = getLevelDB(this.name); public level = getLevelDB(this.name);
private rules: Rules; public rules: Rules;
public locks = new PathLock() public locks = new PathLock()
public changeObservable = new Observable<Change>();
toJSON() { toJSON() {
return { return {
name: this.name, name: this.name,

View File

@ -1,8 +1,7 @@
import { Database } from "./database"; import { Database, Change, ChangeTypes } from "./database";
import Encoder, { DataTypes } from "@hibas123/binary-encoder"; import Encoder, { DataTypes } from "@hibas123/binary-encoder";
import { resNull } from "../storage"; import { resNull } from "../storage";
import { Bytes } from "leveldown";
import { LevelUpChain } from "levelup"; import { LevelUpChain } from "levelup";
import shortid = require("shortid"); import shortid = require("shortid");
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
@ -14,20 +13,14 @@ enum FieldTypes {
interface IField { interface IField {
type: FieldTypes; type: FieldTypes;
// fields?: string[];
value?: any value?: any
} }
const FieldEncoder = new Encoder<IField>({ export const FieldEncoder = new Encoder<IField>({
type: { type: {
index: 1, index: 1,
type: DataTypes.UINT8 type: DataTypes.UINT8
}, },
// fields: {
// index: 2,
// type: DataTypes.STRING,
// array: true
// },
value: { value: {
index: 3, index: 3,
type: DataTypes.AUTO, type: DataTypes.AUTO,
@ -43,6 +36,8 @@ export default class Query {
if (path.find(segment => segment.indexOf("/") >= 0)) { if (path.find(segment => segment.indexOf("/") >= 0)) {
throw new Error("Path cannot contain '/'!"); throw new Error("Path cannot contain '/'!");
} }
this.onChange = this.onChange.bind(this);
} }
private pathToKey(path?: string[]) { private pathToKey(path?: string[]) {
@ -90,15 +85,13 @@ export default class Query {
if (obj.type === FieldTypes.VALUE) { if (obj.type === FieldTypes.VALUE) {
return obj.value; return obj.value;
} else { } else {
let res = {};
let fields = await this.getFields(this.path); 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 sorted = fields.map(field => field.split("/").filter(e => e !== "")).sort((a, b) => a.length - b.length)
let field = await this.getField(path);
Logging.debug("Path:", path, "Field:", field);
let shortened = path.slice(this.path.length);
let res = {};
for (let path of sorted) {
let field = await this.getField(path);
let shortened = path.slice(this.path.length);
let t = res; let t = res;
for (let section of shortened.slice(0, -1)) { for (let section of shortened.slice(0, -1)) {
t = t[section]; t = t[section];
@ -109,9 +102,7 @@ export default class Query {
} else { } else {
t[path[path.length - 1]] = field.value; t[path[path.length - 1]] = field.value;
} }
}) }
await Promise.all(a);
return res; return res;
} }
@ -127,10 +118,17 @@ export default class Query {
let id = shortid.generate(); let id = shortid.generate();
let q = new Query(this.database, [...this.path, id]); let q = new Query(this.database, [...this.path, id]);
await q.set(value); await q.set(value);
this.database.changeObservable.send({
path: [...this.path, id],
type: ChangeTypes.PUSH
})
return id; return id;
} }
async set(value: any) { async set(value: any) {
if (value === null || value === undefined)
return this.delete(value);
const lock = await this.database.locks.lock(this.path); const lock = await this.database.locks.lock(this.path);
let batch = this.database.level.batch(); let batch = this.database.level.batch();
try { try {
@ -152,7 +150,6 @@ export default class Query {
} }
const saveValue = (path: string[], value: any) => { const saveValue = (path: string[], value: any) => {
Logging.debug("Save Value:", path, value);
if (typeof value === "object") { if (typeof value === "object") {
//TODO: Handle case array! //TODO: Handle case array!
// Field type array? // Field type array?
@ -173,6 +170,10 @@ export default class Query {
saveValue(this.path, value); saveValue(this.path, value);
await batch.write(); await batch.write();
this.database.changeObservable.send({
path: this.path,
type: ChangeTypes.SET
})
} catch (err) { } catch (err) {
if (batch.length > 0) if (batch.length > 0)
batch.clear(); batch.clear();
@ -182,7 +183,7 @@ export default class Query {
} }
} }
async delete(batch?: LevelUpChain) { private async delete(batch?: LevelUpChain) {
let lock = batch ? undefined : await this.database.locks.lock(this.path); let lock = batch ? undefined : await this.database.locks.lock(this.path);
const commit = batch ? false : true; const commit = batch ? false : true;
if (!batch) if (!batch)
@ -206,6 +207,54 @@ export default class Query {
} }
} }
async subscribe() { }
async unsubscribe() { } subscription: {
type: ChangeTypes;
send: (data: any) => void;
};
subscribe(type: "set" | "push", send: (data: any) => void) {
this.subscription = {
send,
type: type === "set" ? ChangeTypes.SET : ChangeTypes.PUSH
};
this.database.changeObservable.subscribe(this.onChange);
Logging.debug("Subscribe");
}
async onChange(change: Change) {
Logging.debug("Change:", change);
if (!this.subscription)
return this.database.changeObservable.unsubscribe(this.onChange);
const { type, send } = this.subscription;
if (type === change.type) {
Logging.debug("Path", this.path, change.path);
if (this.path.length === change.path.length - (type === ChangeTypes.PUSH ? 1 : 0)) {
let valid = true;
for (let i = 0; i < this.path.length; i++) {
if (this.path[i] !== change.path[i]) {
valid = false;
break;
}
}
if (valid) {
Logging.debug("Send Change:", change);
if (type === ChangeTypes.PUSH) {
send({
id: change.path[change.path.length - 1],
data: await new Query(this.database, change.path).get()
})
} else {
send(await this.get())
}
}
}
}
}
unsubscribe() {
this.subscription = undefined;
this.database.changeObservable.unsubscribe(this.onChange);
}
} }

View File

@ -57,7 +57,7 @@ export class Rules {
this.rules = analyze(parsed); this.rules = analyze(parsed);
} }
hasPermission(path: string[], session: Session) { hasPermission(path: string[], session: Session): { read: boolean, write: boolean } {
let read = this.rules[".read"] || false; let read = this.rules[".read"] || false;
let write = this.rules[".write"] || false; let write = this.rules[".write"] || false;
@ -65,7 +65,7 @@ export class Rules {
for (let segment of path) { for (let segment of path) {
rules = rules[segment]; rules = rules[segment];
if (rules[segment]) { if (rules) {
if (rules[".read"]) { if (rules[".read"]) {
read = rules[".read"] read = rules[".read"]
} }
@ -79,8 +79,8 @@ export class Rules {
} }
return { return {
read, read: read as boolean,
write write: write as boolean
} }
} }

View File

@ -1,3 +0,0 @@
export function checkRules(rules: any) {
}

View File

@ -3,13 +3,18 @@ import Settings from "../../settings";
import getForm from "../helper/form"; import getForm from "../helper/form";
import Logging from "@hibas123/nodelogging"; import Logging from "@hibas123/nodelogging";
import getTable from "../helper/table"; import getTable from "../helper/table";
import { BadRequestError } from "../helper/errors"; import { BadRequestError, NoPermissionError } from "../helper/errors";
import { DatabaseManager } from "../../database/database"; import { DatabaseManager } from "../../database/database";
import { FieldEncoder } from "../../database/query";
import getTemplate from "../helper/hb";
import config from "../../config";
const AdminRoute = new Router(); const AdminRoute = new Router();
AdminRoute.use((ctx, next) => { AdminRoute.use(async (ctx, next) => {
//TODO: Check permission const { key } = ctx.query;
if (key !== config.general.admin)
throw new NoPermissionError("No permission!");
return next(); return next();
}) })
@ -50,7 +55,7 @@ AdminRoute.get("/data", async ctx => {
}); });
let res = [["key", "value"]]; let res = [["key", "value"]];
stream.on("data", ({ key, value }) => { stream.on("data", ({ key, value }) => {
res.push([key, value]); res.push([key, JSON.stringify(FieldEncoder.decode(value))]);
}) })
stream.on("error", no); stream.on("error", no);

View File

@ -7,7 +7,7 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{title}}</title> <title>{{title}}</title>
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/base.css"> <link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/base.css">
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme@1.2.6/out/light.css"> <link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/light.css">
<style> <style>
#message { #message {
@ -29,24 +29,24 @@
<div id="message"> </div> <div id="message"> </div>
<form id="f1" action="JavaScript:void(null)"> <form id="f1" action="JavaScript:void(null)">
{{#each fields}} {{#each fields}}
<div class="input-group"> <div class="input-group">
<label>{{label}}</label> <label>{{label}}</label>
{{#ifCond type "===" "text"}} {{#ifCond type "===" "text"}}
<input type="text" placeholder="{{label}}" name="{{name}}" value="{{value}}" /> <input type="text" placeholder="{{label}}" name="{{name}}" value="{{value}}" />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "number"}} {{#ifCond type "===" "number"}}
<input type="number" placeholder="{{label}}" name="{{name}}" value="{{value}}" /> <input type="number" placeholder="{{label}}" name="{{name}}" value="{{value}}" />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "boolean"}} {{#ifCond type "===" "boolean"}}
<input type="checkbox" name="{{name}}" checked="{{value}}" /> <input type="checkbox" name="{{name}}" checked="{{value}}" />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "textarea"}} {{#ifCond type "===" "textarea"}}
<textarea class="inp" name="{{name}}" rows="20">{{value}}</textarea> <textarea class="inp" name="{{name}}" rows="20">{{value}}</textarea>
{{/ifCond}} {{/ifCond}}
</div> </div>
{{/each}} {{/each}}
<button class="btn btn-primary" onclick="submitData()">Submit</button> <button class="btn btn-primary" onclick="submitData()">Submit</button>

View File

@ -7,7 +7,7 @@
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{title}}</title> <title>{{title}}</title>
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/base.css"> <link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/base.css">
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme@1.2.6/out/light.css"> <link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/light.css">
<style> <style>
table { table {
@ -37,18 +37,18 @@
<div class="margin" style="margin-top: 4rem;"> <div class="margin" style="margin-top: 4rem;">
<h1>{{title}}</h1> <h1>{{title}}</h1>
{{#if empty}} {{#if empty}}
<h3>No Data available!</h3> <h3>No Data available!</h3>
{{else}} {{else}}
<table style="overflow-x: auto"> <table style="overflow-x: auto">
{{#each table as |row|}} {{#each table as |row|}}
<tr> <tr>
{{#each row as |col|}} {{#each row as |col|}}
<td>{{col}}</td> <td>{{col}}</td>
{{/each}}
</tr>
{{/each}} {{/each}}
</tr> </table>
{{/each}}
</table>
{{/if}} {{/if}}
</div> </div>
</div> </div>