import * as CoreLogic from "./coreLogic" import { Errors } from "../errors" import * as mongodb from "mongodb" import * as Leaf from "leaf-ts" import * as asyncModule from "async" import { Condition } from "./condition" import * as util from "util" import "root-ts" type ModelDefinition = Type.Topology.Model.Definition type BackendDefinition = Type.Topology.Backend.Definition export abstract class Backend { abstract one(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) abstract some(option: { criteria: Type.Topology.Condition.Criteria sort?: { [key: string]: number } offset?: number count?: number }, callback: Callback) abstract remove(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) abstract count(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) abstract inserts(option: { datas: T[] }, callback: Callback) abstract update(option: { criteria: Type.Topology.Condition.Criteria, data: Partial }, callback: Callback) initialize(callback?: Callback) { this.ready.yes() if (callback) callback() } abstract all(option: { sort?: { [key: string]: number } offset?: number count?: number }, callback: Callback) ready = new Leaf.ReadyBehavior() } export abstract class BackendProvider { abstract createBackend(model: ModelDefinition): Backend } export class MemoryBackendProvider extends BackendProvider { constructor(public readonly mem: any = {}) { super() if (!this.mem.map) this.mem.map = {} if (!this.mem.single) this.mem.single = {} if (!this.mem.array) this.mem.array = {} this.providers.push( new MapBackendProvider(this.mem.map), new SingleBackendProvider(this.mem.single) ) } providers: BackendProvider[] = [] createBackend(model: ModelDefinition): Backend { for (let provider of this.providers) { let backend = provider.createBackend(model) if (backend) return backend } return null } } export class MongodbBackendProvider extends BackendProvider { constructor(public readonly db: mongodb.Db) { super() } createBackend(model: ModelDefinition): Backend { if (model.backend.engine !== "mongodb") { return null } if (!model.backend.name && !model.name) { throw new Error("Invalid backend: no model name nor backend.name") } let name = model.backend.name || model.name name = Leaf.Util.uncapitalize(name) return new MongodbBackend(model, this.db.collection(name)) } } export class MongodbBackend extends Backend { constructor(public readonly definition: ModelDefinition, public readonly col: mongodb.Collection) { super() } initialize() { let unique = this.definition.unique if (typeof unique == "string") { unique = [unique] } asyncModule.eachSeries(unique, (key, done) => { this.col.createIndexes([{ key: { [key]: 1 }, unique: true }], (err) => { console.log(`Index created for ${this.definition.name}.${key}`) done(err) }) }, () => { this.ready.yes() }) } one(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { this.col.findOne(this.criteriaToQuery(option.criteria), (err, one) => { if (err || !one) { callback(new Errors.NotFound) return } callback(null, one) }) } count(option: { criteria: Type.Topology.Condition.Criteria offset?: number count?: number sort?: { [key: string]: number } }, callback: Callback) { if (option.criteria && option.criteria.ors.length == 0) { callback(null, 0) return } let query = this.criteriaToQuery(option.criteria) if (!query) { callback(null, 0) return } let cursor = this.col.find(query) if (option.offset > 0) { cursor.skip(option.offset) } if (option.count > 0) { cursor.limit(option.count) } if (option.sort) { cursor.sort(option.sort) } cursor.count((err, count) => { callback(err, count) }) } some(option: { criteria: Type.Topology.Condition.Criteria offset?: number count?: number sort?: { [key: string]: number } }, callback: Callback) { if (option.criteria && option.criteria.ors.length == 0) { callback(null, []) return } let query = this.criteriaToQuery(option.criteria) if (!query) { callback(null, []) return } let cursor = this.col.find(query) if (option.offset > 0) { cursor.skip(option.offset) } if (option.count > 0) { cursor.limit(option.count) } if (option.sort) { cursor.sort(option.sort) } cursor.toArray((err, items) => { callback(err, items) }) } remove(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { this.col.remove(this.criteriaToQuery(option.criteria), (err) => { callback(err) }) } inserts(option: { datas: T[] }, callback: Callback) { if (option.datas.length == 0) { callback(null, []) return } this.col.insertMany(option.datas, (err) => { callback(err, option.datas) }) } update(option: { criteria: Type.Topology.Condition.Criteria, data: Partial }, callback: Callback) { let query = this.criteriaToQuery(option.criteria) let update = option.data this.col.updateMany(query, { $set: update }, (err, result) => { callback() }) } all(option: { criteria?: Type.Topology.Condition.Criteria sort?: { [key: string]: number } offset?: number count?: number }, callback: Callback) { let query = option.criteria && this.criteriaToQuery(option.criteria) || {} let cursor = this.col.find({}) if (option.offset > 0) { cursor.skip(option.offset) } if (option.count > 0) { cursor.limit(option.count) } if (option.sort) { cursor.sort(option.sort) } cursor.toArray(callback) } private criteriaToQuery(criteria: Type.Topology.Condition.Criteria) { if (!criteria) return {} if (!criteria.ors) { console.error("Invalid criteria", util.inspect(criteria, { depth: 4 }), "????") return null } if (criteria.ors.length == 0) throw new Errors.LogicError("Shouldn't get here") if (criteria.ors.length == 1) { if (criteria.ors[0].ands.length == 0) { return null } return this.conditionsToQuery(criteria.ors[0]) } else { return { $or: criteria.ors.map(conditions => this.conditionsToQuery(conditions)) } } } private conditionsToQuery(conditions: Type.Topology.Condition.Matches) { let query = {} for (let condition of conditions.ands) { if (condition.type == "lte") { query[condition.path] = query[condition.path] || {} query[condition.path]["$lte"] = condition.value } else if (condition.type == "gte") { query[condition.path] = query[condition.path] || {} query[condition.path]["$gte"] = condition.value } else if (condition.type == "lt") { query[condition.path] = query[condition.path] || {} query[condition.path]["$lt"] = condition.value } else if (condition.type == "gt") { query[condition.path] = query[condition.path] || {} query[condition.path]["$gt"] = condition.value } else if (condition.type == "eq") { query[condition.path] = query[condition.path] || {} let $in = query[condition.path].$in = query[condition.path].$in || [] $in.push(condition.value) } else if (condition.type == "in") { query[condition.path] = query[condition.path] || {} let $in = query[condition.path].$in = query[condition.path].$in || [] $in.push(...condition.value) } else if (condition.type == "reg") { query[condition.path] = condition.value } else if (condition.type == "wildcard") { throw new Error("Wildcard not supported in mongodb backend") } } for (let key in query) { if (query[key].$in && query[key].$in.length == 1) { query[key].$eq = query[key].$in[0] delete query[key].$in } } return query } } export class MapBackendProvider extends BackendProvider { constructor(public readonly memory = {}) { super() } createBackend(model: ModelDefinition): Backend { if (model.backend.engine !== "map") { return null } if (!model.backend.name && !model.name) { throw new Error("Invalid backend: no model name nor backend.name") } let name = model.backend.name || model.name if (!this.memory[name]) this.memory[name] = {} return new MapBackend(model, this.memory[name]) } } export class MapBackend extends Backend{ constructor(public readonly model: Type.Topology.Model.Definition, public readonly memory: any = {}) { super() } one(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { let keys = Object.keys(this.memory) for (let key of keys) { let value = this.memory[key] if (Condition.test(option.criteria, value)) { callback(null, value) return } } callback(new Errors.NotFound) } count(option: { criteria: Type.Topology.Condition.Criteria offset?: number count?: number sort?: { [key: string]: number } }, callback: Callback) { this.some(option, (err, result) => { if (err) { callback(err) return } callback(null, result.length) }) } some(option: { criteria: Type.Topology.Condition.Criteria offset?: number count?: number sort?: { [key: string]: number } }, callback: Callback) { if (!option.criteria) { callback(null, Object.keys(this.memory).map(key => this.memory[key])) return } let items = [] Object.keys(this.memory).forEach(key => { let value = this.memory[key] if (Condition.test(option.criteria, value)) { items.push(value) } }) if (option.sort && Object.keys(option.sort).length > 0) { items.sort((a, b) => { for (let key in option.sort) { let cmp = 1 let adj = null if (a[key] > b[key]) { adj = 1 } else if (a[key] < b[key]) { adj = -1 } else if (a[key] === b[key]) { continue } if (Leaf.Util.isEmptyValue(a[key]) || Leaf.Util.isEmptyValue(b[key])) { continue } if (adj !== null) { continue } return cmp * adj * option.sort[key] } }) } if (option.offset > 0) { items = items.slice(option.offset) } if (option.count > 0) { items = items.slice(0, option.count) } callback(null, items) } remove(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { Object.keys(this.memory).forEach(key => { let value = this.memory[key] if (Condition.test(option.criteria, value)) { delete this.memory[key] } }) callback() } inserts(option: { datas: T[] }, callback: Callback) { let unique = (this.model.backend as Type.Topology.Backend.MemoryMapBackendDefinition).index as string for (let data of option.datas) { let key = Leaf.Util.getObjectValue(data, unique) this.memory[key] = data } callback(null, option.datas) } update(option: { criteria: Type.Topology.Condition.Criteria, data: Partial }, callback: Callback) { Object.keys(this.memory).forEach(key => { let value = this.memory[key] if (Condition.test(option.criteria, value)) { for (let key in option.data) { value[key] = option.data[key] } } }) callback() } all(option: { criteria?: Type.Topology.Condition.Criteria offset?: number count?: number sort?: { [key: string]: number } }, callback: Callback) { let items = Object.keys(this.memory).map(key => this.memory[key]).filter(item => { if (!option.criteria) return true if (Condition.test(option.criteria, item)) { return true } return false }) if (option.offset > 0) { items = items.slice(option.offset) } if (option.count > 0) { items = items.slice(0, option.count) } if (option.sort && Object.keys(option.sort).length > 0) { items.sort((a, b) => { for (let key in option.sort) { let cmp = 1 let adj = null if (a[key] > b[key]) { adj = 1 } else if (a[key] < b[key]) { adj = -1 } else if (a[key] === b[key]) { continue } if (Leaf.Util.isEmptyValue(a[key]) || Leaf.Util.isEmptyValue(b[key])) { continue } if (adj !== null) { continue } return cmp * adj * option.sort[key] } }) } callback(null, items) } } export class SingleBackendProvider extends BackendProvider { constructor(public readonly memory = {}) { super() } createBackend(model: ModelDefinition): Backend { if (model.backend.engine !== "single") { return null } if (!model.backend.name && !model.name) { throw new Error("Invalid backend: no model name nor backend.name") } let name = model.backend.name || model.name if (!this.memory[name]) this.memory[name] = {} return new SingleBackend(model, this.memory[name]) } } export class SingleBackend extends Backend{ constructor(public readonly model: ModelDefinition, public readonly memory: any = {}) { super() } one(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { callback(null, this.memory) } count(option: any, callback: Callback) { callback(null, 1) } some(option: { criteria: Type.Topology.Condition.Criteria offset?: number count?: number }, callback: Callback) { callback(null, [this.memory]) } remove(option: { criteria: Type.Topology.Condition.Criteria }, callback: Callback) { callback(new Errors.NotImplemented("Single backend don't support remove")) } inserts(option: { datas: T[] }, callback: Callback) { callback(new Errors.NotImplemented("Sinlge backend dont support insert")) } update(option: { criteria?: Type.Topology.Condition.Criteria, data: Partial }, callback: Callback) { let value = this.memory if (Condition.test(option.criteria, value)) { for (let key in option.data) { value[key] = option.data[key] } } callback() } all(option: {}, callback: Callback) { callback(null, [this.memory]) } }