This commit is contained in:
		
							
								
								
									
										21
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | kind: pipeline | ||||||
|  | type: docker | ||||||
|  | name: default | ||||||
|  |  | ||||||
|  | steps: | ||||||
|  |   - name: Build with node | ||||||
|  |     image: node:12 | ||||||
|  |     commands: | ||||||
|  |       - npm install | ||||||
|  |       - npm run build | ||||||
|  |   - name: Publish to docker | ||||||
|  |     image: plugins/docker | ||||||
|  |     settings: | ||||||
|  |       username: | ||||||
|  |         from_secret: docker_username | ||||||
|  |       password: | ||||||
|  |         from_secret: docker_password | ||||||
|  |       auto_tag: true | ||||||
|  |       repo: hibas123.azurecr.io/realtimedb | ||||||
|  |       registry: hibas123.azurecr.io | ||||||
|  |       debug: true | ||||||
| @ -1,4 +1,7 @@ | |||||||
|  | <<<<<<< HEAD | ||||||
| root=true | root=true | ||||||
|  | ======= | ||||||
|  | >>>>>>> 0bfdbce908484560108e20cede6f9e7c46710818 | ||||||
| [*] | [*] | ||||||
| charset = utf-8 | charset = utf-8 | ||||||
| indent_size = 3 | indent_size = 3 | ||||||
|  | |||||||
							
								
								
									
										720
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										720
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										32
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								package.json
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|    "name": "@hibas123/realtimedb", |    "name": "@hibas123/realtimedb", | ||||||
|   "version": "2.0.0-beta.9", |    "version": "2.0.0-beta.19", | ||||||
|    "description": "", |    "description": "", | ||||||
|    "main": "lib/index.js", |    "main": "lib/index.js", | ||||||
|    "private": true, |    "private": true, | ||||||
| @ -17,31 +17,31 @@ | |||||||
|    "license": "ISC", |    "license": "ISC", | ||||||
|    "devDependencies": { |    "devDependencies": { | ||||||
|       "@types/dotenv": "^8.2.0", |       "@types/dotenv": "^8.2.0", | ||||||
|     "@types/jsonwebtoken": "^8.3.5", |       "@types/jsonwebtoken": "^8.3.8", | ||||||
|     "@types/koa": "^2.11.0", |       "@types/koa": "^2.11.2", | ||||||
|     "@types/koa-router": "^7.0.42", |       "@types/koa-router": "^7.4.0", | ||||||
|       "@types/leveldown": "^4.0.2", |       "@types/leveldown": "^4.0.2", | ||||||
|     "@types/levelup": "^3.1.1", |       "@types/levelup": "^4.3.0", | ||||||
|       "@types/nanoid": "^2.1.0", |       "@types/nanoid": "^2.1.0", | ||||||
|     "@types/node": "^12.12.14", |       "@types/node": "^13.9.3", | ||||||
|     "@types/ws": "^6.0.4", |       "@types/ws": "^7.2.3", | ||||||
|     "concurrently": "^5.0.0", |       "concurrently": "^5.1.0", | ||||||
|     "nodemon": "^2.0.1", |       "nodemon": "^2.0.2", | ||||||
|     "typescript": "^3.7.2" |       "typescript": "^3.8.3" | ||||||
|    }, |    }, | ||||||
|    "dependencies": { |    "dependencies": { | ||||||
|     "@hibas123/nodelogging": "^2.1.2", |       "@hibas123/nodelogging": "^2.1.5", | ||||||
|       "@hibas123/utils": "^2.2.3", |       "@hibas123/utils": "^2.2.3", | ||||||
|       "dotenv": "^8.2.0", |       "dotenv": "^8.2.0", | ||||||
|     "handlebars": "^4.5.3", |       "handlebars": "^4.7.3", | ||||||
|       "jsonwebtoken": "^8.5.1", |       "jsonwebtoken": "^8.5.1", | ||||||
|       "koa": "^2.11.0", |       "koa": "^2.11.0", | ||||||
|       "koa-body": "^4.1.1", |       "koa-body": "^4.1.1", | ||||||
|     "koa-router": "^7.4.0", |       "koa-router": "^8.0.8", | ||||||
|     "leveldown": "^5.4.1", |       "leveldown": "^5.5.1", | ||||||
|       "levelup": "^4.3.2", |       "levelup": "^4.3.2", | ||||||
|     "nanoid": "^2.1.7", |       "nanoid": "^2.1.11", | ||||||
|       "what-the-pack": "^2.0.3", |       "what-the-pack": "^2.0.3", | ||||||
|     "ws": "^7.2.0" |       "ws": "^7.2.3" | ||||||
|    } |    } | ||||||
| } | } | ||||||
| @ -7,7 +7,12 @@ import Session from "./session"; | |||||||
| import { LevelUpChain } from "levelup"; | import { LevelUpChain } from "levelup"; | ||||||
|  |  | ||||||
| export type IWriteQueries = "set" | "update" | "delete" | "add"; | export type IWriteQueries = "set" | "update" | "delete" | "add"; | ||||||
| export type ICollectionQueries = "get" | "add" | "keys" | "delete-collection" | "list"; | export type ICollectionQueries = | ||||||
|  |    | "get" | ||||||
|  |    | "add" | ||||||
|  |    | "keys" | ||||||
|  |    | "delete-collection" | ||||||
|  |    | "list"; | ||||||
| export type IDocumentQueries = "get" | "set" | "update" | "delete"; | export type IDocumentQueries = "get" | "set" | "update" | "delete"; | ||||||
|  |  | ||||||
| export interface ITypedQuery<T> { | export interface ITypedQuery<T> { | ||||||
| @ -17,21 +22,30 @@ export interface ITypedQuery<T> { | |||||||
|    options?: any; |    options?: any; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type IQuery = ITypedQuery<ICollectionQueries | IDocumentQueries | "snapshot">; | export type IQuery = ITypedQuery< | ||||||
|  |    ICollectionQueries | IDocumentQueries | "snapshot" | ||||||
|  | >; | ||||||
|  |  | ||||||
| export const MP = MSGPack.initialize(2 ** 20); | export const MP = MSGPack.initialize(2 ** 20); | ||||||
|  |  | ||||||
| const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | const ALPHABET = | ||||||
|  |    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | ||||||
|  |  | ||||||
| const { encode, decode } = MP; | const { encode, decode } = MP; | ||||||
|  |  | ||||||
| type Runner = (collection: string, document: string, batch: LevelUpChain, collectionKey: string) => any; | type Runner = ( | ||||||
|  |    collection: string, | ||||||
|  |    document: string, | ||||||
|  |    batch: LevelUpChain, | ||||||
|  |    collectionKey: string | ||||||
|  | ) => any; | ||||||
|  |  | ||||||
| interface IPreparedQuery { | interface IPreparedQuery { | ||||||
|    createCollection: boolean; |    createCollection: boolean; | ||||||
|    needDocument: boolean; |    needDocument: boolean; | ||||||
|    batchCompatible: boolean; |    batchCompatible: boolean; | ||||||
|    runner: Runner; |    runner: Runner; | ||||||
|  |    permission: "write" | "read"; | ||||||
|    additionalLock?: string[]; |    additionalLock?: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -46,7 +60,9 @@ export abstract class Query { | |||||||
|     * @param path Path to be checked |     * @param path Path to be checked | ||||||
|     */ |     */ | ||||||
|    private validatePath(path: string[]) { |    private validatePath(path: string[]) { | ||||||
|       return path.every(e => (e.match(/[^a-zA-Z0-9_\-\<\>]/g) || []).length === 0); |       return path.every( | ||||||
|  |          e => (e.match(/[^a-zA-Z0-9_\-\<\>]/g) || []).length === 0 | ||||||
|  |       ); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public changes: Change[] = []; |    public changes: Change[] = []; | ||||||
| @ -55,14 +71,24 @@ export abstract class Query { | |||||||
|    public readonly needDocument: boolean; |    public readonly needDocument: boolean; | ||||||
|    public readonly batchCompatible: boolean; |    public readonly batchCompatible: boolean; | ||||||
|    public readonly additionalLock?: string[]; |    public readonly additionalLock?: string[]; | ||||||
|  |    public readonly permission: string; | ||||||
|    private readonly _runner: Runner; |    private readonly _runner: Runner; | ||||||
|  |  | ||||||
|    constructor(protected database: Database, protected session: Session, protected query: IQuery, snapshot = false) { |    constructor( | ||||||
|  |       protected database: Database, | ||||||
|  |       protected session: Session, | ||||||
|  |       protected query: IQuery, | ||||||
|  |       snapshot = false | ||||||
|  |    ) { | ||||||
|       if (query.path.length > 10) { |       if (query.path.length > 10) { | ||||||
|          throw new QueryError("Path is to long. Path is only allowed to be 10 Layers deep!"); |          throw new QueryError( | ||||||
|  |             "Path is to long. Path is only allowed to be 10 Layers deep!" | ||||||
|  |          ); | ||||||
|       } |       } | ||||||
|       if (!this.validatePath(query.path)) { |       if (!this.validatePath(query.path)) { | ||||||
|          throw new QueryError("Path can only contain a-z A-Z 0-9  '-' '-' '<' and '>' "); |          throw new QueryError( | ||||||
|  |             "Path can only contain a-z A-Z 0-9  '-' '-' '<' and '>' " | ||||||
|  |          ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!snapshot) { |       if (!snapshot) { | ||||||
| @ -80,75 +106,116 @@ export abstract class Query { | |||||||
|    protected getDoc(collection: string, document: string) { |    protected getDoc(collection: string, document: string) { | ||||||
|       return this.database.data |       return this.database.data | ||||||
|          .get(Database.getKey(collection, document), { asBuffer: true }) |          .get(Database.getKey(collection, document), { asBuffer: true }) | ||||||
|          .then(res => decode<any>(res as Buffer)).catch(resNull); |          .then(res => decode<any>(res as Buffer)) | ||||||
|  |          .catch(resNull); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    protected sendChange(collection: string, document: string, type: ChangeTypes, data: any) { |    protected sendChange( | ||||||
|  |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       type: ChangeTypes, | ||||||
|  |       data: any | ||||||
|  |    ) { | ||||||
|       let change: Change = { |       let change: Change = { | ||||||
|          type, |          type, | ||||||
|          document, |          document, | ||||||
|          collection, |          collection, | ||||||
|          data, |          data, | ||||||
|          sender: this.session.id |          sender: this.session.id | ||||||
|       } |       }; | ||||||
|  |  | ||||||
|       this.changes.push(change); |       this.changes.push(change); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    protected static getConstructorParams(query: Query): [Database, Session, IQuery] { |    protected static getConstructorParams( | ||||||
|  |       query: Query | ||||||
|  |    ): [Database, Session, IQuery] { | ||||||
|       return [query.database, query.session, query.query]; |       return [query.database, query.session, query.query]; | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    protected abstract checkChange(change: Change): boolean; |    protected abstract checkChange(change: Change): boolean; | ||||||
|    protected abstract firstSend(collection: string, document: string): Promise<any>; |    protected abstract firstSend( | ||||||
|  |       collection: string, | ||||||
|  |       document: string | ||||||
|  |    ): Promise<any>; | ||||||
|  |  | ||||||
|    public run(collection: string, document: string, batch: LevelUpChain, collectionKey: string) { |    public run( | ||||||
|       return this._runner.call(this, collection, document, batch, collectionKey); |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       batch: LevelUpChain, | ||||||
|  |       collectionKey: string | ||||||
|  |    ) { | ||||||
|  |       let perm = this.database.rules.hasPermission( | ||||||
|  |          this.query.path, | ||||||
|  |          this.session | ||||||
|  |       ); | ||||||
|  |       if (this.permission === "read" && !perm.read) { | ||||||
|  |          throw new QueryError("No permission!"); | ||||||
|  |       } else if (this.permission === "write" && !perm.write) { | ||||||
|  |          throw new QueryError("No permission!"); | ||||||
|  |       } | ||||||
|  |       this.query.path = perm.path; | ||||||
|  |       return this._runner.call( | ||||||
|  |          this, | ||||||
|  |          collection, | ||||||
|  |          document, | ||||||
|  |          batch, | ||||||
|  |          collectionKey | ||||||
|  |       ); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public async snapshot(onChange: (change: (DocRes & { type: ChangeTypes })[]) => void) { |    public async snapshot( | ||||||
|  |       onChange: (change: (DocRes & { type: ChangeTypes })[]) => void | ||||||
|  |    ) { | ||||||
|  |       let perm = this.database.rules.hasPermission( | ||||||
|  |          this.query.path, | ||||||
|  |          this.session | ||||||
|  |       ); | ||||||
|  |       if (!perm.read) { | ||||||
|  |          throw new QueryError("No permission!"); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.query.path = perm.path; | ||||||
|  |  | ||||||
|       const receivedChanges = (changes: Change[]) => { |       const receivedChanges = (changes: Change[]) => { | ||||||
|          let res = changes.filter(change => this.checkChange(change)).map(change => { |          let res = changes | ||||||
|  |             .filter(change => this.checkChange(change)) | ||||||
|  |             .map(change => { | ||||||
|                return { |                return { | ||||||
|                   id: change.document, |                   id: change.document, | ||||||
|                   data: change.data, |                   data: change.data, | ||||||
|                   type: change.type |                   type: change.type | ||||||
|             } |                }; | ||||||
|          }) |             }); | ||||||
|          if (res.length > 0) |          if (res.length > 0) onChange(res); | ||||||
|             onChange(res); |  | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       const unsub = this.database.collectionChangeListener.subscribe(change => { |       const unsub = this.database.collectionChangeListener.subscribe(change => { | ||||||
|          if (change.key === collectionKey) { |          if (change.key === collectionKey) { | ||||||
|             if (change.type === "create") |             if (change.type === "create") addSubscriber(change.id); | ||||||
|                addSubscriber(change.id); |             else removeSubscriber(); // Send delete for all elements (Don't know how to do this...) | ||||||
|             else |  | ||||||
|                removeSubscriber(); // Send delete for all elements (Don't know how to do this...) |  | ||||||
|          } |          } | ||||||
|       }) |       }); | ||||||
|  |  | ||||||
|  |       let { collection, document, collectionKey } = await this.database.resolve( | ||||||
|       let { collection, document, collectionKey } = await this.database.resolve(this.query.path) |          this.query.path | ||||||
|  |       ); | ||||||
|       let oldKey: string = undefined; |       let oldKey: string = undefined; | ||||||
|  |  | ||||||
|       const removeSubscriber = () => { |       const removeSubscriber = () => { | ||||||
|          if (!oldKey) |          if (!oldKey) return; | ||||||
|             return; |  | ||||||
|          let s = this.database.changeListener.get(oldKey); |          let s = this.database.changeListener.get(oldKey); | ||||||
|          if (s) { |          if (s) { | ||||||
|             s.delete(receivedChanges); |             s.delete(receivedChanges); | ||||||
|             if (s.size <= 0) |             if (s.size <= 0) this.database.changeListener.delete(oldKey); | ||||||
|                this.database.changeListener.delete(oldKey); |  | ||||||
|          } |          } | ||||||
|          oldKey = undefined; |          oldKey = undefined; | ||||||
|       } |       }; | ||||||
|  |  | ||||||
|       const addSubscriber = (collection: string) => { |       const addSubscriber = (collection: string) => { | ||||||
|          let key = Database.getKey(collection, document); |          let key = Database.getKey(collection, document); | ||||||
|          if (oldKey !== key) { |          if (oldKey !== key) { | ||||||
|             if (oldKey !== undefined) |             if (oldKey !== undefined) removeSubscriber(); | ||||||
|                removeSubscriber(); |  | ||||||
|  |  | ||||||
|             let s = this.database.changeListener.get(key); |             let s = this.database.changeListener.get(key); | ||||||
|             if (!s) { |             if (!s) { | ||||||
| @ -158,7 +225,7 @@ export abstract class Query { | |||||||
|  |  | ||||||
|             s.add(receivedChanges); |             s.add(receivedChanges); | ||||||
|          } |          } | ||||||
|       } |       }; | ||||||
|  |  | ||||||
|       if (collection) { |       if (collection) { | ||||||
|          addSubscriber(collection); |          addSubscriber(collection); | ||||||
| @ -170,7 +237,7 @@ export abstract class Query { | |||||||
|             removeSubscriber(); |             removeSubscriber(); | ||||||
|          }, |          }, | ||||||
|          value: await this.firstSend(collection, document) |          value: await this.firstSend(collection, document) | ||||||
|       } |       }; | ||||||
|    } |    } | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -178,7 +245,7 @@ interface UpdateData { | |||||||
|    [path: string]: { |    [path: string]: { | ||||||
|       type: "value" | "timestamp" | "increment" | "push"; |       type: "value" | "timestamp" | "increment" | "push"; | ||||||
|       value: any; |       value: any; | ||||||
|    } |    }; | ||||||
| } | } | ||||||
| export class DocumentQuery extends Query { | export class DocumentQuery extends Query { | ||||||
|    prepare(query: IQuery): IPreparedQuery { |    prepare(query: IQuery): IPreparedQuery { | ||||||
| @ -189,29 +256,33 @@ export class DocumentQuery extends Query { | |||||||
|                batchCompatible: false, |                batchCompatible: false, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|  |                permission: "read", | ||||||
|                runner: this.get |                runner: this.get | ||||||
|             } |             }; | ||||||
|          case "set": |          case "set": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: true, |                batchCompatible: true, | ||||||
|                createCollection: true, |                createCollection: true, | ||||||
|                needDocument: true, |                needDocument: true, | ||||||
|  |                permission: "write", | ||||||
|                runner: this.set |                runner: this.set | ||||||
|             } |             }; | ||||||
|          case "update": |          case "update": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: true, |                batchCompatible: true, | ||||||
|                createCollection: true, |                createCollection: true, | ||||||
|                needDocument: true, |                needDocument: true, | ||||||
|  |                permission: "write", | ||||||
|                runner: this.update |                runner: this.update | ||||||
|             } |             }; | ||||||
|          case "delete": |          case "delete": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: true, |                batchCompatible: true, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: true, |                needDocument: true, | ||||||
|  |                permission: "write", | ||||||
|                runner: this.delete |                runner: this.delete | ||||||
|             } |             }; | ||||||
|          default: |          default: | ||||||
|             throw new Error("Invalid query type: " + type); |             throw new Error("Invalid query type: " + type); | ||||||
|       } |       } | ||||||
| @ -225,22 +296,28 @@ export class DocumentQuery extends Query { | |||||||
|       return this.getDoc(collection, document); |       return this.getDoc(collection, document); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    private async set(collection: string, document: string, batch?: LevelUpChain) { |    private async set( | ||||||
|  |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       batch?: LevelUpChain | ||||||
|  |    ) { | ||||||
|       const { data, options } = this.query; |       const { data, options } = this.query; | ||||||
|       if (data === null) |       if (data === null) return this.delete(collection, document, batch); | ||||||
|          return this.delete(collection, document, batch); |  | ||||||
|  |  | ||||||
|  |       let isNew = !(await this.getDoc(collection, document)); | ||||||
|       let isNew = !(await this.getDoc(collection, document)) |       batch.put(Database.getKey(collection, document), encode(data)); | ||||||
|       batch.put(Database.getKey(collection, document), encode(data)) |       this.sendChange(collection, document, isNew ? "added" : "modified", data); | ||||||
|       this.sendChange(collection, document, isNew ? "added" : "modified", data) |  | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    private async update(collection: string, document: string, batch?: LevelUpChain) { |    private async update( | ||||||
|  |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       batch?: LevelUpChain | ||||||
|  |    ) { | ||||||
|       const updateData: UpdateData = this.query.data; |       const updateData: UpdateData = this.query.data; | ||||||
|  |  | ||||||
|       let data = await this.getDoc(collection, document); |       let data = await this.getDoc(collection, document); | ||||||
|       let isNew = false |       let isNew = false; | ||||||
|       if (!data) { |       if (!data) { | ||||||
|          isNew = true; |          isNew = true; | ||||||
|          data = {}; |          data = {}; | ||||||
| @ -252,8 +329,7 @@ export class DocumentQuery extends Query { | |||||||
|          let parts = path.split("."); |          let parts = path.split("."); | ||||||
|          while (parts.length > 1) { |          while (parts.length > 1) { | ||||||
|             let seg = parts.shift(); |             let seg = parts.shift(); | ||||||
|             if (!data[seg]) |             if (!data[seg]) data[seg] = {}; | ||||||
|                data[seg] = {} |  | ||||||
|             d = data[seg]; |             d = data[seg]; | ||||||
|          } |          } | ||||||
|  |  | ||||||
| @ -290,23 +366,29 @@ export class DocumentQuery extends Query { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (batch) { |       if (batch) { | ||||||
|          batch.put(Database.getKey(collection, document), encode(data)) |          batch.put(Database.getKey(collection, document), encode(data)); | ||||||
|       } else { |       } else { | ||||||
|          await this.database.data |          await this.database.data.put( | ||||||
|             .put(Database.getKey(collection, document), encode(data)) |             Database.getKey(collection, document), | ||||||
|  |             encode(data) | ||||||
|  |          ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.sendChange(collection, document, isNew ? "added" : "modified", data) |       this.sendChange(collection, document, isNew ? "added" : "modified", data); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    private async delete(collection: string, document: string, batch?: LevelUpChain) { |    private async delete( | ||||||
|  |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       batch?: LevelUpChain | ||||||
|  |    ) { | ||||||
|       if (batch) { |       if (batch) { | ||||||
|          batch.del(Database.getKey(collection, document)) |          batch.del(Database.getKey(collection, document)); | ||||||
|       } else { |       } else { | ||||||
|          await this.database.data.del(Database.getKey(collection, document)); |          await this.database.data.del(Database.getKey(collection, document)); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.sendChange(collection, document, "deleted", null) |       this.sendChange(collection, document, "deleted", null); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    checkChange(change: Change) { |    checkChange(change: Change) { | ||||||
| @ -324,19 +406,19 @@ export class DocumentQuery extends Query { | |||||||
|  |  | ||||||
| type FieldPath = string; | type FieldPath = string; | ||||||
| type WhereFilterOp = | type WhereFilterOp = | ||||||
|    | '<' |    | "<" | ||||||
|    | '<=' |    | "<=" | ||||||
|    | '==' |    | "==" | ||||||
|    | '>=' |    | ">=" | ||||||
|    | '>' |    | ">" | ||||||
|    | 'array-contains' |    | "array-contains" | ||||||
|    | 'in' |    | "in" | ||||||
|    | 'array-contains-any'; |    | "array-contains-any"; | ||||||
|  |  | ||||||
| interface IQueryWhereVerbose { | interface IQueryWhereVerbose { | ||||||
|    fieldPath: FieldPath, |    fieldPath: FieldPath; | ||||||
|    opStr: WhereFilterOp, |    opStr: WhereFilterOp; | ||||||
|    value: any |    value: any; | ||||||
| } | } | ||||||
|  |  | ||||||
| type IQueryWhereArray = [FieldPath, WhereFilterOp, any]; | type IQueryWhereArray = [FieldPath, WhereFilterOp, any]; | ||||||
| @ -346,53 +428,55 @@ type IQueryWhere = IQueryWhereArray | IQueryWhereVerbose; | |||||||
| export class CollectionQuery extends Query { | export class CollectionQuery extends Query { | ||||||
|    private _addId: string; |    private _addId: string; | ||||||
|  |  | ||||||
|  |  | ||||||
|    prepare(query): IPreparedQuery { |    prepare(query): IPreparedQuery { | ||||||
|       switch (query.type as ICollectionQueries) { |       switch (query.type as ICollectionQueries) { | ||||||
|          case "add": |          case "add": | ||||||
|             this._addId = nanoid(ALPHABET, 32) |             this._addId = nanoid(ALPHABET, 32); | ||||||
|             return { |             return { | ||||||
|                batchCompatible: true, |                batchCompatible: true, | ||||||
|                createCollection: true, |                createCollection: true, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|                runner: this.add, |                runner: this.add, | ||||||
|  |                permission: "write", | ||||||
|                additionalLock: [...query.path, this._addId] |                additionalLock: [...query.path, this._addId] | ||||||
|             } |             }; | ||||||
|          case "get": |          case "get": | ||||||
|             const limit = (query.options || {}).limit; |             const limit = (query.options || {}).limit; | ||||||
|             if (limit) |             if (limit) this.limit = limit; | ||||||
|                this.limit = limit; |  | ||||||
|             const where = (query.options || {}).where; |             const where = (query.options || {}).where; | ||||||
|             if (where) |             if (where) this.where = where; | ||||||
|                this.where = where; |  | ||||||
|  |  | ||||||
|             return { |             return { | ||||||
|                batchCompatible: false, |                batchCompatible: false, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|  |                permission: "read", | ||||||
|                runner: this.get |                runner: this.get | ||||||
|             } |             }; | ||||||
|          case "keys": |          case "keys": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: false, |                batchCompatible: false, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|  |                permission: "read", | ||||||
|                runner: this.keys |                runner: this.keys | ||||||
|             } |             }; | ||||||
|          case "list": |          case "list": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: false, |                batchCompatible: false, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|  |                permission: "read", | ||||||
|                runner: this.keys |                runner: this.keys | ||||||
|             } |             }; | ||||||
|          case "delete-collection": |          case "delete-collection": | ||||||
|             return { |             return { | ||||||
|                batchCompatible: false, |                batchCompatible: false, | ||||||
|                createCollection: false, |                createCollection: false, | ||||||
|                needDocument: false, |                needDocument: false, | ||||||
|  |                permission: "write", | ||||||
|                runner: this.deleteCollection |                runner: this.deleteCollection | ||||||
|             } |             }; | ||||||
|          // run = () => q.deleteCollection(); |          // run = () => q.deleteCollection(); | ||||||
|          // break; |          // break; | ||||||
|          default: |          default: | ||||||
| @ -400,32 +484,40 @@ export class CollectionQuery extends Query { | |||||||
|       } |       } | ||||||
|    } |    } | ||||||
|  |  | ||||||
|  |  | ||||||
|    private _where: IQueryWhereArray[] = []; |    private _where: IQueryWhereArray[] = []; | ||||||
|    public set where(value: IQueryWhere[]) { |    public set where(value: IQueryWhere[]) { | ||||||
|       const invalidWhere = new QueryError("Invalid Where"); |       const invalidWhere = new QueryError("Invalid Where"); | ||||||
|       if (!Array.isArray(value)) |       if (!Array.isArray(value)) throw invalidWhere; | ||||||
|          throw invalidWhere; |  | ||||||
|       let c = []; |       let c = []; | ||||||
|       this._where = value.map(cond => { |       this._where = value.map(cond => { | ||||||
|          Logging.debug("Query Condition", cond); |          Logging.debug("Query Condition", cond); | ||||||
|          if (Array.isArray(cond)) { |          if (Array.isArray(cond)) { | ||||||
|             if (cond.length !== 3) |             if (cond.length !== 3) throw invalidWhere; | ||||||
|                throw invalidWhere; |  | ||||||
|             return cond; |             return cond; | ||||||
|          } else { |          } else { | ||||||
|             if (cond && typeof cond === "object" && "fieldPath" in cond && "opStr" in cond && "value" in cond) { |             if ( | ||||||
|  |                cond && | ||||||
|  |                typeof cond === "object" && | ||||||
|  |                "fieldPath" in cond && | ||||||
|  |                "opStr" in cond && | ||||||
|  |                "value" in cond | ||||||
|  |             ) { | ||||||
|                return [cond.fieldPath, cond.opStr, cond.value]; |                return [cond.fieldPath, cond.opStr, cond.value]; | ||||||
|             } else { |             } else { | ||||||
|                throw invalidWhere; |                throw invalidWhere; | ||||||
|             } |             } | ||||||
|          } |          } | ||||||
|       }) |       }); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public limit: number = -1; |    public limit: number = -1; | ||||||
|  |  | ||||||
|    public async add(collection: string, document: string, batch: LevelUpChain, collectionKey: string) { |    public async add( | ||||||
|  |       collection: string, | ||||||
|  |       document: string, | ||||||
|  |       batch: LevelUpChain, | ||||||
|  |       collectionKey: string | ||||||
|  |    ) { | ||||||
|       let q = new DocumentQuery(this.database, this.session, { |       let q = new DocumentQuery(this.database, this.session, { | ||||||
|          type: "set", |          type: "set", | ||||||
|          path: this.additionalLock, |          path: this.additionalLock, | ||||||
| @ -442,28 +534,26 @@ export class CollectionQuery extends Query { | |||||||
|  |  | ||||||
|       let lt = Buffer.alloc(gt.length); |       let lt = Buffer.alloc(gt.length); | ||||||
|       lt.set(gt); |       lt.set(gt); | ||||||
|       lt[gt.length - 1] = 0xFF; |       lt[gt.length - 1] = 0xff; | ||||||
|  |  | ||||||
|       return { |       return { | ||||||
|          gt, |          gt, | ||||||
|          lt |          lt | ||||||
|       } |       }; | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public async keys(collection: string) { |    public async keys(collection: string) { | ||||||
|       if (!collection) |       if (!collection) return []; | ||||||
|          return [] |  | ||||||
|  |  | ||||||
|       return new Promise<string[]>((yes, no) => { |       return new Promise<string[]>((yes, no) => { | ||||||
|          let keys = []; |          let keys = []; | ||||||
|          const stream = this.database.data.createKeyStream({ |          const stream = this.database.data.createKeyStream({ | ||||||
|             ...this.getStreamOptions(collection), |             ...this.getStreamOptions(collection), | ||||||
|             keyAsBuffer: false |             keyAsBuffer: false | ||||||
|          }) |          }); | ||||||
|          stream.on("data", (key: string) => { |          stream.on("data", (key: string) => { | ||||||
|             let s = key.split("/", 2); |             let s = key.split("/", 2); | ||||||
|             if (s.length > 1) |             if (s.length > 1) keys.push(s[1]); | ||||||
|                keys.push(s[1]); |  | ||||||
|          }); |          }); | ||||||
|          stream.on("end", () => yes(keys)); |          stream.on("end", () => yes(keys)); | ||||||
|          stream.on("error", no); |          stream.on("error", no); | ||||||
| @ -477,8 +567,7 @@ export class CollectionQuery extends Query { | |||||||
|          let seg = parts.shift(); |          let seg = parts.shift(); | ||||||
|  |  | ||||||
|          d = data[seg]; |          d = data[seg]; | ||||||
|          if (d === undefined || d === null) |          if (d === undefined || d === null) break; // Undefined/Null has no other fields! | ||||||
|             break; // Undefined/Null has no other fields! |  | ||||||
|       } |       } | ||||||
|       return d; |       return d; | ||||||
|    } |    } | ||||||
| @ -513,21 +602,20 @@ export class CollectionQuery extends Query { | |||||||
|                default: |                default: | ||||||
|                   throw new QueryError("Invalid where operation " + opStr); |                   throw new QueryError("Invalid where operation " + opStr); | ||||||
|             } |             } | ||||||
|          }) |          }); | ||||||
|       } |       } | ||||||
|       return true; |       return true; | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    async get(collection: string) { |    async get(collection: string) { | ||||||
|       if (!collection) |       if (!collection) return []; | ||||||
|          return []; |  | ||||||
|  |  | ||||||
|       return new Promise<DocRes[]>((yes, no) => { |       return new Promise<DocRes[]>((yes, no) => { | ||||||
|          const stream = this.database.data.iterator({ |          const stream = this.database.data.iterator({ | ||||||
|             ...this.getStreamOptions(collection), |             ...this.getStreamOptions(collection), | ||||||
|             keyAsBuffer: false, |             keyAsBuffer: false, | ||||||
|             valueAsBuffer: true |             valueAsBuffer: true | ||||||
|          }) |          }); | ||||||
|  |  | ||||||
|          let values: DocRes[] = []; |          let values: DocRes[] = []; | ||||||
|  |  | ||||||
| @ -535,16 +623,14 @@ export class CollectionQuery extends Query { | |||||||
|             if (err) { |             if (err) { | ||||||
|                no(err); |                no(err); | ||||||
|                stream.end(err => Logging.error(err)); |                stream.end(err => Logging.error(err)); | ||||||
|             } |             } else { | ||||||
|             else { |  | ||||||
|                if (!key && !value) { |                if (!key && !value) { | ||||||
|                   // END |                   // END | ||||||
|                   Logging.debug("Checked all!") |                   Logging.debug("Checked all!"); | ||||||
|                   yes(values); |                   yes(values); | ||||||
|                } else { |                } else { | ||||||
|                   let s = key.split("/", 2); |                   let s = key.split("/", 2); | ||||||
|                   if (s.length <= 1) |                   if (s.length <= 1) return; | ||||||
|                      return; |  | ||||||
|  |  | ||||||
|                   const id = s[1]; |                   const id = s[1]; | ||||||
|  |  | ||||||
| @ -555,9 +641,8 @@ export class CollectionQuery extends Query { | |||||||
|                            id, |                            id, | ||||||
|                            data |                            data | ||||||
|                         }); |                         }); | ||||||
|                      } |                      } else { | ||||||
|                      else { |                         stream.end(err => (err ? no(err) : yes(values))); | ||||||
|                         stream.end((err) => err ? no(err) : yes(values)) |  | ||||||
|                         return; |                         return; | ||||||
|                      } |                      } | ||||||
|                   } |                   } | ||||||
| @ -565,10 +650,10 @@ export class CollectionQuery extends Query { | |||||||
|                   stream.next(onValue); |                   stream.next(onValue); | ||||||
|                } |                } | ||||||
|             } |             } | ||||||
|          } |          }; | ||||||
|  |  | ||||||
|          stream.next(onValue); |          stream.next(onValue); | ||||||
|       }) |       }); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    checkChange(change: Change) { |    checkChange(change: Change) { | ||||||
| @ -576,25 +661,30 @@ export class CollectionQuery extends Query { | |||||||
|    } |    } | ||||||
|  |  | ||||||
|    firstSend(collection: string) { |    firstSend(collection: string) { | ||||||
|       return this.get(collection) |       return this.get(collection); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public async collections() { |    public async collections() { | ||||||
|       if (!this.session.root) |       if (!this.session.root) throw new QueryError("No Permission!"); | ||||||
|          throw new QueryError("No Permission!"); |  | ||||||
|  |  | ||||||
|       return new Promise<string[]>((yes, no) => { |       return new Promise<string[]>((yes, no) => { | ||||||
|          let keys = []; |          let keys = []; | ||||||
|          const stream = this.database.data.createKeyStream({ keyAsBuffer: false }) |          const stream = this.database.data.createKeyStream({ | ||||||
|  |             keyAsBuffer: false | ||||||
|  |          }); | ||||||
|          stream.on("data", (key: string) => keys.push(key.split("/"))); |          stream.on("data", (key: string) => keys.push(key.split("/"))); | ||||||
|          stream.on("end", () => yes(keys)); |          stream.on("end", () => yes(keys)); | ||||||
|          stream.on("error", no); |          stream.on("error", no); | ||||||
|       }); |       }); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    public async deleteCollection(collection: string, document: string, _b: LevelUpChain, collectionKey: string) { |    public async deleteCollection( | ||||||
|       if (!this.session.root) |       collection: string, | ||||||
|          throw new QueryError("No Permission!"); |       document: string, | ||||||
|  |       _b: LevelUpChain, | ||||||
|  |       collectionKey: string | ||||||
|  |    ) { | ||||||
|  |       if (!this.session.root) throw new QueryError("No Permission!"); | ||||||
|  |  | ||||||
|       //TODO: Lock whole collection! |       //TODO: Lock whole collection! | ||||||
|       let batch = this.database.data.batch(); |       let batch = this.database.data.batch(); | ||||||
| @ -615,8 +705,7 @@ export class CollectionQuery extends Query { | |||||||
|             }); |             }); | ||||||
|          } |          } | ||||||
|       } finally { |       } finally { | ||||||
|          if (batch) |          if (batch) batch.clear(); | ||||||
|             batch.clear(); |  | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,13 +2,15 @@ import Session from "./session"; | |||||||
| import Logging from "@hibas123/nodelogging"; | import Logging from "@hibas123/nodelogging"; | ||||||
|  |  | ||||||
| interface IRule<T> { | interface IRule<T> { | ||||||
|    ".write"?: T |    ".write"?: T; | ||||||
|    ".read"?: T |    ".read"?: T; | ||||||
| } | } | ||||||
|  |  | ||||||
| type IRuleConfig<T> = { | type IRuleConfig<T> = | ||||||
|  |    | IRule<T> | ||||||
|  |    | { | ||||||
|         [segment: string]: IRuleConfig<T>; |         [segment: string]: IRuleConfig<T>; | ||||||
| } | IRule<T>; |      }; | ||||||
|  |  | ||||||
| type IRuleRaw = IRuleConfig<string>; | type IRuleRaw = IRuleConfig<string>; | ||||||
| type IRuleParsed = IRuleConfig<boolean>; | type IRuleParsed = IRuleConfig<boolean>; | ||||||
| @ -17,17 +19,16 @@ const resolve = (value: any) => { | |||||||
|    if (value === true) { |    if (value === true) { | ||||||
|       return true; |       return true; | ||||||
|    } else if (typeof value === "string") { |    } else if (typeof value === "string") { | ||||||
|  |  | ||||||
|    } |    } | ||||||
|    return undefined; |    return undefined; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export class Rules { | export class Rules { | ||||||
|    rules: IRuleParsed; |    rules: IRuleParsed; | ||||||
|    constructor(private config: string) { |    constructor(private config: string) { | ||||||
|       let parsed: IRuleRaw = JSON.parse(config); |       let parsed: IRuleRaw = JSON.parse(config); | ||||||
|  |  | ||||||
|       const analyze = (raw: IRuleRaw) => { |       const analyse = (raw: IRuleRaw) => { | ||||||
|          let r: IRuleParsed = {}; |          let r: IRuleParsed = {}; | ||||||
|  |  | ||||||
|          if (raw[".read"]) { |          if (raw[".read"]) { | ||||||
| @ -47,25 +48,34 @@ export class Rules { | |||||||
|          } |          } | ||||||
|  |  | ||||||
|          for (let segment in raw) { |          for (let segment in raw) { | ||||||
|             if (segment.startsWith(".")) |             if (segment.startsWith(".")) continue; | ||||||
|                continue; |  | ||||||
|  |  | ||||||
|             r[segment] = analyze(raw[segment]); |             r[segment] = analyse(raw[segment]); | ||||||
|          } |          } | ||||||
|          return r; |          return r; | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       this.rules = analyse(parsed); | ||||||
|    } |    } | ||||||
|  |  | ||||||
|       this.rules = analyze(parsed); |    hasPermission( | ||||||
|    } |       path: string[], | ||||||
|  |       session: Session | ||||||
|    hasPermission(path: string[], session: Session): { read: boolean, write: boolean } { |    ): { read: boolean; write: boolean; path: string[] } { | ||||||
|  |       if (session.root) | ||||||
|  |          return { | ||||||
|  |             read: true, | ||||||
|  |             write: true, | ||||||
|  |             path: path | ||||||
|  |          }; | ||||||
|       let read = this.rules[".read"] || false; |       let read = this.rules[".read"] || false; | ||||||
|       let write = this.rules[".write"] || false; |       let write = this.rules[".write"] || false; | ||||||
|  |  | ||||||
|       let rules = this.rules; |       let rules = this.rules; | ||||||
|  |  | ||||||
|       for (let segment of path) { |       for (let idx in path) { | ||||||
|          if (segment.startsWith("$") || segment.startsWith(".")) { |          let segment = path[idx]; | ||||||
|  |          if (segment.startsWith(".")) { | ||||||
|             read = false; |             read = false; | ||||||
|             write = false; |             write = false; | ||||||
|             Logging.log("Invalid query path (started with '$' or '.'):", path); |             Logging.log("Invalid query path (started with '$' or '.'):", path); | ||||||
| @ -77,22 +87,25 @@ export class Rules { | |||||||
|             .find(e => { |             .find(e => { | ||||||
|                switch (e) { |                switch (e) { | ||||||
|                   case "$uid": |                   case "$uid": | ||||||
|                      if (segment === session.uid) |                      if (segment === "$uid") { | ||||||
|  |                         path[idx] = session.uid; | ||||||
|                         return true; |                         return true; | ||||||
|  |                      } | ||||||
|  |                      if (segment === session.uid) return true; | ||||||
|                      break; |                      break; | ||||||
|                } |                } | ||||||
|                return false; |                return false; | ||||||
|             }) |             }); | ||||||
|  |  | ||||||
|          rules = (k ? rules[k] : undefined) || rules[segment] || rules["*"]; |          rules = (k ? rules[k] : undefined) || rules[segment] || rules["*"]; | ||||||
|  |  | ||||||
|          if (rules) { |          if (rules) { | ||||||
|             if (rules[".read"]) { |             if (rules[".read"]) { | ||||||
|                read = rules[".read"] |                read = rules[".read"]; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (rules[".write"]) { |             if (rules[".write"]) { | ||||||
|                read = rules[".write"] |                read = rules[".write"]; | ||||||
|             } |             } | ||||||
|          } else { |          } else { | ||||||
|             break; |             break; | ||||||
| @ -101,8 +114,9 @@ export class Rules { | |||||||
|  |  | ||||||
|       return { |       return { | ||||||
|          read: read as boolean, |          read: read as boolean, | ||||||
|          write: write as boolean |          write: write as boolean, | ||||||
|       } |          path | ||||||
|  |       }; | ||||||
|    } |    } | ||||||
|  |  | ||||||
|    toJSON() { |    toJSON() { | ||||||
|  | |||||||
| @ -5,16 +5,26 @@ interface IFormConfigField { | |||||||
|    type: "text" | "number" | "boolean" | "textarea"; |    type: "text" | "number" | "boolean" | "textarea"; | ||||||
|    label: string; |    label: string; | ||||||
|    value?: string; |    value?: string; | ||||||
|  |    disabled?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| type IFormConfig = { [name: string]: IFormConfigField } | type IFormConfig = { [name: string]: IFormConfigField }; | ||||||
|  |  | ||||||
| export default function getForm(url: string, title: string, fieldConfig: IFormConfig): (ctx: Context) => void { | export default function getForm( | ||||||
|    let fields = Object.keys(fieldConfig).map(name => ({ name, ...fieldConfig[name] })) |    url: string, | ||||||
|  |    title: string, | ||||||
|  |    fieldConfig: IFormConfig | ||||||
|  | ): (ctx: Context) => void { | ||||||
|  |    let fields = Object.keys(fieldConfig).map(name => ({ | ||||||
|  |       name, | ||||||
|  |       ...fieldConfig[name], | ||||||
|  |       disabled: fieldConfig.disabled ? "disabled" : "" | ||||||
|  |    })); | ||||||
|  |  | ||||||
|    return ctx => ctx.body = getTemplate("forms")({ |    return ctx => | ||||||
|  |       (ctx.body = getTemplate("forms")({ | ||||||
|          url, |          url, | ||||||
|          title, |          title, | ||||||
|          fields |          fields | ||||||
|    }); |       })); | ||||||
| } | } | ||||||
| @ -2,7 +2,11 @@ import * as Router from "koa-router"; | |||||||
| import Settings from "../../settings"; | import Settings from "../../settings"; | ||||||
| import getForm from "../helper/form"; | import getForm from "../helper/form"; | ||||||
| import getTable from "../helper/table"; | import getTable from "../helper/table"; | ||||||
| import { BadRequestError, NoPermissionError } from "../helper/errors"; | import { | ||||||
|  |    BadRequestError, | ||||||
|  |    NoPermissionError, | ||||||
|  |    NotFoundError | ||||||
|  | } from "../helper/errors"; | ||||||
| import { DatabaseManager } from "../../database/database"; | import { DatabaseManager } from "../../database/database"; | ||||||
| import { MP } from "../../database/query"; | import { MP } from "../../database/query"; | ||||||
| import config from "../../config"; | import config from "../../config"; | ||||||
| @ -13,10 +17,9 @@ const AdminRoute = new Router(); | |||||||
|  |  | ||||||
| AdminRoute.use(async (ctx, next) => { | AdminRoute.use(async (ctx, next) => { | ||||||
|    const { key } = ctx.query; |    const { key } = ctx.query; | ||||||
|    if (key !== config.admin) |    if (key !== config.admin) throw new NoPermissionError("No permission!"); | ||||||
|       throw new NoPermissionError("No permission!"); |  | ||||||
|    return next(); |    return next(); | ||||||
| }) | }); | ||||||
|  |  | ||||||
| AdminRoute.get("/", async ctx => { | AdminRoute.get("/", async ctx => { | ||||||
|    //TODO: Main Interface |    //TODO: Main Interface | ||||||
| @ -33,26 +36,24 @@ AdminRoute.get("/settings", 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, value]); | ||||||
|       }) |       }); | ||||||
|  |  | ||||||
|       stream.on("error", no); |       stream.on("error", no); | ||||||
|       stream.on("end", () => yes(res)) |       stream.on("end", () => yes(res)); | ||||||
|    }) |    }); | ||||||
|  |  | ||||||
|    if (ctx.query.view) { |    if (ctx.query.view) { | ||||||
|       return getTable("Settings", res, ctx); |       return getTable("Settings", res, ctx); | ||||||
|    } else { |    } else { | ||||||
|       ctx.body = res; |       ctx.body = res; | ||||||
|    } |    } | ||||||
| }) | }); | ||||||
|  |  | ||||||
| AdminRoute.get("/data", async ctx => { | AdminRoute.get("/data", async ctx => { | ||||||
|    const { database } = ctx.query; |    const { database } = ctx.query; | ||||||
|    let db = DatabaseManager.getDatabase(database); |    let db = DatabaseManager.getDatabase(database); | ||||||
|    if (!db) |    if (!db) throw new BadRequestError("Database not found"); | ||||||
|       throw new BadRequestError("Database not found"); |  | ||||||
|    let res = await new Promise<string[][]>((yes, no) => { |    let res = await new Promise<string[][]>((yes, no) => { | ||||||
|  |  | ||||||
|       const stream = db.data.createReadStream({ |       const stream = db.data.createReadStream({ | ||||||
|          keys: true, |          keys: true, | ||||||
|          values: true, |          values: true, | ||||||
| @ -61,28 +62,37 @@ AdminRoute.get("/data", async ctx => { | |||||||
|          limit: 1000 |          limit: 1000 | ||||||
|       }); |       }); | ||||||
|       let res = [["key", "value"]]; |       let res = [["key", "value"]]; | ||||||
|       stream.on("data", ({ key, value }: { key: string, value: Buffer }) => { |       stream.on("data", ({ key, value }: { key: string; value: Buffer }) => { | ||||||
|          res.push([key, key.split("/").length > 2 ? value.toString() : JSON.stringify(MP.decode(value))]); |          res.push([ | ||||||
|       }) |             key, | ||||||
|  |             key.split("/").length > 2 | ||||||
|  |                ? value.toString() | ||||||
|  |                : JSON.stringify(MP.decode(value)) | ||||||
|  |          ]); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|       stream.on("error", no); |       stream.on("error", no); | ||||||
|       stream.on("end", () => yes(res)) |       stream.on("end", () => yes(res)); | ||||||
|    }) |    }); | ||||||
|  |  | ||||||
|    if (ctx.query.view) { |    if (ctx.query.view) { | ||||||
|       return getTable("Data from " + database, res, ctx); |       return getTable("Data from " + database, res, ctx); | ||||||
|    } else { |    } else { | ||||||
|       ctx.body = res; |       ctx.body = res; | ||||||
|    } |    } | ||||||
| }) | }); | ||||||
|  |  | ||||||
| AdminRoute | AdminRoute.get("/database", ctx => { | ||||||
|    .get("/database", ctx => { |  | ||||||
|    const isFull = ctx.query.full === "true" || ctx.query.full === "1"; |    const isFull = ctx.query.full === "true" || ctx.query.full === "1"; | ||||||
|    let res; |    let res; | ||||||
|    if (isFull) { |    if (isFull) { | ||||||
|       //TODO: Better than JSON.parse / JSON.stringify |       //TODO: Better than JSON.parse / JSON.stringify | ||||||
|          res = Array.from(DatabaseManager.databases.entries()).map(([name, config]) => ({ name, ...(JSON.parse(JSON.stringify(config))) })); |       res = Array.from(DatabaseManager.databases.entries()).map( | ||||||
|  |          ([name, config]) => ({ | ||||||
|  |             name, | ||||||
|  |             ...JSON.parse(JSON.stringify(config)) | ||||||
|  |          }) | ||||||
|  |       ); | ||||||
|    } else { |    } else { | ||||||
|       res = Array.from(DatabaseManager.databases.keys()); |       res = Array.from(DatabaseManager.databases.keys()); | ||||||
|    } |    } | ||||||
| @ -92,42 +102,31 @@ AdminRoute | |||||||
|    } else { |    } else { | ||||||
|       ctx.body = res; |       ctx.body = res; | ||||||
|    } |    } | ||||||
|    }) | }).post("/database", async ctx => { | ||||||
|    .post("/database", async ctx => { |  | ||||||
|    const { name, rules, publickey, accesskey, rootkey } = ctx.request.body; |    const { name, rules, publickey, accesskey, rootkey } = ctx.request.body; | ||||||
|  |  | ||||||
|       if (!name) |    if (!name) throw new BadRequestError("Name must be set!"); | ||||||
|          throw new BadRequestError("Name must be set!"); |  | ||||||
|  |  | ||||||
|    let db = DatabaseManager.getDatabase(name); |    let db = DatabaseManager.getDatabase(name); | ||||||
|       if (!db) |    if (!db) db = await DatabaseManager.addDatabase(name); | ||||||
|          db = await DatabaseManager.addDatabase(name); |  | ||||||
|  |  | ||||||
|       if (publickey) |    if (publickey) await db.setPublicKey(publickey); | ||||||
|          await db.setPublicKey(publickey); |  | ||||||
|  |  | ||||||
|       if (rules) |    if (rules) await db.setRules(rules); | ||||||
|          await db.setRules(rules); |  | ||||||
|  |  | ||||||
|       if (accesskey) |    if (accesskey) await db.setAccessKey(accesskey); | ||||||
|          await db.setAccessKey(accesskey); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|       if (rootkey) |  | ||||||
|          await db.setRootKey(rootkey); |  | ||||||
|  |  | ||||||
|  |    if (rootkey) await db.setRootKey(rootkey); | ||||||
|  |  | ||||||
|    ctx.body = "Success"; |    ctx.body = "Success"; | ||||||
|    }) | }); | ||||||
|  |  | ||||||
| AdminRoute.get("/collections", async ctx => { | AdminRoute.get("/collections", async ctx => { | ||||||
|    const { database } = ctx.query; |    const { database } = ctx.query; | ||||||
|    let db = DatabaseManager.getDatabase(database); |    let db = DatabaseManager.getDatabase(database); | ||||||
|    if (!db) |    if (!db) throw new BadRequestError("Database not found"); | ||||||
|       throw new BadRequestError("Database not found"); |  | ||||||
|  |  | ||||||
|    let res = await new Promise<string[]>((yes, no) => { |    let res = await new Promise<string[]>((yes, no) => { | ||||||
|  |  | ||||||
|       const stream = db.collections.createKeyStream({ |       const stream = db.collections.createKeyStream({ | ||||||
|          keyAsBuffer: false, |          keyAsBuffer: false, | ||||||
|          limit: 1000 |          limit: 1000 | ||||||
| @ -135,24 +134,23 @@ AdminRoute.get("/collections", async ctx => { | |||||||
|       let res = []; |       let res = []; | ||||||
|       stream.on("data", (key: string) => { |       stream.on("data", (key: string) => { | ||||||
|          res.push(key); |          res.push(key); | ||||||
|       }) |       }); | ||||||
|  |  | ||||||
|       stream.on("error", no); |       stream.on("error", no); | ||||||
|       stream.on("end", () => yes(res)) |       stream.on("end", () => yes(res)); | ||||||
|    }) |    }); | ||||||
|  |  | ||||||
|    if (ctx.query.view) { |    if (ctx.query.view) { | ||||||
|       return getTable("Databases", res, ctx); |       return getTable("Databases", res, ctx); | ||||||
|    } else { |    } else { | ||||||
|       ctx.body = res; |       ctx.body = res; | ||||||
|    } |    } | ||||||
| }) | }); | ||||||
|  |  | ||||||
| AdminRoute.get("/collections/cleanup", async ctx => { | AdminRoute.get("/collections/cleanup", async ctx => { | ||||||
|    const { database } = ctx.query; |    const { database } = ctx.query; | ||||||
|    let db = DatabaseManager.getDatabase(database); |    let db = DatabaseManager.getDatabase(database); | ||||||
|    if (!db) |    if (!db) throw new BadRequestError("Database not found"); | ||||||
|       throw new BadRequestError("Database not found"); |  | ||||||
|  |  | ||||||
|    let deleted = await db.runCleanup(); |    let deleted = await db.runCleanup(); | ||||||
|    if (ctx.query.view) { |    if (ctx.query.view) { | ||||||
| @ -160,14 +158,55 @@ AdminRoute.get("/collections/cleanup", async ctx => { | |||||||
|    } else { |    } else { | ||||||
|       ctx.body = deleted; |       ctx.body = deleted; | ||||||
|    } |    } | ||||||
| }) | }); | ||||||
|  |  | ||||||
| AdminRoute.get("/database/new", getForm("/v1/admin/database", "New/Change Database", { | AdminRoute.get( | ||||||
|    name: { label: "Name", type: "text", }, |    "/database/new", | ||||||
|  |    getForm("/v1/admin/database", "New Database", { | ||||||
|  |       name: { label: "Name", type: "text" }, | ||||||
|       accesskey: { label: "Access Key", type: "text" }, |       accesskey: { label: "Access Key", type: "text" }, | ||||||
|       rootkey: { label: "Root access key", type: "text" }, |       rootkey: { label: "Root access key", type: "text" }, | ||||||
|    rules: { label: "Rules", type: "textarea", value: `{\n   ".write": true, \n   ".read": true \n}` }, |       rules: { | ||||||
|  |          label: "Rules", | ||||||
|  |          type: "textarea", | ||||||
|  |          value: `{\n   ".write": true, \n   ".read": true \n}` | ||||||
|  |       }, | ||||||
|       publickey: { label: "Public Key", type: "textarea" } |       publickey: { label: "Public Key", type: "textarea" } | ||||||
| })) |    }) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | AdminRoute.get("/database/update", async ctx => { | ||||||
|  |    const { database } = ctx.query; | ||||||
|  |    let db = DatabaseManager.getDatabase(database); | ||||||
|  |    if (!db) throw new NotFoundError("Database not found!"); | ||||||
|  |    getForm("/v1/admin/database", "Change Database", { | ||||||
|  |       name: { | ||||||
|  |          label: "Name", | ||||||
|  |          type: "text", | ||||||
|  |          value: db.name, | ||||||
|  |          disabled: true | ||||||
|  |       }, | ||||||
|  |       accesskey: { | ||||||
|  |          label: "Access Key", | ||||||
|  |          type: "text", | ||||||
|  |          value: db.accesskey | ||||||
|  |       }, | ||||||
|  |       rootkey: { | ||||||
|  |          label: "Root access key", | ||||||
|  |          type: "text", | ||||||
|  |          value: db.rootkey | ||||||
|  |       }, | ||||||
|  |       rules: { | ||||||
|  |          label: "Rules", | ||||||
|  |          type: "textarea", | ||||||
|  |          value: db.rules.toJSON() | ||||||
|  |       }, | ||||||
|  |       publickey: { | ||||||
|  |          label: "Public Key", | ||||||
|  |          type: "textarea", | ||||||
|  |          value: db.publickey | ||||||
|  |       } | ||||||
|  |    })(ctx); | ||||||
|  | }); | ||||||
|  |  | ||||||
| export default AdminRoute; | export default AdminRoute; | ||||||
| @ -1,7 +1,11 @@ | |||||||
| import * as Router from "koa-router"; | import * as Router from "koa-router"; | ||||||
| import AdminRoute from "./admin"; | import AdminRoute from "./admin"; | ||||||
| import { DatabaseManager } from "../../database/database"; | import { DatabaseManager } from "../../database/database"; | ||||||
| import { NotFoundError, NoPermissionError, BadRequestError } from "../helper/errors"; | import { | ||||||
|  |    NotFoundError, | ||||||
|  |    NoPermissionError, | ||||||
|  |    BadRequestError | ||||||
|  | } from "../helper/errors"; | ||||||
| import Logging from "@hibas123/nodelogging"; | import Logging from "@hibas123/nodelogging"; | ||||||
| import Session from "../../database/session"; | import Session from "../../database/session"; | ||||||
| import nanoid = require("nanoid"); | import nanoid = require("nanoid"); | ||||||
| @ -28,7 +32,7 @@ V1.post("/db/:database/query", async ctx => { | |||||||
|  |  | ||||||
|    if (db.accesskey) { |    if (db.accesskey) { | ||||||
|       if (!accesskey || accesskey !== db.accesskey) { |       if (!accesskey || accesskey !== db.accesskey) { | ||||||
|          throw new NoPermissionError(""); |          throw new NoPermissionError("Invalid Access Key"); | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
|  |  | ||||||
| @ -36,7 +40,6 @@ V1.post("/db/:database/query", async ctx => { | |||||||
|       let res = await verifyJWT(authkey, db.publickey); |       let res = await verifyJWT(authkey, db.publickey); | ||||||
|       if (!res || !res.uid) { |       if (!res || !res.uid) { | ||||||
|          throw new BadRequestError("Invalid JWT"); |          throw new BadRequestError("Invalid JWT"); | ||||||
|          return; |  | ||||||
|       } else { |       } else { | ||||||
|          session.uid = res.uid; |          session.uid = res.uid; | ||||||
|       } |       } | ||||||
| @ -54,6 +57,6 @@ V1.post("/db/:database/query", async ctx => { | |||||||
|          throw new BadRequestError(err.message); |          throw new BadRequestError(err.message); | ||||||
|       } |       } | ||||||
|       throw err; |       throw err; | ||||||
|    }) |    }); | ||||||
| }) | }); | ||||||
| export default V1; | export default V1; | ||||||
| @ -7,11 +7,19 @@ | |||||||
|       <title>Admin Interface</title> |       <title>Admin Interface</title> | ||||||
|       <link |       <link | ||||||
|          rel="stylesheet" |          rel="stylesheet" | ||||||
|  | <<<<<<< HEAD | ||||||
|          href="https://unpkg.com/@hibas123/theme@1/out/base.css" |          href="https://unpkg.com/@hibas123/theme@1/out/base.css" | ||||||
|       /> |       /> | ||||||
|       <link |       <link | ||||||
|          rel="stylesheet" |          rel="stylesheet" | ||||||
|          href="https://unpkg.com/@hibas123/theme@1/out/light.css" |          href="https://unpkg.com/@hibas123/theme@1/out/light.css" | ||||||
|  | ======= | ||||||
|  |          href="https://unpkg.com/@hibas123/theme/out/base.css" | ||||||
|  |       /> | ||||||
|  |       <link | ||||||
|  |          rel="stylesheet" | ||||||
|  |          href="https://unpkg.com/@hibas123/theme/out/light.css" | ||||||
|  | >>>>>>> 0bfdbce908484560108e20cede6f9e7c46710818 | ||||||
|       /> |       /> | ||||||
|  |  | ||||||
|       <script src="https://unpkg.com/handlebars/dist/handlebars.min.js"></script> |       <script src="https://unpkg.com/handlebars/dist/handlebars.min.js"></script> | ||||||
| @ -60,7 +68,11 @@ | |||||||
|                style="margin: 1rem;" |                style="margin: 1rem;" | ||||||
|             ></div> |             ></div> | ||||||
|          </div> |          </div> | ||||||
|  | <<<<<<< HEAD | ||||||
|          <div style="position: relative;"> |          <div style="position: relative;"> | ||||||
|  | ======= | ||||||
|  |          <div style="position:relative;"> | ||||||
|  | >>>>>>> 0bfdbce908484560108e20cede6f9e7c46710818 | ||||||
|             <iframe id="content"></iframe> |             <iframe id="content"></iframe> | ||||||
|          </div> |          </div> | ||||||
|       </div> |       </div> | ||||||
| @ -102,6 +114,7 @@ | |||||||
|    <button class=btn onclick="loadView('data', {database:'${database}'})">Data</button> |    <button class=btn onclick="loadView('data', {database:'${database}'})">Data</button> | ||||||
|    <button class=btn onclick="loadView('collections', {database:'${database}'})">Collections</button> |    <button class=btn onclick="loadView('collections', {database:'${database}'})">Collections</button> | ||||||
|    <button class=btn onclick="loadView('collections/cleanup', {database:'${database}'})">Clean</button> |    <button class=btn onclick="loadView('collections/cleanup', {database:'${database}'})">Clean</button> | ||||||
|  |    <button class=btn onclick="loadView('database/update', {database:'${database}'})">Change</button> | ||||||
| </div>` | </div>` | ||||||
|                   ) |                   ) | ||||||
|                ) |                ) | ||||||
|  | |||||||
| @ -32,19 +32,19 @@ | |||||||
|             <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}}" {{disabled}} /> | ||||||
|                {{/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}}" {{disabled}} /> | ||||||
|                {{/ifCond}} |                {{/ifCond}} | ||||||
|  |  | ||||||
|                {{#ifCond type "===" "boolean"}} |                {{#ifCond type "===" "boolean"}} | ||||||
|                <input type="checkbox" name="{{name}}" checked="{{value}}" /> |                <input type="checkbox" name="{{name}}" checked="{{value}}" {{disabled}} /> | ||||||
|                {{/ifCond}} |                {{/ifCond}} | ||||||
|  |  | ||||||
|                {{#ifCond type "===" "textarea"}} |                {{#ifCond type "===" "textarea"}} | ||||||
|                <textarea class="inp" name="{{name}}" rows="20">{{value}}</textarea> |                <textarea class="inp" name="{{name}}" rows="20" {{disabled}}>{{value}}</textarea> | ||||||
|                {{/ifCond}} |                {{/ifCond}} | ||||||
|             </div> |             </div> | ||||||
|             {{/each}} |             {{/each}} | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Fabian Stamm
					Fabian Stamm