MAS · JavaScript 使用指南
---------------------

### 1.  简介
MAS（MIS Application Service）提供了快速开发MIS的多项功能，旨在开发MIS系统时减少与后端的联调沟通，让前端能够轻松HOLD住一个完整MIS的开发。下面我们就MAS最重要的数据存储功能与传统数据库的对比来简要介绍一下MAS的特点。

在传统数据库中，当我们要进行向Todo表中一条数据的增加，我们会这样做：
```sql
INSERT INTO Todo (title,content) VALUES ('周会'，'周二下午2点整');
```

那么当我们使用MAS的数据存储功能时，实现代码如下：
```javascript
var todo = MAS.Object.new('Todo');
todo.set('title','周会');
todo.set('content','周二下午2点整');
todo.save().then(function(){
	// success handle
}).catch(function(){
	// error handle
})
```

使用MAS的特点在于：
* 不需要单独的维护表结构。比如需要新增字段你只需要这样改动代码

```javascript
var todo = MAS.Object.new('Todo');
todo.set('title','周会');
todo.set('content','周二下午2点整');
todo.set('location', '全民直播6楼会议室');
todo.save().then(function(todo){
	// success handle
}).catch(function(){
	// error handle
})
```

* Schema Free，数据可以随用随加
* 能够提供一套统一的SDK，给予不同语言和不同环境的支持

MAS与传统数据库的区别在于：
1. Schema Free／Not free 的差异；
2. 数据接口上，MAS 是面向对象的（数据操作接口都是基于 Object 的），开放的（所有移动端都可以直接访问），DB 是面向结构的，封闭的（一般在 Server 内部访问）

