@eyugame/dao
Version:
A modelling tool for RealtimeDatabase(firebase) & Dynamodb(Amazon) & Redis
654 lines (601 loc) • 22.5 kB
JavaScript
;
const dynamodb = require('dynamodb');
const joi = require('joi');
const utils = require('../utils');
/**
* 已定义的实体
*/
const ENTITY_CACHE = new Map();
const symSchema = Symbol('SCHEMA');
const symFirebaseIgnored = Symbol('FIREBASE-IGNORED');
//最终一致性
const OPTION_CONSISTENT_READ = false;
//dynamodb.log.level('DEBUG');
/**
* 加载实体
* @param {Object} partitionKey
*/
async function load(partitionKey) {
if (DEBUG) {
console.log(`实体${this.prototype._target.tableName()}加载${JSON.stringify(partitionKey)}`);
}
const self = this;
return new Promise((resolve, reject) => {
self.prototype._target.get(partitionKey, {
ConsistentRead: OPTION_CONSISTENT_READ
}, function (err, value) {
if (err) {
console.log(`实体${self.prototype._target.tableName()}加载异常${JSON.stringify(err)}`);
reject(new Error(`实体${self.prototype._target.tableName()}加载异常${JSON.stringify(err)}`));
return;
}
if (!value) {
resolve(null);
return;
}
let entity = new self.prototype.constructor(value.attrs);
resolve(utils.wrapperEntity(entity));
});
});
}
/**
* 创建对象
* @param {Object} data
*/
async function create(data) {
let model = new this.prototype.constructor(data);
const self = this;
return new Promise((resolve, reject) => {
model._target.create(model._entity, {
overwrite: true
}, function (error, dbEntity) {
if (error) {
console.log(`创建实体${model._target.tableName()}异常, ${JSON.stringify(error)}`);
reject(new Error(`创建实体${model._target.tableName()}异常`));
return;
}
const entity = new self.prototype.constructor(dbEntity.attrs);
resolve(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.info('loadOrCreate', ex);
return Promise.reject(new Error(`LoadOrCreate实体${this.prototype._table}异常, ${JSON.stringify(ex)}`));
}
}
/**
* 实体是否存在(静态方法)
* @param {object}} partitionKey
*/
async function exists(partitionKey) {
if (DEBUG) {
console.log(`实体${this.prototype._target.tableName()}加载${JSON.stringify(partitionKey)}`);
}
try {
let data = await this.prototype._target.get(partitionKey);
if (!data) {
return false;
}
return true;
} catch (ex) {
console.log('exists', ex);
throw new Error(`实体${this.prototype._target.tableName()}加载异常${JSON.stringify(ex)}`);
}
}
/**
* 更新实体 指定属性
* @param {Object} entitySomeData
* @param {Object} options
*/
function update(entitySomeData, options) {
//检查 hashKey
if (!entitySomeData[this.prototype._hashKey]) {
throw new Error(`实体${this.prototype._table}更新参数无hashKey${this.prototype._hashKey}`);
}
//检查 rangeKey
if (this.prototype._rangeKey && !entitySomeData[this.prototype._rangeKey]) {
throw new Error(`实体${this.prototype._table}更新参数无rangeKey`);
}
const self = this;
if (options && options.transaction) {
const params = {};
let entityVersion = entitySomeData[utils.ENTITY_ATTR_VERSION];
if (entityVersion) {
params.ConditionExpression = '#v = :v';
params.ExpressionAttributeNames = {
'#v': utils.ENTITY_ATTR_VERSION
};
params.ExpressionAttributeValues = {
':v': entityVersion
};
} else {
params.ConditionExpression = 'attribute_not_exists(#v)';
params.ExpressionAttributeNames = {
'#v': utils.ENTITY_ATTR_VERSION
};
}
if (DEBUG) {
console.log('使用事务......', params);
}
let newVersion = (entityVersion || 0) + 1;
entitySomeData[utils.ENTITY_ATTR_VERSION] = newVersion;
return new Promise((resolve, reject) => {
this.prototype._target.update(entitySomeData, params, function (error, dbEntity) {
if (error) {
console.log(`更新实体${self.prototype._table}异常, ${JSON.stringify(error)}`);
reject(new Error(`更新实体${self.prototype._table}异常`));
return;
}
if (DEBUG) {
console.log('~~~~~dbEntity', dbEntity.attrs);
}
let result = new self.prototype.constructor(dbEntity.attrs);
if (DEBUG) {
console.log('~~~~~dbEntity result: ', result);
}
resolve(utils.wrapperEntity(result));
});
});
} else {
return new Promise((resolve, reject) => {
this.prototype._target.update(entitySomeData, {}, function (error, dbEntity) {
if (error) {
console.log(`更新实体${self.prototype._table}异常, ${JSON.stringify(error)}`);
reject(new Error(`更新实体${self.prototype._table}异常`));
return;
}
if (DEBUG) {
console.log('~~~~~dbEntity', dbEntity.attrs);
}
let result = new self.prototype.constructor(dbEntity.attrs);
if (DEBUG) {
console.log('~~~~~dbEntity result: ', result);
}
resolve(utils.wrapperEntity(result));
});
});
}
}
/**
* 附加更新表达式
*/
function appendUpdateExpression(params, conditionExpression, expressionAttributeNames, expressionAttributeValues) {
if (conditionExpression) {
if (!params.ConditionExpression) {
params.ConditionExpression = '(' + conditionExpression + ')';
} else {
params.ConditionExpression += ' and (' + conditionExpression + ')';
}
}
if (expressionAttributeNames) {
if (!params.ExpressionAttributeNames) {
params.ExpressionAttributeNames = expressionAttributeNames;
} else {
Object.assign(params.ExpressionAttributeNames, expressionAttributeNames);
}
}
if (expressionAttributeValues) {
if (!params.ExpressionAttributeValues) {
params.ExpressionAttributeValues = expressionAttributeValues;
} else {
Object.assign(params.ExpressionAttributeValues, expressionAttributeValues);
}
}
}
/**
* 修改实体数值字段(静态方法, 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._hashKey;
const rangeKeyName = this.prototype._rangeKey;
if (!partitionKey[hashKeyName]) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作参数无hashKey${hashKeyName}`));
}
if (rangeKeyName) {
if (!partitionKey[rangeKeyName]) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作参数无rangeKey${rangeKeyName}`));
}
}
//检查 keyValue
if (utils.isEmptyObject(keyValues)) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作未提供要更新的属性, ${JSON.stringify(keyValues)}`));
}
const updateBody = {
...partitionKey
};
for (let [k, v] of Object.entries(keyValues)) {
if (!utils.isNumber(v)) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作, key: ${k}, value: ${v} 非数字`));
}
if (v === 0) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作, key: ${k}, value: ${v} === 0`));
}
//检查schema
let schemaType = this.prototype[symSchema].schema[k];
//todo:lg 判断 joi类型
if (!schemaType) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作, key: ${k}无schema定义`));
}
updateBody[k] = {
$add: v
};
}
const params = {};
//检查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._table}, alter操作, key: ${k}, max: ${currentLimit.max} 非数字`));
}
//更新的值 不能大于 上限
if (v > currentLimit.max) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作, key: ${k}, 更新值: ${v} > 上限: ${currentLimit.max}`));
}
let tempCurrentKey = `#current_${k}`;
let tempMax = `:max_${k}`;
let exprNames = {};
let exprValues = {};
exprNames[tempCurrentKey] = k;
exprValues[tempMax] = currentLimit.max - v;
appendUpdateExpression(params, `attribute_not_exists(${tempCurrentKey}) or ${tempCurrentKey} <= ${tempMax}`, exprNames, exprValues)
}
} else {
//扣除 检查最小值
if ('min' in currentLimit) {
if (!utils.isNumber(currentLimit.min)) {
return Promise.reject(new Error(`实体${this.prototype._table}, alter操作, key: ${k}, min: ${currentLimit.min} 非数字`));
}
let tempCurrentKey = `#current_${k}`;
let tempMin = `:min_${k}`;
let exprNames = {};
let exprValues = {};
exprNames[tempCurrentKey] = k;
exprValues[tempMin] = currentLimit.min - v;
appendUpdateExpression(params, `${tempCurrentKey} >= ${tempMin}`, exprNames, exprValues)
}
}
}
}
if (DEBUG) {
console.log(`实体${this.prototype._table}, alter操作, 更新数据: ${JSON.stringify(updateBody)}, 条件: ${JSON.stringify(params)}`);
}
const self = this;
return new Promise((resolve, reject) => {
self.prototype._target.update(updateBody, params, function (error, dbValue) {
if (error) {
const errMsg = new Error(`实体${self.prototype._table}, alter异常${JSON.stringify(error)}`);
reject(errMsg);
return;
}
let entity = new self.prototype.constructor(dbValue.attrs);
resolve(utils.wrapperEntity(entity));
})
});
}
/**
* 删除实体 (静态方法)
* @param partitionKey
* @returns {Promise<any>}
*/
function destroy(partitionKey) {
const self = this;
return new Promise((resolve, reject) => {
self.prototype._target.destroy(partitionKey, function (error) {
if (error) {
const errMsg = new Error(`实体${self.prototype._table}, destroy异常${JSON.stringify(error)}`);
reject(errMsg);
return;
}
resolve();
});
});
}
/**
* 查询所有 (schema必须有rangeKey)
* @param rangeKey
* @param options {limit:100,desc:false}
* @returns {Promise<*|*>}
*/
async function listAll(rangeKey, options) {
if (!this.prototype._rangeKey) {
return Promise.reject(new Error(`实体${this.prototype._table}不支持listAll查询`));
}
if (DEBUG) {
console.log(`实体${this.prototype._table}全局索引${this.prototype._indexName}, listAll查询`);
}
const selfProto = this.prototype;
return new Promise((resolve, reject) => {
let query = this.prototype._target.query(rangeKey).usingIndex(selfProto._indexName);
if (options && options.limit) {
query = query.limit(options.limit);
if (options.desc) {
query = query.descending();
}
}
query.exec((error, data) => {
if (error) {
const err = new Error(`实体${this.prototype._table}, listAll查询异常${JSON.stringify(error)}`);
reject(err);
return;
}
if (!data) {
resolve([]);
return;
}
const entities = [];
for (let item of data.Items) {
let entity = new selfProto.constructor(item.attrs);
entities.push(utils.wrapperEntity(entity));
}
resolve(entities);
});
});
}
/**
* 批量获取 (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._table;
return new Promise((resolve, reject) => {
selfProto._target.getItems(keys, {
ConsistentRead: OPTION_CONSISTENT_READ
}, function (err, entities) {
if (err) {
console.log(`实体${tableName}, 批量获取异常:`, err);
reject(new Error(`实体${tableName}, 批量获取异常`));
return;
}
let result = [];
for (let item of entities) {
let entity = new selfProto.constructor(item.attrs);
result.push(utils.wrapperEntity(entity));
}
resolve(result);
})
});
}
/**
* 保存
* @param {Object} options
*/
function save(options) {
const selfProto = Object.getPrototypeOf(this);
const self = this;
if (options && options.transaction) {
const params = {};
let entityVersion = this._entity[utils.ENTITY_ATTR_VERSION];
if (DEBUG) {
console.log(`实体${selfProto._table}保存, 版本号${entityVersion}`);
}
if (entityVersion) {
params.ConditionExpression = '#v = :v';
params.ExpressionAttributeNames = {
'#v': utils.ENTITY_ATTR_VERSION
};
params.ExpressionAttributeValues = {
':v': entityVersion
};
} else {
params.ConditionExpression = 'attribute_not_exists(#v)';
params.ExpressionAttributeNames = {
'#v': utils.ENTITY_ATTR_VERSION
};
}
const updateBody = {
...this._entity
};
updateBody[utils.ENTITY_ATTR_VERSION] = (entityVersion || 0) + 1;
return new Promise((resolve, reject) => {
selfProto._target.update(updateBody, params, function (error, dbEntity) {
if (error) {
console.log(`实体${selfProto._table}保存异常, ${JSON.stringify(error)}`);
console.log(`实体${selfProto._table}保存异常, 条件检查详细: ${JSON.stringify(params)}, entity-version: ${entityVersion}`);
reject(new Error(`实体${selfProto._table}保存异常`));
return;
}
self._entity = dbEntity.attrs;
if (DEBUG) {
console.debug('self entity: ', selfProto._table, self._entity);
}
resolve();
});
});
} else {
const updateBody = {
...this._entity
};
return new Promise((resolve, reject) => {
selfProto._target.update(updateBody, {}, function (error, dbEntity) {
if (error) {
console.log(`实体${selfProto._table}保存异常, ${JSON.stringify(error)}`);
reject(new Error(`实体${selfProto._table}保存异常`));
return;
}
self._entity = dbEntity.attrs;
resolve();
});
});
}
}
/**
* 删除实体 (实例方法)
* @returns {Promise<any>}
*/
function remove() {
const selfProto = Object.getPrototypeOf(this);
const partitionKey = {};
partitionKey[selfProto._schema.hashKey] = this._entity[selfProto._schema.hashKey];
if (selfProto._schema.rangeKey) {
partitionKey[selfProto._schema.rangeKey] = this._entity[selfProto._schema.rangeKey];
}
return new Promise((resolve, reject) => {
selfProto._target.destroy(partitionKey, function (error) {
if (error) {
const errMsg = new Error(`实体${selfProto._table}, remove异常${JSON.stringify(error)}`);
reject(errMsg);
return;
}
if (DEBUG) {
console.log(`实体: ${selfProto._schema.tableName}, [${JSON.stringify(partitionKey)}]删除成功`);
}
resolve();
});
});
}
const define = (entityName, schema) => {
//todo:lg 修改表名
let cached = ENTITY_CACHE.get(entityName);
if (cached) {
if (DEBUG) {
console.log(`实体${entityName}已定义...`);
}
return cached;
}
//检查Schema
utils.checkSchema(entityName, schema);
let table;
if (schema.tableName) {
table = DAO_CONFIG.prefix + schema.tableName;
} else {
table = DAO_CONFIG.prefix + entityName;
}
const firebaseIgnoredAttrs = schema[utils.SCHEMA_FIREBASE_IGNORED];
//删除Firebase 相关配置
if (firebaseIgnoredAttrs) {
let flag = delete schema[utils.SCHEMA_FIREBASE_IGNORED];
if (DEBUG) {
console.log(`实体${entityName}删除Firebase属性${utils.SCHEMA_FIREBASE_IGNORED}, ${flag ? "成功" : "失败"}`)
}
}
//添加 乐观锁版本号
if (!schema.schema[utils.ENTITY_ATTR_VERSION]) {
schema.schema[utils.ENTITY_ATTR_VERSION] = joi.number().positive();
}
//修改 tableName
schema.tableName = table;
//构造函数
const entityProxy = 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[utils.SCHEMA_BUILDER]) {
entityProxy.prototype._builder = schema[utils.SCHEMA_BUILDER];
delete schema[utils.SCHEMA_BUILDER];
} else {
entityProxy.prototype._builder = utils.entityBuilder;
}
entityProxy.loadOrCreate = loadOrCreate;
const instanceMethods = schema[utils.SCHEMA_INSTANCE_METHODS];
if (instanceMethods) {
delete schema[utils.SCHEMA_INSTANCE_METHODS];
}
//添加全局索引
let indexName;
if (schema.rangeKey) {
indexName = table + '_' + schema.rangeKey;
const globalSecondaryIndex = {
hashKey: schema.rangeKey,
rangeKey: schema.hashKey,
name: indexName,
type: 'global'
};
schema.indexes = [globalSecondaryIndex];
}
entityProxy.prototype._target = dynamodb.define(entityName, schema);
entityProxy.prototype._hashKey = schema.hashKey;
entityProxy.prototype._rangeKey = schema.rangeKey;
entityProxy.prototype._table = table;
//添加全局索引名称
entityProxy.prototype._indexName = indexName;
entityProxy.prototype[symSchema] = schema;
entityProxy.prototype._schema = schema;
entityProxy.prototype._firebaseIgnored = firebaseIgnoredAttrs;
//LOAD & CRUD
entityProxy.load = load;
entityProxy.create = create;
entityProxy.update = update;
entityProxy.destroy = destroy;
entityProxy.exists = exists;
entityProxy.listAll = listAll;
entityProxy.alter = alter;
entityProxy.batchGet = batchGet;
//instance methods
entityProxy.prototype.save = save;
entityProxy.prototype.remove = remove;
entityProxy.prototype.toVO = utils.toVO;
//添加自定义方法
if (instanceMethods) {
for (let funcName in instanceMethods) {
entityProxy.prototype[funcName] = instanceMethods[funcName];
}
}
entityProxy.prototype.toEntity = function () {
const result = {
...this._entity
};
//删除版本号
delete result[utils.ENTITY_ATTR_VERSION];
return result;
}
//更改entity属性
entityProxy.prototype.modifyEntity = function (kv) {
for (let key in kv) {
const val = kv[key];
this._entity[key] = val;
}
};
//添加 copyVersion方法
entityProxy.copyAttrVersion = function (updateData, sourceEntity) {
if (utils.ENTITY_ATTR_VERSION in sourceEntity._entity) {
updateData[utils.ENTITY_ATTR_VERSION] = sourceEntity._entity[utils.ENTITY_ATTR_VERSION];
}
};
ENTITY_CACHE.set(entityName, entityProxy);
//稍厚 dao.init 创建所有表
AWS_TABLES[schema.tableName] = DAO_CONFIG.throughput;
return entityProxy;
};
module.exports = define;