DB API Reads

This commit is contained in:
garrettmills
2020-03-01 15:37:52 -06:00
parent fc5fc14b3f
commit dfcaf046c6
17 changed files with 585 additions and 18 deletions

View File

@@ -0,0 +1,121 @@
const Controller = require('libflitter/controller/Controller')
/*
* DatabaseAPI Controller
* -------------------------------------------------------------
* Put some description here!
*/
class DatabaseAPI extends Controller {
static get services() {
return [...super.services, 'models', 'utility']
}
async databases(req, res, next) {
const Database = this.models.get('api:db:Database')
const dbs = await Database.visible_by_user(req.user)
return res.api(dbs.map(x => x.to_api_object()))
}
async get_database(req, res, next) {
return res.api(req.form.database.to_api_object())
}
async get_columns(req, res, next) {
const db = req.form.database
const cols = (await db.get_columns()).map(x => x.to_api_object())
return res.api(cols)
}
async get_columns_order(req, res, next) {
return res.api(req.form.database.ColumnIds)
}
async get_data(req, res, next) {
const DBEntry = this.models.get('api:db:DBEntry')
const db = req.form.database
const cursor = await DBEntry.cursor({DatabaseId: db.UUID})
if ( req.query.sort ) {
if ( this.utility.is_json(req.query.sort) ) {
const sort = JSON.parse(req.query.sort)
if ( typeof sort !== 'object' ) return res.status(400).message('Invalid sort field. Should be JSON object.').api()
const sort_obj = {}
for ( const field in sort ) {
if ( !sort.hasOwnProperty(field) ) continue
sort_obj[`RowData.${field}`] = (Number(sort[field]) < 0 ? -1 : 1)
}
cursor.sort(sort_obj)
} else if ( req.query.sort ) {
const sort_obj = {}
sort_obj[`RowData.${req.query.sort}`] = (req.query.reverse ? -1 : 1)
cursor.sort(sort_obj)
}
}
if ( req.query.limit ) {
const limit = Number(req.query.limit)
if ( !isNaN(limit) ) cursor.limit(limit)
}
if ( req.query.skip ) {
const skip = Number(req.query.skip)
if ( !isNaN(skip) ) cursor.skip(skip)
}
if ( req.query.where ) {
if ( !this.utility.is_json(req.query.where) ) {
return res.status(400).message('Invalid where field. Should be JSON object.').api()
}
const wheres = JSON.parse(req.query.where)
if ( typeof wheres !== 'object' ) {
return res.status(400).message('Invalid where field. Should be JSON object.').api()
}
const wheres_object = {}
for ( const field in wheres ) {
if ( !wheres.hasOwnProperty(field) ) continue
const value = wheres[field]
if ( typeof value !== 'object' ) {
wheres_object[`RowData.${field}`] = value
} else {
const sub_where = {}
}
}
}
const data = (await DBEntry.from_cursor(cursor)).map(x => {
x = x.to_api_object()
if ( req.query.flatten ) {
x = {
uuid: x.uuid,
database_id: x.database_id,
...x.data,
}
}
return x
})
return res.api(data)
}
async get_record(req, res, next) {
const DBEntry = this.models.get('api:db:DBEntry')
const db = req.form.database
const record = await DBEntry.findOne({UUID: req.params.record_id, DatabaseId: db.UUID})
if ( record ) {
const api_obj = record.to_api_object()
if ( req.query.flatten ) {
return res.api({
uuid: api_obj.uuid,
database_id: api_obj.database_id,
...api_obj.data,
})
}
return res.api(api_obj)
}
else return res.status(404).message('Database record not found with that ID.').api()
}
}
module.exports = exports = DatabaseAPI

View File

