@eyugame/dao
Version:
A modelling tool for RealtimeDatabase(firebase) & Dynamodb(Amazon) & Redis
752 lines (694 loc) • 26.3 kB
JavaScript
/**
* Firebase define
*/
;
const joi = require('joi');
const admin = require('firebase-admin');
const utils = require('../utils');
/**
* 构建Ref
* @param {Object} partitionKey
* @param {String} table
* @param {String} hashKeyName
* @param {String} rangeKeyName
*/
function makeRef(partitionKey, table, hashKeyName, rangeKeyName) {
let result;
const hashKey = partitionKey[hashKeyName];
if (rangeKeyName) {
const rangeKey = partitionKey[rangeKeyName];
result = `${DAO_CONFIG.rootPath}/${table}/${rangeKey}/${hashKey}`;
} else {
result = `${DAO_CONFIG.rootPath}/${table}/${hashKey}`;
}
if (DEBUG) {
console.log('路径:', result);
}
return result;
}
/**
* 构建rangeKey Ref
* @param table
* @param rangeKey
* @returns {string}
*/
function makeRangeKeyRef(table, rangeKey) {
const result = `${DAO_CONFIG.rootPath}/${table}/${rangeKey}`;
if (DEBUG) {
console.log(`路径: ${result}`);
}
return result;
}
/**
* 构建Group Ref(用于batchGet)
*/
function makeGroupRef(table) {
return `${DAO_CONFIG.rootPath}/${table}/`;
}
/**
* 获取对象
* @param {Object} partitionKey
*/
function load(partitionKey) {
//检查 hashKey
const schemaHashKey = this.prototype._schema.hashKey;
if (!partitionKey[schemaHashKey]) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}获取异常, 无hashKey`));
}
const rangeKeyName = this.prototype._schema.rangeKey;
const rangeKeyValue = partitionKey[rangeKeyName];
if (rangeKeyName && !rangeKeyValue) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}获取异常, 无rangeKey`));
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, schemaHashKey, rangeKeyName);
const self = this;
return admin.database().ref(ref).once('value')
.then(snapshot => {
if (!snapshot.exists()) {
return null;
}
const val = snapshot.val();
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}获取对象原始数据${JSON.stringify(val)}`)
}
let entityValue = {
...partitionKey
};
Object.assign(entityValue, val);
const entity = new self.prototype.constructor(entityValue);
const result = utils.wrapperEntity(entity);
return result;
});
}
/**
* 创建实体
* @param {string} modelName
* @param {object} schema
*/
function create(data) {
//检查schema
const {
error,
value
} = joi.validate(data, this.prototype._schema.schema);
if (error) {
throw new Error(`实体${this.prototype._schema.tableName}创建异常, 数据校验失败: ${JSON.stringify(error.details)}, 实体数据:${JSON.stringify(data)}`);
}
const partitionKey = {};
const hashKeyName = this.prototype._schema.hashKey;
const rangeKeyName = this.prototype._schema.rangeKey;
partitionKey[hashKeyName] = data[hashKeyName];
if (rangeKeyName) {
partitionKey[rangeKeyName] = data[rangeKeyName];
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, hashKeyName, rangeKeyName);
//过滤字段
let copyEntity;
if (this.prototype._schema.FIREBASE_IGNORED) {
copyEntity = {};
for (let key in value) {
if (this.prototype._schema.FIREBASE_IGNORED.includes(key)) {
continue;
}
copyEntity[key] = value[key];
}
} else {
copyEntity = value;
}
const self = this;
return admin.database().ref(ref).set(copyEntity)
.then(() => {
const entity = new self.prototype.constructor(value);
return utils.wrapperEntity(entity);
});
}
/**
* 加载or创建
* @param {Object} partitionKey
* @param {Object} builder 构造器
*/
async function loadOrCreate(partitionKey, builder) {
try {
let result = await this.load(partitionKey);
if (!result) {
let data;
if (builder) {
if (utils.isAsyncFunc(builder)) {
data = await builder(partitionKey);
} else {
data = builder(partitionKey);
}
} else {
//使用默认的builder or schema定义的builder
data = this.prototype._builder(partitionKey);
}
result = await this.create(data);
}
return result;
} catch (ex) {
console.log('LoadOrCreate', ex);
return Promise.reject(new Error(`LoadOrCreate实体${this.prototype._schema.tableName}异常, ${JSON.stringify(ex)}`));
}
}
/**
* 实体是否存在(静态方法)
* @param {object} partitionKey
*/
function exists(partitionKey) {
//检查 hashKey
const schemaHashKey = this.prototype._schema.hashKey;
if (!partitionKey[schemaHashKey]) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}获取异常, 无hashKey`));
}
const rangeKeyName = this.prototype._schema.rangeKey;
const rangeKeyValue = partitionKey[rangeKeyName];
if (rangeKeyName && !rangeKeyValue) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}获取异常, 无rangeKey`));
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, schemaHashKey, rangeKeyName);
return admin.database().ref(ref).once('value')
.then(snapshot => {
if (!snapshot.exists()) {
return false;
}
return true;
});
}
/**
* 更新实体 指定属性
* @param {Object} entitySomeData
* @param {Object} options
*/
function update(entitySomeData, options) {
const partitionKey = {};
const hashKeyName = this.prototype._schema.hashKey;
const rangeKeyName = this.prototype._schema.rangeKey;
const hashKeyValue = entitySomeData[hashKeyName];
if (!hashKeyValue) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}更新参数无hashKey${hashKeyName}`));
}
partitionKey[hashKeyName] = hashKeyValue;
if (rangeKeyName) {
const rangeKeyValue = entitySomeData[rangeKeyName];
if (!rangeKeyValue) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}更新参数无rangeKey`));
}
partitionKey[rangeKeyName] = rangeKeyValue;
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, hashKeyName, rangeKeyName);
let copyEntity;
if (this.prototype._schema.FIREBASE_IGNORED) {
copyEntity = {};
for (let key in entitySomeData) {
//过滤 无需保存的属性
if (this.prototype._schema.FIREBASE_IGNORED.includes(key)) {
continue;
}
copyEntity[key] = entitySomeData[key];
}
} else {
copyEntity = {
...entitySomeData
};
}
const selfProto = this.prototype;
//使用事务
if (options && options.transaction) {
return admin.database().ref(ref).transaction(val => {
if (!val) {
copyEntity[utils.ENTITY_ATTR_VERSION] = 1;
return copyEntity;
}
//比较版本号
let dbVersion = val[utils.ENTITY_ATTR_VERSION] || 0;
let entityVersion = entitySomeData[utils.ENTITY_ATTR_VERSION] || 0;
if (dbVersion != entityVersion) {
if (DEBUG) {
console.log(`实体: ${selfProto._schema.tableName}, update操作, dbVersion: ${dbVersion}, entityVersion: ${entityVersion}`);
}
return;
}
let nextVersion = entityVersion + 1;
entitySomeData[utils.ENTITY_ATTR_VERSION] = nextVersion;
copyEntity[utils.ENTITY_ATTR_VERSION] = nextVersion;
for (let key in copyEntity) {
val[key] = copyEntity[key];
}
return val;
}).then(result => {
if (!result.committed) {
return Promise.reject(`实体${selfProto._schema.tableName}, 乐观锁事务失败`);
}
return Promise.resolve();
}, (error) => {
return Promise.reject(error);
});
} else {
return admin.database().ref(ref).update(copyEntity).then(() => {
return Promise.resolve();
}).catch(ex => Promise.reject(ex));
}
}
/**
* 修改实体数值字段(静态方法, ACID)
* @param partitionKey
* @param keyValues 例1: {COIN: 10, DIAMOND: -10} 例2: {COIN: -10, DIAMOND: -10}
* @param condition 例1: {LIFE: {min: 0, max: 200}}, 例2: {DIAMOND: {min: 0}}
*/
function alter(partitionKey, keyValues, condition) {
const hashKeyName = this.prototype._schema.hashKey;
const rangeKeyName = this.prototype._schema.rangeKey;
if (!partitionKey[hashKeyName]) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作参数无hashKey${hashKeyName}`));
}
if (rangeKeyName) {
if (!partitionKey[rangeKeyName]) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作参数无rangeKey${rangeKeyName}`));
}
}
//检查 keyValue
if (utils.isEmptyObject(keyValues)) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作未提供要更新的属性, ${JSON.stringify(keyValues)}`));
}
for (let [k, v] of Object.entries(keyValues)) {
if (!utils.isNumber(v)) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, value: ${v} 非数字`));
}
if (v === 0) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, value: ${v} === 0`));
}
//检查schema
let schemaType = this.prototype._schema.schema[k];
//todo:lg 判断 joi类型
if (!schemaType) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}无schema定义`));
}
}
//检查condition 类型
if (condition) {
for (let [k, v] of Object.entries(keyValues)) {
let currentLimit = condition[k];
if (!currentLimit) {
continue;
}
if (v > 0) {
//增加 检查最大值
if ('max' in currentLimit) {
if (!utils.isNumber(currentLimit.max)) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, max: ${currentLimit.max} 非数字`));
}
//更新的值 不能大于 上限
if (v > currentLimit.max) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, 更新值: ${v} > 上限: ${currentLimit.max}`));
}
}
} else {
//扣除 检查最小值
if ('min' in currentLimit) {
if (!utils.isNumber(currentLimit.min)) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, min: ${currentLimit.min} 非数字`));
}
}
}
}
}
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}, alter操作, 更新数据: ${JSON.stringify(keyValues)}, 条件: ${JSON.stringify(condition)}`);
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, hashKeyName, rangeKeyName);
const self = this;
return admin.database().ref(ref).transaction(val => {
if (!val) {
return keyValues;
}
for (let [k, v] of Object.entries(keyValues)) {
let oldValue = val[k] || 0;
let newValue = oldValue + v;
if (condition) {
let currentLimit = condition[k];
if (currentLimit) {
if (v > 0) {
if (('max' in currentLimit) && newValue > currentLimit.max) {
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, old: ${oldValue}, new: ${newValue}, 超出上限${currentLimit.max}`);
}
return;
}
} else {
if (('min' in currentLimit) && newValue < currentLimit.min) {
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, old: ${oldValue}, new: ${newValue}, 低出下限${currentLimit.min}`);
}
return;
}
}
}
}
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}, alter操作, key: ${k}, old: ${oldValue}, new: ${newValue}`);
}
val[k] = newValue;
}
//增加版本号
let oldVersion = val[utils.ENTITY_ATTR_VERSION] || 0;
val[utils.ENTITY_ATTR_VERSION] = oldVersion + 1;
return val;
}).then(result => {
console.log('alter 结果: ', JSON.stringify(result));
if (!result.committed) {
return Promise.reject(`实体${self.prototype._schema.tableName}, alert事务失败`);
}
let entityVal = {
...partitionKey
};
Object.assign(entityVal, result.snapshot.val());
let entity = new self.prototype.constructor(entityVal);
return utils.wrapperEntity(entity);
}, (error) => {
return Promise.reject(error);
});
}
/**
* 删除实体 (静态方法)
* @param partitionKey
* @returns {Promise<void>}
*/
async function destroy(partitionKey) {
//检查 hashKey
const schemaHashKey = this.prototype._schema.hashKey;
if (!partitionKey[schemaHashKey]) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}删除异常, 无hashKey`));
}
const rangeKeyName = this.prototype._schema.rangeKey;
const rangeKeyValue = partitionKey[rangeKeyName];
if (rangeKeyName && !rangeKeyValue) {
return Promise.reject(new Error(`模型: ${this.prototype._schema.tableName}删除异常, 无rangeKey`));
}
const ref = makeRef(partitionKey, this.prototype._schema.tableName, schemaHashKey, rangeKeyName);
return admin.database().ref(ref)
.remove()
.catch(ex => {
console.log(`模型: ${this.prototype._schema.tableName}删除异常, ${JSON.stringify(ex)}`);
return Promise.reject(ex);
})
}
/**
* 查询所有 (schema必须有rangeKey)
* @param rangeKey
* @param options {limit:100,desc:false}
* @returns {Promise<*|*>}
*/
async function listAll(rangeKey, options) {
if (!this.prototype._schema.rangeKey) {
return Promise.reject(new Error(`实体${this.prototype._schema.tableName}不支持listAll查询`));
}
const ref = makeRangeKeyRef(this.prototype._schema.tableName, rangeKey);
const selfProto = this.prototype;
let query = admin.database().ref(ref);
if (DEBUG) {
console.log(`实体${this.prototype._schema.tableName}, listAll, options: ${JSON.stringify(options)}`);
}
if (options && options.limit) {
if (typeof options.limit === 'string') {
options.limit = Number.parseInt(options.limit);
}
if (typeof options.desc === 'string') {
options.desc = (options.desc == 'true');
}
if (options.desc) {
query = query.limitToLast(options.limit);
} else {
query = query.limitToFirst(options.limit);
}
}
return query
.once('value')
.then((snapshot) => {
const entities = [];
const hashKeyName = selfProto._schema.hashKey;
const rangeKeyName = selfProto._schema.rangeKey;
if (snapshot.exists()) {
for (let [key, itemVal] of Object.entries(snapshot.val())) {
//注入 hashKey
if (!(hashKeyName in itemVal)) {
itemVal[hashKeyName] = key;
}
//注入 rangeKey
if (!(rangeKeyName in itemVal)) {
itemVal[rangeKeyName] = rangeKey;
}
let entity = new selfProto.constructor(itemVal);
entities.push(utils.wrapperEntity(entity));
}
}
return Promise.resolve(entities);
})
.catch(ex => Promise.reject(ex));
}
/**
* 批量获取 (static) 当只有hashKey时, 可以简写 ['id1','id2']
* @param {arry} keys ['id1','id2'], [{hashKey:'1', rangeKey: '1'},{hashKey:'2', rangeKey: '2'}]
*/
function batchGet(keys) {
const selfProto = this.prototype;
const tableName = selfProto._schema.tableName;
const hashKeyName = selfProto._schema.hashKey;
const rangeKeyName = selfProto._schema.rangeKey;
const queryKeys = [];
for (let itemKey of keys) {
let hashKeyValue;
let keyType = utils.getType(itemKey);
if (rangeKeyName) {
if (keyType != 'object') {
console.log(`实体: ${tableName}, 批量获取, 键值: ${JSON.stringify(itemKey)}非法`);
throw new Error(`实体: ${tableName}, 批量获取, 键值非法`);
}
hashKeyValue = itemKey[hashKeyName];
const rangeKeyValue = itemKey[rangeKeyName];
if (!hashKeyValue) {
console.log(`实体: ${tableName}, 批量获取, 键值: ${JSON.stringify(itemKey)}非法`);
throw new Error(`实体: ${tableName}, 批量获取, 键值非法`);
}
if (!rangeKeyValue) {
console.log(`实体: ${tableName}, 批量获取, 键值: ${JSON.stringify(itemKey)}非法`);
throw new Error(`实体: ${tableName}, 批量获取, 键值非法`);
}
queryKeys.push({
child: `${rangeKeyValue}/${hashKeyValue}`,
rangeKey: rangeKeyValue,
hashKey: hashKeyValue
});
} else {
if (keyType == 'object') {
hashKeyValue = itemKey[hashKeyName];
if (!hashKeyValue) {
console.log(`实体: ${tableName}, 批量获取, 键值: ${JSON.stringify(itemKey)}非法`);
throw new Error(`实体: ${tableName}, 批量获取, 键值非法`);
}
} else {
hashKeyValue = itemKey;
}
queryKeys.push({
child: hashKeyValue,
hashKey: hashKeyValue
});
}
}
const groupRef = makeGroupRef(tableName);
const children = admin.database().ref(groupRef);
return new Promise(async (resolve, reject) => {
try {
let entities = [];
for (let itemQuery of queryKeys) {
let snapshot = await children.child(itemQuery.child).once('value');
if (!snapshot.exists()) {
continue;
}
let value = snapshot.val();
//注入 partitionKey
value[hashKeyName] = itemQuery.hashKey;
if (rangeKeyName) {
value[rangeKeyName] = itemQuery.rangeKey;
}
let entity = new selfProto.constructor(value);
entities.push(utils.wrapperEntity(entity));
}
resolve(entities);
} catch (err) {
console.log(`实体: ${tableName}, 批量获取异常:`, err);
reject(`实体: ${tableName}, 批量获取异常`);
}
})
}
/**
* 对象保存 instance
* @param {Object} options
*/
function save(options) {
const selfProto = Object.getPrototypeOf(this);
const self = this;
const partitionKey = {};
const hashKeyName = selfProto._schema.hashKey;
const rangeKeyName = selfProto._schema.rangeKey;
partitionKey[hashKeyName] = this._entity[hashKeyName];
if (rangeKeyName) {
partitionKey[rangeKeyName] = this._entity[rangeKeyName];
}
const ref = makeRef(partitionKey, selfProto._schema.tableName, hashKeyName, rangeKeyName);
let copyEntity;
if (selfProto._schema.FIREBASE_IGNORED) {
copyEntity = {};
for (let key in this._entity) {
if (selfProto._schema.FIREBASE_IGNORED.includes(key)) {
continue;
}
copyEntity[key] = this._entity[key];
}
} else {
copyEntity = {
...this._entity
};
}
//使用事务
if (options && options.transaction) {
return admin.database().ref(ref).transaction(val => {
if (!val) {
copyEntity[utils.ENTITY_ATTR_VERSION] = 1;
return copyEntity;
}
//比较版本号
let dbVersion = val[utils.ENTITY_ATTR_VERSION] || 0;
let entityVersion = self._entity[utils.ENTITY_ATTR_VERSION] || 0;
if (dbVersion != entityVersion) {
if (DEBUG) {
console.log(`实体: ${selfProto._schema.tableName}, save操作, dbVersion: ${dbVersion}, entityVersion: ${entityVersion}`);
}
return;
}
//更新自身entity 版本号
let nextVersion = entityVersion + 1;
self._entity[utils.ENTITY_ATTR_VERSION] = nextVersion;
copyEntity[utils.ENTITY_ATTR_VERSION] = nextVersion;
return copyEntity;
}).then(result => {
if (!result.committed) {
return Promise.reject(`实体${selfProto._schema.tableName}, 乐观锁事务失败`);
}
return Promise.resolve();
}, (error) => {
return Promise.reject(error);
});
} else {
return admin.database().ref(ref).update(copyEntity).then(() => Promise.resolve()).catch(ex => Promise.reject(ex));
}
}
/**
* 删除实体 (实例方法)
*/
function remove() {
const selfProto = Object.getPrototypeOf(this);
const partitionKey = {};
const hashKeyName = selfProto._schema.hashKey;
const rangeKeyName = selfProto._schema.rangeKey;
partitionKey[hashKeyName] = this._entity[hashKeyName];
if (rangeKeyName) {
partitionKey[rangeKeyName] = this._entity[rangeKeyName];
}
const ref = makeRef(partitionKey, selfProto._schema.tableName, hashKeyName, rangeKeyName);
return admin.database().ref(ref)
.remove()
.catch(ex => {
console.log(`模型: ${selfProto._schema.tableName}删除异常, ${JSON.stringify(ex)}`);
return Promise.reject(ex);
});
}
/**
* 定义Schema
* @param {String} entityName
* @param {Object} schema
*/
const define = (entityName, schema) => {
//检查Schema
utils.checkSchema(entityName, schema);
//构造函数
const entity = function (data) {
//检查数据
const {
error,
value
} = joi.validate(data, schema.schema);
if (error) {
throw new Error(`[模型${entityName}]原始数据${JSON.stringify(data)}验证异常: ${JSON.stringify(error.details)}`);
}
this._entity = value;
}
//添加 乐观锁版本号
if (!schema.schema[utils.ENTITY_ATTR_VERSION]) {
schema.schema[utils.ENTITY_ATTR_VERSION] = joi.number().positive();
}
if (!schema.tableName) {
schema.tableName = entityName;
}
if (!schema.FIREBASE_IGNORED) {
if (schema.rangeKey) {
schema.FIREBASE_IGNORED = [schema.hashKey, schema.rangeKey];
} else {
schema.FIREBASE_IGNORED = [schema.hashKey];
}
if (DEBUG) {
console.log(`实体: ${schema.tableName}无自定义FIREBASE_IGNORED, 添加默认: ${JSON.stringify(schema.FIREBASE_IGNORED)}`)
}
}
//属性值
entity.prototype._schema = schema;
entity.prototype.toEntity = function () {
const result = {
...this._entity
};
//删除 hashKey
delete result[Object.getPrototypeOf(this)._schema.hashKey];
//删除 version
delete result[utils.ENTITY_ATTR_VERSION];
return result;
}
//更改entity属性
entity.prototype.modifyEntity = function (kv) {
for (let key in kv) {
this._entity[key] = kv[key];
}
};
//添加 LOAD
entity.load = load;
//添加 CRUD
entity.create = create;
entity.update = update;
entity.destroy = destroy;
entity.exists = exists;
entity.loadOrCreate = loadOrCreate;
if (schema[utils.SCHEMA_BUILDER]) {
entity.prototype._builder = schema[utils.SCHEMA_BUILDER];
} else {
entity.prototype._builder = utils.entityBuilder;
}
entity.listAll = listAll;
entity.alter = alter;
entity.batchGet = batchGet;
entity.prototype.save = save;
entity.prototype.remove = remove;
entity.prototype.toVO = utils.toVO;
//添加自定义方法
const instanceMethods = schema[utils.SCHEMA_INSTANCE_METHODS];
if (instanceMethods) {
for (let funcName in instanceMethods) {
entity.prototype[funcName] = instanceMethods[funcName];
}
}
//添加 copyVersion方法
entity.copyAttrVersion = function (updateData, sourceEntity) {
if (utils.ENTITY_ATTR_VERSION in sourceEntity._entity) {
updateData[utils.ENTITY_ATTR_VERSION] = sourceEntity._entity[utils.ENTITY_ATTR_VERSION];
}
};
return entity;
};
module.exports = define;