目前，MAS针对MIS开发的特点集成了常用的功能：
1. [数据存储](#storage)
2. [ACL](#acl)
3. [缓存](#cache)
4. [HTTP跨域代理](#httpProxy)
5. [邮件](#mail)
6. [短信](#sms)
7. [文件上传](#upload)
8. [报表服务](#report)

接下来我们会一一介绍各个功能的使用

### <a href="javscript:void(0)" name="install" id="install">2. SDK安装</a>
对于浏览器环境，只需要引入对应的sdk即可
```html
<script src="./mas.js"></script>
```

对于node环境来说，你可以使用npm进行安装
```shell
npm install mas-js-sdk --save
```
之后我们就可以进行MAS的使用了
```javascript
// 浏览器环境
var appId = 'your appId'
var appKey = 'your appKey'
window.MAS = require('MAS')(appId, appKey );

// node环境
var appId = 'your appId'
var appKey = 'your appKey'
req.__MAS__ = require('mas-js-sdk')(appId, appKey );
```
 

### <a href="javscript:void(0)" name="auth" id="auth">3. 安全验证</a>
MAS的安全验证有一套严格的流程，通过这个流程我们可以将数据权限细化到一个数据的读写，其流程步骤为：
1. 对app的访问权限验证（通过应用创建的appId及appKey进行验证）
2. 对用户进行权限验证 （通过MAS.User对象login后获得的uid和token进行验证）
3. 对CRUD的Class进行权限验证（判断用户是否在Class的CRUD相关权限列表中）
4. 对数据的read和write进行权限验证（通过创建数据时的ACL进行验证）

### <a href="javscript:void(0)" name="storage" id="storage">4. 数据存储</a>
#### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1 对象</a>
MAS.Object是MAS对数据存储过程的复杂封装，每个MAS.Object的实例包含了诸多的键值对（key-value）。属性的值严格与JSON方式兼容的数据。当数据进行保存时，MAS.Object会对数据进行JSON.stringify。这个数据是无模式化的（Schema Free），这意味着你不需要提前标注每个对象有哪些key，你只需要随意的添加它就好了，服务器会按照相关逻辑保存它（注意：如果在MAS平台上，没有对添加字段进行相关Class列的添加，那么在查询时会被自动的过滤掉）。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.1 数据类型</a>
MAS.Object支持部分标准的JS数据类型，如下：
```javascript
var todo = MAS.Object.new('Todo');

var number = 2014;
var string = 'famous film name is ' + number;
var date = new Date();
var array = [string, number];
var object = { number: number, string: string };

todo.set('testNumber', number);
todo.set('testString', string);
todo.set('testDate', date);
todo.set('testArray', array);
todo.set('testObject', object);
todo.set('testNull', null);
todo.save().then(function(todo) {
  // success
}, function() {
  // fail
});
```
注意，MAS.Object不能存储二进制数据（例如Blob相关），如果要对Blob数据进行存储，那么请使用 [MAS.File](#file)。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.2 创建对象</a>
 创建对象可以使用两种方式：
```javascript
// 1.创建Class后再进行实例化
var Todo = MAS.Object.extend('Todo');
var todo = new Todo();

// 2.直接实例化
var todo = MAS.Object.new('Todo');
```
但需要注意的是，不管是extend还是new，对应的参数都应该准确对应你创建应用的Class名称。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.3 保存对象</a>
 我们假设已经创建了一个叫做Todo的Class，并且其包含了title、content、location三个自定义列，那么当需要新建一条数据时对应的代码如下：
```javascript
var todo = MAS.Object.new('Todo');
todo.set('title','new title');
todo.set('content','new content');
todo.set('location','new location');
todo.save().then(function(todo){
	// success
}).catch(function(){
	// error
});
```
为提高代码的可读性，我们建议使用驼峰式命名法（CamelCase）为Class及属性进行命名。类使用大驼峰方式，如UserDetail，属性使用小驼峰，如updatedAt。

此外，在保存对象时我们可以进行fetchWhenSave的设定，fetchWhenSave用于对象成功保存后，自动返回本地已改动属性在云端的最新值，而不是本地save的数据，其默认为false，我们会在更新数据时讲解它的使用场景。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.4 获取对象</a>
 每个被保存在服务端的数据都会有一个objectId标示，我们可以通过objectId获得对应的数据：
```javascript
 var query = new MAS.Query('Todo');
 query.get('57328ca079bc44005c2472d0').then(function (todo) {
   // success
   // data 就是 objectId 为 57328ca079bc44005c2472d0 的 Todo 对象实例
 }).catch(function () {
   // error
 });
```
如果不想使用查询，还可以通过从本地构建一个 id，然后调用接口从云端把这个 id 的数据拉取到本地，示例代码如下：
```javascript
var todo = MAS.Object.new('Todo');

todo.setId('57328ca079bc44005c2472d0');
// or
todo.set('objectId','57328ca079bc44005c2472d0');

todo.fetch().then(function(todo){
	// success
	var title = todo.get('title');// 读取 title
    var content = todo.get('content');// 读取 content
}).catch(function(){
	// error
});
```
##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.5 获取objectId</a>
每一次对象存储成功之后，云端都会返回 objectId，它是一个Class中全局唯一的属性。
```javascript
var todo = MAS.Object.new('Todo');
todo.set('title','new title');
todo.set('content','new content');
todo.set('location','new location');
todo.save().then(function(todo){
	// success
	var objectId = todo.getId();
	// or
	objectId = todo.get('objectId');
	
}).catch(function(){
	// error
});
```

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.6 访问对象属性</a>
访问Todo的对象属性的方法为：
```javascript
var todo = MAS.Object.new('Todo');
todo.setId('57328ca079bc44005c2472d0');
todo.fetch().then(function(todo){
	// success
	var objectId = todo.getId();
	var acl = todo.getACL();
	var title = todo.get('title');
    var content = todo.get('content');
	var createdAt = todo.get('createdAt');
	var updatedAt = todo.get('updatedAt');
}).catch(function(){
	// error
});
```
如果访问了并不存在的属性，SDK 并不会抛出异常，而是会返回空值。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.7 默认属性</a>
MAS创建Class会有对应的默认属性，它包括了objectId、createdUid、updatedUid、createdAt、updatedAt。
* objectId：Class中数据的全局唯一标示，相当于关系型数据库中的主键。
* createdUid：创建当条数据的用户Id
* updatedUid：修改数据的用户Id
* createdAt：创建数据的时间，Unix时间戳
* updatedAt：修改数据的时间，Unix时间戳

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.8  同步对象</a>
多终端共享一个数据时，为了确保当前客户端拿到的对象数据是最新的，可以调用刷新接口来确保本地数据与云端的同步：
```javascript
 // 使用已知 objectId 构建一个 MAS.Object
 var todo = new Todo();
 todo.setId('5590cdfde4b00f7adb5860c8');
 todo.fetch().then(function (todo) {
   // todo 是从服务器加载到本地的 Todo 对象
   var objectId = todo.getId();
 }).catch(function (error) {

 });
```

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.9  更新对象</a>
MAS 上的更新对象都是针对单个对象，云端会根据 <u>有没有 objectId</u> 来决定是新增还是更新一个对象。
```javascript
 // 使用已知 objectId 构建一个 MAS.Object
var todo = new Todo();
todo.setId('5590cdfde4b00f7adb5860c8');
todo.fetch().then(function (todo) {
	// todo 是从服务器加载到本地的 Todo 对象
	todo.set('title', '需求临时变更通知');
	todo.set('content', '需求被产品汪变更了，我们需要改时间');
	
	// 更新了服务端objectId为5590cdfde4b00f7adb5860c8的title和content字段
	return todo.save();
}).then(function(todo){
	var title = todo.get('title'); // title = 需求临时变更通知
	var content = todo.get('content'); // content = 需求被产品汪变更了，我们需要改时间
}).catch(function(){
});
```
更新操作是覆盖式的，云端会根据最后一次提交到服务器的有效请求来更新数据。更新是字段级别的操作，未更新的字段不会产生变动，这一点请不用担心。

由于更新会根据最后一次提交到服务器的请求来判断（乐观锁机制），因此为了保证在多人同时修改同一条数据时，你可以使用fetchWhenSave保证数据与服务端的同步，MAS.Object的fetchWhenSave默认为false。

考虑这样一个场景：一篇 wiki 文章允许任何人来修改，它的数据表字段有：content（wiki 内容）、version（版本号）。每当 wiki 内容被更新后，其 version 也需要更新（+1）。用户 A 要修改这篇 wiki，从数据表中取出时其 version 值为 3，当用户 A 完成编辑要保存新内容时，如果数据表中的 version 仍为 3，表明这段时间没有其他用户更新过这篇 wiki，可以放心保存；如果不是 3而是更高的值，那么此次修改应该被丢弃，当设置了fetchWhenSave为true时，客户端将会得到最新的修改值，保证了数据的同步（fetchWhenSave的依据逻辑为updatedAt字段）。

```javascript
new MAS.Query('Wiki').first().then(function (wiki) {
	var currentVersion = wiki.get('version');
	wiki.fetchWhenSave(true);
	wiki.set('version', currentVersion + 1);
	return wiki.save();
}).then(function (wiki) {
	// 保存成功，version为最后一次修改的version
}).catch(function (error) {
	// 异常处理
});
```
##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.10  数值更新</a>
 对Number数值的更新MAS提供了increment方法
```javascript
 var todo = MAS.Object.new('Todo');
 todo.setId('57328ca079bc44005c2472d0');
 todo.set('views', 0);
 todo.save().then(function (todo) {
   todo.increment('views', 1);
   todo.fetchWhenSave(true);
   return todo.save();
 }).then(function (todo) {
   // 使用了 fetchWhenSave 选项，save 成功之后即可得到最新的 views 值
 }).catch(function(){
 
 });
```
##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.11  更新数组</a>
更新数组是原子操作。使用以下方法可以方便地维护数组类型的数据：
* MAS.Object.prototype.add(attrKey, value)
将指定对象附加到数组末尾。
* MAS.Object.addUnique(attrKey, value)
如果数组中不包含指定对象，将该对象加入数组末尾
* MAS.Object.remove(attrKey, value)
从数组字段中删除指定对象的所有实例

例如，Todo 对象有一个提醒时间 reminders 字段，是一个数组，代表这个日程会在哪些时间点提醒用户。比如有个拖延症患者把闹钟设为早上的 7:10、7:20、7:30：
```javascript
 var reminder1 = +new Date('2015-11-11 07:10:00');
 var reminder2 = +new Date('2015-11-11 07:20:00');
 var reminder3 = +new Date('2015-11-11 07:30:00');

 var reminders = [reminder1, reminder2, reminder3];

 var todo = MAS.Object.new('Todo');
 // 指定 reminders 是一个 Unix时间戳 对象数组
 todo.addUnique('reminders', reminders);
 todo.save().then(function (todo) {
   console.log(todo.get('reminders')); // equalTo reminders
 }).catch(function () {
   // 异常处理
 });
```
##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.12  删除对象</a>
假如某一个 Todo 完成了，用户想要删除这个 Todo 对象，可以如下操作：
```javascript
var todo = MAS.Object.new('Todo');
todo.setId('57328ca079bc44005c2472d0');
todo.destroy().then(function () {
  // 删除成功
}, function () {
  // 删除失败
});
```
删除对象是一个较为敏感的操作。在控制台创建对象的时候，请认真考虑Class对应的权限设置，对于数据的删除，我们推荐定义一个字段isDeleted，依靠isDeleted的值来判断数据是否被删除的方式。

##### <a href="javscript:void(0)" name="storage-object" id="storage-object">4.1.12  批量操作</a>
为了减少网络交互的次数太多带来的时间浪费，你可以在一个请求中对多个对象进行创建、更新、删除、获取。接口都在 MAS.Object 这个类下面：

```javascript
var objects = []; // 构建一个本地的 MAS.Object 对象数组

 // 批量创建（更新）
MAS.Object.saveAll(objects).then(function (objects) {
  // 成功
}).catch(function () {
  // 异常处理
});

// 批量删除
MAS.Object.destroyAll(objects).then(function () {
  // 成功
}).catch(function () {
  // 异常处理
});

// 批量获取
MAS.Object.fetchAll(objects).then(function (objects) {
  // 成功
}).catch(function () {
  // 异常处理
});
```

批量设置 Todo 已经完成：

```javascript
var query = new MAS.Query('Todo');
query.find().then(function (todos) {
  todos.forEach(function(todo) {
    todo['status'] = 1;
  });
  return MAS.Object.saveAll(todos);
}).then(function(todos) {
  // 更新成功
}).catch(function () {
  // 异常处理
});
```

不同类型的批量操作所引发不同数量的 API 调用，假设对象数量为n，fetchAll及saveAll发送n个请求，destroyAll发送1个请求。

#### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2  查询</a>
MAS.Query 是构建针对 MAS.Object 查询的基础类。每次查询默认最多返回 10 条符合条件的结果，要更改这一数值，需要使用到limit方法。

##### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.1  创建查询</a>
```javascript
var query = new MAS.Query('Todo');
```

##### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.2  根据objectId进行查询</a>
```javascript
 var query = new MAS.Query('Todo');
 query.get('57328ca079bc44005c2472d0').then(function (todo) {
   // success
   // data 就是 objectId 为 57328ca079bc44005c2472d0 的 Todo 对象实例
 }).catch(function () {
   // error
 });
```

##### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3  条件查询</a>
根据不同条件来过滤结果，比如查询最迫切需要完成的日程列表 Todo，此时基于 priority 构建一个查询就可以得到符合条件的对象：
```javascript
var query = new MAS.Query('Todo');
// 查询 priority 是 0 的 Todo
query.equalTo('priority', 0);
query.find().then(function (results) {
    var priorityEqualsZeroTodos = results;
}).catch(function () {

});
```
每次查询默认最多返回 10条符合条件的结果，要更改这一数值，需要使用limit方法。
将以上逻辑用 SQL 语句表达：
```sql
SELECT * FROM Todo WHERE priority = 0
```
当多个查询条件并存时，它们之间默认为 AND 关系，即查询只返回满足了全部条件的结果。建立 OR 关系则需要使用 MAS.Query.or方法。

请注意，在简单查询中，如果对一个对象的同一属性设置多个条件，那么先前的条件会被覆盖，查询只返回满足最后一个条件的结果。例如，我们要找出优先级为 0 和 1 的所有 Todo，错误写法是：
```javascript
 var query = new MAS.Query('Todo');
 query.equalTo('priority', 0);
 query.equalTo('priority', 1);
 query.find().then(function (results) {
 // 如果这样写，第二个条件将覆盖第一个条件，查询只会返回 priority = 1 的结果
 }).catch(function () {
 
 });
```
正确作法是使用 OR 关系 来构建条件。

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.1  比较查询</a>
1. 等于：equalTo
2. 不等于： notEqualTo
3. 大于：greaterThan
4. 大于等于：greaterThanOrEqualTo
5. 小于：lessThan
6. 小于等于：lessThanOrEqualTo

利用上述介绍的逻辑操作的接口，我们可以很快地构建条件查询。

例如，查询优先级小于 2 的所有 Todo ：
```javascript
var query = new MAS.Query('Todo');
query.lessThan('priority', 2);
```

要查询优先级大于等于 2 的 Todo：
```javascript
query.greaterThanOrEqualTo('priority',2);
```

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.2  正则匹配查询</a>
正则匹配查询是指在查询条件中使用正则表达式来匹配数据，查询指定的 key 对应的 value 符合正则表达式的所有对象。 例如，要查询标题包含中文的 Todo 对象可以使用如下代码：
```javascript
var query = new MAS.Query('Todo');
var regExp = new RegExp('[\u4e00-\u9fa5]', 'i');
query.matches('title', regExp);
query.find().then(function (results) {

}).catch(function () {

});
```
正则匹配查询只适用于字符串类型的数据。

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.3  包含查询</a>
包含查询类似于传统 SQL 语句里面的 LIKE %keyword% 的查询，比如查询标题包含「龙神」的 Todo：
```javascript
query.contains('title','龙神');
```
翻译成 SQL 语句就是：
```sql
SELECT * FROM Todo WHERE title LIKE '%龙神%'
```
不包含查询与包含查询是对立的，不包含指定关键字的查询，可以使用 正则匹配方法 来实现。例如，查询标题不包含「机票」的 Todo，正则表达式为 ^((?!机票).)*$：
```javascript
var query = new MAS.Query('Todo');
var regExp = new RegExp('^((?!机票).)*#39;, 'i');
query.matches('title', regExp);
```
但是基于正则的模糊查询有两个缺点：
* 当数据量逐步增大后，查询效率将越来越低
* 没有文本相关性排序

还有一个接口可以精确匹配不等于，比如查询标题不等于「出差、休假」的 Todo 对象：
```javascript
var query = new MAS.Query('Todo');
var filterArray = ['出差', '休假'];
query.notContainedIn('title', filterArray);
```
###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.3  数组查询</a>
当一个对象有一个属性是数组的时候，针对数组的元数据查询可以有多种方式。例如，在 数组 一节中我们为 Todo 设置了 reminders 属性，它就是一个日期数组，现在我们需要查询所有在 8:30 会响起闹钟的 Todo 对象：
```javascript
var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00')];
query.containsAll('reminders', reminderFilter);

// 也可以使用 equals 接口实现这一需求
var targetDateTime = +new Date('2015-11-11 08:30:00');
query.equalTo('reminders', targetDateTime);
```
如果你要查询精确匹配 8:30、9:30 这两个时间点响起闹钟的 Todo，可以使用如下代码：
```javascript
var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00'), +new Date('2015-11-11 09:30:00')];
query.containsAll('reminders', reminderFilter);
```
注意这里是精确关系，假如有一个 Todo 会在 8:30、9:30、10:30 响起闹钟，它不会被查询出来的。

如果要使用类似于SQL的IN操作，那么可以使用 containedIn 和 notContainedIn ：
```javascript
var query = new MAS.Query('Todo');
var reminderFilter = [+new Date('2015-11-11 08:30:00'), +new Date('2015-11-11 09:30:00')];
query.containedIn('reminders', reminderFilter);
```
这里变为了包含关系，假如有一个 Todo 会在 8:30、9:30、10:30 响起闹钟，它会被查询出来的。

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.4  字符串匹配</a>
使用 startsWith 可以过滤出以特定字符串开头的结果，这有点像 SQL 的 LIKE 条件。因为支持索引，所以该操作对于大数据集也很高效。
```javascript
// 找出开头是「早餐」的 Todo
var query = new MAS.Query('Todo');
query.startsWith('content', '早餐');
```
另外你也可以使用endWith，但它与matches一样不支持索引:
```javascript
// 找出结尾是「早餐」的 Todo
var query = new MAS.Query('Todo');
query.endWith('content', '早餐');
```

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.5  OR查询</a>
OR 操作表示多个查询条件符合其中任意一个即可。 例如，查询优先级是大于等于 3 或者已经完成了的 Todo：
```javascript
var priorityQuery = new MAS.Query('Todo');
priorityQuery.greaterThanOrEqualTo('priority', 3);

var statusQuery = new MAS.Query('Todo');
statusQuery.equalTo('status', 1);

var query = MAS.Query.or(priorityQuery, statusQuery);
// 返回 priority 大于等于 3 或 status 等于 1 的 Todo
```
###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.6  查询结果</a>
例如很多应用场景下，只要获取满足条件的一个结果即可，例如获取满足条件的第一条 Todo：
```javascript
var query = new MAS.Query('Comment');
query.equalTo('priority', 0);
query.first().then(function (data) {
  // data 就是符合条件的第一个 MAS.Object
}).catch(function (error) {

});
```
为了防止查询出来的结果过大，云端默认针对查询结果有一个数量限制，即 limit，它的默认值是 10。比如一个查询会得到 10000 个对象，那么一次查询只会返回符合条件的 100 个结果。limit 允许取值范围是 1 ~ Number.MAV_VALUE。例如设置返回 10 条结果：
```javascript
var query = new MAS.Query('Todo');
var now = +new Date();
query.lessThanOrEqualTo('createdAt', now);//查询今天之前创建的 Todo
query.limit(100);// 最多返回 100 条结果
```
注意，我们不太建议设定太大的limit，这样会导致数据查询及传输很慢致使压垮数据库。

设置 skip 这个参数可以告知云端本次查询要跳过多少个结果。将 skip 与 limit 搭配使用可以实现翻页效果，这在客户端做列表展现时，尤其在数据量庞大的情况下就使用技术。例如，在翻页中，一页显示的数量是 10 个，要获取第 3 页的对象：
```javascript
var query = new MAS.Query('Todo');
var now = +new Date();
query.lessThanOrEqualTo('createdAt', now);//查询今天之前创建的 Todo
query.limit(100);// 最多返回 10 条结果
query.skip(20);// 跳过 20 条结果
```

通常列表展现的时候并不是需要展现某一个对象的所有属性，例如，Todo 这个对象列表展现的时候，我们一般展现的是 title 以及 content，我们在设置查询的时候，也可以告知云端需要返回的属性有哪些，这样既满足需求又节省了流量，也可以提高一部分的性能，代码如下：
```javascript
var query = new MAS.Query('Todo');
query.select('title', 'content');
query.first().then(function (todo) {
  console.log(todo.get('title')); // √
  console.log(todo.get('content')); // √
  console.log(todo.get('location')); // undefined
}).catch(function (error) {
  // 异常处理
});
```
###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.7  统计总数</a>
通常用户在执行完搜索后，结果页面总会显示出诸如「搜索到符合条件的结果有 1020 条」这样的信息。例如，查询一下今天一共完成了多少条 Todo：
```javascript
var query = new MAS.Query('Todo');
query.equalTo('status', 1);
query.count().then(function (count) {
    console.log(count);
}).catch(function (error) {

});
```

###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.8  排序</a>
对于数字、字符串、日期类型的数据，可对其进行升序或降序排列。
```javascript
// 按时间，升序排列
query.addAscending('createdAt');

// 按时间，降序排列
query.addDescending('createdAt');
```
一个查询可以附加多个排序条件，如按 priority 升序、createdAt 降序排列：
```javascript
var query = new MAS.Query('Todo');
query.ascending('priority');
query.descending('createdAt');
```
###### <a href="javscript:void(0)" name="storage-query" id="storage-query">4.2.3.9  查询性能优化</a>
影响查询性能的因素很多。特别是当查询结果的数量超过 10 万，查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式，开发者可以据此进行有针对性的调整和优化，或尽量避免使用。
* 不等于和不包含查询（无法使用索引）
* 通配符在前面的字符串查询（无法使用索引）
* 有条件的 count（需要扫描所有数据）
* skip 跳过较多的行数（相当于需要先查出被跳过的那些行）
* 无索引的排序（另外除非复合索引同时覆盖了查询和排序，否则只有其中一个能使用索引）
* 无索引的查询（另外除非复合索引同时覆盖了所有条件，否则未覆盖到的条件无法使用索引，如果未覆盖的条件区分度较低将会扫描较多的数据）

#### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3 用户</a>
用户系统几乎是每款应用都要加入的功能。除了基本的注册、登录和密码重置，甚至还会使用手机号一键登录、短信验证码登录等功能。

MAS.User 是用来描述一个用户的特殊对象，它是 <u>MAS.Object的子类</u> ，与之相关的数据都保存在 _User 数据表中，其默认fetchWhenSave为true。

##### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.1  用户的属性</a>
用户名、密码、邮箱及电话是默认提供的四个属性，访问方式如下：
```javascript
var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {
    var username = user.getUsername();
    var password = user.getPassword();
    var email = user.getEmail();
    var phonenumber = user.getPhonenumber();
 }).catch(function (error) {
 
 });
```

用户对象和普通对象一样也支持添加自定义属性。例如，为当前用户添加年龄属性：
```javascript
var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {
    user.set('age', 25);
    return user.save();
 }).catch(function (error) {
 
 });
```

#####<a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.2  注册</a>
例如，注册一个用户的示例代码如下（用户名 Tom 密码 cat!@#123）：
```javascript
var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.set('email', '395693101@qq.com');
user.set('phonenumber', '18500742221');
user.resgiter().then(function (user) {

 }).catch(function (error) {
 
 });
```
请注意，MAS并不会加密你的密码，因此你需要自己对密码进行加密处理。

##### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.3  登录</a>
```javascript
var user = new MAS.User();
user.set('username','Tom');
user.set('password','cat!@#123');
user.login().then(function (user) {

}).catch(function (error) {

});
```

##### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.4  当前用户</a>
开微博或者微信，它不会每次都要求用户都登录，这是因为它将用户数据缓存在了客户端。同样，只要是调用了登录相关的接口，MAS JS SDK 都会自动缓存登录用户的数据。 例如，判断当前用户是否为空，为空就跳转到登录页面让用户登录，如果不为空就跳转到首页：
```javascript
var currentUser = MAS.User.current;
if (currentUser) {
   // 跳转到首页
}else {
   //currentUser 为空时，可打开用户注册界面…
}
```
##### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.4  SessionToken</a>
所有登录接口调用成功之后，云端会返回一个 SessionToken 给客户端，客户端在发送 HTTP 请求的时候，JavaScript SDK 会在 HTTP 请求里面自动添加上当前用户的 SessionToken 和其objectId  作为这次请求发起者 MAS.User 的身份认证信息。

##### <a href="javscript:void(0)" name="storage-user" id="storage-user">4.3.5  用户查询</a>
查询用户代码如下：
```javascript
var query = new MAS.Query(MAS.User);
```

#### <a href="javscript:void(0)" name="storage-role" id="storage-role">4.3 角色</a>
角色可以被称为组，其目的是为了将对应的user进行分类，比如：CEO、CTO、运营、技术、产品等。

MAS.Role 是用来描述一个组的特殊对象，它同样是 <u>MAS.Object的子类</u> ，与之相关的数据都保存在 _Role 数据表中，其默认fetchWhenSave为true。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">4.3.1  角色的属性</a>
名称和用户是默认提供的两个属性，访问方式如下：
```javascript
var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function (role) {
    var name = role.get('name');
    var users = role.getUsers(); // 返回MAS.User的实例的数组
 }).catch(function (error) {
 
 });
```
##### <a href="javscript:void(0)" name="storage-role" id="storage-role">4.3.2  添加用户</a>
调用addUser方法，可以将一个用户添加到角色中：
```javascript
var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function(r){
	role = r;
	var query = new MAS.Query('_User');
	query.equalTo('username','Tom');
	return query.first();
}).then(function(user){
	// 将Tom用户添加到此角色中
	role.addUser(user);
	// 保存到数据库中
	return role.save();
}).then(function(){
	
}).catch(function(){

});
```
##### <a href="javscript:void(0)" name="storage-role" id="storage-role">4.3.2  删除用户</a>
调用removeUser方法，可以将一个用户从角色中删除：
```javascript
var role = new MAS.Role();
role.setId('58asloxdiw19szdkssss');
role.fetch().then(function(r){
	role = r;
	var query = new MAS.Query('_User');
	query.equalTo('username','Tom');
	return query.first();
}).then(function(user){
	// 将Tom用户从此角色中删除
	role.removeUser(user);
	// 保存到数据库中
	return role.save();
}).then(function(){
	
}).catch(function(){

});
```
##### <a href="javscript:void(0)" name="storage-role" id="storage-role">4.3.3  角色查询</a>
查询用户代码如下：
```javascript
var query = new MAS.Query(MAS.Role);
```

### <a href="javscript:void(0)" name="acl" id="acl">5.  ACL</a>
数据安全在应用开发的任何阶段都应该被重视。因此在这里我们对MAS的ACL记性讨论，如何使用MAS提供的安全功能模块为应用以及数据提供安全保障。

列举一个场景： 假设我们要做一个极简的论坛：用户只能修改或者删除自己发的帖子，其他用户则只能查看。

#### <a href="javscript:void(0)" name="acl-user" id="acl-user">5.1 基于用户的权限管理</a>

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.1  单用户权限设置</a>
以上需求在 MAS 中实现的步骤如下：
1. 写一篇帖子
2. 设置帖子的「读」权限为所有人可读。
3. 设置帖子的「写」权限为作者可写。
4. 保存帖子

实例代码如下：
```javascript
 // 新建一个帖子对象
  var Post = MAS.Object.extend('Post');
  var post = new Post();
  post.set('title', '大家好，我是新人');

  // 新建一个 ACL 实例
  var acl = new MAS.ACL();
  acl.setPublicReadAccess(true);
  acl.setWriteAccess(MAS.User.current, true);

  // 将 ACL 实例赋予 Post 对象
  post.setACL(acl);
  post.save().then(function() {
    // 保存成功
  }).catch(function() {
  
  });
```
以上代码产生的效果在 MAS平台的Post 表 可以看到，这条记录的 ACL 列上的值为：
```javascript
{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true}}
```
此时，这种 ACL 值的表示：所有用户均有「读」权限，而 objectId 为 55b9df0400b0f6d7efaa8801 拥有「写」权限，其他用户不具备「写」权限。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.2  多用户权限设置</a>
假如需求增加为：帖子的作者允许某个特定的用户可以修改帖子，除此之外的其他人不可修改。 实现步骤就是额外指定一个用户，为他设置帖子的「写」权限：
```javascript
 // 创建一个针对 User 的查询
 var query = new MAS.Query('_User');
 query.get('55098d49e4b02ad5826831f6').then(function(otherUser) {
   var post = new MAS.Object('Post');
   post.set('title', '大家好，我是新人');

   // 新建一个 ACL 实例
   var acl = new MAS.ACL();
   acl.setPublicReadAccess(true);
   acl.setWriteAccess(MAS.User.current, true);
   acl.setWriteAccess(otherUser, true);

   // 将 ACL 实例赋予 Post 对象
   post.setACL(acl);

   // 保存到云端
   return post.save();
 }).then(function() {
   // 保存成功
 }).catch(function() {
 
 });
```
执行完毕上面的代码，回到MAS系统，可以看到，该条 Post 记录里面的 ACL 列的内容如下：
```javascript
{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true},"55f1572460b2ce30e8b7afde":{"write":true}}
```
从结果可以看出，该条 Post 已经允许 Id 为 55b9df0400b0f6d7efaa8801 以及 55f1572460b2ce30e8b7afde 两个用户（MAS.User）可以修改，他们拥有 write：ture 的权限，也就是「写」权限。

基于用户的权限管理比较简单直接，理解起来成本较低。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.3 局限性探讨</a>
再进一步的场景： 论坛升级，需要一个特定的管理员（Administrator）来统一管理论坛的帖子，他可以修改帖子的内容，删除不合适的帖子。

论坛升级之后，用户发布帖子的步骤需要针对上一小节做如下调整：

1. 写一篇帖子
2. 设置帖子的「读」权限为所有人。
3. 设置帖子的「写」权限为作者以及管理员
4. 保存帖子

我们可以设想一下，每当论坛产生一篇帖子，就得为管理员添加这篇帖子的「写」权限。

假如做权限管理功能的时候都依赖基于用户的权限管理，那么一旦产生变化就会发现这种实现方式的局限性。

比如新增了一个管理员，新的管理员需要针对目前论坛所有的帖子拥有管理员应有的权限，那么我们需要把数据库现有的所有帖子循环一遍，为新的管理员增加「写」权限。

假如论坛又一次升级了，付费会员享有特殊帖子的读权限，那么我们需要在发布新帖子的时候，设置「读」权限给部分人（付费会员）。这需要查询所有付费会员并一一设置。

毫无疑问，这种实现方式是完全失控的，基于用户的权限管理，在针对简单的私密分享类的应用是可行的，但是一旦产生需求变更，这种实现方式是不被推荐的。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.4 基于角色的权限设置</a>
管理员，会员，普通用户这三种概念在程序设计中，被定义为「角色」。 我们可以看出，在列出的需求场景中，「权限」的作用是用来区分某一数据是否允许某种角色的用户进行操作。

「权限」只和「角色」对应，而用户也和「角色」对应，为用户赋予「角色」，然后管理「角色」的权限，完成了权限与用户的解耦。

因此我们来解释 MAS 中「权限」和「角色」的概念。

「权限」在 MAS 服务端只存在两种权限：读、写。 「角色」在 MAS 服务端没有限制，唯一要求的就是在一个应用内，角色的名字唯一即可，至于某一个「角色」在当前应用内对某条数据是否拥有读写的「权限」应该是有开发者的业务逻辑决定，而 MAS 提供了一系列的接口帮助开发者快速实现基于角色的权限管理。

为了方便开发者实现基于角色的权限管理，MAS在 SDK 中集成了一套完整的 ACL (Access Control List) 系统。通俗的解释就是为每一个数据创建一个访问的白名单列表，只有在名单上的用户（MAS.User）或者具有某种角色（MAS.Role）的用户才能被允许访问。

为了更好地保证用户数据安全性， MAS 表中每一张都有一个 ACL 列。当然，MAS 还提供了进一步的读写权限控制。

一个 User 必须拥有读权限（或者属于一个拥有读权限的 Role）才可以获取一个对象的数据，同时，一个 User 需要写权限（或者属于一个拥有写权限的 Role）才可以更改或者删除一个对象。下面列举几种常见的 ACL 使用范例。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.5 ACL 权限管理</a>

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.5.1  默认权限</a>

在没有显式指定的情况下，LeanCloud 中的每一个对象都会有一个默认的 ACL 值。这个值代表了所有的用户对这个对象都是可读可写的。此时你可以在数据管理的表中 ACL 属性中看到这样的值:
```javascript
  {"*":{"read":true,"write":true}}
```

在 基于用户的权限管理 中，已经在代码里面演示了通过 ACL 来实现基于用户的权限管理，那么基于角色的权限管理也是依赖 ACL 来实现的，只是在介绍详细的操作之前需要介绍「角色」这个重要的概念。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.6 角色的权限管理</a>

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.6.1 角色的创建</a>
首先，我们来创建一个 Administrator 的角色。

这里有一个需要特别注意的地方，因为 MAS.Role 本身也是一个 AVObject，它自身也有 ACL 控制，并且它的权限控制应该更严谨，如同「论坛的管理员有权力任命版主，而版主无权任命管理员」一样的道理，所以创建角色的时候需要显式地设定该角色的 ACL，而角色是一种较为稳定的对象：

```javascript
 // 新建一个角色，并把为当前用户赋予该角色
 var roleAcl = new MAS.ACL();
 roleAcl.setPublicReadAccess(true);
 roleAcl.setPublicWriteAccess(false);

 // 当前用户是该角色的创建者，因此具备对该角色的写权限
 roleAcl.setWriteAccess(MAS.User.current true);

 //新建角色
 var administratorRole = new MAS.Role('Administrator', roleAcl);
 administratorRole.save().then(function(role) {
   // 创建成功
 }).catch(function() {
 
 });
```

执行完毕之后，可以查看 _Role 表里已经存在了一个 Administrator 的角色。 另外需要注意的是：可以直接通过 系统的权限设置 直接设置权限。并且我们要强调的是：

ACL 可以精确到 Class，也可以精确到具体的每一个对象（表中的每一条记录）。

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.6.2  为对象设置角色的访问权限</a>

我们现在已经创建了一个有效的角色，接下来为 Post 对象设置 Administrator 的访问「可读可写」的权限，设置成功以后，任何具备 Administrator 角色的用户都可以对 Post 对象进行「可读可写」的操作了：

```javascript
// 新建一个帖子对象
 var Post = MAS.Object.extend('Post');
 var post = new Post();
 post.set('title', '大家好，我是新人');

 // 新建一个角色，并把为当前用户赋予该角色
 var administratorRole = new MAS.Role('Administrator');

 //为当前用户赋予该角色
 administratorRole.addUser(MAS.User.current);

 //角色保存成功
 administratorRole.save().then(function(administratorRole) {
   // 新建一个 ACL 实例
   var acl = new MAS.ACL();
   acl.setPublicReadAccess(true);
   acl.setRoleWriteAccess(administratorRole, true);

   // 将 ACL 实例赋予 Post 对象
   post.setACL(acl);
   return post.save();
 }).then(function(post) {
   // 保存成功
 }).catch(function() {
 
 });
```

##### <a href="javscript:void(0)" name="storage-role" id="storage-role">5.1.6.3  用户角色的赋予和剥夺</a>
经过以上两步，我们还差一个给具体的用户设置角色的操作，这样才可以完整地实现基于角色的权限管理。

在通常情况下，角色和用户之间本是多对多的关系，比如需要把某一个用户提升为某一个版块的版主，亦或者某一个用户被剥夺了版主的权力，以此类推，在应用的版本迭代中，用户的角色都会存在增加或者减少的可能，因此，MAS 也提供了为用户赋予或者剥夺角色的方式。 注意：在代码级别，为角色添加用户 与 为用户赋予角色 实现的代码是一样的。 此类操作的逻辑顺序是：
* 赋予角色：首先判断该用户是否已经被赋予该角色，如果已经存在则无需添加，如果不存在则将该用户(MAS.User)添加到角色实例中。

```javascript
// 构建 MAS.Role 的查询
 var roleQuery = new MAS.Query('_Role');
 roleQuery.equalTo('name', 'Administrator');
 roleQuery.find().then(function(results) {
   if (results.length > 0) {

     // 如果角色存在
     var administratorRole = results[0];
     roleQuery.containedIn('users', MAS.User.current.getId());
     return roleQuery.find();
   } else {
     // 如果角色不存在新建角色
     var administratorRole = new MAS.Role('Administrator');
     
     //为当前用户赋予该角色
     administratorRole.addUser(MAS.User.current)
     administratorRole.save();
   }
 }).then(function(userForRole) {
   //该角色存在，但是当前用户未被赋予该角色
   if (userForRole.length === 0) {
     // 为当前用户赋予该角色
     var administratorRole = new MAS.Role('Administrator');
     administratorRole.addUser(MAS.User.current)
     administratorRole.save();
   }
 }).catch(function() {
 
 });
```


角色赋予成功之后，基于角色的权限管理的功能才算完成。

另外，此处不得不提及的就是角色的剥夺：

* 剥夺角色： 首先判断该用户是否已经被赋予该角色，如果未曾赋予则不做修改，如果已被赋予，则将对应的用户（MAS.User）从该角色中删除。

```javascript
// 构建 MAS.Role 的查询
var roleQuery = new MAS.Query('_Role');
roleQuery.equalTo('name', 'Moderator');
roleQuery.find().then(function(results) {
 // 如果角色存在
 if (results.length > 0) {
   var moderatorRole = results[0];
   roleQuery.containedIn('users', MAS.User.current.getId())
   return roleQuery.find();
 }
}).then(function(userForRole) {
 //该角色存在，并且也拥有该角色
 if (userForRole.length > 0) {
   // 剥夺角色
   moderatorRole.removeUser(MAS.User.current());
   return moderatorRole.save();
 }
}).then(function() {
 // 保存成功
}).catch(function() {

});
```

### <a href="javscript:void(0)" name="cache" id="cache">6. 缓存</a>
MAS.Cache是对Redis的代理，Cache能加快查询，减少数据库的压力，目前Cache能支持大部分的Redis方法（但例如pub和sub是不被允许的），如果不熟悉Redis的API，你可以从[这里](http://doc.redisfans.com/)学习如何使用。

例如我们使用缓存来规避用户反复提交数据，其实现如下：
```javascript
var MAX_SUBMIT_COUNT = 20;
var objectId = MAS.User.current.getId();
var key = 'qmtv_cache_' + objectId;
MAS.Cache.command('get ' + key).then(function(body){
	var count = 0;
	if(body.data && body.data.result){
		count = Number(body.data.result);
	}
	
	if(count >= 20){
		return Promise.reject(new Error('submit too often'));
	}
	
	count += 1;
	return MAS.Cache.command('set ' + key + ' ' + count);
}).then(function(){
	return MAS.Cache.command('expire ' + key + ' 3600');
}).then(function(){
	// 其余逻辑
}).catch(function(){
	// 拒绝此次提交
});
```

### <a href="javscript:void(0)" name="httpProxy" id="httpProxy">7. HTTP请求代理</a>
在开发过程中，我们可能会对一些接口进行HTTP请求，例如请求PC主站的主播列表，或者说我们需要去调用后端的Service接口。一般情况下我们可以通过CORS来进行跨域，但是为了安全起见，PC主站或一些Service接口只允许特定域下的CORS，这个时候我们就可以使用HTTP请求代理功能方便的获得这些数据，例如：
```javascript

MAS.HttpProxy.send(
    "GET",
    'http://www.quanmin.tv'
).then(function () {
    done();
});

// application/www-form-urlencode
MAS.HttpProxy.send(
	"POST",
	'http://www.quanmin.tv/homeapi/rank',
	{p: {}},
	{"Content-Type": "application/json"}
).then(function () {
   done();
});

// multipart/form-data
MAS.HttpProxy.send(
	"POST",
	'http://www.quanmin.tv/homeapi/rank',
	{p: {}},
	null,
	__dirname + '/conf.js'
).then(function () {
   done();
});
```
 注意，在node环境中，文件只需要是一个文件路径即可，但在浏览器环境中，需要传入file对象。
 
### <a href="javscript:void(0)" name="mail" id="mail">8. 邮件</a>
```javascript

// 单发邮件
MAS.Mail(
    "395693101@qq.com", // from
    "395693101@qq.com", // to
    "test1", // subject
    "hello world!" // html
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});

// 群发邮件
MAS.Mail(
    "395693101@qq.com",
    ["395693101@qq.com", "395693102@qq.com"],
    "test1",
    "hello world!"
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});

// 携带文件
MAS.Mail(
    "395693101@qq.com",
    "395693101@qq.com",
    "test2",
    "hello world!",
    __dirname + '/conf.js'
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});
```
 注意，在node环境中，文件只需要是一个文件路径即可，但在浏览器环境中，需要传入file对象。

### <a href="javscript:void(0)" name="sms" id="sms">9. 短信</a>
```javascript
// 单发短信
MAS.SMS(
	'18500742221', 
	'just a test! num 0!'
).then(function (res) {
   if (res.body && res.body.error == 0) {
       done();
   }
});

// 群发短信
MAS.SMS(
	['18500742221', '17011964287'], 
	'just a test! num 1!'
).then(function (res) {
    if (res.body && res.body.error == 0) {
        done();
    }
});
```

### <a href="javscript:void(0)" name="upload" id="upload">10. 文件上传</a>
```javascript
MAS.UploadFile(__dirname + '/conf.js').then(function (res) {
    if (res.body && res.body.error == 0) {
        console.log(res.body.data.url);
    }
})
```

### <a href="javscript:void(0)" name="report" id="report">11. 报表服务</a>
在MIS开发中我们会涉及到导出功能，为了满足这一需求，我们抽象了报表服务，快速的进行报表的开发，其使用涉及到create/writeHeaders/writeData/close四个API
```javascript
var report = new MAS.Report();
report.create({
    type: MAS.Report.TYPE.EXCEL, // 支持excel: MAS.Report.TYPE.EXCEL，csv：MAS.Report.TYPE.CSV
    mailOpt: { // 是否通过邮件发送生成的报表，可选
        from: '395693101@qq.com',
        to: '395693101@qq.com',
        subject: 'report test!',
        html: 'xlsx report test!'
    },
    callbackOpt: { // 是否进行回调，获得生成的报表
        url: 'http://115.159.63.176/mas/report/receive', // 回调地址
        ext: JSON.stringify({username: 1})  // 附带参数
    }
}).then(function () {
    return report.writeHeaders([{ // 头部信息，可选
        "header": "Id",
        "key": "id",
        "width": 50
    }, {
        "header": "Name",
        "key": "name",
        "width": 50
    }, {
        "header": "D.O.B.",
        "key": "DOB",
        "width": 50
    }]);
}).then(function () {
    return report.writeData([ // 写入报表数据
        {
            "id": 1,
            "name": "John Doe",
            "DOB": "2016-01-01 12:00"
        }, {
            "id": 2,
            "name": "Jane Doe",
            "DOB": "2016-03-03 13:00"
        }
    ]);
}).then(function(){
    return report.writeData([
        {
            "id": 1,
            "name": "John Doe",
            "DOB": "2016-01-01 12:00"
        }, {
            "id": 2,
            "name": "Jane Doe",
            "DOB": "2016-03-03 13:00"
        }
    ]);
}).then(function () {
    return report.close();
}).then(function () {
    done();
})

```