@@ -8,12 +8,18 @@ const Page = require("../../../models/api/Page.model")
* Put some description here!
*/
class Misc extends Controller {
#default_token_grants = ['database']
hello_world(req, res) {
return res.api({
hello: 'world',
})
static get services() {
return [...super.services, 'models']
}
async get_token(req, res, next) {
const Token = this.models.get('api:Token')
const token = await Token.for_user(req.user)
return res.api(token.token)
}
}
module.exports = exports = Misc

View File

@@ -7,6 +7,10 @@ const uuid = require('uuid/v4');
* Put some description here!
*/
class Node extends Model {
static get services() {
return [...super.services, 'models']
}
static get schema() {
// Return a flitter-orm schema here.
return {
@@ -26,7 +30,7 @@ class Node extends Model {
// Static and instance methods can go here
get page() {
const Page = this.model.get("api:Page")
const Page = this.models.get('api:Page')
return this.belongs_to_one(Page, "PageId", "_id")
}

View File

@@ -44,6 +44,64 @@ class Page extends Model {
static scopes = [new ActiveScope]
static async visible_by_user(user) {
const user_root = await user.get_root_page()
const user_root_children = await user_root.visible_flat_children(user)
const view_only_trees = await this.find({ shared_users_view: user._id })
let view_only_children = []
for ( const tree of view_only_trees ) {
if ( await tree.is_accessible_by(user) ) {
view_only_children.push(tree)
view_only_children = [...view_only_children, await tree.visible_flat_children(user)]
}
}
const update_trees = await this.find({ shared_users_update: user._id })
let update_children = []
for ( const tree of update_trees ) {
if ( await tree.is_accessible_by(user) ) {
update_children.push(tree)
update_children = [...update_children, await tree.visible_flat_children(user)]
}
}
const manage_trees = await this.find({ shared_users_manage: user._id })
let manage_children = []
for ( const tree of manage_trees ) {
if ( await tree.is_accessible_by(user) ) {
manage_children.push(tree)
manage_children = [...manage_children, await tree.visible_flat_children(user)]
}
}
const all_children = [...user_root_children, ...view_only_children, ...update_children, ...manage_children]
const unique_children = []
const seen_UUIDs = []
all_children.forEach(child => {
if ( !seen_UUIDs.includes(child.UUID) ) {
unique_children.push(child)
seen_UUIDs.push(child.UUID)
}
})
return unique_children
}
async visible_flat_children(user) {
const children = await this.childPages
let visible = []
if ( children ) {
for ( const child of children ) {
if ( !(await child.is_accessible_by(user)) ) continue
visible.push(child)
visible = [...visible, ...(await child.visible_flat_children(user))]
}
}
return visible
}
is_shared() {
return this.shared_users_view.length > 0 || this.shared_users_update.length > 0 || this.shared_users_manage.length > 0
}

View File

@@ -0,0 +1,95 @@
const Model = require('flitter-orm/src/model/Model')
const { ObjectId } = require('mongodb')
const User = require('../auth/User.model')
const ValidScope = require('../scopes/Valid.scope')
const uuid = require('uuid/v4')
const jwt = require('jsonwebtoken')
/*
* Token Model
* -------------------------------------------------------------
* Put some description here!
*/
class Token extends Model {
static #default_grants = ['database']
static get services() {
return [...super.services, 'configs']
}
static get schema() {
// Return a flitter-orm schema here.
return {
user_id: ObjectId,
token: String,
issued: { type: Date, default: () => new Date },
expires: {
type: Date,
default: () => {
const d = new Date
d.setDate(d.getDate() + 7)
return d
},
},
valid: { type: Boolean, default: true },
grants: [String],
}
}
static scopes = [new ValidScope]
static async create(user) {
const token = new this({ user_id: user._id, grants: this.#default_grants })
await token.save()
token.token = token.generate()
return await token.save()
}
static async for_user(user) {
const token = await this.findOne({ user_id: user._id })
return token ? token : await this.create(user)
}
static verify(token) {
const secret = this.prototype.configs.get('server.session.secret')
const server = this.prototype.configs.get('app.url')
return new Promise((res, rej) => {
jwt.verify(token, secret, (err, decoded) => {
if ( err ) rej(err)
else if ( decoded.iss !== server ) rej(new Error('Invalid token issuer: '+decoded.iss))
else {
this.findOne({ user_id: ObjectId(decoded.sub) }).then(result => {
if ( !result ) rej(new Error('Unable to find associated token. It may have been manually invalidated.'))
else res(result)
})
}
})
})
}
generate() {
const secret = this.configs.get('server.session.secret')
const server = this.configs.get('app.url')
const payload = {
sub: String(this.user_id),
iss: server,
permissions: this.grants.join(','),
exp: (this.expires.getTime()/1000 | 0),
iat: (this.issued.getTime()/1000 | 0),
}
return jwt.sign(payload, secret)
}
user() {
return this.has_one(User, 'user_id', '_id')
}
can(grant) {
return this.grants.includes(grant)
}
}
module.exports = exports = Token

View File

@@ -33,6 +33,16 @@ class ColumnDef extends Model {
data() {
return JSON.parse(this.additionalData ? this.additionalData : '{}')
}
to_api_object() {
return {
name: this.headerName,
uuid: this.UUID,
database_id: this.DatabaseId,
type: this.Type,
metadata: this.data()
}
}
}
module.exports = exports = ColumnDef

View File

@@ -17,6 +17,22 @@ class DBEntry extends Model {
}
// Static and instance methods can go here
to_api_object() {
return {
uuid: this.UUID,
database_id: this.DatabaseId,
data: this.RowData,
}
}
static async from_cursor(cursor) {
const arr = await cursor.toArray()
const collection = []
for ( const rec of arr ) {
collection.push(new this(rec))
}
return collection
}
}
module.exports = exports = DBEntry

View File

@@ -1,6 +1,9 @@
const Model = require('flitter-orm/src/model/Model')
const uuid = require('uuid/v4')
const ColumnDef = require('./ColumnDef.model')
const Page = require('../Page.model')
const Node = require('../Node.model')
const ActiveScope = require('../../scopes/Active.scope')
/*
* Database Model
@@ -16,18 +19,50 @@ class Database extends Model {
PageId: String,
ColumnIds: [String],
UUID: { type: String, default: () => uuid() },
Active: { type: Boolean, default: true },
}
}
accessible_by(user, mode = 'view') {
return user.can(`database:${this.UUID}:${mode}`)
static scopes = [new ActiveScope]
static async visible_by_user(user) {
const page_ids = (await Page.visible_by_user(user)).map(x => x.UUID)
return this.find({PageId: {$in: page_ids}})
}
async is_accessible_by(user, mode = 'view') {
const page = await this.page
return page.is_accessible_by(user, mode)
}
async get_columns() {
return ColumnDef.find({DatabaseId: this.UUID});
const cols = await ColumnDef.find({DatabaseId: this.UUID})
const assoc_cols = {}
cols.forEach(col => assoc_cols[col.UUID] = col)
return this.ColumnIds.map(x => assoc_cols[x])
}
get page() {
return this.belongs_to_one(Page, 'PageId', 'UUID')
}
get node() {
return this.belongs_to_one(Node, 'NodeId', 'UUID')
}
async delete() {
this.Active = false
await this.save()
}
to_api_object() {
return {
name: this.Name,
uuid: this.UUID,
page_id: this.PageId,
column_ids: this.ColumnIds,
}
}
// Static and instance methods can go here
}
module.exports = exports = Database

View File

@@ -0,0 +1,11 @@
const Scope = require('flitter-orm/src/model/Scope')
class ValidScope extends Scope {
async filter(to_filter) {
return to_filter.equal('valid', true)
.greater_than('expires', new Date)
.less_than('issued', new Date)
}
}
module.exports = exports = ValidScope

View File

@@ -0,0 +1,30 @@
const Middleware = require('libflitter/middleware/Middleware')
/*
* DatabaseRoute Middleware
* -------------------------------------------------------------
* Put some description here!
*/
class DatabaseRoute extends Middleware {
static get services() {
return [...super.services, 'models']
}
async test(req, res, next, args = {}){
const Database = this.models.get('api:db:Database')
const id = req.params.database_id ? req.params.database_id : (req.query.database_id ? req.query.database_id : false)
if ( !id ) return res.status(400).message('Missing required: database_id').api()
const db = await Database.findOne({UUID: id})
if ( !db ) return res.status(404).message('Unable to find database with that ID.').api()
if ( !(await db.is_accessible_by(req.user)) ) return req.security.deny()
if ( !req.form ) req.form = {}
req.form.database = db
next()
}
}
module.exports = exports = DatabaseRoute

View File

@@ -0,0 +1,52 @@
const Middleware = require('libflitter/middleware/Middleware')
/*
* BearerToken Middleware
* -------------------------------------------------------------
* Put some description here!
*/
class BearerToken extends Middleware {
static get services() {
return [...super.services, 'models']
}
/*
* Run the middleware test.
* This method is required by all Flitter middleware.
* It should either call the next function in the stack,
* or it should handle the response accordingly.
*/
async test(req, res, next, args = []){
const Token = this.models.get('api:Token')
const token_string = req.headers.authorization
if ( !token_string ) return this.fail(res, )
else if ( !token_string.startsWith('bearer ') ) return this.fail(res, 'Invalid authorization token. Prefix with "bearer".')
try {
const token = await Token.verify(token_string.replace('bearer ', ''))
const user = await token.user()
if ( !user || !token ) return this.fail(res)
if ( Array.isArray(args) ) {
for (const grant of args) {
if (!token.can(grant)) {
return this.fail(res)
}
}
}
req.user = user
req.token = token
next()
} catch (e) {
return this.fail(res, String(e))
}
}
fail(res, msg = 'Unauthorized') {
return res.status(401).message(msg).api({})
}
}
module.exports = exports = BearerToken

View File

@@ -12,6 +12,10 @@ const index = {
],
get: {
'/token': [
'controller::api:v1:Misc.get_token',
],
// Get the file ref node config for the specified file ref
'/files/:PageId/:NodeId/get/:FilesId': ['controller::api:v1:File.get_config'],

View File

@@ -0,0 +1,43 @@
module.exports = exports = {
prefix: '/db_api/v1',
middleware: [
// JWT authorization middleware. Sets req.user and req.token.
// Second param is array of required grants.
['api:auth:BearerToken', ['database']],
],
get: {
'/': [ 'controller::api:v1:DatabaseAPI.databases' ],
'/:database_id': [
'middleware::api:DatabaseRoute',
'controller::api:v1:DatabaseAPI.get_database',
],
'/:database_id/columns': [
'middleware::api:DatabaseRoute',
'controller::api:v1:DatabaseAPI.get_columns',
],
'/:database_id/columns/order': [
'middleware::api:DatabaseRoute',
'controller::api:v1:DatabaseAPI.get_columns_order',
],
'/:database_id/data': [
'middleware::api:DatabaseRoute',
'controller::api:v1:DatabaseAPI.get_data',
],
'/:database_id/record/:record_id': [
'middleware::api:DatabaseRoute',
'controller::api:v1:DatabaseAPI.get_record',
],
},
post: {
},
}