UNPKG

@eyugame/dao

Version:

A modelling tool for RealtimeDatabase(firebase) & Dynamodb(Amazon) & Redis

654 lines (601 loc) 22.5 kB
'use strict'; 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;