├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── MIT-License ├── README.md ├── index.js ├── lib ├── actions.js ├── client.js ├── crc8.js ├── filter_parser.js ├── filter_runner.js ├── ots2.js └── plainbuffer.js ├── package.json ├── spec ├── PlainBuffer.md ├── filter.dsl ├── table_store.proto └── table_store_filter.proto └── test ├── batch.integration.js ├── client.integration.js ├── common.js ├── encode.test.js ├── filter.demo.js ├── filter.test.js ├── plainbuffer.test.js ├── row.integration.js └── table.integration.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 8 | "linebreak-style": [ 9 | 2, 10 | "unix" 11 | ], 12 | "semi": [2, "always"], 13 | "strict": [2, "global"], 14 | "curly": 2, 15 | "eqeqeq": 2, 16 | "no-eval": 2, 17 | "guard-for-in": 2, 18 | "no-caller": 2, 19 | "no-else-return": 2, 20 | "no-eq-null": 2, 21 | "no-extend-native": 2, 22 | "no-extra-bind": 2, 23 | "no-floating-decimal": 2, 24 | "no-implied-eval": 2, 25 | "no-labels": 2, 26 | "no-with": 2, 27 | "no-loop-func": 1, 28 | "no-native-reassign": 2, 29 | "no-redeclare": [2, {"builtinGlobals": true}], 30 | "no-delete-var": 2, 31 | "no-shadow-restricted-names": 2, 32 | "no-undef-init": 2, 33 | "no-use-before-define": 2, 34 | "no-unused-vars": [2, {"args": "none"}], 35 | "no-undef": 2, 36 | "callback-return": [2, ["callback", "cb", "next"]], 37 | "global-require": 0, 38 | "no-console": 0, 39 | "require-yield": 0, 40 | "space-infix-ops": ["error", {"int32Hint": false}], 41 | "arrow-spacing": "error", 42 | "block-spacing": "error", 43 | "space-before-blocks": "error" 44 | }, 45 | "env": { 46 | "es6": true, 47 | "node": true, 48 | "browser": true 49 | }, 50 | "globals": { 51 | "describe": true, 52 | "it": true, 53 | "xit": true, 54 | "before": true, 55 | "after": true 56 | }, 57 | "parserOptions": { 58 | "ecmaVersion": "2018", 59 | "sourceType": "script", 60 | "ecmaFeatures": { 61 | "jsx": true 62 | } 63 | }, 64 | "extends": "eslint:recommended" 65 | } 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .logs 2 | node_modules 3 | coverage 4 | hydrogen-*.cfg 5 | node-*.log 6 | node.log 7 | config.js 8 | .nyc_output/* 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "12" 5 | - "10" 6 | - "8" 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | script: 13 | - npm run ci 14 | - test -z $ACCESS_KEY_ID -a -z $ACCESS_KEY_SECRET || npm run test-integration 15 | -------------------------------------------------------------------------------- /MIT-License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jackson Tian 2 | http://weibo.com/shyvo 3 | 4 | The MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aliyun OTS client for Node.js(ES6) 2 | ================================== 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | [![codecov][cov-image]][cov-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/@alicloud/ots2.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/@alicloud/ots2 10 | [travis-image]: https://img.shields.io/travis/ali-sdk/ots2/master.svg?style=flat-square 11 | [travis-url]: https://travis-ci.org/ali-sdk/ots2 12 | [cov-image]: https://codecov.io/gh/ali-sdk/ots2/branch/master/graph/badge.svg 13 | [cov-url]: https://codecov.io/gh/ali-sdk/ots2 14 | 15 | Aliyun OTS数据库服务Node.js客户端。 16 | 17 | ## OTS介绍 18 | OTS是构建在阿里云飞天分布式系统之上的NoSQL数据库服务,提供海量结构化数据的存储和实时访问。OTS以实例和表的形式组织数据,通过数据分片和负载均衡技术,达到规模的无缝扩展。OTS向应用程序屏蔽底层硬件平台的故障和错误,能自动从各类错误中快速恢复,提供非常高的服务可用性。OTS管理的数据全部存储在SSD中并具有多个备份,提供了快速的访问性能和极高的数据可靠性。用户在使用OTS服务时,只需要按照预留和使用的资源进行付费,无需关心数据库的软硬件升级维护、集群缩容扩容等复杂问题。 19 | 20 | 更多细节请参见: 21 | 22 | ## 安装 23 | 24 | ```sh 25 | $ npm install @alicloud/ots2 --save 26 | ``` 27 | 28 | ## 使用 29 | 30 | ### 创建客户端 31 | ```js 32 | const ots = require('@alicloud/ots2'); 33 | var client = ots.createClient({ 34 | accessKeyID: '', 35 | accessKeySecret: '', 36 | instance: '', 37 | region: '', 38 | keepAliveMsecs: 1000, // default 1000 39 | timeout: 3000 // default 3000ms 40 | }); 41 | ``` 42 | 43 | ### 调用API 44 | 45 | 详细API文档请参见: 46 | 47 | 所有表的操作 48 | ```js 49 | // 列出所有表名 50 | await client.listTable(); 51 | // 创建表 52 | var keys = [{ 'name': 'uid', 'type': 'STRING' }]; 53 | // 若实例为‘容量型’,read 和 write 值必须为0,否则会报错 OTSParameterInvalidError: Can not reserve read capacity unit on capacity cluster 54 | var capacityUnit = {read: 0, write: 0}; 55 | var options = { 56 | table_options: { 57 | time_to_live: -1,// 数据的过期时间, 单位秒, -1代表永不过期. 假如设置过期时间为一年, 即为 365 * 24 * 3600. 58 | max_versions: 1 59 | } 60 | }; 61 | var response = await client.createTable('metrics', keys, capacityUnit, options); 62 | // 更新表 63 | var capacityUnit = {read: 2, write: 1}; 64 | var response = await client.updateTable('metrics', capacityUnit); 65 | // 查看表信息 66 | var response = await client.describeTable('metrics'); 67 | // 删除表 68 | var response = await client.deleteTable('metrics'); 69 | ``` 70 | 71 | 所有行的操作 72 | 73 | ```js 74 | // 写入行 75 | var name = 'metrics'; 76 | var condition = { 77 | row_existence: ots.RowExistenceExpectation.IGNORE 78 | }; 79 | var primaryKeys = {uid: 'test_uid'}; 80 | 81 | var columns = {test: 'test_value'}; 82 | 83 | var response = await client.putRow(name, condition, primaryKeys, columns); 84 | 85 | // 读取行 86 | var name = 'metrics'; 87 | var primaryKeys = {uid: 'test_uid'}; 88 | 89 | var columns = ['test']; 90 | var response = await client.getRow(name, primaryKeys, columns); 91 | 92 | // 更新行 93 | var name = 'metrics'; 94 | var condition = { 95 | row_existence: ots.RowExistenceExpectation.IGNORE 96 | }; 97 | var primaryKeys = {uid: 'test_uid'}; 98 | 99 | var columns = { 100 | test: ots.$put('test_value_replaced') 101 | }; 102 | 103 | var response = await client.updateRow(name, condition, primaryKeys, columns); 104 | 105 | // 删除行 106 | var name = 'metrics'; 107 | var condition = { 108 | row_existence: ots.RowExistenceExpectation.IGNORE 109 | }; 110 | var primaryKeys = { 111 | uid: 'test_uid' 112 | }; 113 | 114 | var response = await client.deleteRow(name, condition, primaryKeys); 115 | ``` 116 | 117 | 批量操作 118 | 119 | ```js 120 | // 批量写 121 | var tables = [ 122 | { 123 | table_name: 'metrics', 124 | put_rows: [ 125 | { 126 | condition: { 127 | row_existence: ots.RowExistenceExpectation.IGNORE 128 | }, 129 | primary_key: { 130 | uid: 'test_uid' 131 | }, 132 | attribute_columns: { 133 | test: 'test_value' 134 | } 135 | } 136 | ], 137 | update_rows: {}, 138 | delete_rows: {} 139 | } 140 | ]; 141 | var response = await client.batchWriteRow(tables); 142 | 143 | // 批量读 144 | var tables = [ 145 | { 146 | table_name: 'metrics', 147 | rows: [ 148 | { 149 | primary_key: { 150 | uid: 'test_uid' 151 | } 152 | } 153 | ], 154 | columns_to_get: ['test'] 155 | } 156 | ]; 157 | var response = await client.batchGetRow(tables); 158 | 159 | // 范围读 160 | var start = { 161 | uid: ots.InfMin 162 | }; 163 | 164 | var end = { 165 | uid: ots.InfMax 166 | }; 167 | 168 | var request = { 169 | table_name: 'metrics', 170 | direction: ots.Direction.FORWARD, 171 | columns_to_get: ['test'], 172 | limit: 4, 173 | inclusive_start_primary_key: start, 174 | exclusive_end_primary_key: end 175 | }; 176 | var response = await client.getRange(request); 177 | ``` 178 | 179 | ### 条件更新(Conditional Update)和过滤器(Filter)支持 180 | 181 | 条件更新和过滤器是一个 ConditionColumn 类型,是一种类似 SQL 中的 where 条件。 182 | 183 | 本模块提供一个makeFilter方法来快速生成一个 ConditionColumn 对象。 184 | 185 | ```js 186 | ots.makeFilter('column_name < @name true', { 187 | name: 'Jackson Tian' 188 | }); 189 | // column_name => 表中的列名 190 | // < => 操作符:==, !=, <, <=, >, >= 191 | // @name 上下文中的属性名 192 | // true => 列不存在时,默认结果 193 | ``` 194 | 195 | 其中组合条件的语法(DSL)如下: 196 | 197 | ```js 198 | name == @name true 199 | NOT name == @name true 200 | NOT NOT name == @name true 201 | name > @name true AND age <= @age false 202 | name > @name true OR age <= @age false 203 | NOT name > @name true AND age <= @age false 204 | name > @name true AND age <= @age false AND gender == @gender true 205 | name > @name true OR age <= @age false AND gender == @gender true 206 | ``` 207 | 208 | 优先级顺序为:NOT > AND > OR 209 | 210 | ## License 211 | OTS服务由阿里云提供。但本模块在MIT许可下自由使用。 212 | 213 | (The MIT license) 214 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ots2 = require('./lib/ots2'); 4 | const parser = require('./lib/filter_parser'); 5 | const runner = require('./lib/filter_runner'); 6 | const Client = require('./lib/client'); 7 | const { 8 | PUT, DELETE, DELETE_ALL, 9 | InfMin, InfMax, 10 | serialize 11 | } = require('./lib/plainbuffer'); 12 | 13 | /** 14 | * 根据选项创建实例的客户端 15 | * 16 | * Example: 17 | * ``` 18 | * var ots = require('ots2'); 19 | * var client = ots.createClient({ 20 | * accessKeyID: '', 21 | * accessKeySecret: '', 22 | * instance: '', 23 | * region: '' 24 | * }); 25 | * // or with endpoint 26 | * var client = ots.createClient({ 27 | * accessKeyID: '', 28 | * accessKeySecret: '', 29 | * endpoint: '' 30 | * }); 31 | * ``` 32 | * @param {Object} opts 选项 33 | */ 34 | exports.createClient = function (opts) { 35 | return new Client(opts); 36 | }; 37 | 38 | /** 39 | * 相关Enum类型 40 | * 41 | * Example: 42 | * ``` 43 | * enum PrimaryKeyType { 44 | * INTEGER = 1; 45 | * STRING = 2; 46 | * BINARY = 3; 47 | * } 48 | * 49 | * enum PrimaryKeyOption { 50 | * AUTO_INCREMENT = 1; 51 | * } 52 | * 53 | * enum RowExistenceExpectation { 54 | * IGNORE = 0; 55 | * EXPECT_EXIST = 1; 56 | * EXPECT_NOT_EXIST = 2; 57 | * } 58 | * 59 | * enum ReturnType { 60 | * RT_NONE = 0; 61 | * RT_PK = 1; 62 | * } 63 | * 64 | * enum OperationType { 65 | * PUT = 1; 66 | * UPDATE = 2; 67 | * DELETE = 3; 68 | * } 69 | * ``` 70 | */ 71 | // 拷贝所有的Enum 72 | for (var key in ots2) { 73 | if (typeof ots2[key] !== 'function') { 74 | exports[key] = ots2[key]; 75 | } 76 | } 77 | 78 | exports.InfMin = InfMin; 79 | exports.InfMax = InfMax; 80 | 81 | exports.$put = function (value) { 82 | return { 83 | type: PUT, 84 | value: value 85 | }; 86 | }; 87 | 88 | exports.$delete = function (timestamp) { 89 | return {type: DELETE, timestamp: timestamp}; 90 | }; 91 | 92 | exports.$deleteAll = function () { 93 | return {type: DELETE_ALL}; 94 | }; 95 | 96 | exports.makeFilter = function (input, locals) { 97 | var ast = parser.parse(input); 98 | return runner.parseFilter(ast, locals); 99 | }; 100 | 101 | exports.serialize = serialize; 102 | -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | serialize, deserialize 5 | } = require('./plainbuffer'); 6 | 7 | function getOne(list) { 8 | return list && list.length > 0 && list[0] || null; 9 | } 10 | 11 | /** 12 | * 根据给定的表结构信息创建相应的表。 13 | * 14 | * Example: 15 | * ``` 16 | * var keys = [{ 'name': 'uid', 'type': 'STRING' }]; 17 | * var capacityUnit = {read: 1, write: 1}; 18 | * yield client.createTable('metrics', keys, capacityUnit); 19 | * ``` 20 | * @param {String} name 表名 21 | * @param {Array} keys 主键列表 22 | * @param {Object} capacity 保留容量单元 23 | * @return {Object} 返回值 24 | */ 25 | exports.createTable = async function (name, primaryKeys, capacity, options = {}) { 26 | return this.request('CreateTable', { 27 | table_meta: { 28 | table_name: name, 29 | primary_key: primaryKeys 30 | }, 31 | reserved_throughput: { 32 | capacity_unit: capacity 33 | }, 34 | table_options: options.table_options, 35 | // partitions: options.partitions, 36 | // stream_spec: options.stream_spec 37 | }); 38 | }; 39 | 40 | /** 41 | * 更新指定表的预留读吞吐量或预留写吞吐量设置,新设定将于更新成功一分钟内生效。 42 | * 43 | * Example: 44 | * ``` 45 | * var capacityUnit = {read: 2, write: 1}; 46 | * yield client.updateTable('metrics', capacityUnit); 47 | * ``` 48 | * @param {String} name 表名 49 | * @param {Object} capacity 保留容量单元 50 | * @return {Object} 返回值 51 | */ 52 | exports.updateTable = async function (name, capacity) { 53 | return this.request('UpdateTable', { 54 | table_name: name, 55 | reserved_throughput: { 56 | capacity_unit: capacity 57 | } 58 | }); 59 | }; 60 | 61 | /** 62 | * 获取当前实例下已创建的所有表的表名。 63 | * 64 | * Example: 65 | * ``` 66 | * yield client.listTable(); 67 | * ``` 68 | * @return {Object} 返回值 69 | */ 70 | exports.listTable = async function () { 71 | return this.request('ListTable', {}); 72 | }; 73 | 74 | /** 75 | * 查询指定表的结构信息和预留读写吞吐量设置信息。 76 | * Example: 77 | * ``` 78 | * yield client.describeTable('metrics'); 79 | * ``` 80 | * @param {String} name 表名 81 | * @return {Object} 返回值 82 | */ 83 | exports.describeTable = async function (name) { 84 | return this.request('DescribeTable', { 85 | table_name: name 86 | }); 87 | }; 88 | 89 | /** 90 | * 删除本实例下指定的表 91 | * Example: 92 | * ``` 93 | * yield client.deleteTable('metrics'); 94 | * ``` 95 | * @param {String} name 表名 96 | * @return {Object} 返回值 97 | */ 98 | exports.deleteTable = async function (name) { 99 | return this.request('DeleteTable', { 100 | table_name: name 101 | }); 102 | }; 103 | 104 | /** 105 | * 插入数据到指定的行,如果该行不存在,则新增一行;若该行存在,则覆盖原有行。 106 | * 107 | * Example: 108 | * @param {String} name 表名 109 | * @return {Object} 返回值 110 | */ 111 | exports.putRow = async function (name, condition, primaryKeys, columns, returnContent) { 112 | return this.request('PutRow', { 113 | table_name: name, 114 | row: serialize(primaryKeys, columns), 115 | condition: condition, 116 | return_content: returnContent 117 | }); 118 | }; 119 | 120 | // message GetRowRequest { 121 | // required string table_name = 1; 122 | // required bytes primary_key = 2; // encoded as InplaceRowChangeSet, but only has primary key 123 | // repeated string columns_to_get = 3; // 不指定则读出所有的列 124 | // optional TimeRange time_range = 4; 125 | // optional int32 max_versions = 5; 126 | // optional bytes filter = 7; 127 | // optional string start_column = 8; 128 | // optional string end_column = 9; 129 | // optional bytes token = 10; 130 | // } 131 | 132 | /** 133 | * 根据给定的主键读取单行数据。 134 | * 135 | * Example: 136 | * ``` 137 | * ``` 138 | * @param {String} name 表名 139 | * @return {Object} 返回值 140 | */ 141 | exports.getRow = async function (name, primaryKeys, columns, options) { 142 | var data = Object.assign({ 143 | table_name: name, 144 | primary_key: serialize(primaryKeys), 145 | columns_to_get: columns, 146 | max_versions: 1 147 | }, options); 148 | // time_range: range, 149 | // max_versions: maxVersions; 150 | // filter: filter, 151 | // start_column; 152 | // end_column; 153 | // token; 154 | var result = await this.request('GetRow', data); 155 | result.row = getOne(deserialize(result.row)); 156 | return result; 157 | }; 158 | 159 | /** 160 | * 更新指定行的数据,如果该行不存在,则新增一行; 161 | * 若该行存在,则根据请求的内容在这一行中新增、修改或者删除指定列的值。 162 | * 163 | * Example: 164 | * ``` 165 | * ``` 166 | * @param {String} name 表名 167 | * @return {Object} 返回值 168 | */ 169 | exports.updateRow = function (name, primaryKeys, columns, condition, returnContent) { 170 | return this.request('UpdateRow', { 171 | table_name: name, 172 | row_change: serialize(primaryKeys, columns), 173 | condition: condition, 174 | // return_content: returnContent 175 | }); 176 | }; 177 | 178 | /** 179 | * 删除一行数据。 180 | * 181 | * Example: 182 | * ``` 183 | * ``` 184 | * @param {String} name 表名 185 | * @return {Object} 返回值 186 | */ 187 | exports.deleteRow = function (name, primaryKeys, columns, deleteMarker, condition, returnContent) { 188 | return this.request('DeleteRow', { 189 | table_name: name, 190 | primary_key: serialize(primaryKeys, columns, deleteMarker), // only with primaryKeys 191 | condition: condition, 192 | return_content: returnContent 193 | }); 194 | }; 195 | 196 | /** 197 | * 读取指定主键范围内的数据。 198 | * 199 | * Example: 200 | * ``` 201 | * var start = { 202 | * uid: OTS.InfMin 203 | * }; 204 | * var end = { 205 | * uid: OTS.InfMax 206 | * }; 207 | * var request = { 208 | * table_name: 'metrics', 209 | * direction: OTS.Direction.FORWARD, 210 | * columns_to_get: ['test'], 211 | * limit: 4, 212 | * inclusive_start_primary_key: start, 213 | * exclusive_end_primary_key: end 214 | * }; 215 | * var response = yield client.getRange(request); 216 | * ``` 217 | * @param {String} name 表名 218 | * @return {Object} 返回值 219 | */ 220 | exports.getRange = async function (request) { 221 | request.inclusive_start_primary_key = serialize(request.inclusive_start_primary_key); 222 | request.exclusive_end_primary_key = serialize(request.exclusive_end_primary_key); 223 | 224 | var response = await this.request('GetRange', request); 225 | 226 | response.rows = deserialize(response.rows); 227 | 228 | return response; 229 | }; 230 | 231 | /** 232 | * 批量读取一个或多个表中的若干行数据。 233 | * BatchGetRow操作可视为多个GetRow操作的集合,各个操作独立执行,独立返回结果, 234 | * 独立计算服务能力单元。与执行大量的GetRow操作相比,使用BatchGetRow操作可以有效减少 235 | * 请求的响应时间,提高数据的读取速率。 236 | * 237 | * Example: 238 | * ``` 239 | * var tables = [ 240 | * { 241 | * table_name: 'metrics', 242 | * put_rows: [ 243 | * { 244 | * condition: { 245 | * row_existence: OTS.RowExistenceExpectation.IGNORE 246 | * }, 247 | * primary_key: { 248 | * uid: 'test_uid' 249 | * }, 250 | * attribute_columns: { 251 | * test: 'test_value' 252 | * } 253 | * } 254 | * ], 255 | * update_rows: [], 256 | * delete_rows: [] 257 | * } 258 | * ]; 259 | * var response = yield client.batchWriteRow(tables); 260 | * ``` 261 | * @param {String} name 表名 262 | * @return {Object} 返回值 263 | */ 264 | exports.batchGetRow = async function (tables) { 265 | var response = await this.request('BatchGetRow', { 266 | tables: tables 267 | }); 268 | 269 | var _tables = response.tables; 270 | for (var i = 0; i < _tables.length; i++) { 271 | var table = _tables[i]; 272 | var rows = table.rows; 273 | for (var j = 0; j < rows.length; j++) { 274 | var row = rows[j]; 275 | if (row.is_ok) { 276 | row.row = getOne(deserialize(row.row)); 277 | } 278 | } 279 | } 280 | 281 | return response; 282 | }; 283 | 284 | /** 285 | * 批量插入,修改或删除一个或多个表中的若干行数据。 286 | * BatchWriteRow操作可视为多个PutRow、UpdateRow、DeleteRow操作的 287 | * 集合,各个操作独立执行,独立返回结果,独立计算服务能力单元。 288 | * 与执行大量的单行写操作相比,使用BatchWriteRow操作可以有效减少请求 289 | * 的响应时间,提高数据的写入速率。 290 | * 291 | * Example: 292 | * ``` 293 | * 294 | * ``` 295 | * @param {Array} tables 对应了每个table下各操作的响应信息, 296 | * 包括是否成功执行,错误码和消耗的服务能力单元。 297 | * @return {Object} 返回值 298 | */ 299 | exports.batchWriteRow = function (tables) { 300 | return this.request('BatchWriteRow', { 301 | tables: tables 302 | }); 303 | }; 304 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = require('url'); 4 | const httpx = require('httpx'); 5 | const kitx = require('kitx'); 6 | 7 | const actions = require('./actions'); 8 | const ots2 = require('./ots2'); 9 | const OTSError = ots2.Error; 10 | const debug = require('debug')('ots'); 11 | 12 | var getCanonicalHeaders = function (headers) { 13 | var keys = Object.keys(headers); 14 | var selectedKeys = []; 15 | for (var i = 0; i < keys.length; i++) { 16 | var key = keys[i]; 17 | if (key.indexOf('x-ots-') !== -1 && key !== 'x-ots-signature') { 18 | selectedKeys.push(key); 19 | } 20 | } 21 | 22 | selectedKeys.sort(); 23 | 24 | var str = ''; 25 | for (var j = 0; j < selectedKeys.length; j++) { 26 | var name = selectedKeys[j]; 27 | str += name + ':' + headers[name].trim() + '\n'; 28 | } 29 | 30 | return str; 31 | }; 32 | 33 | /** 34 | * OTS Client 35 | * @param {Object} opts 36 | * - instance {String} instance 37 | * - region {String} region 38 | * - internal 39 | * - endpoint 40 | * - accessKeyID 41 | * - accessKeySecret 42 | * - keepAliveMsecs {Number} http(s) agent keep socket conn time, unit ms 43 | * - timeout 44 | */ 45 | class Client { 46 | constructor(opts) { 47 | if (!opts || typeof opts !== 'object') { 48 | throw new TypeError('must pass in opts as Object'); 49 | } 50 | 51 | this.accessKeyID = opts.accessKeyID; 52 | this.accessKeySecret = opts.accessKeySecret; 53 | if (!this.accessKeyID || !this.accessKeySecret) { 54 | throw new Error('must pass in accessKeyID and accessKeySecret'); 55 | } 56 | 57 | this.instance = opts.instance; 58 | if (!this.instance) { 59 | throw new Error('must pass in instance'); 60 | } 61 | 62 | if (opts.endpoint) { 63 | this.endpoint = opts.endpoint; 64 | } else { 65 | this.region = opts.region; 66 | if (!this.region) { 67 | throw new Error('must pass in region'); 68 | } 69 | 70 | var env = opts.internal ? 'ots-internal' : 'ots'; 71 | 72 | this.endpoint = `http://${opts.instance}.${opts.region}.${env}.aliyuncs.com/`; 73 | } 74 | 75 | var urlobj = url.parse(this.endpoint); 76 | var httplib = urlobj.protocol === 'http:' ? require('http') : require('https'); 77 | 78 | this.keepAliveAgent = httplib.Agent({ 79 | keepAlive: true, 80 | keepAliveMsecs: opts.keepAliveMsecs 81 | }); 82 | 83 | if (typeof opts.timeout === 'number') { 84 | this.timeout = opts.timeout; 85 | } 86 | } 87 | 88 | sign(input) { 89 | return kitx.sha1(input, this.accessKeySecret, 'base64'); 90 | } 91 | 92 | buildHeaders(canonicalURI, content) { 93 | var headers = { 94 | 'x-ots-date': (new Date()).toISOString(), 95 | 'x-ots-apiversion': '2015-12-31', 96 | 'x-ots-accesskeyid': this.accessKeyID, 97 | 'x-ots-instancename': this.instance, 98 | 'x-ots-contentmd5': kitx.md5(content, 'base64'), 99 | 'content-length': Buffer.byteLength(content) 100 | // 'User-Agent': 'aliyun-tablestore-sdk/' 101 | }; 102 | 103 | var canonicalHeaders = getCanonicalHeaders(headers); 104 | var method = 'POST'; 105 | 106 | var stringToSign = `${canonicalURI}\n${method}\n\n${canonicalHeaders}`; 107 | debug('basestring is %s', stringToSign); 108 | headers['x-ots-signature'] = this.sign(stringToSign); 109 | return headers; 110 | } 111 | 112 | async request(operation, data) { 113 | var url = this.endpoint + operation; 114 | // get body 115 | var Request = ots2[operation + 'Request']; 116 | var body = new Request(data).toBuffer(); 117 | 118 | var headers = this.buildHeaders('/' + operation, body); 119 | var opts = { 120 | method: 'POST', 121 | data: body, 122 | headers: headers, 123 | agent: this.keepAliveAgent 124 | }; 125 | 126 | if (this.timeout) { 127 | opts.timeout = this.timeout; 128 | } 129 | 130 | debug('url is %s', url); 131 | 132 | var res = await httpx.request(url, opts); 133 | debug('response headers is %j', res.headers); 134 | 135 | var buff = await httpx.read(res); 136 | 137 | if (res.statusCode < 200 || res.statusCode >= 400) { 138 | var error = OTSError.decode(buff); 139 | var err = new Error(error.message); 140 | err.name = error.code + 'Error'; 141 | err.data = data; 142 | throw err; 143 | } 144 | 145 | if (!this.checkResponseSign(operation, res.headers)) { 146 | throw new Error('响应签名不对'); 147 | } 148 | 149 | if (!this.checkResponseDate(res.headers)) { 150 | throw new Error('响应时间不对'); 151 | } 152 | 153 | if (!this.checkResponseContent(res.headers, buff)) { 154 | throw new Error('响应内容不对'); 155 | } 156 | 157 | return ots2[operation + 'Response'].decode(buff); 158 | } 159 | 160 | checkResponseSign(operation, headers) { 161 | var canonicalURI = '/' + operation; 162 | var canonicalHeaders = getCanonicalHeaders(headers); 163 | var stringToSign = canonicalHeaders + canonicalURI; 164 | debug('basestring is %s', stringToSign); 165 | var signature = this.sign(stringToSign); 166 | var auth = 'OTS ' + this.accessKeyID + ':' + signature; 167 | return headers['authorization'] === auth; 168 | } 169 | 170 | checkResponseDate(headers) { 171 | var now = new Date().getTime(); 172 | var date = Date.parse(headers['x-ots-date']); 173 | return Math.abs(now - date) < 15 * 60 * 1000; // 对比时间,不能相差超过15分钟 174 | } 175 | 176 | checkResponseContent(headers, content) { 177 | var hash = headers['x-ots-contentmd5']; 178 | return kitx.md5(content, 'base64') === hash; 179 | } 180 | } 181 | 182 | 183 | Object.assign(Client.prototype, actions); 184 | 185 | module.exports = Client; 186 | -------------------------------------------------------------------------------- /lib/crc8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const spaceSize = 256; 4 | const crc8Table = new Int8Array(spaceSize); 5 | 6 | /** 7 | * 初始化表 8 | */ 9 | (function() { 10 | for (var i = 0; i < spaceSize; ++i) { 11 | var x = i; 12 | for (var j = 8; j > 0; --j) { 13 | x = ((x << 1) ^ (((x & 0x80) !== 0) ? 0x07 : 0)); 14 | } 15 | crc8Table[i] = x; 16 | } 17 | })(); 18 | 19 | var crc8Int8 = function(crc, val) { 20 | return crc8Table[(crc ^ val) & 0xff]; 21 | }; 22 | 23 | var crc8Bytes = function(crc, bytes) { 24 | for (var i = 0; i < bytes.length; i++) { 25 | crc = crc8Int8(crc, bytes[i]); 26 | } 27 | 28 | return crc; 29 | }; 30 | 31 | exports.crc8Int8 = crc8Int8; 32 | exports.crc8Bytes = crc8Bytes; 33 | -------------------------------------------------------------------------------- /lib/filter_parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const peg = require('pegjs'); 7 | 8 | const specPath = path.join(__dirname, '../spec/filter.dsl'); 9 | const code = fs.readFileSync(specPath, 'utf8'); 10 | 11 | const parser = peg.generate(code); 12 | 13 | exports.parse = function (source) { 14 | try { 15 | return parser.parse(source); 16 | } catch (ex) { 17 | console.log(source); 18 | var {start, end} = ex.location; 19 | console.log(' '.repeat(start.offset) + '^'.repeat(end.offset - start.offset)); 20 | throw ex; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/filter_runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ots2 = require('./ots2'); 4 | 5 | const newValue = require('./plainbuffer').newValue; 6 | 7 | const ComparatorType = ots2.ComparatorType; 8 | const comparators = { 9 | '==': ComparatorType.CT_EQUAL, 10 | '!=': ComparatorType.CT_NOT_EQUAL, 11 | '>': ComparatorType.CT_GREATER_THAN, 12 | '>=': ComparatorType.CT_GREATER_EQUAL, 13 | '<': ComparatorType.CT_LESS_THAN, 14 | '<=': ComparatorType.CT_LESS_EQUAL 15 | }; 16 | 17 | var getComparatorType = function (comparator) { 18 | var type = comparators[comparator]; 19 | if (!type) { 20 | throw new Error(`unsupported comparator '${comparator}'`); 21 | } 22 | return type; 23 | }; 24 | 25 | const LogicalOperator = ots2.LogicalOperator; 26 | const combinators = { 27 | 'NOT': LogicalOperator.LO_NOT, 28 | 'AND': LogicalOperator.LO_AND, 29 | 'OR': LogicalOperator.LO_OR 30 | }; 31 | 32 | var getCombinatorType = function (combinator) { 33 | var type = combinators[combinator]; 34 | if (!type) { 35 | throw new Error(`unsupported combinator '${combinator}'`); 36 | } 37 | return type; 38 | }; 39 | 40 | const FilterType = ots2.FilterType; 41 | 42 | var parseSingleColumnValueFilter = function (condition, locals) { 43 | var [ 44 | columnName, 45 | comparator, 46 | columnValue, 47 | passIfMissing, 48 | latestVersionOnly 49 | ] = condition; 50 | 51 | return new ots2.Filter({ 52 | type: FilterType.FT_SINGLE_COLUMN_VALUE, 53 | filter: new ots2.SingleColumnValueFilter({ 54 | comparator: getComparatorType(comparator), 55 | column_name: columnName, 56 | column_value: newValue(locals[columnValue]).toBytes(), 57 | filter_if_missing: passIfMissing, 58 | latest_version_only: latestVersionOnly 59 | }).toBuffer() 60 | }).toBuffer(); 61 | }; 62 | 63 | var parseCompositeColumnValueFilter = function (combinator, conditions, locals, $$) { 64 | return new ots2.Filter({ 65 | type: FilterType.FT_COMPOSITE_COLUMN_VALUE, 66 | filter: new ots2.CompositeColumnValueFilter({ 67 | combinator: getCombinatorType(combinator), 68 | sub_filters: conditions.map(function (node) { 69 | return exports.parseFilter(node, locals); 70 | }) 71 | }).toBuffer() 72 | }).toBuffer(); 73 | }; 74 | 75 | exports.parseFilter = function (node, locals) { 76 | switch (node.type) { 77 | case 'CompositeColumnValueFilter': 78 | return parseCompositeColumnValueFilter(node.combinar, node.conditions, locals); 79 | case 'SingleColumnValueFilter': 80 | return parseSingleColumnValueFilter(node.condition, locals); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /lib/ots2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const pb = require('protobufjs'); 7 | 8 | const builder = pb.newBuilder(); 9 | 10 | const protoPath = path.join(__dirname, '../spec/table_store.proto'); 11 | const protoContent = fs.readFileSync(protoPath, 'utf8'); 12 | pb.loadProto(protoContent, builder, protoPath); 13 | 14 | const filterProtoPath = path.join(__dirname, '../spec/table_store_filter.proto'); 15 | const protoFilterContent = fs.readFileSync(filterProtoPath, 'utf8'); 16 | pb.loadProto(protoFilterContent, builder, filterProtoPath); 17 | 18 | /** 19 | * ProtoBuf定义 20 | * @name ots2 21 | */ 22 | module.exports = builder.build('com.aliyun.tablestore.protocol'); 23 | -------------------------------------------------------------------------------- /lib/plainbuffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crc8Int8 = require('./crc8').crc8Int8; 4 | const crc8Bytes = require('./crc8').crc8Bytes; 5 | const Long = require('long'); 6 | 7 | // 各个const字段的值: 各个Tag的值(除header外,类型int8_t): 8 | const Tag = { 9 | 'HEADER': 0x75, // 4 byte 10 | 'PK': 0x01, // 1 byte 11 | 'ATTR': 0x02, 12 | 'CELL': 0x03, 13 | 'CELL_NAME': 0x04, 14 | 'CELL_VALUE': 0x05, 15 | 'CELL_OP': 0x06, 16 | 'CELL_TS': 0x07, 17 | 'DELETE_MARKER': 0x08, 18 | 'ROW_CHECKSUM': 0x09, 19 | 'CELL_CHECKSUM': 0x0A 20 | }; 21 | exports.Tag = Tag; 22 | 23 | //各个COLUMN VALUE的Type定义(与SQLVariant保持兼容): 24 | const ColumnType = { 25 | 'VT_INTEGER': 0x0, 26 | 'VT_DOUBLE': 0x1, 27 | 'VT_BOOLEAN': 0x2, 28 | 'VT_STRING': 0x3, 29 | 'VT_NULL': 0x6, 30 | 'VT_BLOB': 0x7, 31 | 'VT_INF_MIN': 0x9, 32 | 'VT_INF_MAX': 0xa, 33 | 'VT_AUTO_INCREMENT': 0xb 34 | }; 35 | exports.ColumnType = ColumnType; 36 | 37 | var types = { 38 | 'STRING': 'String', 39 | 'BLOB': 'Blob', 40 | 'BOOLEAN': 'Boolean', 41 | 'NULL': 'Null', 42 | 'DOUBLE': 'Double', 43 | 'INTEGER': 'Integer', 44 | 'INF_MIN': 'InfMin', 45 | 'INF_MAX': 'InfMax', 46 | 'AUTO_INCREMENT': 'AutoIncrement' 47 | }; 48 | 49 | var getColumnValue = function(type) { 50 | var rawType = ''; 51 | switch (type) { 52 | case types.INTEGER: 53 | rawType = ColumnType.VT_INTEGER; 54 | break; 55 | case types.DOUBLE: 56 | rawType = ColumnType.VT_DOUBLE; 57 | break; 58 | case types.BOOLEAN: 59 | rawType = ColumnType.VT_BOOLEAN; 60 | break; 61 | case types.STRING: 62 | rawType = ColumnType.VT_STRING; 63 | break; 64 | case types.NULL: 65 | rawType = ColumnType.VT_NULL; 66 | break; 67 | case types.BLOB: 68 | rawType = ColumnType.VT_BLOB; 69 | break; 70 | case types.INF_MIN: 71 | rawType = ColumnType.VT_INF_MIN; 72 | break; 73 | case types.INF_MAX: 74 | rawType = ColumnType.VT_INF_MAX; 75 | break; 76 | case types.AUTO_INCREMENT: 77 | rawType = ColumnType.VT_AUTO_INCREMENT; 78 | break; 79 | default: 80 | throw new Error('unknown type:' + type); 81 | } 82 | 83 | return rawType; 84 | }; 85 | 86 | /** 87 | * 生成不同类型的Column 88 | * 89 | * Example: 90 | * ``` 91 | * ots.StringColumn('name', 'value'); 92 | * ots.BlobColumn('name', [0,1]); 93 | * ots.NullColumn('name'); 94 | * ots.BooleanColumn('name', true); 95 | * ots.DoubleColumn('name', 1.1); 96 | * ots.IntegerColumn('name', 10); 97 | * ots.InfMinColumn('name'); 98 | * ots.InfMaxColumn('name'); 99 | * ``` 100 | * @name createXXXColumn 101 | * @param {String} name column name 102 | * @param {MIX} value column value 103 | */ 104 | Object.keys(types).forEach(function(key) { 105 | var shortName = types[key] + 'Column'; 106 | exports[shortName] = function(name, value) { 107 | return { 108 | name: name, 109 | type: getColumnValue(types[key]), 110 | value: value 111 | }; 112 | }; 113 | }); 114 | 115 | //各个TAG_CELL_OP_TYPE的Type定义(只保存DeleteCell相关操作,row操作放在跟PlainBuffer平级的PB对象中(如果是deleterow,那么mark也是encode的)] 116 | const TagCellOpType = { 117 | 'DELETE_ALL_VERSION': 0x1, 118 | 'DELETE_ONE_VERSION': 0x3 119 | }; 120 | exports.TagCellOpType = TagCellOpType; 121 | 122 | exports.PutColumn = function (name, value) { 123 | var type; 124 | if (typeof value === 'string') { 125 | type = types.STRING; 126 | } else if (typeof value === 'number') { 127 | if (Number.isInteger(value)) { 128 | type = types.INTEGER; 129 | } else { 130 | type = types.DOUBLE; 131 | } 132 | } else if (Buffer.isBuffer(value)) { 133 | type = types.BLOB; 134 | } else if (typeof value === 'undefined' || value === null) { 135 | // ignore null value 136 | // columns.push(exports.NullColumn(key, value)); 137 | } else if (value === true || value === false) { 138 | type = types.BOOLEAN; 139 | } 140 | 141 | return { 142 | name: name, 143 | type: getColumnValue(type), 144 | value: value 145 | }; 146 | }; 147 | 148 | exports.DeleteColumn = function (name, timestamp) { 149 | return { 150 | name: name, 151 | ts: timestamp, 152 | op: TagCellOpType.DELETE_ONE_VERSION 153 | }; 154 | }; 155 | 156 | exports.DeleteAllColumn = function (name, ts) { 157 | return { 158 | name: name, 159 | op: TagCellOpType.DELETE_ALL_VERSION 160 | }; 161 | }; 162 | 163 | exports.InfMin = Symbol('InfMin'); 164 | exports.InfMax = Symbol('InfMax'); 165 | 166 | const Header = { 167 | LITTLE_ENDIAN_32_SIZE: 4, 168 | LITTLE_ENDIAN_64_SIZE: 8 169 | }; 170 | exports.Header = Header; 171 | 172 | class Row { 173 | constructor(pk, attr, deleteMarker) { 174 | this.pk = pk; 175 | this.attr = attr; 176 | this.deleteMarker = deleteMarker; 177 | } 178 | 179 | toBytes() { 180 | var buffs = []; 181 | var checksum = 0x00; 182 | // row = ( pk [attr] | [pk] attr | pk attr ) [tag_delete_marker] row_checksum; 183 | 184 | // pk 185 | if (this.pk) { 186 | buffs.push(this.pk.toBytes()); 187 | checksum = this.pk.getChecksum(checksum); 188 | } 189 | 190 | // attr 191 | if (this.attr) { 192 | buffs.push(this.attr.toBytes()); 193 | checksum = this.attr.getChecksum(checksum); 194 | } 195 | 196 | // tag_delete_marker 197 | var del = 0x00; 198 | if (typeof this.deleteMarker !== 'undefined') { 199 | buffs.push(Buffer.from([Tag.DELETE_MARKER])); 200 | del = 0x01; 201 | } 202 | 203 | // 没有deleteMarker, 要与0x0做crc. 204 | checksum = crc8Int8(checksum, del); 205 | 206 | // row_checksum = tag_row_checksum row_crc8 207 | buffs.push(Buffer.from([Tag.ROW_CHECKSUM, checksum])); 208 | 209 | return Buffer.concat(buffs); 210 | } 211 | } 212 | 213 | var Value = function(type, value) { 214 | this.type = type; 215 | this.value = value; 216 | }; 217 | 218 | Value.prototype.toBytes = function() { 219 | // formated_value = value_type value_len value_data 220 | var type = Buffer.from([this.type]); 221 | var buffs = []; 222 | switch (this.type) { 223 | case ColumnType.VT_INTEGER: 224 | buffs.push(type); 225 | var integer = Buffer.alloc(8); 226 | var val = Long.fromNumber(this.value); 227 | integer.writeInt32LE(val.low, 0); 228 | integer.writeInt32LE(val.high, 4); 229 | buffs.push(integer); 230 | break; 231 | case ColumnType.VT_DOUBLE: 232 | buffs.push(type); 233 | var double = Buffer.alloc(Header.LITTLE_ENDIAN_64_SIZE); 234 | double.writeDoubleLE(this.value, 0); 235 | buffs.push(double); 236 | break; 237 | case ColumnType.VT_BOOLEAN: 238 | buffs.push(type); 239 | buffs.push(Buffer.from([(this.value ? 1 : 0)])); 240 | break; 241 | case ColumnType.VT_STRING: 242 | var valueLength = Buffer.alloc(Header.LITTLE_ENDIAN_32_SIZE); 243 | var value = Buffer.from('' + this.value); 244 | valueLength.writeInt32LE(value.length, 0); 245 | buffs.push(type); 246 | buffs.push(valueLength); 247 | buffs.push(value); 248 | break; 249 | case ColumnType.VT_NULL: 250 | break; 251 | case ColumnType.VT_BLOB: 252 | buffs.push(type); 253 | var buf = Buffer.alloc(Header.LITTLE_ENDIAN_32_SIZE); 254 | buf.writeInt32LE(this.value.length, 0); 255 | buffs.push(buf); 256 | buffs.push(this.value); 257 | break; 258 | case ColumnType.VT_INF_MIN: 259 | case ColumnType.VT_INF_MAX: 260 | buffs.push(type); 261 | break; 262 | } 263 | 264 | return Buffer.concat(buffs); 265 | }; 266 | 267 | exports.Value = Value; 268 | 269 | class Cells { 270 | constructor(tag, cells) { 271 | this.tag = Buffer.from([tag]); 272 | this.cells = cells; 273 | } 274 | 275 | toBytes() { 276 | // attr = tag_attr cell1 [cell_2] [cell_3] 277 | var buffs = []; 278 | 279 | if (this.cells.length) { 280 | buffs.push(this.tag); 281 | 282 | for (var i = 0; i < this.cells.length; i++) { 283 | var cell = this.cells[i]; 284 | buffs.push(cell.toBytes()); 285 | } 286 | } 287 | 288 | return Buffer.concat(buffs); 289 | } 290 | 291 | getChecksum(checksum) { 292 | for (var i = 0; i < this.cells.length; i++) { 293 | var cell = this.cells[i]; 294 | checksum = crc8Int8(checksum, cell.checksum); 295 | } 296 | 297 | return checksum; 298 | } 299 | } 300 | 301 | class PK extends Cells { 302 | constructor(cells) { 303 | super(Tag.PK, cells); 304 | } 305 | } 306 | 307 | class Attr extends Cells { 308 | constructor(cells) { 309 | super(Tag.ATTR, cells); 310 | } 311 | } 312 | 313 | class Cell { 314 | constructor(cell) { 315 | this.name = cell.name; 316 | this.type = cell.type; 317 | this.value = cell.value; 318 | this.op = cell.op; 319 | this.ts = cell.ts; 320 | this.checksum = null; 321 | } 322 | 323 | toBytes() { 324 | // cell = tag_cell cell_name [cell_value] [cell_op] [cell_ts] cell_checksum 325 | var checksum = 0x00; 326 | // tag_cell 327 | var buffs = [ 328 | Buffer.from([Tag.CELL]) 329 | ]; 330 | 331 | // cell_name = tag_cell_name formated_value 332 | // tag_cell_name 333 | buffs.push(Buffer.from([Tag.CELL_NAME])); 334 | // formated_value for name 335 | // value_len value_data 336 | var len = Buffer.alloc(Header.LITTLE_ENDIAN_32_SIZE); 337 | var cellName = Buffer.from(this.name); 338 | len.writeInt32LE(cellName.length, 0); 339 | 340 | buffs.push(len); 341 | buffs.push(cellName); 342 | 343 | checksum = crc8Bytes(checksum, cellName); 344 | 345 | // cell_value = tag_cell_value formated_value 346 | if (typeof this.value !== 'undefined' || 347 | this.type === ColumnType.VT_INF_MAX || 348 | this.type === ColumnType.VT_INF_MIN) { 349 | // tag_cell_value 350 | buffs.push(Buffer.from([Tag.CELL_VALUE])); 351 | // formated_value 352 | var cellValue = new Value(this.type, this.value).toBytes(); 353 | // cell value length 354 | var totalLength = Buffer.alloc(Header.LITTLE_ENDIAN_32_SIZE); 355 | totalLength.writeInt32LE(cellValue.length); 356 | 357 | buffs.push(totalLength); 358 | buffs.push(cellValue); 359 | checksum = crc8Bytes(checksum, cellValue); 360 | } 361 | 362 | // cell_op = tag_cell_op cell_op_value 363 | if (this.op) { 364 | // delete_all_version = 0x01 (1byte) 365 | // delete_one_version = 0x03 (1byte) 366 | buffs.push(Buffer.from([ 367 | Tag.CELL_OP, // tag_cell_op 368 | this.op // cell_op_value 369 | ])); 370 | } 371 | 372 | // cell_ts = tag_cell_ts cell_ts_value 373 | if (this.ts) { 374 | // tag_cell_ts 375 | buffs.push(Buffer.from([Tag.CELL_TS])); 376 | // cell_ts_value 377 | var buff = Buffer.alloc(8); 378 | var val = Long.fromNumber(this.ts); 379 | buff.writeInt32LE(val.low, 0); 380 | buff.writeInt32LE(val.high, 4); 381 | buffs.push(buff); 382 | checksum = crc8Bytes(checksum, buff); 383 | } 384 | 385 | // 先算 ts 再算 op 的 checksum 386 | if (this.op) { 387 | checksum = crc8Int8(checksum, this.op); 388 | } 389 | 390 | // cell_checksum = tag_cell_checksum row_crc8 391 | buffs.push(Buffer.from([Tag.CELL_CHECKSUM, checksum])); 392 | 393 | this.checksum = checksum; 394 | 395 | return Buffer.concat(buffs); 396 | } 397 | } 398 | 399 | const TagHeader = Buffer.from([ 400 | Tag.HEADER, 0x00, 0x00, 0x00 401 | ]); 402 | 403 | class PlainBuffer { 404 | constructor(rows) { 405 | // 小端 406 | this.tag_header = TagHeader; 407 | this.rows = rows; 408 | } 409 | 410 | toBytes() { 411 | // plainbuffer = tag_header row1 [row2] [row3] 412 | var buffs = []; 413 | 414 | // tag_header 415 | buffs.push(this.tag_header); 416 | 417 | // rows 418 | for (var i = 0; i < this.rows.length; i++) { 419 | var row = this.rows[i]; 420 | buffs.push(row.toBytes()); 421 | } 422 | 423 | return Buffer.concat(buffs); 424 | } 425 | } 426 | 427 | var createRow = function(primaryKeys, columns, deleteMarker) { 428 | var pk; 429 | if (primaryKeys && Object.keys(primaryKeys).length > 0) { 430 | let cells = exports.$(primaryKeys).map(function(item) { 431 | return new Cell(item); 432 | }); 433 | pk = new PK(cells); 434 | } 435 | 436 | var attr; 437 | if (columns && Object.keys(columns).length > 0) { 438 | let cells = exports.$(columns).map(function(item) { 439 | return new Cell(item); 440 | }); 441 | attr = new Attr(cells); 442 | } 443 | 444 | return new Row(pk, attr, deleteMarker); 445 | }; 446 | 447 | exports.serializeRows = function(list) { 448 | if (!list.length) { 449 | return undefined; 450 | } 451 | 452 | return list.map(function(item) { 453 | return exports.serialize(item.primaryKeys, item.columns, item.deleteMarker); 454 | }); 455 | }; 456 | 457 | var readString = function(buff, index, len) { 458 | return buff.slice(index, index + len).toString(); 459 | }; 460 | 461 | var readName = function(buff, index) { 462 | var len = buff.readInt32LE(index); 463 | index += 4; 464 | var name = readString(buff, index, len); 465 | index += len; 466 | return { 467 | name: name, 468 | index: index 469 | }; 470 | }; 471 | 472 | var readValue = function(buff, index, cell) { 473 | // var len = buff.readInt32LE(index); 474 | index += 4; 475 | var type = buff.readInt8(index); 476 | cell.type = type; 477 | index++; 478 | if (type === ColumnType.VT_STRING) { 479 | let len = buff.readInt32LE(index); 480 | index += 4; 481 | cell.value = readString(buff, index, len); 482 | index += len; 483 | } else if (type === ColumnType.VT_INTEGER) { 484 | var low = buff.readInt32LE(index); 485 | var high = buff.readInt32LE(index + 4); 486 | cell.value = Long.fromBits(low, high).toNumber(); 487 | index += 8; 488 | } else if (type === ColumnType.VT_DOUBLE) { 489 | cell.value = buff.readDoubleLE(index); 490 | index += 8; 491 | } else if (type === ColumnType.VT_BLOB) { 492 | let len = buff.readInt32LE(index); 493 | index += 4; 494 | cell.value = buff.slice(index, index + len); 495 | index += len; 496 | } else if (type === ColumnType.VT_BOOLEAN) { 497 | let val = buff.readInt8(index); 498 | index++; 499 | cell.value = val === 0x01; 500 | } 501 | 502 | return { 503 | index: index 504 | }; 505 | }; 506 | 507 | var readCell = function(buff, index) { 508 | var cell = new Cell({}); 509 | var tag = buff.readInt8(index); 510 | if (tag === Tag.CELL_NAME) { 511 | index++; 512 | var result = readName(buff, index); 513 | cell.name = result.name; 514 | index = result.index; 515 | tag = buff.readInt8(index); 516 | 517 | if (tag === Tag.CELL_VALUE) { 518 | index++; 519 | result = readValue(buff, index, cell); 520 | index = result.index; 521 | tag = buff.readInt8(index); 522 | } 523 | 524 | if (tag === Tag.CELL_OP) { 525 | index++; 526 | // TODO 527 | tag = buff.readInt8(index); 528 | } 529 | 530 | if (tag === Tag.CELL_TS) { 531 | index++; 532 | var low = buff.readInt32LE(index); 533 | var high = buff.readInt32LE(index + 4); 534 | cell.ts = Long.fromBits(low, high).toNumber(); 535 | index += 8; 536 | tag = buff.readInt8(index); 537 | } 538 | 539 | if (tag === Tag.CELL_CHECKSUM) { 540 | index++; 541 | cell.checksum = buff.readUInt8(index); 542 | index++; 543 | } 544 | } 545 | 546 | return { 547 | cell: cell, 548 | index: index 549 | }; 550 | }; 551 | 552 | var readCells = function(buff, index) { 553 | var cells = []; 554 | while (buff.readInt8(index) === Tag.CELL) { 555 | index++; 556 | var result = readCell(buff, index); 557 | cells.push(result.cell); 558 | index = result.index; 559 | } 560 | 561 | return { 562 | cells: cells, 563 | index: index 564 | }; 565 | }; 566 | 567 | var readRow = function(buff, index) { 568 | var row = { 569 | primaryKeys: [], 570 | columns: [] 571 | }; 572 | // read row 573 | var tag = buff.readInt8(index); 574 | 575 | if (tag === Tag.PK) { 576 | index++; 577 | let result = readCells(buff, index); 578 | row.primaryKeys = result.cells; 579 | index = result.index; 580 | tag = buff.readInt8(index); 581 | } 582 | 583 | if (tag === Tag.ATTR) { 584 | index++; 585 | let result = readCells(buff, index); 586 | row.columns = result.cells; 587 | index = result.index; 588 | tag = buff.readInt8(index); 589 | } 590 | 591 | if (tag === Tag.DELETE_MARKER) { 592 | index++; 593 | tag = buff.readInt8(index); 594 | } 595 | 596 | if (tag === Tag.ROW_CHECKSUM) { 597 | index++; 598 | row.checksum = buff.readUInt8(index); 599 | index++; 600 | } 601 | 602 | return { 603 | row: row, 604 | index: index 605 | }; 606 | }; 607 | 608 | var flatten = function(row) { 609 | var obj = {}; 610 | if (row.primaryKeys) { 611 | for (let i = 0; i < row.primaryKeys.length; i++) { 612 | let cell = row.primaryKeys[i]; 613 | obj[cell.name] = cell.value; 614 | } 615 | } 616 | 617 | if (row.columns) { 618 | for (let i = 0; i < row.columns.length; i++) { 619 | let cell = row.columns[i]; 620 | obj[cell.name] = cell.value; 621 | } 622 | } 623 | 624 | return obj; 625 | }; 626 | 627 | var readRows = function(buff, index) { 628 | var rows = []; 629 | 630 | var result; 631 | 632 | while (index < buff.length) { 633 | result = readRow(buff, index); 634 | rows.push(flatten(result.row)); 635 | index = result.index; 636 | } 637 | 638 | return rows; 639 | }; 640 | 641 | /** 642 | * 反序列化行 643 | * @param row 644 | * @returns {Buffer|*} 645 | */ 646 | exports.deserialize = function(bytes) { 647 | if (!bytes) { 648 | return bytes; 649 | } 650 | 651 | var buff = bytes.buffer.slice(bytes.offset, bytes.limit); 652 | 653 | // nothing 654 | if (buff.length === 0) { 655 | return []; 656 | } 657 | 658 | var tag = buff.readInt32LE(0); //read tag 659 | if (tag !== Tag.HEADER) { 660 | throw Error('No Header'); 661 | } 662 | 663 | var index = 4; 664 | 665 | return readRows(buff, index); 666 | }; 667 | 668 | const PUT = Symbol('PUT'); 669 | const DELETE = Symbol('DELETE'); 670 | const DELETE_ALL = Symbol('DELETE_ALL'); 671 | 672 | /** 673 | * flatten Object to Columns 674 | * 675 | * @param {Object} obj 对象 676 | */ 677 | exports.$ = function(obj) { 678 | var keys = Object.keys(obj); 679 | var columns = []; 680 | for (var i = 0; i < keys.length; i++) { 681 | var key = keys[i]; 682 | var value = obj[key]; 683 | 684 | if (typeof value === 'string') { 685 | columns.push(exports.StringColumn(key, value)); 686 | } else if (typeof value === 'number') { 687 | if (Number.isInteger(value)) { 688 | columns.push(exports.IntegerColumn(key, value)); 689 | } else { 690 | columns.push(exports.DoubleColumn(key, value)); 691 | } 692 | } else if (Buffer.isBuffer(value)) { 693 | columns.push(exports.BlobColumn(key, value)); 694 | } else if (typeof value === 'undefined' || value === null) { 695 | // ignore null value 696 | // columns.push(exports.NullColumn(key, value)); 697 | } else if (value === true || value === false) { 698 | columns.push(exports.BooleanColumn(key, value)); 699 | } else if (value === exports.InfMin) { 700 | columns.push(exports.InfMinColumn(key)); 701 | } else if (value === exports.InfMax) { 702 | columns.push(exports.InfMaxColumn(key)); 703 | } else if (value && value.type === PUT) { 704 | columns.push(exports.PutColumn(key, value.value)); 705 | } else if (value && value.type === DELETE) { 706 | columns.push(exports.DeleteColumn(key, value.timestamp)); 707 | } else if (value && value.type === DELETE_ALL) { 708 | columns.push(exports.DeleteAllColumn(key)); 709 | } else { 710 | throw new TypeError(`unknown value type ${typeof value}`); 711 | } 712 | } 713 | 714 | return columns; 715 | }; 716 | 717 | exports.newValue = function (value) { 718 | var type; 719 | if (typeof value === 'string') { 720 | type = types.STRING; 721 | } else if (typeof value === 'number') { 722 | if (Number.isInteger(value)) { 723 | type = types.INTEGER; 724 | } else { 725 | type = types.DOUBLE; 726 | } 727 | } else if (Buffer.isBuffer(value)) { 728 | type = types.BLOB; 729 | } else if (typeof value === 'undefined' || value === null) { 730 | // ignore null value 731 | // columns.push(exports.NullColumn(key, value)); 732 | } else if (value === true || value === false) { 733 | type = types.BOOLEAN; 734 | } else { 735 | throw new TypeError('unsupported type'); 736 | } 737 | 738 | return new Value(getColumnValue(type), value); 739 | }; 740 | 741 | /** 742 | * 行序列化 743 | * @param row 744 | * @returns {number} 745 | */ 746 | exports.serialize = function(primaryKeys, columns, deleteMarker) { 747 | if (!primaryKeys && !columns) { 748 | throw new Error('Must have primary keys or attr keys'); 749 | } 750 | 751 | var row = createRow(primaryKeys, columns, deleteMarker); 752 | 753 | var buffer = new PlainBuffer([row]); 754 | 755 | return buffer.toBytes(); 756 | }; 757 | 758 | exports.PUT = PUT; 759 | exports.DELETE = DELETE; 760 | exports.DELETE_ALL = DELETE_ALL; 761 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alicloud/ots2", 3 | "version": "2.1.0", 4 | "description": "OTS(2015-12-31) client for Node.js(ES6)", 5 | "main": "./index", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "lint": "eslint --fix lib/ test/", 11 | "test": "mocha -R spec test/*.test.js", 12 | "test-cov": "nyc -r=html -r=text -r=lcov mocha -t 3000 -R spec test/*.test.js", 13 | "test-integration": "mocha -R spec test/*.integration.js", 14 | "ci": "npm run lint && npm run test-cov && codecov" 15 | }, 16 | "author": "Jackson Tian", 17 | "license": "MIT", 18 | "dependencies": { 19 | "debug": "^2.2.0", 20 | "httpx": "^2.0.0", 21 | "kitx": "^1.1.0", 22 | "protobufjs": "^5.0.0", 23 | "pegjs": "^0.10.0" 24 | }, 25 | "devDependencies": { 26 | "codecov": "^3.5.0", 27 | "eslint": "^6.2.2", 28 | "expect.js": "^0.3.1", 29 | "mocha": "^4.0.1", 30 | "nyc": "^11.2.1" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/ali-sdk/ots2.git" 35 | }, 36 | "keywords": [ 37 | "OTS", 38 | "OTS2", 39 | "Aliyun" 40 | ], 41 | "bugs": { 42 | "url": "https://github.com/ali-sdk/ots2/issues" 43 | }, 44 | "homepage": "https://github.com/ali-sdk/ots2#readme", 45 | "files": [ 46 | "lib", 47 | "spec", 48 | "index.js" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /spec/PlainBuffer.md: -------------------------------------------------------------------------------- 1 | 因为 Protocol Buffer 序列化和解析小对象的性能很差,所以 OTS 自定义了 PlainBuffer 数据格式用来表示行数据。 2 | 3 | ## 格式定义 4 | 5 | ``` 6 | plainbuffer = tag_header row1 [row2] [row3] 7 | row = ( pk [attr] | [pk] attr | pk attr ) [tag_delete_marker] row_checksum; 8 | pk = tag_pk cell_1 [cell_2] [cell_3] 9 | attr = tag_attr cell1 [cell_2] [cell_3] 10 | cell = tag_cell cell_name [cell_value] [cell_op] [cell_ts] cell_checksum 11 | cell_name = tag_cell_name formated_value 12 | cell_value = tag_cell_value formated_value 13 | cell_op = tag_cell_op cell_op_value 14 | cell_ts = tag_cell_ts cell_ts_value 15 | row_checksum = tag_row_checksum row_crc8 16 | cell_checksum = tag_cell_checksum row_crc8 17 | formated_value = value_type value_len value_data 18 | value_type = int8 19 | value_len = int32 20 | cell_op_value = delete_all_version | delete_one_version 21 | cell_ts_value = int64 22 | delete_all_version = 0x01 (1byte) 23 | delete_one_version = 0x03 (1byte) 24 | ``` 25 | 26 | ## Tag取值 27 | 28 | ``` 29 | tag_header = 0x75 (4byte) 30 | tag_pk = 0x01 (1byte) 31 | tag_attr = 0x02 (1byte) 32 | tag_cell = 0x03 (1byte) 33 | tag_cell_name = 0x04 (1byte) 34 | tag_cell_value = 0x05 (1byte) 35 | tag_cell_op = 0x06 (1byte) 36 | tag_cell_ts = 0x07 (1byte) 37 | tag_delete_marker = 0x08 (1byte) 38 | tag_row_checksum = 0x09 (1byte) 39 | tag_cell_checksum = 0x0A (1byte) 40 | ``` 41 | 42 | ## ValueType 取值 43 | 44 | formated_value 中 value_type 的取值如下: 45 | 46 | ``` 47 | VT_INTEGER = 0x0 48 | VT_DOUBLE = 0x1 49 | VT_BOOLEAN = 0x2 50 | VT_STRING = 0x3 51 | VT_NULL = 0x6 52 | VT_BLOB = 0x7 53 | VT_INF_MIN = 0x9 54 | VT_INF_MAX = 0xa 55 | VT_AUTO_INCREMENT = 0xb 56 | ``` 57 | 58 | ## 计算 Checksum 59 | 60 | 计算 checksum 的基本逻辑是: 61 | 62 | 针对每个 cell 的 name/value/type/timestamp 计算。 63 | 针对 row 里面的 delete 计算,其中有 delete mark 补单字节1;若没有,补单字节0。 64 | 因为对每个 cell 计算了 checksum,所以计算 row 的 checksum 的时候直接利用 cell 的checksum 来计算, 即 row 的 crc 只对 cell 的 checksum 做 crc, 不对数据做 crc。 65 | C++实现: 66 | 67 | ```cpp 68 | void GetChecksum(uint8_t* crc, const InplaceCell& cell) 69 | { 70 | Crc8(crc, cell.GetName()); 71 | Crc8(crc, cell.GetValue().GetInternalSlice()); 72 | Crc8(crc, cell.GetTimestamp()); 73 | Crc8(crc, cell.GetOpType()); 74 | } 75 | void GetChecksum(uint8_t* crc, const InplaceRow& row) 76 | { 77 | const std::deque& pk = row.GetPrimaryKey(); 78 | for (size_t i = 0; i < pk.size(); i++) { 79 | uint8_t* cellcrc; 80 | *cellcrc = 0; 81 | GetChecksum(cellcrc, pk[i]); 82 | Crc8(crc, *cellcrc); 83 | } 84 | for (int i = 0; i < row.GetCellCount(); i++) { 85 | uint8_t* cellcrc; 86 | *cellcrc = 0; 87 | GetChecksum(cellcrc, row.GetCell(i)); 88 | Crc8(crc, *cellcrc); 89 | } 90 | uint8_t del = 0; 91 | if (row.HasDeleteMarker()) { 92 | del = 1; 93 | } 94 | Crc8(crc, del); 95 | } 96 | ``` 97 | 98 | ## 举例 99 | 100 | 假设一行数据有两列主键,4 列数据。其中主键类型为 string 和 integer,3 列数据中分别是 string、integer 和 double,版本分别是 1001、1002 和 1003,还有一列是删除所有版本。 101 | 102 | 主键列: 103 | [pk1:string:iampk] 104 | [pk2:integer:100] 105 | 属性列: 106 | [column1:string:bad:1001] 107 | [column2:integer:128:1002] 108 | [column3:double:34.2:1003] 109 | [column4:del_all_versions] 110 | 编码: 111 | 112 | ``` 113 | [0x75] 114 | <主键列开始>[0x1] 115 | [0x3][0x4][3][pk1][0x5][3][5][iampk] 116 | [0x3][0x4][3][pk2][0x5][0][100] 117 | <属性列开始>[0x2] 118 | [0x3][0x4][7][column1][0x5][0x3][3][bad][0x7][1001] 119 | [0x3][0x4][7][column2][0x5][0x0][128][0x7][1002] 120 | [0x3][0x4][7][column3][0x5][0x1][34.2][0x7][1003] 121 | [0x3][0x4][7][column4][0x5][0x6][1] 122 | ``` 123 | -------------------------------------------------------------------------------- /spec/filter.dsl: -------------------------------------------------------------------------------- 1 | // E = E OR E 2 | OrCompositeColumnValueFilter 3 | = left:AndCompositeColumnValueFilter _ "OR" _ right:OrCompositeColumnValueFilter { 4 | return { 5 | type: 'CompositeColumnValueFilter', 6 | combinar: "OR", 7 | conditions: [left, right] 8 | }; 9 | } 10 | / AndCompositeColumnValueFilter 11 | 12 | // E = E AND E 13 | AndCompositeColumnValueFilter 14 | = left:NotCompositeColumnValueFilter _ "AND" _ right:AndCompositeColumnValueFilter { 15 | return { 16 | type: 'CompositeColumnValueFilter', 17 | combinar: "AND", 18 | conditions: [left, right] 19 | }; 20 | } 21 | / NotCompositeColumnValueFilter 22 | 23 | // E = NOT E 24 | NotCompositeColumnValueFilter 25 | = "NOT" _ condition:NotCompositeColumnValueFilter { 26 | return { 27 | type: 'CompositeColumnValueFilter', 28 | combinar: "NOT", 29 | conditions: [condition] 30 | }; 31 | } 32 | / SingleColumnValueFilter // E = NOT e 33 | 34 | // E = e 35 | SingleColumnValueFilter 36 | = column_name:ColumnName _ comparator:Comparetor _ column_value:Value _ passIfMissing:ifMissing _ latestVersionOnly:latestVersionOnly { 37 | return { 38 | type: 'SingleColumnValueFilter', 39 | condition: [column_name, comparator, column_value, passIfMissing, latestVersionOnly] 40 | }; 41 | } 42 | 43 | ColumnName 44 | = start:NameStart part:NamePart { 45 | return start + part; 46 | } 47 | 48 | Comparetor 49 | = "==" 50 | / "!=" 51 | / ">=" 52 | / ">" 53 | / "<=" 54 | / "<" 55 | 56 | Value 57 | = "@" value_name:ColumnName { 58 | return value_name; 59 | } 60 | 61 | BOOL 62 | = "true" { 63 | return true; 64 | } 65 | / "false" { 66 | return false; 67 | } 68 | 69 | ifMissing 70 | = BOOL 71 | 72 | latestVersionOnly 73 | = BOOL 74 | 75 | NameStart 76 | = [a-zA-Z_] 77 | 78 | NamePart 79 | = chars:[a-zA-Z0-9_]* { 80 | return chars.join(""); 81 | } 82 | 83 | _ "whitespace" 84 | = [ \t\n\r]* 85 | -------------------------------------------------------------------------------- /spec/table_store.proto: -------------------------------------------------------------------------------- 1 | package com.aliyun.tablestore.protocol; 2 | 3 | message Error { 4 | required string code = 1; 5 | optional string message = 2; 6 | } 7 | 8 | enum PrimaryKeyType { 9 | INTEGER = 1; 10 | STRING = 2; 11 | BINARY = 3; 12 | } 13 | 14 | enum PrimaryKeyOption { 15 | AUTO_INCREMENT = 1; 16 | } 17 | 18 | message PrimaryKeySchema { 19 | required string name = 1; 20 | required PrimaryKeyType type = 2; 21 | optional PrimaryKeyOption option = 3; 22 | } 23 | 24 | message PartitionRange { 25 | required bytes begin = 1; // encoded as SQLVariant 26 | required bytes end = 2; // encoded as SQLVariant 27 | } 28 | 29 | message TableOptions { 30 | optional int32 time_to_live = 1; // 可以动态更改 31 | optional int32 max_versions = 2; // 可以动态更改 32 | optional int64 deviation_cell_version_in_sec = 5; // 可以动态修改 33 | } 34 | 35 | message TableMeta { 36 | required string table_name = 1; 37 | repeated PrimaryKeySchema primary_key = 2; 38 | } 39 | 40 | enum RowExistenceExpectation { 41 | IGNORE = 0; 42 | EXPECT_EXIST = 1; 43 | EXPECT_NOT_EXIST = 2; 44 | } 45 | 46 | message Condition { 47 | required RowExistenceExpectation row_existence = 1; 48 | optional bytes column_condition = 2; 49 | } 50 | 51 | message CapacityUnit { 52 | optional int32 read = 1; 53 | optional int32 write = 2; 54 | } 55 | 56 | message ReservedThroughputDetails { 57 | required CapacityUnit capacity_unit = 1; // 表当前的预留吞吐量的值。 58 | required int64 last_increase_time = 2; // 最后一次上调预留吞吐量的时间。 59 | optional int64 last_decrease_time = 3; // 最后一次下调预留吞吐量的时间。 60 | } 61 | 62 | message ReservedThroughput { 63 | required CapacityUnit capacity_unit = 1; 64 | } 65 | 66 | message ConsumedCapacity { 67 | required CapacityUnit capacity_unit = 1; 68 | } 69 | 70 | /* ############################################# CreateTable ############################################# */ 71 | /** 72 | * table_meta用于存储表中不可更改的schema属性,可以更改的ReservedThroughput和TableOptions独立出来,作为UpdateTable的参数。 73 | * 加入GlobalIndex和LocalIndex之后,结构会变为: 74 | * message CreateTableRequest { 75 | * required TableMeta table_meta = 1; 76 | * required ReservedThroughput reserved_throughput = 2; 77 | * required TableOptions table_options = 3; 78 | * repeated LocalIndex local_indexes = 4; // LocalIndex不再单独包含ReservedThroughput和TableOptions,其与主表共享配置。 79 | * repeated GlobalIndex global_indexes = 5; // GlobalIndex内单独包含ReservedThroughput和TableOptions 80 | * } 81 | */ 82 | message CreateTableRequest { 83 | required TableMeta table_meta = 1; 84 | required ReservedThroughput reserved_throughput = 2; // 未放在TableOptions内,原因是UpdateTableResponse中会返回ReservedThroughputDetails,而TableOptions没有类似的返回结构。 85 | optional TableOptions table_options = 3; 86 | repeated PartitionRange partitions = 4; 87 | } 88 | 89 | message CreateTableResponse { 90 | } 91 | 92 | /* ######################################################################################################### */ 93 | 94 | 95 | /* ############################################# UpdateTable ############################################# */ 96 | message UpdateTableRequest { 97 | required string table_name = 1; 98 | optional ReservedThroughput reserved_throughput = 2; 99 | optional TableOptions table_options = 3; 100 | } 101 | 102 | message UpdateTableResponse { 103 | required ReservedThroughputDetails reserved_throughput_details = 1; 104 | required TableOptions table_options = 2; 105 | } 106 | /* ######################################################################################################### */ 107 | 108 | /* ############################################# DescribeTable ############################################# */ 109 | message DescribeTableRequest { 110 | required string table_name = 1; 111 | } 112 | 113 | message DescribeTableResponse { 114 | required TableMeta table_meta = 1; 115 | required ReservedThroughputDetails reserved_throughput_details = 2; 116 | required TableOptions table_options = 3; 117 | repeated bytes shard_splits = 6; 118 | } 119 | /* ########################################################################################################### */ 120 | 121 | /* ############################################# ListTable ############################################# */ 122 | message ListTableRequest { 123 | } 124 | 125 | /** 126 | * 当前只返回一个简单的名称列表,需要讨论是否有业务场景需要获取除了表名之外的其他信息。 127 | * 其他信息可以包含预留吞吐量以及表的状态,这个信息只能是一个粗略的信息,表的详细信息还是需要通过DescribeTable来获取。 128 | */ 129 | message ListTableResponse { 130 | repeated string table_names = 1; 131 | } 132 | /* ####################################################################################################### */ 133 | 134 | /* ############################################# DeleteTable ############################################# */ 135 | message DeleteTableRequest { 136 | required string table_name = 1; 137 | } 138 | 139 | message DeleteTableResponse { 140 | } 141 | /* ######################################################################################################### */ 142 | 143 | /* ############################################# LoadTable ############################################# */ 144 | message LoadTableRequest { 145 | required string table_name = 1; 146 | } 147 | 148 | message LoadTableResponse { 149 | } 150 | /* ######################################################################################################### */ 151 | 152 | /* ############################################# UnloadTable ############################################# */ 153 | message UnloadTableRequest { 154 | required string table_name = 1; 155 | } 156 | 157 | message UnloadTableResponse { 158 | 159 | } 160 | /* ########################################################################################################## */ 161 | 162 | /** 163 | * 时间戳的取值最小值为0,最大值为INT64.MAX 164 | * 1. 若要查询一个范围,则指定start_time和end_time 165 | * 2. 若要查询一个特定时间戳,则指定specific_time 166 | */ 167 | message TimeRange { 168 | optional int64 start_time = 1; 169 | optional int64 end_time = 2; 170 | optional int64 specific_time = 3; 171 | } 172 | 173 | /* ############################################# GetRow ############################################# */ 174 | 175 | enum ReturnType { 176 | RT_NONE = 0; 177 | RT_PK = 1; 178 | } 179 | 180 | message ReturnContent { 181 | optional ReturnType return_type = 1; 182 | } 183 | 184 | /** 185 | * 1. 支持用户指定版本时间戳范围或者特定的版本时间来读取指定版本的列 186 | * 2. 目前暂不支持行内的断点 187 | */ 188 | message GetRowRequest { 189 | required string table_name = 1; 190 | required bytes primary_key = 2; // encoded as InplaceRowChangeSet, but only has primary key 191 | repeated string columns_to_get = 3; // 不指定则读出所有的列 192 | optional TimeRange time_range = 4; 193 | optional int32 max_versions = 5; 194 | optional bytes filter = 7; 195 | optional string start_column = 8; 196 | optional string end_column = 9; 197 | optional bytes token = 10; 198 | } 199 | 200 | message GetRowResponse { 201 | required ConsumedCapacity consumed = 1; 202 | required bytes row = 2; // encoded as InplaceRowChangeSet 203 | optional bytes next_token = 3; 204 | } 205 | /* #################################################################################################### */ 206 | 207 | /* ############################################# UpdateRow ############################################# */ 208 | message UpdateRowRequest { 209 | required string table_name = 1; 210 | required bytes row_change = 2; 211 | required Condition condition = 3; 212 | optional ReturnContent return_content = 4; 213 | } 214 | 215 | message UpdateRowResponse { 216 | required ConsumedCapacity consumed = 1; 217 | optional bytes row = 2; 218 | } 219 | /* ####################################################################################################### */ 220 | 221 | /* ############################################# PutRow ############################################# */ 222 | 223 | 224 | /** 225 | * 这里允许用户为每列单独设置timestamp,而不是强制整行统一一个timestamp。 226 | * 原因是列都是用统一的结构,该结构本身是带timestamp的,其次强制统一timestamp增强了规范性但是丧失了灵活性,且该规范性没有明显的好处,反而带来了结构的复杂。 227 | */ 228 | message PutRowRequest { 229 | required string table_name = 1; 230 | required bytes row = 2; // encoded as InplaceRowChangeSet 231 | required Condition condition = 3; 232 | optional ReturnContent return_content = 4; 233 | } 234 | 235 | message PutRowResponse { 236 | required ConsumedCapacity consumed = 1; 237 | optional bytes row = 2; // encoded as InplaceRowChangeSet 238 | } 239 | /* #################################################################################################### */ 240 | 241 | /* ############################################# DeleteRow ############################################# */ 242 | /** 243 | * OTS只支持删除该行的所有列所有版本,不支持: 244 | * 1. 删除所有列的所有小于等于某个版本的所有版本 245 | */ 246 | message DeleteRowRequest { 247 | required string table_name = 1; 248 | required bytes primary_key = 2; // encoded as InplaceRowChangeSet, but only has primary key 249 | required Condition condition = 3; 250 | optional ReturnContent return_content = 4; 251 | } 252 | 253 | message DeleteRowResponse { 254 | required ConsumedCapacity consumed = 1; 255 | optional bytes row = 2; 256 | } 257 | /* ####################################################################################################### */ 258 | 259 | /* ############################################# BatchGetRow ############################################# */ 260 | message TableInBatchGetRowRequest { 261 | required string table_name = 1; 262 | repeated bytes primary_key = 2; // encoded as InplaceRowChangeSet, but only has primary key 263 | repeated bytes token = 3; 264 | repeated string columns_to_get = 4; // 不指定则读出所有的列 265 | optional TimeRange time_range = 5; 266 | optional int32 max_versions = 6; 267 | optional bytes filter = 8; 268 | optional string start_column = 9; 269 | optional string end_column = 10; 270 | } 271 | 272 | message BatchGetRowRequest { 273 | repeated TableInBatchGetRowRequest tables = 1; 274 | } 275 | 276 | message RowInBatchGetRowResponse { 277 | required bool is_ok = 1; 278 | optional Error error = 2; 279 | optional ConsumedCapacity consumed = 3; 280 | optional bytes row = 4; // encoded as InplaceRowChangeSet 281 | optional bytes next_token = 5; 282 | } 283 | 284 | message TableInBatchGetRowResponse { 285 | required string table_name = 1; 286 | repeated RowInBatchGetRowResponse rows = 2; 287 | } 288 | 289 | message BatchGetRowResponse { 290 | repeated TableInBatchGetRowResponse tables = 1; 291 | } 292 | /* ######################################################################################################### */ 293 | 294 | /* ############################################# BatchWriteRow ############################################# */ 295 | 296 | enum OperationType { 297 | PUT = 1; 298 | UPDATE = 2; 299 | DELETE = 3; 300 | } 301 | 302 | message RowInBatchWriteRowRequest { 303 | required OperationType type = 1; 304 | required bytes row_change = 2; // encoded as InplaceRowChangeSet 305 | required Condition condition = 3; 306 | optional ReturnContent return_content = 4; 307 | } 308 | 309 | message TableInBatchWriteRowRequest { 310 | required string table_name = 1; 311 | repeated RowInBatchWriteRowRequest rows = 2; 312 | } 313 | 314 | message BatchWriteRowRequest { 315 | repeated TableInBatchWriteRowRequest tables = 1; 316 | } 317 | 318 | message RowInBatchWriteRowResponse { 319 | required bool is_ok = 1; 320 | optional Error error = 2; 321 | optional ConsumedCapacity consumed = 3; 322 | optional bytes row = 4; // encoded as InplaceRowChangeSet 323 | } 324 | 325 | message TableInBatchWriteRowResponse { 326 | required string table_name = 1; 327 | repeated RowInBatchWriteRowResponse rows = 2; 328 | } 329 | 330 | message BatchWriteRowResponse { 331 | repeated TableInBatchWriteRowResponse tables = 1; 332 | } 333 | /* ########################################################################################################### */ 334 | 335 | /* ############################################# GetRange ############################################# */ 336 | enum Direction { 337 | FORWARD = 0; 338 | BACKWARD = 1; 339 | } 340 | 341 | /** 342 | * HBase支持以下参数: 343 | * 1. TimeRange或指定time 344 | * 2. Filter(根据列值或列名来过滤) 345 | * 我们只支持给同版本的选择条件。 346 | */ 347 | message GetRangeRequest { 348 | required string table_name = 1; 349 | required Direction direction = 2; 350 | repeated string columns_to_get = 3; // 不指定则读出所有的列 351 | optional TimeRange time_range = 4; 352 | optional int32 max_versions = 5; 353 | optional int32 limit = 6; 354 | required bytes inclusive_start_primary_key = 7; // encoded as InplaceRowChangeSet, but only has primary key 355 | required bytes exclusive_end_primary_key = 8; // encoded as InplaceRowChangeSet, but only has primary key 356 | optional bytes filter = 10; 357 | optional string start_column = 11; 358 | optional string end_column = 12; 359 | optional bytes token = 13; 360 | } 361 | 362 | message GetRangeResponse { 363 | required ConsumedCapacity consumed = 1; 364 | required bytes rows = 2; // encoded as InplaceRowChangeSet 365 | optional bytes next_start_primary_key = 3; // 若为空,则代表数据全部读取完毕. encoded as InplaceRowChangeSet, but only has primary key 366 | optional bytes next_token = 4; 367 | } 368 | /* ###################################################################################################### */ 369 | 370 | -------------------------------------------------------------------------------- /spec/table_store_filter.proto: -------------------------------------------------------------------------------- 1 | package com.aliyun.tablestore.protocol; 2 | 3 | enum FilterType { 4 | FT_SINGLE_COLUMN_VALUE = 1; 5 | FT_COMPOSITE_COLUMN_VALUE = 2; 6 | FT_COLUMN_PAGINATION = 3; 7 | } 8 | 9 | enum ComparatorType { 10 | CT_EQUAL = 1; 11 | CT_NOT_EQUAL = 2; 12 | CT_GREATER_THAN = 3; 13 | CT_GREATER_EQUAL = 4; 14 | CT_LESS_THAN = 5; 15 | CT_LESS_EQUAL = 6; 16 | } 17 | 18 | message SingleColumnValueFilter { 19 | required ComparatorType comparator = 1; 20 | required string column_name = 2; 21 | required bytes column_value = 3; // Serialized SQLVariant 22 | required bool filter_if_missing = 4; 23 | required bool latest_version_only = 5; 24 | } 25 | 26 | enum LogicalOperator { 27 | LO_NOT = 1; 28 | LO_AND = 2; 29 | LO_OR = 3; 30 | } 31 | 32 | message CompositeColumnValueFilter { 33 | required LogicalOperator combinator = 1; 34 | repeated Filter sub_filters = 2; 35 | } 36 | 37 | message ColumnPaginationFilter { 38 | required int32 offset = 1; 39 | required int32 limit = 2; 40 | } 41 | 42 | message Filter { 43 | required FilterType type = 1; 44 | required bytes filter = 2; // Serialized string of filter of the type 45 | } 46 | -------------------------------------------------------------------------------- /test/batch.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | const kitx = require('kitx'); 5 | 6 | const client = require('./common'); 7 | const OTS = require('../'); 8 | 9 | const { serialize } = require('../lib/plainbuffer'); 10 | 11 | describe('batch', function () { 12 | before(async function () { 13 | this.timeout(12000); 14 | var keys = [{ 'name': 'uid', 'type': 'STRING' }]; 15 | var capacityUnit = {read: 1, write: 1}; 16 | var options = { 17 | table_options: { 18 | time_to_live: -1,// 数据的过期时间, 单位秒, -1代表永不过期. 假如设置过期时间为一年, 即为 365 * 24 * 3600. 19 | max_versions: 1 20 | } 21 | }; 22 | var response = await client.createTable('metrics', keys, capacityUnit, options); 23 | expect(response).to.be.ok(); 24 | await kitx.sleep(5000); 25 | }); 26 | 27 | after(async function () { 28 | var response = await client.deleteTable('metrics'); 29 | expect(response).to.be.ok(); 30 | }); 31 | 32 | it('batchWriteRow should ok', async function () { 33 | var tables = [ 34 | { 35 | table_name: 'metrics', 36 | rows: [ 37 | { 38 | type: OTS.OperationType.PUT, 39 | condition: { 40 | row_existence: OTS.RowExistenceExpectation.IGNORE 41 | }, 42 | row_change: serialize({ 43 | uid: 'test_uid' 44 | }, { 45 | test: 'test_value' 46 | }), 47 | return_content: null 48 | } 49 | ] 50 | } 51 | ]; 52 | var response = await client.batchWriteRow(tables); 53 | expect(response).to.be.ok(); 54 | expect(response.tables.length).to.be.above(0); 55 | var table = response.tables[0]; 56 | expect(table.table_name).to.be('metrics'); 57 | expect(table.rows.length).to.be.above(0); 58 | var row = table.rows[0]; 59 | expect(row.is_ok).to.be(true); 60 | expect(row.consumed.capacity_unit.read).to.be(0); 61 | expect(row.consumed.capacity_unit.write).to.be(1); 62 | }); 63 | 64 | it('batchWriteRow delete_rows should ok', async function () { 65 | var tables = [ 66 | { 67 | table_name: 'metrics', 68 | rows: [ 69 | { 70 | type: OTS.OperationType.DELETE, 71 | row_change: serialize({ 72 | uid: 'test-uid' 73 | }, null, true), 74 | condition: { 75 | row_existence: OTS.RowExistenceExpectation.IGNORE 76 | } 77 | } 78 | ] 79 | } 80 | ]; 81 | var response = await client.batchWriteRow(tables); 82 | expect(response).to.be.ok(); 83 | expect(response.tables.length).to.be.above(0); 84 | var table = response.tables[0]; 85 | expect(table.table_name).to.be('metrics'); 86 | expect(table.rows.length).to.be.above(0); 87 | var row = table.rows[0]; 88 | expect(row.is_ok).to.be(true); 89 | expect(row.consumed.capacity_unit.read).to.be(0); 90 | expect(row.consumed.capacity_unit.write).to.be(1); 91 | }); 92 | 93 | it('batchGetRow should ok', async function () { 94 | var tables = [ 95 | { 96 | table_name: 'metrics', 97 | primary_key: [ 98 | serialize({uid: 'test_uid'}) 99 | ], 100 | columns_to_get: ['test'], 101 | max_versions: 1 102 | } 103 | ]; 104 | var response = await client.batchGetRow(tables); 105 | expect(response).to.be.ok(); 106 | expect(response.tables.length).to.be.above(0); 107 | var table = response.tables[0]; 108 | expect(table.table_name).to.be('metrics'); 109 | expect(table.rows.length).to.be.above(0); 110 | var row = table.rows[0]; 111 | expect(row.is_ok).to.be(true); 112 | expect(row.row).to.eql({ 113 | uid: 'test_uid', 114 | test: 'test_value' 115 | }); 116 | expect(row.consumed.capacity_unit.read).to.be(1); 117 | expect(row.consumed.capacity_unit.write).to.be(0); 118 | }); 119 | 120 | it('batchGetRow with filter should ok', async function () { 121 | var tables = [ 122 | { 123 | table_name: 'metrics', 124 | primary_key: [ 125 | serialize({uid: 'test_uid'}) 126 | ], 127 | columns_to_get: ['test'], 128 | max_versions: 1, 129 | filter: OTS.makeFilter('test != @test false true', { 130 | test: 'test_value' 131 | }) 132 | } 133 | ]; 134 | var response = await client.batchGetRow(tables); 135 | expect(response).to.be.ok(); 136 | expect(response.tables.length).to.be.above(0); 137 | var table = response.tables[0]; 138 | expect(table.table_name).to.be('metrics'); 139 | expect(table.rows.length).to.be.above(0); 140 | var row = table.rows[0]; 141 | expect(row.is_ok).to.be(true); 142 | expect(row.row).to.eql(null); 143 | expect(row.consumed.capacity_unit.read).to.be(1); 144 | expect(row.consumed.capacity_unit.write).to.be(0); 145 | }); 146 | 147 | it('getRange should ok', async function () { 148 | var start = { 149 | uid: OTS.InfMin 150 | }; 151 | var end = { 152 | uid: OTS.InfMax 153 | }; 154 | 155 | var request = { 156 | table_name: 'metrics', 157 | direction: OTS.Direction.FORWARD, 158 | columns_to_get: ['test'], 159 | max_versions: 1, 160 | limit: 4, 161 | inclusive_start_primary_key: start, 162 | exclusive_end_primary_key: end 163 | }; 164 | var response = await client.getRange(request); 165 | expect(response).to.be.ok(); 166 | expect(response.consumed.capacity_unit.read).to.be(1); 167 | expect(response.consumed.capacity_unit.write).to.be(0); 168 | expect(response.rows.length).to.be.above(0); 169 | var row = response.rows[0]; 170 | expect(row).to.eql({ uid: 'test_uid', test: 'test_value' }); 171 | }); 172 | 173 | it('getRange with filter should ok', async function () { 174 | var start = { 175 | uid: OTS.InfMin 176 | }; 177 | var end = { 178 | uid: OTS.InfMax 179 | }; 180 | 181 | var request = { 182 | table_name: 'metrics', 183 | direction: OTS.Direction.FORWARD, 184 | columns_to_get: ['test'], 185 | limit: 4, 186 | max_versions: 1, 187 | inclusive_start_primary_key: start, 188 | exclusive_end_primary_key: end, 189 | filter: OTS.makeFilter('test != @test false true', { 190 | test: 'test_value' 191 | }) 192 | }; 193 | var response = await client.getRange(request); 194 | expect(response).to.be.ok(); 195 | expect(response.consumed.capacity_unit.read).to.be(1); 196 | expect(response.consumed.capacity_unit.write).to.be(0); 197 | expect(response.rows.length).to.be(0); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/client.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | 5 | var ots = require('../'); 6 | 7 | describe('client', function () { 8 | it('should ok', async function () { 9 | var client = ots.createClient({ 10 | accessKeyID: 'QpavStUJkTjCGixrx', 11 | accessKeySecret: 'xxx', 12 | instance: 'testalinode', 13 | region: 'cn-hangzhou' 14 | }); 15 | 16 | try { 17 | await client.listTable(); 18 | } catch (e) { 19 | expect(e.name).to.be('OTSAuthFailedError'); 20 | expect(e.message).to.be('The AccessKeyID does not exist.'); 21 | return; 22 | } 23 | expect(false).to.be.ok(); 24 | }); 25 | 26 | it('should ok with inexist id', async function () { 27 | var client = ots.createClient({ 28 | accessKeyID: 'xxxxx', 29 | accessKeySecret: 'xxx', 30 | instance: 'testalinode', 31 | region: 'cn-hangzhou' 32 | }); 33 | 34 | try { 35 | await client.listTable(); 36 | } catch (e) { 37 | expect(e.name).to.be('OTSAuthFailedError'); 38 | expect(e.message).to.be('The AccessKeyID does not exist.'); 39 | return; 40 | } 41 | expect(false).to.be.ok(); 42 | }); 43 | 44 | // it('createInteger should ok', function () { 45 | // var value = ots.createInteger(1); 46 | // expect(value).to.eql({ type: 2, v_int: 1 }); 47 | // }); 48 | 49 | // it('createString should ok', function () { 50 | // var value = ots.createString('hi'); 51 | // expect(value).to.eql({ type: 3, v_string: 'hi' }); 52 | // }); 53 | 54 | // it('createBoolean should ok', function () { 55 | // var value = ots.createBoolean(true); 56 | // expect(value).to.eql({ type: 4, v_bool: true }); 57 | // }); 58 | 59 | // it('createDouble should ok', function () { 60 | // var value = ots.createDouble(1.2); 61 | // expect(value).to.eql({ type: 5, v_double: 1.2 }); 62 | // }); 63 | 64 | // it('createBinary should ok', function () { 65 | // var value = ots.createBinary(1); 66 | // expect(value).to.eql({ type: 6, v_binary: 1 }); 67 | // }); 68 | 69 | // it('$ should ok', function () { 70 | // var columns = ots.$({ 71 | // uid: 'test_uid', 72 | // boolean: true, 73 | // integer: 1, 74 | // double: 1.1, 75 | // binary: new Buffer([1]), 76 | // infmin: ots.InfMin, 77 | // infmax: ots.InfMax 78 | // }); 79 | 80 | // expect(columns).to.eql([ 81 | // {name: 'uid', value: { type: 3, v_string: 'test_uid' }}, 82 | // {name: 'boolean', value: { type: 4, v_bool: true }}, 83 | // {name: 'integer', value: { type: 2, v_int: 1 }}, 84 | // {name: 'double', value: { type: 5, v_double: 1.1 }}, 85 | // {name: 'binary', value: { type: 6, v_binary: new Buffer([1]) }}, 86 | // {name: 'infmin', value: { type: 0 }}, 87 | // {name: 'infmax', value: { type: 1 }} 88 | // ]); 89 | // }); 90 | 91 | // it('$put should ok', function () { 92 | // var columns = ots.$({ 93 | // put_string: ots.$put('string') 94 | // }); 95 | 96 | // expect(columns).to.eql([ 97 | // { 98 | // name: 'put_string', 99 | // type: 1, 100 | // value: { 101 | // type: 3, 102 | // v_string: 'string' 103 | // } 104 | // } 105 | // ]); 106 | // }); 107 | 108 | // it('$delete should ok', function () { 109 | // var columns = ots.$({ 110 | // delete_string: ots.$delete() 111 | // }); 112 | 113 | // expect(columns).to.eql([ 114 | // { 115 | // name: 'delete_string', 116 | // type: 2 117 | // } 118 | // ]); 119 | // }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ots = require('../'); 4 | 5 | const { 6 | ACCESS_KEY_ID, 7 | ACCESS_KEY_SECRET, 8 | OTS_INSTANCE, 9 | OTS_ENDPOINT 10 | } = process.env; 11 | 12 | if (!ACCESS_KEY_ID || !ACCESS_KEY_SECRET || !OTS_INSTANCE || !OTS_ENDPOINT) { 13 | throw new Error(`Must set env variables ACCESS_KEY_ID, ACCESS_KEY_SECRET, OTS_INSTANCE, OTS_ENDPOINT for test`); 14 | } 15 | 16 | var options = { 17 | accessKeyID: ACCESS_KEY_ID, 18 | accessKeySecret: ACCESS_KEY_SECRET, 19 | instance: OTS_INSTANCE, 20 | endpoint: OTS_ENDPOINT 21 | }; 22 | 23 | module.exports = ots.createClient(options); 24 | -------------------------------------------------------------------------------- /test/encode.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | 5 | const ots2 = require('../lib/ots2'); 6 | 7 | function getBuffer(bytebuff) { 8 | var { 9 | offset, limit, buffer 10 | } = bytebuff; 11 | return buffer.slice(offset, offset + limit); 12 | } 13 | 14 | describe('encode', function () { 15 | it('CapacityUnit', function () { 16 | var capacityUnit = {read: 5, write: 5}; 17 | var bytebuff = ots2.CapacityUnit.encode(capacityUnit); 18 | expect(getBuffer(bytebuff)).to.be.eql([ 19 | 0x08, 0x05, 0x10, 0x05 20 | ]); 21 | }); 22 | 23 | it('should encode ok', function () { 24 | var primaryKeys = [{ 'name': 'uid', 'type': 'STRING' }]; 25 | var capacityUnit = {read: 5, write: 5}; 26 | var name = 'metrics'; 27 | 28 | var request = { 29 | table_meta: { 30 | table_name: name, 31 | primary_key: primaryKeys 32 | }, 33 | reserved_throughput: { 34 | capacity_unit: capacityUnit 35 | } 36 | }; 37 | 38 | var bytebuff = ots2.CreateTableRequest.encode(request); 39 | expect(getBuffer(bytebuff)).to.have.length(28); 40 | expect(Buffer.from(JSON.stringify(request))).to.have.length(147); 41 | }); 42 | 43 | it('enum', function () { 44 | var primaryKeySchema = { 'name': 'uid', 'type': 'STRING' }; 45 | var bytebuff = ots2.PrimaryKeySchema.encode(primaryKeySchema); 46 | var schema = ots2.PrimaryKeySchema.decode(bytebuff); 47 | expect(schema).to.have.property('name', 'uid'); 48 | expect(schema).to.have.property('type', ots2.PrimaryKeySchema.STRING); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/filter.demo.js: -------------------------------------------------------------------------------- 1 | // BNF 2 | 3 | // E = E OR E 4 | // E = E AND E 5 | // E = NOT E 6 | // E = e 7 | // e = name operator value ifMiss 8 | 9 | // name = [A-Za-z_] + [A-Za-z_\d]* 10 | // operator = == | != | > | >= | < | <= 11 | // value = @ + [A-Za-z_] + [A-Za-z_\d]* 12 | // ifMiss = true | false 13 | 14 | 'use strict'; 15 | 16 | var parse = require('../lib/filter_parser').parse; 17 | 18 | var tree = parse('name == @name true true'); 19 | console.log(JSON.stringify(tree, null, 2)); 20 | 21 | tree = parse('NOT name == @name true true'); 22 | console.log(JSON.stringify(tree, null, 2)); 23 | 24 | tree = parse('NOT NOT name == @name true true'); 25 | console.log(JSON.stringify(tree, null, 2)); 26 | 27 | try { 28 | tree = parse('name > @name true true AND age <= @age false true'); 29 | console.log(JSON.stringify(tree, null, 2)); 30 | } catch (ex) { 31 | console.log(ex); 32 | } 33 | 34 | tree = parse('NOT name > @name true true AND age <= @age false true'); 35 | console.log(JSON.stringify(tree, null, 2)); 36 | 37 | tree = parse('name > @name true true AND age <= @age false true AND gender == @gender true true'); 38 | console.log(JSON.stringify(tree, null, 2)); 39 | 40 | tree = parse('name > @name true true OR age <= @age false true AND gender == @gender true true'); 41 | console.log(JSON.stringify(tree, null, 2)); 42 | -------------------------------------------------------------------------------- /test/filter.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const expect = require('expect.js'); 4 | // const kitx = require('kitx'); 5 | 6 | // const client = require('./common'); 7 | // const OTS = require('../'); 8 | 9 | describe('filter', function () { 10 | 11 | xit('makeFilter should ok', async function () { 12 | // var filter = OTS.makeFilter('type == @type1 false true OR type == @type2 false true', { 13 | // type1: 1, 14 | // type2: 2 15 | // }); 16 | 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /test/plainbuffer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | const ots = require('../lib/client'); 5 | const plainbuffer = require('../lib/plainbuffer'); 6 | 7 | describe('plainbuffer', function() { 8 | it('serialize(primaryKeys) should ok', function() { 9 | var primaryKeys = { 10 | 'uid': 'test_uid', 11 | 'id': 1 12 | }; 13 | 14 | var bytes = plainbuffer.serialize(primaryKeys); 15 | expect(bytes).to.eql(Buffer.from([ 16 | 0x75, 0x00, 0x00, 0x00, // tag_header 17 | // row 18 | 0x01, // tag_pk 19 | 0x03, // tag_cell 20 | 0x04, // tag_cell_name 21 | // value_type value_len value_data 22 | // value_len 23 | 0x03, 0x00, 0x00, 0x00, 24 | // value_data: uid 25 | 0x75, 0x69, 0x64, 26 | 0x05, // tag_cell_value 27 | 0x0d, 0x00, 0x00, 0x00, 28 | 0x03, 29 | // value_type value_len value_data 30 | 0x08, 0x00, 0x00, 0x00, 31 | 0x74, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x69, 0x64, 32 | // cell_checksum = tag_cell_checksum row_crc8 33 | 0x0a, 178, 34 | 0x03, // tag_cell 35 | 0x04, // tag_cell_name 36 | // value_type value_len value_data 37 | // value_len 38 | 0x02, 0x00, 0x00, 0x00, 39 | // value_data: id 40 | 0x69, 0x64, 41 | 0x05, // tag_cell_value 42 | // value_type value_len value_data 43 | 0x09, 0x00, 0x00, 0x00, 44 | 0x00, // VT_INTEGER 45 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | // cell_checksum = tag_cell_checksum row_crc8 47 | 0x0a, 10, 48 | // row_checksum = tag_row_checksum row_crc8 49 | 0x09, 190 50 | ])); 51 | }); 52 | 53 | it('serialize(primaryKeys, columns) should ok', function() { 54 | var primaryKeys = { 55 | 'uid': 'test_uid', 56 | 'id': 1 57 | }; 58 | 59 | var columns = { 60 | 'value': 1 61 | }; 62 | 63 | var bytes = plainbuffer.serialize(primaryKeys, columns); 64 | expect(bytes).to.eql(Buffer.from([ 65 | 0x75, 0x00, 0x00, 0x00, // tag_header 66 | // row 67 | 0x01, // tag_pk 68 | 0x03, // tag_cell 69 | 0x04, // tag_cell_name 70 | // value_type value_len value_data 71 | // value_len 72 | 0x03, 0x00, 0x00, 0x00, 73 | // value_data: uid 74 | 0x75, 0x69, 0x64, 75 | 0x05, // tag_cell_value 76 | 0x0d, 0x00, 0x00, 0x00, 77 | // value_type value_len value_data 78 | 0x03, // VT_STRING 79 | 0x08, 0x00, 0x00, 0x00, 80 | 0x74, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x69, 0x64, 81 | // cell_checksum = tag_cell_checksum row_crc8 82 | 0x0a, 178, 83 | 0x03, // tag_cell 84 | 0x04, // tag_cell_name 85 | // value_type value_len value_data 86 | // value_len 87 | 0x02, 0x00, 0x00, 0x00, 88 | // value_data: id 89 | 0x69, 0x64, 90 | 0x05, // tag_cell_value 91 | // value_type value_len value_data 92 | 0x09, 0x00, 0x00, 0x00, 93 | 0x00, // VT_INTEGER 94 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | // cell_checksum = tag_cell_checksum row_crc8 96 | 0x0a, 0x0a, 97 | // attr 98 | 0x02, // tag_attr 99 | 0x03, // tag_cell 100 | 0x04, // tag_cell_name 101 | // value_type value_len value_data 102 | // value_len 103 | 0x05, 0x00, 0x00, 0x00, 104 | // value_data: value 105 | 0x76, 0x61, 0x6c, 0x75, 0x65, 106 | 0x05, // tag_cell_value 107 | // value_type value_len value_data 108 | 0x09, 0x00, 0x00, 0x00, 109 | 0x00, // VT_INTEGER 110 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x0a, 144, 112 | // row_checksum = tag_row_checksum row_crc8 113 | 0x09, 210 114 | ])); 115 | }); 116 | 117 | it('serialize(primaryKeys, {}) should ok', function() { 118 | var primaryKeys = { 119 | 'appid': 1, 120 | 'agentid': 'alibaba:JacksonTian-Air.local', 121 | 'timestamp': '2016-06-29T08:48:26.622Z' 122 | }; 123 | 124 | var bytes = plainbuffer.serialize(primaryKeys, {}); 125 | expect(bytes).to.eql(Buffer.from([ 126 | 0x75, 0x00, 0x00, 0x00, // tag_header 127 | // row 128 | 0x01, // tag_pk 129 | 0x03, // tag_cell 130 | 0x04, // tag_cell_name 131 | // value_type value_len value_data 132 | // value_len 133 | 0x05, 0x00, 0x00, 0x00, 134 | // value_data: appid 135 | 0x61, 0x70, 0x70, 0x69, 0x64, 136 | 0x05, // tag_cell_value 137 | 0x09, 0x00, 0x00, 0x00, 138 | // value_type value_len value_data 139 | 0x00, // VT_INTEGER 140 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1 141 | // cell_checksum = tag_cell_checksum row_crc8 142 | 0x0a, 241, 143 | 0x03, // tag_cell 144 | 0x04, // tag_cell_name 145 | // value_type value_len value_data 146 | // value_len 147 | 0x07, 0x00, 0x00, 0x00, 148 | // value_data: agentid 149 | 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 150 | 0x05, // tag_cell_value 151 | 0x22, 0x00, 0x00, 0x00, 152 | // value_type value_len value_data 153 | 0x03, // VT_STRING 154 | 0x1d, 0x00, 0x00, 0x00, // string length 155 | 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x3a, 156 | 0x4a, 0x61, 0x63, 0x6b, 0x73, 0x6f, 0x6e, 0x54, 157 | 0x69, 0x61, 0x6e, 0x2d, 0x41, 0x69, 0x72, 0x2e, 158 | 0x6c, 0x6f, 0x63, 0x61, 0x6c, // alibaba:JacksonTian-Air.local 159 | // cell_checksum = tag_cell_checksum row_crc8 160 | 0x0a, 156, 161 | 0x03, // tag_cell 162 | 0x04, // tag_cell_name 163 | // value_type value_len value_data 164 | // value_len 165 | 0x09, 0x00, 0x00, 0x00, 166 | // value_data: timestamp 167 | 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 168 | 0x70, 169 | 0x05, // tag_cell_value 170 | 0x1d, 0x00, 0x00, 0x00, 171 | // value_type value_len value_data 172 | 0x03, // VT_STRING 173 | 0x18, 0x00, 0x00, 0x00, // string length 174 | 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x36, 0x2d, 175 | 0x32, 0x39, 0x54, 0x30, 0x38, 0x3a, 0x34, 0x38, 176 | 0x3a, 0x32, 0x36, 0x2e, 0x36, 0x32, 0x32, 0x5a, // 177 | // cell_checksum = tag_cell_checksum row_crc8 178 | 0x0a, 174, 179 | 0x09, 136 180 | ])); 181 | }); 182 | 183 | it('Value(double) should ok', function() { 184 | var Value = plainbuffer.Value; 185 | var data = plainbuffer.DoubleColumn('name', 1.1); 186 | var value = new Value(data.type, data.value); 187 | expect(value.toBytes()).to.eql(Buffer.from([ 188 | 0x01, // DOUBLE 189 | 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xf1, 0x3f 190 | ])); 191 | }); 192 | 193 | it('Value(boolean/true) should ok', function() { 194 | var Value = plainbuffer.Value; 195 | var data = plainbuffer.BooleanColumn('name', true); 196 | var value = new Value(data.type, data.value); 197 | expect(value.toBytes()).to.eql(Buffer.from([ 198 | 0x02, // BOOLEAN 199 | 0x01 200 | ])); 201 | }); 202 | 203 | it('Value(boolean/false) should ok', function() { 204 | var Value = plainbuffer.Value; 205 | var data = plainbuffer.BooleanColumn('name', false); 206 | var value = new Value(data.type, data.value); 207 | expect(value.toBytes()).to.eql(Buffer.from([ 208 | 0x02, // BOOLEAN 209 | 0x00 210 | ])); 211 | }); 212 | 213 | xit('Value(null) should ok', function() { 214 | var Value = plainbuffer.Value; 215 | var data = ots.NullColumn('name'); 216 | var value = new Value(data.type, data.value); 217 | expect(value.toBytes()).to.eql(Buffer.from([ 218 | 0x06, 219 | 0x01 220 | ])); 221 | }); 222 | 223 | it('Value(blob) should ok', function() { 224 | var Value = plainbuffer.Value; 225 | var data = plainbuffer.BlobColumn('name', Buffer.from([0x01, 0x02])); 226 | var value = new Value(data.type, data.value); 227 | expect(value.toBytes()).to.eql(Buffer.from([ 228 | 0x07, 229 | 0x02, 0x00, 0x00, 0x00, 230 | 0x01, 0x02 231 | ])); 232 | }); 233 | 234 | it('deserialize should ok', function() { 235 | var buff = Buffer.from([ 236 | // tag header 237 | 0x75, 0x0, 0x0, 0x0, 238 | 0x1, // tag_pk 239 | 0x3, // tag_cell 240 | 0x4, // tag_cell_name 241 | 0x3, 0x0, 0x0, 0x0, // length 242 | 0x75, 0x69, 0x64, // uid 243 | 0x5, // tag_cell_value 244 | 0xd, 0x0, 0x0, 0x0, // length 245 | 0x3, // type 246 | 0x8, 0x0, 0x0, 0x0, // length 247 | 0x74, 0x65, 0x73, 0x74, 0x5f, 0x75, 0x69, 0x64, 248 | // tag_cell_checksum 249 | 0xa, 0xb2, 250 | // tag_attr 251 | 0x2, 252 | 0x3, // tag_cell 253 | 0x4, // tag_cell_name 254 | 0x4, 0x0, 0x0, 0x0, 255 | 0x74, 0x65, 0x73, 0x74, 256 | 0x5, // tag_cell_value 257 | 0xf, 0x0, 0x0, 0x0, // length 258 | 0x3, // type 259 | 0xa, 0x0, 0x0, 0x0, // length 260 | 0x74, 0x65, 0x73, 0x74, 0x5f, 0x76, 0x61, 0x6c, 261 | 0x75, 0x65, // value 262 | 0x7, // tag_cell_ts 263 | 0xd5, 0xe4, 0x88, 0x90, 0x55, 0x1, 0x0, 0x0, 264 | // tag_cell_checksum 265 | 0xa, 0xd9, 266 | // tag_row_checksum 267 | 0x9, 0x3b 268 | ]); 269 | 270 | var row = { 271 | buffer: buff, 272 | offset: 0, 273 | markedOffset: -1, 274 | limit: 88, 275 | littleEndian: true, 276 | noAssert: false 277 | }; 278 | 279 | var rows = plainbuffer.deserialize(row); 280 | var deserialized = rows[0]; 281 | 282 | expect(deserialized).to.eql({ 283 | uid: 'test_uid', 284 | test: 'test_value' 285 | }); 286 | }); 287 | 288 | it('deserialize(range) should ok', function() { 289 | var buff = Buffer.from([ 290 | 0x75, 0x0, 0x0, 0x0, // tag header 291 | 0x1, // tag_pk 292 | 0x3, // tag_cell 293 | 0x4, // tag_cell_name 294 | 0x5, 0x0, 0x0, 0x0, // length 295 | 0x61, 0x70, 0x70, 0x69, 0x64, // name 296 | 0x5, // tag_cell_value 297 | 0x9, 0x0, 0x0, 0x0, // length 298 | 0x0, // type interger 299 | 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // value 300 | 0xa, 0xf1, // tag_cell_checksum checksum 301 | 0x3, // tag_cell 302 | 0x4, 303 | 0x7, 0x0, 0x0, 0x0, 304 | 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 305 | 0x5, // tag_cell_value 306 | 0xe, 0x0, 0x0, 0x0, // length 307 | 0x3, // type string 308 | 0x9, 0x0, 0x0, 0x0, // string length 309 | 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 310 | 0xa, 0x6d, // tag_cell_checksum checksum 311 | 0x3, 312 | 0x4, 313 | 0x9, 0x0, 0x0, 0x0, 314 | 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 315 | 0x5, // tag_cell_value 316 | 0x1d, 0x0, 0x0, 0x0, 317 | 0x3, // type string 318 | 0x18, 0x0, 0x0, 0x0, // string length 319 | 0x32, 0x30, 0x31, 0x35, 0x2d, 0x31, 0x31, 0x2d, 320 | 0x32, 0x39, 0x54, 0x31, 0x35, 0x3a, 0x35, 0x35, 321 | 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x5a, 322 | 0xa, 0x4d, // tag_cell_checksum checksum 323 | 0x2, 324 | 0x3, 0x4, 0x3, 0x0, 0x0, 0x0, 0x63, 0x70, 0x75, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x6c, 0x7f, 0x4b, 0xbb, 0xc3, 0x64, 0xc3, 0x3f, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xc8, 325 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x66, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0xcc, 0xa4, 0xf4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xb5, 326 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xdd, 0x3f, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x30, 327 | 0x3, 0x4, 0x6, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xd6, 0x3f, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x9e, 328 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe8, 0xda, 0x3f, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x96, 329 | 0x3, 0x4, 0x8, 0x0, 0x0, 0x0, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x21, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x7, 0x51, 0x62, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xc8, 0x9, 0xf0, 0x1, 330 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x61, 0x70, 0x70, 0x69, 0x64, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xf1, 331 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 0x5, 0xe, 0x0, 0x0, 0x0, 0x3, 0x9, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0xa, 0x6d, 332 | 0x3, 0x4, 0x9, 0x0, 0x0, 0x0, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5, 0x1d, 0x0, 0x0, 0x0, 0x3, 0x18, 0x0, 0x0, 0x0, 0x32, 0x30, 0x31, 0x35, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x39, 0x54, 0x31, 0x35, 0x3a, 0x35, 0x36, 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x5a, 0xa, 0xc6, 0x2, 333 | 0x3, 0x4, 0x3, 0x0, 0x0, 0x0, 0x63, 0x70, 0x75, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x6c, 0x7f, 0x4b, 0xbb, 0xc3, 0x64, 0xc3, 0x3f, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xf6, 334 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x66, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x88, 0xa7, 0xf4, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xdb, 335 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xdd, 0x3f, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xe, 336 | 0x3, 0x4, 0x6, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xd6, 0x3f, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xa0, 337 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe8, 0xda, 0x3f, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xa8, 338 | 0x3, 0x4, 0x8, 0x0, 0x0, 0x0, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x21, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x7, 0x5f, 0x64, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xf6, 0x9, 0xc9, 0x1, 339 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x61, 0x70, 0x70, 0x69, 0x64, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xf1, 340 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 0x5, 0xe, 0x0, 0x0, 0x0, 0x3, 0x9, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0xa, 0x6d, 341 | 0x3, 0x4, 0x9, 0x0, 0x0, 0x0, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5, 0x1d, 0x0, 0x0, 0x0, 0x3, 0x18, 0x0, 0x0, 0x0, 0x32, 0x30, 0x31, 0x35, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x39, 0x54, 0x31, 0x35, 0x3a, 0x35, 0x37, 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x5a, 0xa, 0xbf, 0x2, 342 | 0x3, 0x4, 0x3, 0x0, 0x0, 0x0, 0x63, 0x70, 0x75, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x6c, 0x7f, 0x4b, 0xbb, 0xc3, 0x64, 0xc3, 0x3f, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xc0, 343 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x66, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x80, 0x63, 0x45, 0x2, 0x0, 0x0, 0x0, 0x0, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xac, 344 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xdd, 0x3f, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x38, 345 | 0x3, 0x4, 0x6, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xd6, 0x3f, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x96, 346 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe8, 0xda, 0x3f, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x9e, 347 | 0x3, 0x4, 0x8, 0x0, 0x0, 0x0, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x21, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x7, 0x70, 0x66, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xc0, 0x9, 0xae, 0x1, 348 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x61, 0x70, 0x70, 0x69, 0x64, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xf1, 349 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 0x5, 0xe, 0x0, 0x0, 0x0, 0x3, 0x9, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0xa, 0x6d, 350 | 0x3, 0x4, 0x9, 0x0, 0x0, 0x0, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5, 0x1d, 0x0, 0x0, 0x0, 0x3, 0x18, 0x0, 0x0, 0x0, 0x32, 0x30, 0x31, 0x35, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x39, 0x54, 0x31, 0x35, 0x3a, 0x35, 0x38, 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x5a, 0xa, 0x16, 0x2, 351 | 0x3, 0x4, 0x3, 0x0, 0x0, 0x0, 0x63, 0x70, 0x75, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x6c, 0x7f, 0x4b, 0xbb, 0xc3, 0x64, 0xc3, 0x3f, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x7c, 352 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x66, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd6, 0xe9, 0x1, 0x0, 0x0, 0x0, 0x0, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xec, 353 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xdd, 0x3f, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x84, 354 | 0x3, 0x4, 0x6, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0xd6, 0x3f, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x2a, 355 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe8, 0xda, 0x3f, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x22, 356 | 0x3, 0x4, 0x8, 0x0, 0x0, 0x0, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x21, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x7, 0xb0, 0x67, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x7c, 0x9, 0x61, 0x1, 357 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x61, 0x70, 0x70, 0x69, 0x64, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0xf1, 358 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x69, 0x64, 0x5, 0xe, 0x0, 0x0, 0x0, 0x3, 0x9, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, 0xa, 0x6d, 359 | 0x3, 0x4, 0x9, 0x0, 0x0, 0x0, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5, 0x1d, 0x0, 0x0, 0x0, 0x3, 0x18, 0x0, 0x0, 0x0, 0x32, 0x30, 0x31, 0x35, 0x2d, 0x31, 0x31, 0x2d, 0x32, 0x39, 0x54, 0x31, 0x35, 0x3a, 0x35, 0x39, 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x30, 0x30, 0x5a, 0xa, 0x6f, 0x2, 360 | 0x3, 0x4, 0x3, 0x0, 0x0, 0x0, 0x63, 0x70, 0x75, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x6c, 0x7f, 0x4b, 0xbb, 0xc3, 0x64, 0xc3, 0x3f, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xf7, 361 | 0x3, 0x4, 0x7, 0x0, 0x0, 0x0, 0x66, 0x72, 0x65, 0x65, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0x70, 0x8e, 0x13, 0x0, 0x0, 0x0, 0x0, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xed, 362 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xd9, 0x3f, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xdf, 363 | 0x3, 0x4, 0x6, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x31, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xdc, 0x3f, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0x4e, 364 | 0x3, 0x4, 0x5, 0x0, 0x0, 0x0, 0x6c, 0x6f, 0x61, 0x64, 0x35, 0x5, 0x9, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe0, 0x3f, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xe8, 365 | 0x3, 0x4, 0x8, 0x0, 0x0, 0x0, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x6d, 0x65, 0x6d, 0x5, 0x9, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc0, 0x21, 0x7d, 0x0, 0x0, 0x0, 0x0, 0x7, 0xbb, 0x69, 0x54, 0x9a, 0x55, 0x1, 0x0, 0x0, 0xa, 0xf7, 0x9, 0x23 366 | ]); 367 | 368 | var row = { 369 | buffer: buff, 370 | offset: 0, 371 | markedOffset: -1, 372 | limit: 1684, 373 | littleEndian: true, 374 | noAssert: false 375 | }; 376 | 377 | var rows = plainbuffer.deserialize(row); 378 | 379 | expect(rows).to.have.length(5); 380 | expect(rows[0]).to.eql({ 381 | appid: 1, 382 | agentid: 'localhost', 383 | timestamp: '2015-11-29T15:55:00.000Z', 384 | cpu: 0.15151259083995272, 385 | freemem: 32810188, 386 | load1: 0.46484375, 387 | load15: 0.35546875, 388 | load5: 0.42041015625, 389 | totalmem: 2099363840 390 | }); 391 | }); 392 | }); 393 | -------------------------------------------------------------------------------- /test/row.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('expect.js'); 4 | const kitx = require('kitx'); 5 | 6 | const client = require('./common'); 7 | const OTS = require('../'); 8 | 9 | describe('row', function () { 10 | before(async function () { 11 | this.timeout(12000); 12 | var keys = [{ 'name': 'uid', 'type': 'STRING' }]; 13 | var capacityUnit = {read: 1, write: 1}; 14 | var options = { 15 | table_options: { 16 | time_to_live: -1,// 数据的过期时间, 单位秒, -1代表永不过期. 假如设置过期时间为一年, 即为 365 * 24 * 3600. 17 | max_versions: 1 18 | } 19 | }; 20 | var response = await client.createTable('metrics', keys, capacityUnit, options); 21 | expect(response).to.be.ok(); 22 | await kitx.sleep(5000); 23 | }); 24 | 25 | after(async function () { 26 | var response = await client.deleteTable('metrics'); 27 | expect(response).to.be.ok(); 28 | }); 29 | 30 | it('putRow should ok', async function () { 31 | var name = 'metrics'; 32 | var condition = { 33 | row_existence: OTS.RowExistenceExpectation.IGNORE 34 | }; 35 | var primaryKeys = {uid: 'test_uid'}; 36 | var columns = { 37 | test: 'test_value', 38 | integer: 1, 39 | double: 1.1, 40 | boolean: true, 41 | binary: Buffer.from([0x01]) 42 | }; 43 | 44 | var response = await client.putRow(name, condition, primaryKeys, columns); 45 | expect(response).to.be.ok(); 46 | expect(response.consumed.capacity_unit.read).to.be(0); 47 | expect(response.consumed.capacity_unit.write).to.be(1); 48 | }); 49 | 50 | it('getRow should ok', async function () { 51 | var name = 'metrics'; 52 | var primaryKeys = {uid: 'test_uid'}; 53 | var columns = ['test', 'integer', 'double', 'boolean', 'binary']; 54 | var response = await client.getRow(name, primaryKeys, columns); 55 | expect(response).to.be.ok(); 56 | expect(response.row).to.be.eql({ 57 | uid: 'test_uid', 58 | test: 'test_value', 59 | boolean: true, 60 | integer: 1, 61 | double: 1.1, 62 | binary: Buffer.from([0x01]) 63 | }); 64 | expect(response.consumed.capacity_unit.read).to.be(1); 65 | expect(response.consumed.capacity_unit.write).to.be(0); 66 | }); 67 | 68 | it('getRow with filter should ok', async function () { 69 | var name = 'metrics'; 70 | var primaryKeys = {uid: 'test_uid'}; 71 | var columns = ['test', 'integer', 'double', 'boolean', 'binary']; 72 | var filter = OTS.makeFilter('test == @test false true', { 73 | test: '!testvalue' 74 | }); 75 | var response = await client.getRow(name, primaryKeys, columns, { 76 | filter 77 | }); 78 | expect(response).to.be.ok(); 79 | expect(response.row).to.be.eql(null); 80 | expect(response.consumed.capacity_unit.read).to.be(1); 81 | expect(response.consumed.capacity_unit.write).to.be(0); 82 | }); 83 | 84 | it('updateRow should ok', async function () { 85 | const name = 'metrics'; 86 | var condition = { 87 | row_existence: OTS.RowExistenceExpectation.IGNORE 88 | }; 89 | var primaryKeys = { 90 | uid: 'test_uid' 91 | }; 92 | var columns = { 93 | test: 'test_value_replaced_origin' 94 | }; 95 | 96 | var response = await client.updateRow(name, primaryKeys, columns, condition); 97 | expect(response).to.be.ok(); 98 | expect(response.consumed.capacity_unit.read).to.be(0); 99 | expect(response.consumed.capacity_unit.write).to.be(1); 100 | 101 | response = await client.getRow(name, primaryKeys, ['test']); 102 | expect(response).to.be.ok(); 103 | expect(response.row).to.be.eql({ 104 | 'test': 'test_value_replaced_origin', 105 | 'uid': 'test_uid' 106 | }); 107 | expect(response.consumed.capacity_unit.read).to.be(1); 108 | expect(response.consumed.capacity_unit.write).to.be(0); 109 | }); 110 | 111 | it('updateRow with $put should ok', async function () { 112 | const name = 'metrics'; 113 | var condition = { 114 | row_existence: OTS.RowExistenceExpectation.IGNORE 115 | }; 116 | var primaryKeys = { 117 | uid: 'test_uid' 118 | }; 119 | var columns = { 120 | test: OTS.$put('test_value_replaced_put') 121 | }; 122 | 123 | var response = await client.updateRow(name, primaryKeys, columns, condition); 124 | expect(response).to.be.ok(); 125 | expect(response.consumed.capacity_unit.read).to.be(0); 126 | expect(response.consumed.capacity_unit.write).to.be(1); 127 | 128 | response = await client.getRow(name, primaryKeys, ['test']); 129 | expect(response).to.be.ok(); 130 | expect(response.row).to.be.eql({ 131 | 'test': 'test_value_replaced_put', 132 | 'uid': 'test_uid' 133 | }); 134 | expect(response.consumed.capacity_unit.read).to.be(1); 135 | expect(response.consumed.capacity_unit.write).to.be(0); 136 | }); 137 | 138 | xit('updateRow with delete should ok', async function () { 139 | const name = 'metrics'; 140 | var condition = { 141 | row_existence: OTS.RowExistenceExpectation.IGNORE 142 | }; 143 | var primaryKeys = { 144 | uid: 'test_uid' 145 | }; 146 | var columns = { 147 | test: OTS.$delete(Date.now()) 148 | }; 149 | var response = await client.updateRow(name, primaryKeys, columns, condition, columns); 150 | expect(response).to.be.ok(); 151 | expect(response.consumed.capacity_unit.read).to.be(0); 152 | expect(response.consumed.capacity_unit.write).to.be(1); 153 | response = await client.getRow(name, primaryKeys, ['uid', 'test']); 154 | expect(response).to.be.ok(); 155 | expect(response.row).to.not.have.property('test'); 156 | expect(response.consumed.capacity_unit.read).to.be(1); 157 | expect(response.consumed.capacity_unit.write).to.be(0); 158 | }); 159 | 160 | it('updateRow with deleteAll should ok', async function () { 161 | const name = 'metrics'; 162 | var condition = { 163 | row_existence: OTS.RowExistenceExpectation.IGNORE 164 | }; 165 | var primaryKeys = { 166 | uid: 'test_uid' 167 | }; 168 | var columns = { 169 | test: OTS.$deleteAll() 170 | }; 171 | var response = await client.updateRow(name, primaryKeys, columns, condition, columns); 172 | expect(response).to.be.ok(); 173 | expect(response.consumed.capacity_unit.read).to.be(0); 174 | expect(response.consumed.capacity_unit.write).to.be(1); 175 | response = await client.getRow(name, primaryKeys, ['uid', 'test']); 176 | expect(response).to.be.ok(); 177 | expect(response.row).to.not.have.property('test'); 178 | expect(response.consumed.capacity_unit.read).to.be(1); 179 | expect(response.consumed.capacity_unit.write).to.be(0); 180 | }); 181 | 182 | it('deleteRow should ok', async function () { 183 | const name = 'metrics'; 184 | var condition = { 185 | row_existence: OTS.RowExistenceExpectation.IGNORE 186 | }; 187 | var primaryKeys = {uid: 'test_uid'}; 188 | var response = await client.deleteRow(name, primaryKeys, null, true, condition); 189 | expect(response).to.be.ok(); 190 | expect(response.consumed.capacity_unit.read).to.be(0); 191 | expect(response.consumed.capacity_unit.write).to.be(1); 192 | 193 | var columns = ['test']; 194 | response = await client.getRow(name, primaryKeys, columns); 195 | expect(response).to.be.ok(); 196 | expect(response.row).to.be.eql(null); 197 | expect(response.consumed.capacity_unit.read).to.be(1); 198 | expect(response.consumed.capacity_unit.write).to.be(0); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/table.integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect.js'); 4 | var client = require('./common'); 5 | 6 | describe('table', function () { 7 | it('createTable should ok', async function () { 8 | var keys = [{ 'name': 'uid', 'type': 'STRING' }]; 9 | var capacityUnit = {read: 1, write: 1}; 10 | var options = { 11 | table_options: { 12 | time_to_live: -1,// 数据的过期时间, 单位秒, -1代表永不过期. 假如设置过期时间为一年, 即为 365 * 24 * 3600. 13 | max_versions: 1 14 | } 15 | }; 16 | var response = await client.createTable('metrics', keys, capacityUnit, options); 17 | expect(response).to.be.ok(); 18 | }); 19 | 20 | it('updateTable should ok', async function () { 21 | try { 22 | var capacityUnit = {read: 2, write: 1}; 23 | var response = await client.updateTable('metrics', capacityUnit); 24 | expect(response).to.be.ok(); 25 | expect(response.capacity_unit_details.capacity_unit.read).to.be(2); 26 | expect(response.capacity_unit_details.capacity_unit.write).to.be(1); 27 | } catch (e) { 28 | expect(e.name).to.be('OTSTooFrequentReservedThroughputAdjustmentError'); 29 | expect(e.message).to.be('Reserved throughput adjustment is too frequent.'); 30 | return; 31 | } 32 | }); 33 | 34 | it('listTable should ok', async function () { 35 | var response = await client.listTable(); 36 | expect(response.table_names).to.be.ok(); 37 | expect(response.table_names.length).to.be.above(0); 38 | }); 39 | 40 | it('describeTable should ok', async function () { 41 | var response = await client.describeTable('metrics'); 42 | expect(response).to.be.ok(); 43 | expect(response.table_meta.table_name).to.be('metrics'); 44 | expect(response.reserved_throughput_details).to.be.ok(); 45 | }); 46 | 47 | it('deleteTable should ok', async function () { 48 | var response = await client.deleteTable('metrics'); 49 | expect(response).to.be.ok(); 50 | }); 51 | }); 52 | --------------------------------------------------------------------------------