type-arango
Version:
ArangoDB Foxx decorators and utilities for TypeScript
380 lines (320 loc) • 12.5 kB
text/typescript
import {Joi} from '../joi'
import {collections, config, isActive, logger} from '..'
import {Document, getDocumentForContainer, Route as RouteModel, Scalar} from './index'
import {argumentResolve, arraySample, concatUnique, db, enjoi, isObject, queryBuilder} from '../utils'
import {
CreateCollectionOptions,
DecoratorId,
DecoratorStorage,
QueryOpt,
Roles,
RouteOpt,
RoutePathParam,
SchemaStructure
} from '../types'
import {getRouteForCollection} from '.'
export type RoleTypes = 'creators' | 'readers' | 'updaters' | 'deleters'
export interface CollectionRoles {
creators: Roles
readers: Roles
updaters: Roles
deleters: Roles
}
const METHODS_BODYLESS = ['get', 'delete']
/**
* Creates a new Collection for a decorated class
*/
function createCollectionFromContainer(someClass: any): Collection {
let c = new Collection(someClass)
collections.push(c)
return c
}
/**
* Finds a collection for a decorated class
*/
export function findCollectionForContainer(someClass: any): Collection | undefined {
return collections.find(c => someClass === c.Class || someClass.prototype instanceof c.Class)
}
/**
* Returns the respective collection instance for a decorated class
*/
export function getCollectionForContainer(someClass: any): Collection {
let col = findCollectionForContainer(someClass)
if(col) return col
return createCollectionFromContainer(someClass)
}
/**
* Collections represent tables in ArangoDB
*/
export class Collection {
public name: string
public db: ArangoDB.Collection
public completed: boolean = false
public opt?: CreateCollectionOptions
public schema: SchemaStructure = {}
public routes: RouteModel[] = []
public roles: CollectionRoles = {
creators: [],
readers: [],
updaters: [],
deleters: []
}
public doc?: Document<any>
private decorator: DecoratorStorage = {}
/**
* Returns a valid collection name
*/
static toName(input: string){
return config.prefixCollectionName ? module.context.collectionName(input) : input
}
/**
* Creates a new collection instance
*/
constructor(public Class: any){
this.name = Collection.toName(Class.name)
this.db = isActive ? db._collection(this.name) : null
}
/**
* Whether users can provide custom document keys on creation
*/
public get allowUserKeys(){
const keyOptions = this.opt && this.opt.keyOptions
return !keyOptions ? false : (keyOptions && keyOptions.allowUserKeys === true && keyOptions.type !== 'autoincrement')
}
public get route(){
let { name } = this
name = name.charAt(0).toLowerCase() + name.substr(1)
if(config.dasherizeRoutes)
name = name.replace(/[A-Z]/g, m => '-' + m.toLowerCase())
return name
}
public addRoles(key: RoleTypes, roles: Roles, onlyWhenEmpty: boolean = true){
if(onlyWhenEmpty && this.roles[key].length) return
this.roles[key] = concatUnique(this.roles[key], roles)
if(this.doc) this.doc!.roles = concatUnique(this.doc!.roles, roles)
// else console.log('CANNOT ADD ROLES, DOCUMENT NOT READY')
}
public decorate(decorator: DecoratorId, data: any){
this.decorator[decorator] = [...(this.decorator[decorator]||[]), {...data,decorator}]
}
get routeAuths(){
return (this.decorator['Route.auth']||[]).map(d => d.authorizeFn).reverse()
}
get routeRoles(){
return (this.decorator['Route.roles']||[]).map(d => d.rolesFn).reverse()
}
query(q: string | QueryOpt){
if(typeof q === 'string'){
return db._query(q)
}
if(!q.keep){
q.unset = []
if(config.stripDocumentId) q.unset.push('_id')
if(config.stripDocumentRev) q.unset.push('_rev')
if(config.stripDocumentKey) q.unset.push('_key')
}
return db._query(queryBuilder(this.name, q))
}
finalize(){
const { Collection, Route, Task, Function } = this.decorator
let { ofDocument, options = {} } = Collection![0]
this.opt = options
if(options.name) {
this.name = options.name
}
const doc = this.doc = getDocumentForContainer(argumentResolve(ofDocument))
doc.col = this
if(options.creators) this.addRoles('creators', options.creators, false)
if(options.readers) this.addRoles('readers', options.readers, false)
if(options.updaters) this.addRoles('updaters', options.updaters, false)
if(options.deleters) this.addRoles('deleters', options.deleters, false)
if(isActive){
// create collection
if(!this.db){
logger.info('Creating ArangoDB Collection "%s"', this.name)
const { of, creators, readers, updaters, deleters, roles, auth, routes, ...opt } = options
this.db = doc.isEdge
? db._createEdgeCollection(this.name, opt || {})
: db._createDocumentCollection(this.name, opt || {})
}
// create indices
for(let {options} of doc.indexes!){
this.db.ensureIndex(options)
}
const task = require('@arangodb/tasks')
const tasks = task.get()
// setup Tasks
if(Task) for(let {
prototype, attribute, period, offset, id, name, params
} of Task){
const opt: any = {
id,
offset,
name,
params
}
eval('opt.command = function '+prototype[attribute!].toString())
if(period > 0)
opt.period = period
if(tasks.find((t: any) => t.id === id)){
logger.debug('Unregister previously active task', id)
task.unregister(id)
}
logger.debug('Register task', opt)
try { task.register(opt) } catch(e){
logger.error('Could not register task:', e.message)
}
}
const aqlfunction = require('@arangodb/aql/functions')
// unregister old
if(config.unregisterAQLFunctionEntityGroup){
logger.debug('Unregister AQLFunction-group "'+this.name+'::*"')
aqlfunction.unregisterGroup(this.name)
}
// setup AQLFunctions
if(Function) for(let {
prototype, attribute, name, isDeterministic
} of Function){
logger.debug('Register AQLFunction "'+name+'"')
let f = null
eval('f = function '+prototype[attribute!].toString())
aqlfunction.register(name, f, isDeterministic)
}
}
if(Route) for(let {
prototype, attribute, method, pathOrRolesOrOptions, schemaOrRolesOrSummary,
rolesOrSchemaOrSummary, summaryOrSchemaOrRoles, options
} of Route){
let schema: any
const a: any = argumentResolve(pathOrRolesOrOptions, (inp: any) => enjoi(inp, 'required'), Joi)
let opt: RouteOpt = Object.assign({
queryParams: []
},
typeof a === 'string'
? {path:a}
: Array.isArray(a)
? {roles:a}
: typeof a === 'object' && a && !a.method
? {schema:a.isJoi ? a : enjoi(a)}
: a || {}
)
// allow options for schema param
if(isObject(schemaOrRolesOrSummary)){
opt = Object.assign(schemaOrRolesOrSummary, opt)
} else {
schema = argumentResolve(schemaOrRolesOrSummary, (inp: any) => enjoi(inp, 'required'), Joi)
if(schema instanceof Array){
opt.roles = schema
} else if(typeof schema === 'string'){
opt.summary = schema
} else if(typeof schema === 'object' && schema){
} else schema = null
}
// allow options for roles param
if(isObject(rolesOrSchemaOrSummary)){
opt = Object.assign(rolesOrSchemaOrSummary, opt)
} else {
let roles = argumentResolve(rolesOrSchemaOrSummary, (inp: any) => enjoi(inp, 'required'), Joi)
if(roles instanceof Array){
opt.roles = roles
} else if(typeof roles === 'string'){
opt.summary = roles
} else if(typeof roles === 'object' && roles) {
schema = roles
}
}
// allow options for summary param
if(isObject(summaryOrSchemaOrRoles)){
opt = Object.assign(summaryOrSchemaOrRoles, opt)
} else {
let summary = argumentResolve(summaryOrSchemaOrRoles, (inp: any) => enjoi(inp, 'required'), Joi)
if(summary instanceof Array){
opt.roles = summary
} else if(typeof summary === 'string'){
opt.summary = summary
} else if(typeof summary === 'object' && summary) {
schema = summary
}
}
if(options)
opt = Object.assign(options, opt)
if(!schema && opt.schema){
schema = argumentResolve(opt.schema, (inp: any) => enjoi(inp, 'required'), Joi)
}
if(method === 'LIST'){
method = 'get'
opt.action = 'list'
} else method = method.toLowerCase()
opt = RouteModel.parsePath(opt, this.route)
if(schema) {
if(schema.isJoi){}
else {
// support anonymous object syntax (joi => ({my:'object'}))
schema = enjoi(schema) as typeof Joi
}
// treat schema as queryParam or pathParam
if(schema._type === 'object'){
// allow optional request body but default to required()
schema = schema._flags.presence === 'optional' ? schema : schema.required()
// init params
opt.pathParams = opt.pathParams || []
opt.queryParams = opt.queryParams || []
let i = 0
// loop schema keys
for(const attr of schema._inner.children){
// attributes with a default value are optional
if(attr.schema._flags.default){
attr.schema._flags.presence = 'optional'
}
// check schema attr in pathParams
if(opt.pathParams.find(p => p[0] === attr.key)){
// override pathParam schema with route schema in order to specify more details
opt.pathParams = opt.pathParams.map(p => p[0] === attr.key ? [p[0], attr.schema, p[2]] as RoutePathParam : p)
// remove attr from schema
schema._inner.children = schema._inner.children.filter((a: any) => a.key !== attr.key)
continue
} else if(opt.queryParams.find(q => q[0] === attr.key)){
// override queryParam schema with route schema in order to specify more details
opt.queryParams = opt.queryParams.map(p => p[0] === attr.key ? [p[0], attr.schema, p[2]] as RoutePathParam : p)
// remove attr from schema
schema._inner.children = schema._inner.children.filter((a: any) => a.key !== attr.key)
} else {
i++
}
if(METHODS_BODYLESS.includes(method)){
const isRequired = attr.schema._flags.presence === 'required'
const operators = attr.schema._flags.operators
opt.queryParams.push([
attr.key,
attr.schema,
Scalar.iconRequired(isRequired) + ' ' + (attr.schema._description ? '**'+attr.schema._description+'**' : (isRequired
? '**Required'
: '**Optional'
) + ` query parameter** ${operators ? 'with operator': ''} \`[ ${attr.key}: ${operators ? '[operator, value]' : attr.schema._type} ]\``)
+ (operators ? `
\`Operators: ${operators.join(', ')}\`` : '')
+ `
\`Example: ?${attr.key}=${operators ? arraySample(operators)+config.paramOperatorSeparator:''}${attr.schema._examples[0] || attr.schema._type}\``
])
}
}
if(!METHODS_BODYLESS.includes(method) && i){
opt.body = [schema]
}
// if(i && !ROUTEmethod !== 'get' && method !== 'delete'){
// opt.body = [schema]
// }
}
}
// is MethodDecorator, replace callback with method
if(attribute) {
opt.handler = prototype[attribute]
opt.handlerName = attribute
}
getRouteForCollection(method, opt, this)
}
this.completed = true
// this.complete()
logger.info('Completed collection "%s"', this.name)
}
}