diff --git a/.gitignore b/.gitignore index 1964a1f92..469b916fe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ coverage *.swp dist/js-sdk-api-docs npm-debug.log -demo/test-es5.js .nyc_output dist docs diff --git a/.travis.yml b/.travis.yml index 28aea5303..142b05d4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,15 @@ node_js: - "4" sudo: false - +env: + global: + - REGION=us + - APPID=QvNM6AG2khJtBQo6WRMWqfLV-gzGzoHsz + - APPKEY=be2YmUduiuEnCB2VR9bLRnnV + - MASTERKEY=1AqFJWElESSui6JKqHiKnLTY + - HOOKKEY=Y7RVPi20qOKQg4Lp8CyY35Lq + - STATUS_TARGET_USER_ID=57d7b3c28a51a2004eb9b31d + - FILE_ID=577258d732070000567dea7e before_install: - if [[ `npm -v` != 3* ]]; then npm i -g npm; fi install: @@ -14,7 +22,6 @@ script: - npm test && codecov - npm run build after_success: - - if [[ "$TRAVIS_BRANCH" == "master" ]] && [[ "${TRAVIS_PULL_REQUEST}" = "false" ]]; then + - if [[ "$TRAVIS_BRANCH" == "v2" ]] && [[ "${TRAVIS_PULL_REQUEST}" = "false" ]]; then ./script/release.sh; - ./script/deploy.sh; fi diff --git a/README.md b/README.md index 24d9b53ac..3b6cca9bb 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ bower install leancloud-storage --save * `fork` 这个项目 * `npm install` 安装相关依赖 * 开发和调试 - * 浏览器环境执行 `gulp dev`,会自动启动 `demo` 目录,可在 `test-es6.js` 中修改和测试,`test-es5.js` 为自动生成的代码 - * Nodejs 环境同样在 `demo` 目录中,通过执行 `node test-es6.js` 开发与调试。推荐安装 `node inspector` 来调试,安装后执行 `node-debug test-es6.js`。每次修改代码后,如果开发代码引用的是 dist 目录中的代码,需要执行 `gulp release` * 确保测试全部通过 `npm run test`,浏览器环境打开 `test/test.html` * 提交并发起 `Pull Request` diff --git a/bower.json b/bower.json index 9a625bd7e..aeea71ebf 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "leancloud-storage", - "version": "2.1.2", + "version": "2.5.4", "homepage": "https://github.com/leancloud/javascript-sdk", "authors": [ "LeanCloud " diff --git a/changelog.md b/changelog.md index 0d10f0dbf..1f69fc47f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,70 @@ +## 2.5.4 (2017-09-25) +### Bug Fixes +* 修复了使用应用内社交模块 `inboxQuery` 查询时可能出现 `URI too long` 异常的问题。 + +## 2.5.3 (2017-08-01) +### Bug Fixes +* 修复了一些 TypeScript 定义文件的问题。 + +## 2.5.2 (2017-07-03) +### Bug Fixes +* 修复了使用 `new AV.User(data, { parse: true })` 方式构造的 User 没有数据的问题。 + +## 2.5.1 (2017-06-28) +### Bug Fixes +* 修复了应用内社交模块对 AuthOptions 支持不完整的问题 +* 修复了应用内社交模块在云引擎中使用时错误的打印了 `AV.User.currentAsync` 方法不可用警告的问题 + +# 2.5.0 (2017-06-01) +### Bug Fixes +* 修复了查询 `Role` 时错误的打印了 deprecation 警告的问题 + +### Features +* `User#follow` 增加了一种重载,现在可以通过 `options.attributes` 参数为创建的 `Follower` 与 `Followee` 增加自定义属性,方便之后通过 `User#followerQuery` 与 `User#followerQuery` 进行查询。 + +# 2.4.0 (2017-05-19) +### Bug Fixes +* **可能导致不兼容** 修复了 `Query#get` 方法在目标对象不存在的情况下会返回一个没有数据的 `AV.Object` 实例的问题,现在该方法会正确地抛出 `Object not found` 异常。这个问题是在 2.0.0 版本中引入的。 + +### Features +* 增加了 `Conversation#broadcast` 方法用于广播系统消息 + +## 2.3.2 (2017-05-12) +### Bug Fixes +* 修复了获取图形验证码会导致栈溢出的问题。 + +# 2.3.0 (2017-05-11) +### Features +* 增加了 `AV.Conversation` 类。现在可以直接使用 SDK 来创建、管理会话,发送消息。 +* 改进了验证码 API。增加了 `AV.Captcha`,可以通过 `AV.Captcha.request` 方法获取一个 Captcha 实例。特别的,在浏览器中,可以直接使用 `Captcha#bind` 方法将 Captcha 与 DOM 元素进行绑定。 + +## 2.2.1 (2017-04-26) +### Bug Fixes +* 修复了 `User.requestLoginSmsCode`,`User.requestMobilePhoneVerify` 与 `User.requestPasswordResetBySmsCode` 方法 `authOptions.validateToken` 参数的拼写错误。 + +# 2.2.0 (2017-04-25) +### Bug Fixes +* 修复了 Safari 隐身模式下用户无法登录的问题 + +### Features +* 短信支持图形验证码(需要在控制台应用选项「启用短信图形验证码」) + * 新增 `Cloud.requestCaptcha` 与 `Cloud.verifyCaptcha` 方法请求、校验图形验证码。 + * `Cloud.requestSmsCode`,`User.requestLoginSmsCode`,`User.requestMobilePhoneVerify` 与 `User.requestPasswordResetBySmsCode` 方法增加了 `authOptions.validateToken` 参数。没有提供有效的 validateToken 的请求会被拒绝。 +* 支持客户端查询 ACL(需要在控制台应用选项启用「查询时返回值包括 ACL」) + * 增加 `Query#includeACL` 方法。 + * `Object#fetch` 与 `File#fetch` 方法增加了 `fetchOptions.includeACL` 参数。 + +## 2.1.4 (2017-03-27) +### Bug Fixes +* 如果在创建 `Role` 时不指定 `acl` 参数,SDK 会自动为其设置一个「默认 acl」,这导致了通过 Query 得到或使用 `Object.createWithoutData` 方法得到 `Role` 也会被意外的设置 acl。这个版本修复了这个问题。 +* 修复了在 React Native for Android 中使用 blob 方式上传文件失败的问题 + +## 2.1.3 (2017-03-13) +### Bug Fixes +* 修复了调用 `User#refreshSessionToken` 刷新用户的 sessionToken 后本地存储中的用户没有更新的问题 +* 修复了初始化可能会造成 disableCurrentUser 配置失效的问题 +* 修复了 `Query#destroyAll` 方法 `options` 参数无效的问题 + ## 2.1.2 (2017-02-17) ### Bug Fixes * 修复了文件上传时,如果 `fileName` 没有指定扩展名会导致上传文件 `mime-type` 不符合预期的问题 @@ -7,7 +74,7 @@ ### Bug Fixes * 修复了使用 masterKey 获取一个 object 后再次 save 可能会报 ACL 格式不正确的问题。 -## 2.1.0 (2017-01-20) +# 2.1.0 (2017-01-20) ### Bug Fixes * 修复了 `File#toJSON` 序列化结果中缺失 objectId 等字段的问题 * 修复了使用 `Query#containsAll`、`Query#containedIn` 或 `Query#notContainedIn` 方法传入大数组时查询结果可能为空的问题 diff --git a/demo/index.html b/demo/index.html index 8d3f9fb4a..7f447813f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -10,8 +10,11 @@

LeanCloud

为开发加速

欢迎调试 JavaScript SDK,请打开浏览器控制台

+ + +
- + diff --git a/demo/test-es6.js b/demo/test-es6.js deleted file mode 100644 index c26f74518..000000000 --- a/demo/test-es6.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - 请参考 README.md 中的开发方式, - 执行 gulp dev 该文件会被编译为 test-es5.js 并自动运行此文件 -*/ - -/* eslint no-console: ["error", { allow: ["log"] }] */ -/* eslint no-undef: ["error", { "AV": true }] */ - -'use strict'; - -let av; - -// 检测是否在 Nodejs 环境下运行 -if (typeof(process) !== 'undefined' && process.versions && process.versions.node) { - av = require('../dist/node/av'); -} else { - av = window.AV; -} - -// 初始化 -const appId = 'a5CDnmOX94uSth8foK9mjHfq-gzGzoHsz'; -const appKey = 'Ue3h6la9zH0IxkUJmyhLjk9h'; -const region = 'cn'; - -// const appId = 'QvNM6AG2khJtBQo6WRMWqfLV-gzGzoHsz'; -// const appKey = 'be2YmUduiuEnCB2VR9bLRnnV'; -// const region = 'us'; - -av.init({ appId, appKey, region }); - -// 基本存储 -const TestClass = av.Object.extend('TestClass'); -const testObj = new TestClass(); -testObj.set({ - name: 'hjiang', - phone: '123123123', -}); - -testObj.save().then(() => { - console.log('success'); -}).catch((err) => { - console.log('failed'); - console.log(err); -}); - -// 存储文件 -const base64 = 'd29ya2luZyBhdCBhdm9zY2xvdWQgaXMgZ3JlYXQh'; -const file = new av.File('myfile.txt', { base64 }); -file.metaData('format', 'txt file'); -file.save().then(() => { - console.log(file.get('url')); -}).catch((error) => { - console.log(error); -}); - -// 查找文件 -const query = new av.Query(TestClass); -query.equalTo('name', 'hjiang'); -query.find().then((list) => { - console.log(list); -}); - -// 用户登录 -AV.User.login('ttt', '123456') -.then((res) => console.log(res)) -.catch(err => console.log(err)); diff --git a/demo/test.js b/demo/test.js new file mode 100644 index 000000000..349be37c5 --- /dev/null +++ b/demo/test.js @@ -0,0 +1,30 @@ +var av = void 0; + +// 检测是否在 Nodejs 环境下运行 +if (typeof process !== 'undefined' && process.versions && process.versions.node) { + av = require('../dist/node/av'); +} else { + av = window.AV; +} + +// 初始化 +var appId = 'a5CDnmOX94uSth8foK9mjHfq-gzGzoHsz'; +var appKey = 'Ue3h6la9zH0IxkUJmyhLjk9h'; +var region = 'cn'; + +// const appId = 'QvNM6AG2khJtBQo6WRMWqfLV-gzGzoHsz'; +// const appKey = 'be2YmUduiuEnCB2VR9bLRnnV'; +// const region = 'us'; + +av.init({ appId: appId, appKey: appKey, region: region }); + +av.Captcha.request().then(captcha => { + captcha.bind({ + textInput: 'code', + image: 'captcha', + verifyButton: 'verify', + }, { + success: validateCode => console.log('validateCode: ' + validateCode), + error: console.error, + }); +}); diff --git a/package.json b/package.json index bf332f2d8..f8763c5df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "leancloud-storage", - "version": "2.1.2", + "version": "2.5.4", "main": "./dist/node/index.js", "description": "LeanCloud JavaScript SDK.", "repository": { @@ -8,7 +8,9 @@ "url": "https://github.com/leancloud/javascript-sdk" }, "scripts": { - "test": "NODE_ENV=test nyc --reporter lcov --reporter text mocha --timeout 300000 test/index.js", + "lint": "tsc storage.d.ts", + "test": "npm run lint && npm run test:node", + "test:node": "NODE_ENV=test nyc --reporter lcov --reporter text mocha --timeout 300000 test/index.js", "docs": "jsdoc src README.md package.json -d docs -c .jsdocrc.json", "build:node": "gulp babel-node", "build:browser": "CLIENT_PLATFORM=Browser webpack --config webpack/browser.js", @@ -16,7 +18,8 @@ "build:weapp": "CLIENT_PLATFORM=Weapp webpack --config webpack/weapp.js", "uglify:browser": "cd dist; uglifyjs av.js -m -c -o av-min.js --in-source-map av.js.map --source-map av-min.js.map; cd ..;", "uglify:weapp": "cd dist; uglifyjs av-weapp.js -m -c -o av-weapp-min.js --in-source-map av-weapp.js.map --source-map av-weapp-min.js.map; cd ..;", - "build": "gulp build" + "build": "gulp build", + "prepublishOnly": "./script/check-version.js" }, "dependencies": { "debug": "^2.2.0", @@ -49,6 +52,7 @@ "nyc": "^8.1.0", "should": "^11.1.0", "uglify-js": "git+https://github.com/Swaagie/UglifyJS2.git#fcb4f2f21584dc5b21af4c10e17733e1686135e4", + "typescript": "^2.4.1", "weapp-polyfill": "^1.1.0", "webpack": "^2.2.0-rc.3" }, diff --git a/script/check-version.js b/script/check-version.js new file mode 100755 index 000000000..d5f5b171c --- /dev/null +++ b/script/check-version.js @@ -0,0 +1,4 @@ +#!/usr/bin/env node +const assert = require('assert'); +assert(require('../').version === require('../package.json').version); +assert(require('../bower.json').version === require('../package.json').version); diff --git a/script/release.sh b/script/release.sh index 5163a3343..935b8c504 100755 --- a/script/release.sh +++ b/script/release.sh @@ -8,6 +8,6 @@ test "$(git config user.name)" = '' && ( ) git add dist -f; git commit -m "chore(build): build ${REV} [skip ci]"; -git push -qf https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git ${BRANCH}:dist > /dev/null 2>&1; +git push -qf https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git ${BRANCH}:v2-dist > /dev/null 2>&1; git reset HEAD~1; echo "done."; diff --git a/src/av.js b/src/av.js index ab3e7c06f..43719ef79 100644 --- a/src/av.js +++ b/src/av.js @@ -291,7 +291,7 @@ AV._decode = function(value, key) { var className; if (value.__type === "Pointer") { className = value.className; - var pointer = AV.Object._create(className); + var pointer = AV.Object._create(className, undefined, undefined, /* noDefaultACL*/ true); if(Object.keys(value).length > 3) { const v = _.clone(value); delete v.__type; @@ -308,7 +308,7 @@ AV._decode = function(value, key) { const v = _.clone(value); delete v.__type; delete v.className; - var object = AV.Object._create(className); + var object = AV.Object._create(className, undefined, undefined, /* noDefaultACL*/ true); object._finishFetch(v, true); return object; } diff --git a/src/captcha.js b/src/captcha.js new file mode 100644 index 000000000..3c66903a6 --- /dev/null +++ b/src/captcha.js @@ -0,0 +1,142 @@ +const { tap } = require('./utils'); + +module.exports = (AV) => { + /** + * @class + * @example + * AV.Captcha.request().then(captcha => { + * captcha.bind({ + * textInput: 'code', // the id for textInput + * image: 'captcha', + * verifyButton: 'verify', + * }, { + * success: (validateCode) => {}, // next step + * error: (error) => {}, // present error.message to user + * }); + * }); + */ + AV.Captcha = function Captcha(options, authOptions) { + this._options = options; + this._authOptions = authOptions; + /** + * The image url of the captcha + * @type string + */ + this.url = undefined; + /** + * The captchaToken of the captcha. + * @type string + */ + this.captchaToken = undefined; + /** + * The validateToken of the captcha. + * @type string + */ + this.validateToken = undefined; + }; + + /** + * Refresh the captcha + * @return {Promise.} a new capcha url + */ + AV.Captcha.prototype.refresh = function refresh() { + return AV.Cloud._requestCaptcha(this._options, this._authOptions).then(({ + captchaToken, url, + }) => { + Object.assign(this, { captchaToken, url }); + return url; + }); + }; + + /** + * Verify the captcha + * @param {String} code The code from user input + * @return {Promise.} validateToken if the code is valid + */ + AV.Captcha.prototype.verify = function verify(code) { + return AV.Cloud.verifyCaptcha(code, this.captchaToken) + .then(tap(validateToken => (this.validateToken = validateToken))); + }; + + if (process.env.CLIENT_PLATFORM === 'Browser') { + /** + * Bind the captcha to HTMLElements. ONLY AVAILABLE in browsers. + * @param [elements] + * @param {String|HTMLInputElement} [elements.textInput] An input element typed text, or the id for the element. + * @param {String|HTMLImageElement} [elements.image] An image element, or the id for the element. + * @param {String|HTMLElement} [elements.verifyButton] A button element, or the id for the element. + * @param [callbacks] + * @param {Function} [callbacks.success] Success callback will be called if the code is verified. The param `validateCode` can be used for further SMS request. + * @param {Function} [callbacks.error] Error callback will be called if something goes wrong, detailed in param `error.message`. + */ + AV.Captcha.prototype.bind = function bind({ + textInput, + image, + verifyButton, + }, { + success, + error, + }) { + if (typeof textInput === 'string') { + textInput = document.getElementById(textInput); + if (!textInput) throw new Error(`textInput with id ${textInput} not found`); + } + if (typeof image === 'string') { + image = document.getElementById(image); + if (!image) throw new Error(`image with id ${image} not found`); + } + if (typeof verifyButton === 'string') { + verifyButton = document.getElementById(verifyButton); + if (!verifyButton) throw new Error(`verifyButton with id ${verifyButton} not found`); + } + + this.__refresh = () => this.refresh().then(url => { + image.src = url; + if (textInput) { + textInput.value = ''; + textInput.focus(); + } + }).catch(err => console.warn(`refresh captcha fail: ${err.message}`)); + if (image) { + this.__image = image; + image.src = this.url; + image.addEventListener('click', this.__refresh); + } + + this.__verify = () => { + const code = textInput.value; + this.verify(code).catch(err => { + this.__refresh(); + throw err; + }).then(success, error).catch(err => console.warn(`verify captcha fail: ${err.message}`)); + }; + if (textInput && verifyButton) { + this.__verifyButton = verifyButton; + verifyButton.addEventListener('click', this.__verify); + } + }; + + /** + * unbind the captcha from HTMLElements. ONLY AVAILABLE in browsers. + */ + AV.Captcha.prototype.unbind = function unbind() { + if (this.__image) this.__image.removeEventListener('click', this.__refresh); + if (this.__verifyButton) this.__verifyButton.removeEventListener('click', this.__verify); + }; + } + + + /** + * Request a captcha + * @param [options] + * @param {Number} [options.width] width(px) of the captcha, ranged 60-200 + * @param {Number} [options.height] height(px) of the captcha, ranged 30-100 + * @param {Number} [options.size=4] length of the captcha, ranged 3-6. MasterKey required. + * @param {Number} [options.ttl=60] time to live(s), ranged 10-180. MasterKey required. + * @return {Promise.} + */ + AV.Captcha.request = (options, authOptions) => { + const captcha = new AV.Captcha(options, authOptions); + return captcha.refresh().then(() => captcha); + }; +}; diff --git a/src/cloudfunction.js b/src/cloudfunction.js index 9700f7926..4b7e6bf48 100644 --- a/src/cloudfunction.js +++ b/src/cloudfunction.js @@ -1,5 +1,6 @@ const _ = require('underscore'); const AVRequest = require('./request').request; +const Promise = require('./promise'); module.exports = function(AV) { /** @@ -9,6 +10,7 @@ module.exports = function(AV) { *

* * @namespace + * @borrows AV.Captcha.request as requestCaptcha */ AV.Cloud = AV.Cloud || {}; @@ -64,20 +66,29 @@ module.exports = function(AV) { /** * Makes a call to request a sms code for operation verification. - * @param {Object} data The mobile phone number string or a JSON - * object that contains mobilePhoneNumber,template,op,ttl,name etc. - * @return {Promise} A promise that will be resolved with the result - * of the function. + * @param {String|Object} data The mobile phone number string or a JSON + * object that contains mobilePhoneNumber,template,sign,op,ttl,name etc. + * @param {String} data.mobilePhoneNumber + * @param {String} [data.template] sms template name + * @param {String} [data.sign] sms signature name + * @param {AuthOptions} [options] AuthOptions plus: + * @param {String} [options.validateToken] a validate token returned by {@link AV.Cloud.verifyCaptcha} + * @return {Promise} A promise that will be resolved if the request succeed */ - requestSmsCode: function(data){ + requestSmsCode: function(data, options = {}) { if(_.isString(data)) { data = { mobilePhoneNumber: data }; } if(!data.mobilePhoneNumber) { throw new Error('Missing mobilePhoneNumber.'); } + if (options.validateToken) { + data = _.extend({}, data, { + validate_token: options.validateToken, + }); + } var request = AVRequest("requestSmsCode", null, null, 'POST', - data); + data, options); return request; }, @@ -99,6 +110,35 @@ module.exports = function(AV) { var request = AVRequest("verifySmsCode", code, null, 'POST', params); return request; - } + }, + + _requestCaptcha(options, authOptions) { + return AVRequest('requestCaptcha', null, null, 'GET', options, authOptions).then(({ + captcha_url: url, + captcha_token: captchaToken, + }) => ({ + captchaToken, + url, + })); + }, + + /** + * Request a captcha. + */ + requestCaptcha: AV.Captcha.request, + + /** + * Verify captcha code. This is the low-level API for captcha. + * Checkout {@link AV.Captcha} for high abstract APIs. + * @param {String} code the code from user input + * @param {String} captchaToken captchaToken returned by {@link AV.Cloud.requestCaptcha} + * @return {Promise.} validateToken if the code is valid + */ + verifyCaptcha(code, captchaToken) { + return AVRequest('verifyCaptcha', null, null, 'POST', { + captcha_code: code, + captcha_token: captchaToken, + }).then(({ validate_token: validateToken }) => validateToken); + }, }); }; diff --git a/src/conversation.js b/src/conversation.js new file mode 100644 index 000000000..5db13a227 --- /dev/null +++ b/src/conversation.js @@ -0,0 +1,176 @@ +'use strict'; + +const _ = require('underscore'); +const request = require('./request').request; +const AV = require('./av'); + +/** + *

An AV.Conversation is a local representation of a LeanCloud realtime's + * conversation. This class is a subclass of AV.Object, and retains the + * same functionality of an AV.Object, but also extends it with various + * conversation specific methods, like get members, creators of this conversation. + *

+ * + * @class AV.Conversation + * @param {String} name The name of the Role to create. + * @param {Boolean} [options.isSystem] Set this conversation as system conversation. + * @param {Boolean} [options.isTransient] Set this conversation as transient conversation. + */ +module.exports = AV.Object.extend('_Conversation', /** @lends AV.Conversation.prototype */ { + constructor: function(name, options = {}) { + AV.Object.prototype.constructor.call(this, null, null); + this.set('name', name); + if (options.isSystem !== undefined) { + this.set('sys', options.isSystem ? true: false); + } + if (options.isTransient !== undefined) { + this.set('tr', options.isTransient ? true : false); + } + }, + /** + * Get current conversation's creator. + * + * @return {String} + */ + getCreator: function() { + return this.get('c'); + }, + + /** + * Get the last message's time. + * + * @return {Date} + */ + getLastMessageAt: function() { + return this.get('lm'); + }, + + /** + * Get this conversation's members + * + * @return {String[]} + */ + getMembers: function() { + return this.get('m'); + }, + + /** + * Add a member to this conversation + * + * @param {String} member + */ + addMember: function(member) { + return this.add('m', member); + }, + + /** + * Get this conversation's members who set this conversation as muted. + * + * @return {String[]} + */ + getMutedMembers: function() { + return this.get('mu'); + }, + + /** + * Get this conversation's name field. + * + * @return String + */ + getName: function() { + return this.get('name'); + }, + + /** + * Returns true if this conversation is transient conversation. + * + * @return {Boolean} + */ + isTransient: function() { + return this.get('tr'); + }, + + /** + * Returns true if this conversation is system conversation. + * + * @return {Boolean} + */ + isSystem: function() { + return this.get('sys'); + }, + + /** + * Send realtime message to this conversation, using HTTP request. + * + * @param {String} fromClient Sender's client id. + * @param {(String|Object)} message The message which will send to conversation. + * It could be a raw string, or an object with a `toJSON` method, like a + * realtime SDK's Message object. See more: {@link https://leancloud.cn/docs/realtime_guide-js.html#消息} + * @param {Boolean} [options.transient] Whether send this message as transient message or not. + * @param {String[]} [options.toClients] Ids of clients to send to. This option can be used only in system conversation. + * @param {Object} [options.pushData] Push data to this message. See more: {@link https://url.leanapp.cn/pushData 推送消息内容} + * @param {AuthOptions} [authOptions] + * @return {Promise} + */ + send: function(fromClient, message, options={}, authOptions={}) { + if (typeof message.toJSON === 'function') { + message = message.toJSON(); + } + if (typeof message !== 'string') { + message = JSON.stringify(message); + } + const data = { + from_peer: fromClient, + conv_id: this.id, + transient: false, + message: message, + }; + if (options.toClients !== undefined) { + data.to_peers = options.toClients; + } + if (options.transient !== undefined) { + data.transient = options.transient ? true : false; + } + if (options.pushData !== undefined) { + data.push_data = options.pushData; + } + return request('rtm', 'messages', null, 'POST', data, authOptions); + }, + + /** + * Send realtime broadcast message to all clients, with this conversation, using HTTP request. + * + * @param {String} fromClient Sender's client id. + * @param {(String|Object)} message The message which will send to conversation. + * It could be a raw string, or an object with a `toJSON` method, like a + * realtime SDK's Message object. See more: {@link https://leancloud.cn/docs/realtime_guide-js.html#消息}. + * @param {Object} [options.pushData] Push data to this message. See more: {@link https://url.leanapp.cn/pushData 推送消息内容}. + * @param {Object} [options.validTill] The message will valid till this time. + * @param {AuthOptions} [authOptions] + * @return {Promise} + */ + broadcast: function(fromClient, message, options={}, authOptions={}) { + if (typeof message.toJSON === 'function') { + message = message.toJSON(); + } + if (typeof message !== 'string') { + message = JSON.stringify(message); + } + const data = { + from_peer: fromClient, + conv_id: this.id, + message: message, + }; + if (options.pushData !== undefined) { + data.push = options.pushData; + } + if (options.validTill !== undefined) { + let ts = options.validTill; + if (_.isDate(ts)) { + ts = ts.getTime(); + } + options.valid_till = ts; + } + return request('rtm', 'broadcast', null, 'POST', data, authOptions); + } +}); diff --git a/src/file.js b/src/file.js index 6dd03950c..1b8495757 100644 --- a/src/file.js +++ b/src/file.js @@ -5,8 +5,9 @@ const s3 = require('./uploader/s3'); const AVError = require('./error'); const AVRequest = require('./request').request; const Promise = require('./promise'); -const { tap } = require('./utils'); +const { tap, transformFetchOptions } = require('./utils'); const debug = require('debug')('leancloud:file'); +const parseBase64 = require('./utils/parse-base64'); module.exports = function(AV) { @@ -101,7 +102,16 @@ module.exports = function(AV) { base64: '', }; + if (_.isString(data)) { + throw new TypeError("Creating an AV.File from a String is not yet supported."); + } + if (_.isArray(data)) { + this.attributes.metaData.size = data.length; + data = { base64: encodeBase64(data) }; + } + this._extName = ''; + this._data = data; let owner; if (data && data.owner) { @@ -117,46 +127,10 @@ module.exports = function(AV) { } } } - - this.attributes.metaData = { - owner: (owner ? owner.id : 'unknown') - }; + + this.attributes.metaData.owner = owner ? owner.id : 'unknown'; this.set('mime_type', mimeType); - - if (_.isArray(data)) { - this.attributes.metaData.size = data.length; - data = { base64: encodeBase64(data) }; - } - if (data && data.base64) { - var parseBase64 = require('./utils/parse-base64'); - var dataBase64 = parseBase64(data.base64, mimeType); - this._source = Promise.resolve({ data: dataBase64, type: mimeType }); - } else if (data && data.blob) { - if (!data.blob.type && mimeType) { - data.blob.type = mimeType; - } - if (!data.blob.name) { - data.blob.name = name; - } - if (process.env.CLIENT_PLATFORM === 'ReactNative' || process.env.CLIENT_PLATFORM === 'Weapp') { - this._extName = extname(data.blob.uri); - } - this._source = Promise.resolve({ data: data.blob, type: mimeType }); - } else if (typeof File !== "undefined" && data instanceof File) { - if (data.size) { - this.attributes.metaData.size = data.size; - } - if (data.name) { - this._extName = extname(data.name); - } - this._source = Promise.resolve({ data, type: mimeType }); - } else if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) { - this.attributes.metaData.size = data.length; - this._source = Promise.resolve({ data, type: mimeType }); - } else if (_.isString(data)) { - throw new Error("Creating a AV.File from a String is not yet supported."); - } }; /** @@ -450,37 +424,68 @@ module.exports = function(AV) { throw new Error('File already saved. If you want to manipulate a file, use AV.Query to get it.'); } if (!this._previousSave) { - if (this._source) { - this._previousSave = this._source.then(({ data, type }) => - this._fileToken(type) - .then(uploadInfo => { - if (uploadInfo.mime_type) { - this.set('mime_type', uploadInfo.mime_type); + if (this._data) { + let mimeType = this.get('mime_type'); + this._previousSave = this._fileToken(mimeType).then(uploadInfo => { + if (uploadInfo.mime_type) { + mimeType = uploadInfo.mime_type; + this.set('mime_type', mimeType); + } + this._token = uploadInfo.token; + return Promise.resolve().then(() => { + const data = this._data; + if (data && data.base64) { + return parseBase64(data.base64, mimeType); + } + if (data && data.blob) { + if (!data.blob.type && mimeType) { + data.blob.type = mimeType; } - this._token = uploadInfo.token; - - let uploadPromise; - switch (uploadInfo.provider) { - case 's3': - uploadPromise = s3(uploadInfo, data, this, options); - break; - case 'qcloud': - uploadPromise = cos(uploadInfo, data, this, options); - break; - case 'qiniu': - default: - uploadPromise = qiniu(uploadInfo, data, this, options); - break; + if (!data.blob.name) { + data.blob.name = this.get('name'); } - return uploadPromise.then( - tap(() => this._callback(true)), - (error) => { - this._callback(false); - throw error; - } - ); - }) - ); + if (process.env.CLIENT_PLATFORM === 'ReactNative' || process.env.CLIENT_PLATFORM === 'Weapp') { + this._extName = extname(data.blob.uri); + } + return data.blob; + } + if (typeof File !== "undefined" && data instanceof File) { + if (data.size) { + this.attributes.metaData.size = data.size; + } + if (data.name) { + this._extName = extname(data.name); + } + return data; + } + if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) { + this.attributes.metaData.size = data.length; + return data; + } + throw new TypeError('malformed file data'); + }).then(data => { + let uploadPromise; + switch (uploadInfo.provider) { + case 's3': + uploadPromise = s3(uploadInfo, data, this, options); + break; + case 'qcloud': + uploadPromise = cos(uploadInfo, data, this, options); + break; + case 'qiniu': + default: + uploadPromise = qiniu(uploadInfo, data, this, options); + break; + } + return uploadPromise.then( + tap(() => this._callback(true)), + (error) => { + this._callback(false); + throw error; + } + ); + }); + }); } else if (this.attributes.url && this.attributes.metaData.__source === 'external') { // external link file. const data = { @@ -510,19 +515,20 @@ module.exports = function(AV) { result: success, }).catch(debug); delete this._token; + delete this._data; }, /** * fetch the file from server. If the server's representation of the * model differs from its current attributes, they will be overriden, - * @param {AuthOptions} options AuthOptions plus 'keys' and 'include' option. + * @param {Object} fetchOptions Optional options to set 'keys', + * 'include' and 'includeACL' option. + * @param {AuthOptions} options * @return {Promise} A promise that is fulfilled when the fetch * completes. */ - fetch: function(options) { - var options = null; - - var request = AVRequest('files', null, this.id, 'GET', options); + fetch: function(fetchOptions, options) { + var request = AVRequest('files', null, this.id, 'GET', transformFetchOptions(fetchOptions), options); return request.then(this._finishFetch.bind(this)); }, _finishFetch: function(response) { diff --git a/src/index.js b/src/index.js index 6de1aeeae..7bc4f6edf 100644 --- a/src/index.js +++ b/src/index.js @@ -26,12 +26,15 @@ require('./object')(AV); require('./role')(AV); require('./user')(AV); require('./query')(AV); +require('./captcha')(AV); require('./cloudfunction')(AV); require('./push')(AV); require('./status')(AV); require('./search')(AV); require('./insight')(AV); +AV.Conversation = require('./conversation'); + module.exports = AV; /** diff --git a/src/init.js b/src/init.js index 2673a246f..b3b02b241 100644 --- a/src/init.js +++ b/src/init.js @@ -29,32 +29,24 @@ const masterKeyWarn = () => { */ AV.init = (...args) => { - switch (args.length) { - case 1: - const options = args[0]; - if (typeof options === 'object') { - if (process.env.CLIENT_PLATFORM && options.masterKey) { - masterKeyWarn(); - } - initialize(options.appId, options.appKey, options.masterKey, options.hookKey); - request.setServerUrlByRegion(options.region); - AV._config.disableCurrentUser = options.disableCurrentUser; - } else { - throw new Error('AV.init(): Parameter is not correct.'); - } - break; - // 兼容旧版本的初始化方法 - case 2: - case 3: - console.warn('Please use AV.init() to replace AV.initialize(), ' + - 'AV.init() need an Object param, like { appId: \'YOUR_APP_ID\', appKey: \'YOUR_APP_KEY\' } . ' + - 'Docs: https://leancloud.cn/docs/sdk_setup-js.html'); - if (process.env.CLIENT_PLATFORM && args.length === 3) { + if (args.length === 1) { + const options = args[0]; + if (typeof options === 'object') { + if (process.env.CLIENT_PLATFORM && options.masterKey) { masterKeyWarn(); } - initialize(...args); - request.setServerUrlByRegion('cn'); - break; + initialize(options.appId, options.appKey, options.masterKey, options.hookKey); + request.setServerUrlByRegion(options.region); + } else { + throw new Error('AV.init(): Parameter is not correct.'); + } + } else { + // 兼容旧版本的初始化方法 + if (process.env.CLIENT_PLATFORM && args[3]) { + masterKeyWarn(); + } + initialize(...args); + request.setServerUrlByRegion('cn'); } }; diff --git a/src/object.js b/src/object.js index 39ac6e89d..4666137c2 100644 --- a/src/object.js +++ b/src/object.js @@ -808,23 +808,17 @@ module.exports = function(AV) { * Fetch the model from the server. If the server's representation of the * model differs from its current attributes, they will be overriden, * triggering a "change" event. - * @param {Object} fetchOptions Optional options to set 'keys' and - * 'include' option. + * @param {Object} fetchOptions Optional options to set 'keys', + * 'include' and 'includeACL' option. * @param {AuthOptions} options * @return {Promise} A promise that is fulfilled when the fetch * completes. */ - fetch: function(fetchOptions = {}, options) { - if (_.isArray(fetchOptions.keys)) { - fetchOptions.keys = fetchOptions.keys.join(','); - } - if (_.isArray(fetchOptions.include)) { - fetchOptions.include = fetchOptions.include.join(','); - } + fetch: function(fetchOptions, options) { var self = this; var request = AVRequest('classes', this.className, this.id, 'GET', - fetchOptions, options); + utils.transformFetchOptions(fetchOptions), options); return request.then(function(response) { self._finishFetch(self.parse(response), true); return self; @@ -1011,7 +1005,7 @@ module.exports = function(AV) { output[key] = AV._parseDate(output[key]); } }); - if (!output.updatedAt) { + if (output.createdAt && !output.updatedAt) { output.updatedAt = output.createdAt; } return output; @@ -1239,7 +1233,7 @@ module.exports = function(AV) { * @return {AV.Object} A new subclass instance of AV.Object. */ AV.Object.createWithoutData = function(className, id, hasData){ - var result = new AV.Object(className); + var result = AV.Object._create(className, undefined, undefined, /* noDefaultACL*/ true); result.id = id; result._hasData = hasData; return result; @@ -1293,9 +1287,9 @@ module.exports = function(AV) { * Creates an instance of a subclass of AV.Object for the given classname. * @private */ - AV.Object._create = function(className, attributes, options) { + AV.Object._create = function(className, attributes, options, noDefaultACL) { var ObjectClass = AV.Object._getSubclass(className); - return new ObjectClass(attributes, options); + return new ObjectClass(attributes, options, noDefaultACL); }; // Set up a map of className to class so that we can create new instances of diff --git a/src/push.js b/src/push.js index deee12e26..5d1f5ee7e 100644 --- a/src/push.js +++ b/src/push.js @@ -20,7 +20,8 @@ module.exports = function(AV) { * a set of installations to push to. * @param {String} [data.cql] A CQL statement over AV.Installation that is used to match * a set of installations to push to. - * @param {Date} data.data The data to send as part of the push + * @param {Object} data.data The data to send as part of the push. + More details: https://url.leanapp.cn/pushData * @param {AuthOptions} [options] * @return {Promise} */ diff --git a/src/query.js b/src/query.js index 312a630ea..2556a889b 100644 --- a/src/query.js +++ b/src/query.js @@ -3,7 +3,7 @@ const debug = require('debug')('leancloud:query'); const Promise = require('./promise'); const AVError = require('./error'); const AVRequest = require('./request').request; -const { ensureArray } = require('./utils'); +const { ensureArray, transformFetchOptions } = require('./utils'); const requires = (value, message) => { if (value === undefined) { @@ -187,18 +187,22 @@ module.exports = function(AV) { throw errorObject; } - var self = this; - - var obj = self._newObject(); + var obj = this._newObject(); obj.id = objectId; - var queryJSON = self.toJSON(); + var queryJSON = this.toJSON(); var fetchOptions = {}; if (queryJSON.keys) fetchOptions.keys = queryJSON.keys; if (queryJSON.include) fetchOptions.include = queryJSON.include; + if (queryJSON.includeACL) fetchOptions.includeACL = queryJSON.includeACL; - return obj.fetch(fetchOptions, options); + return AVRequest('classes', this.className, objectId, 'GET', transformFetchOptions(fetchOptions), options) + .then((response) => { + if (_.isEmpty(response)) throw new AVError(AVError.OBJECT_NOT_FOUND, 'Object not found.'); + obj._finishFetch(obj.parse(response), true); + return obj; + }); }, /** @@ -216,6 +220,9 @@ module.exports = function(AV) { if (this._select.length > 0) { params.keys = this._select.join(","); } + if (this._includeACL !== undefined) { + params.returnACL = this._includeACL; + } if (this._limit >= 0) { params.limit = this._limit; } @@ -234,16 +241,16 @@ module.exports = function(AV) { }, _newObject: function(response){ - var obj; if (response && response.className) { - obj = new AV.Object(response.className); - } else { - obj = new this.objectClass(); + return new AV.Object(response.className); + } + if (this.objectClass === AV.Role) { + return new AV.Role(undefined, undefined, /* noDefaultACL */ true); } - return obj; + return new this.objectClass(); }, _createRequest(params = this.toJSON(), options) { - if (JSON.stringify(params).length > 2000) { + if (encodeURIComponent(JSON.stringify(params)).length > 2000) { const body = { requests: [{ method: 'GET', @@ -380,7 +387,7 @@ module.exports = function(AV) { destroyAll: function(options){ var self = this; return self.find(options).then(function(objects){ - return AV.Object.destroyAll(objects); + return AV.Object.destroyAll(objects, options); }); }, @@ -929,6 +936,16 @@ module.exports = function(AV) { return this; }, + /** + * Include the ACL. + * @param {Boolean} [value=true] Whether to include the ACL + * @return {AV.Query} Returns the query, so you can chain this call. + */ + includeACL: function(value = true) { + this._includeACL = value; + return this; + }, + /** * Restrict the fields of the returned AV.Objects to include only the * provided keys. If this is called multiple times, then all of the keys diff --git a/src/request.js b/src/request.js index 935452d71..921019d48 100644 --- a/src/request.js +++ b/src/request.js @@ -59,7 +59,7 @@ const ajax = (method, resourceUrl, data, headers = {}, onprogress) => { }); }; -const setAppId = (headers, signKey) => { +const setAppKey = (headers, signKey) => { if (signKey) { headers['X-LC-Sign'] = sign(AV.applicationKey); } else { @@ -87,10 +87,10 @@ const setHeaders = (authOptions = {}, signKey) => { } } else { console.warn('masterKey is not set, fall back to use appKey'); - setAppId(headers, signKey); + setAppKey(headers, signKey); } } else { - setAppId(headers, signKey); + setAppKey(headers, signKey); } if (AV.hookKey) { headers['X-LC-Hook-Key'] = AV.hookKey; @@ -278,7 +278,7 @@ const AVRequest = (route, className, objectId, method, dataObject = {}, authOpti } return getServerURLPromise.then(() => { const apiURL = createApiUrl(route, className, objectId, method, dataObject); - return setHeaders(authOptions).then( + return setHeaders(authOptions, route !== 'bigquery').then( headers => ajax(method, apiURL, dataObject, headers) .then( null, diff --git a/src/role.js b/src/role.js index b0131a6dc..1a994aa6a 100644 --- a/src/role.js +++ b/src/role.js @@ -21,7 +21,7 @@ module.exports = function(AV) { * @param {AV.ACL} [acl] The ACL for this role. if absent, the default ACL * `{'*': { read: true }}` will be used. */ - constructor: function(name, acl) { + constructor: function(name, acl, noDefaultACL) { if (_.isString(name)) { AV.Object.prototype.constructor.call(this, null, null); this.setName(name); @@ -29,10 +29,13 @@ module.exports = function(AV) { AV.Object.prototype.constructor.call(this, name, acl); } if (acl === undefined) { - var defaultAcl = new AV.ACL(); - defaultAcl.setPublicReadAccess(true); - if(!this.getACL()) { - this.setACL(defaultAcl); + if (!noDefaultACL) { + if(!this.getACL()) { + console.warn('DEPRECATED: To create a Role without ACL(a default ACL will be used) is deprecated. Please specify an ACL.'); + var defaultAcl = new AV.ACL(); + defaultAcl.setPublicReadAccess(true); + this.setACL(defaultAcl); + } } } else if (!(acl instanceof AV.ACL)) { throw new TypeError('acl must be an instance of AV.ACL'); diff --git a/src/status.js b/src/status.js index a23d58b2e..cd50a7426 100644 --- a/src/status.js +++ b/src/status.js @@ -1,9 +1,15 @@ const _ = require('underscore'); const AVRequest = require('./request').request; +const { getSessionToken } = require('./utils'); module.exports = function(AV) { - const getUser = (options = {}) => AV.User.currentAsync() - .then(currUser => currUser || AV.User._fetchUserBySessionToken(options.sessionToken)); + const getUser = (options = {}) => { + const sessionToken = getSessionToken(options); + if (sessionToken) { + return AV.User._fetchUserBySessionToken(getSessionToken(options)); + } + return AV.User.currentAsync(); + }; const getUserPointer = options => getUser(options) .then(currUser => AV.Object.createWithoutData('_User', currUser.id)._toPointer()); @@ -90,7 +96,7 @@ module.exports = function(AV) { * }); */ send: function(options = {}){ - if(!options.sessionToken && !AV.User.current()) { + if(!getSessionToken(options) && !AV.User.current()) { throw new Error('Please signin an user.'); } if(!this.query){ @@ -146,7 +152,7 @@ module.exports = function(AV) { * }); */ AV.Status.sendStatusToFollowers = function(status, options = {}) { - if(!options.sessionToken && !AV.User.current()){ + if(!getSessionToken(options) && !AV.User.current()){ throw new Error('Please signin an user.'); } return getUserPointer(options).then(currUser => { @@ -189,7 +195,7 @@ module.exports = function(AV) { * }); */ AV.Status.sendPrivateStatus = function(status, target, options = {}) { - if(!options.sessionToken && !AV.User.current()){ + if(!getSessionToken(options) && !AV.User.current()){ throw new Error('Please signin an user.'); } if(!target){ @@ -236,7 +242,7 @@ module.exports = function(AV) { */ AV.Status.countUnreadStatuses = function(owner, inboxType = 'default', options = {}){ if (!_.isString(inboxType)) options = inboxType; - if(!options.sessionToken && owner == null && !AV.User.current()) { + if(!getSessionToken(options) && owner == null && !AV.User.current()) { throw new Error('Please signin an user or pass the owner objectId.'); } return getUser(options).then(owner => { @@ -263,7 +269,7 @@ module.exports = function(AV) { */ AV.Status.resetUnreadCount = function(owner, inboxType = 'default', options = {}){ if (!_.isString(inboxType)) options = inboxType; - if(!options.sessionToken && owner == null && !AV.User.current()) { + if(!getSessionToken(options) && owner == null && !AV.User.current()) { throw new Error('Please signin an user or pass the owner objectId.'); } return getUser(options).then(owner => { @@ -307,9 +313,27 @@ module.exports = function(AV) { _newObject: function(){ return new AV.Status(); }, - _createRequest: function(params, options){ - return AVRequest('subscribe/statuses', null, null, 'GET', - params || this.toJSON(), options); + _createRequest: function (params = this.toJSON(), options) { + if (encodeURIComponent(JSON.stringify(params)).length > 2000) { + const body = { + requests: [{ + method: 'GET', + path: '/1.1/subscribe/statuses', + params, + }], + }; + return AVRequest('batch', null, null, 'POST', body, options) + .then(response => { + const result = response[0]; + if (result.success) { + return result.success; + } + const error = new Error(result.error.error || 'Unknown batch error'); + error.code = result.error.code; + throw error; + }); + } + return AVRequest('subscribe/statuses', null, null, 'GET', params, options); }, diff --git a/src/user.js b/src/user.js index 50fd5cd1b..34afa513f 100644 --- a/src/user.js +++ b/src/user.js @@ -46,7 +46,7 @@ module.exports = function(AV) { this._sessionToken = attrs.sessionToken; delete attrs.sessionToken; } - AV.User.__super__._mergeMagicFields.call(this, attrs); + return AV.User.__super__._mergeMagicFields.call(this, attrs); }, /** @@ -376,44 +376,56 @@ module.exports = function(AV) { /** * Follow a user * @since 0.3.0 - * @param {AV.User | String} target The target user or user's objectId to follow. - * @param {AuthOptions} options + * @param {Object | AV.User | String} options if an AV.User or string is given, it will be used as the target user. + * @param {AV.User | String} options.user The target user or user's objectId to follow. + * @param {Object} [options.attributes] key-value attributes dictionary to be used as + * conditions of followerQuery/followeeQuery. + * @param {AuthOptions} [authOptions] */ - follow: function(target, options){ + follow: function(options, authOptions){ if(!this.id){ throw new Error('Please signin.'); } - if(!target){ - throw new Error('Invalid target user.'); + let user; + let attributes; + if (options.user) { + user = options.user; + attributes = options.attributes; + } else { + user = options; } - var userObjectId = _.isString(target) ? target: target.id; + var userObjectId = _.isString(user) ? user: user.id; if(!userObjectId){ throw new Error('Invalid target user.'); } var route = 'users/' + this.id + '/friendship/' + userObjectId; - var request = AVRequest(route, null, null, 'POST', null, options); + var request = AVRequest(route, null, null, 'POST', AV._encode(attributes), authOptions); return request; }, /** * Unfollow a user. * @since 0.3.0 - * @param {AV.User | String} target The target user or user's objectId to unfollow. - * @param {AuthOptions} options + * @param {Object | AV.User | String} options if an AV.User or string is given, it will be used as the target user. + * @param {AV.User | String} options.user The target user or user's objectId to unfollow. + * @param {AuthOptions} [authOptions] */ - unfollow: function(target, options){ + unfollow: function(options, authOptions){ if(!this.id){ throw new Error('Please signin.'); } - if(!target){ - throw new Error('Invalid target user.'); + let user; + if (options.user) { + user = options.user; + } else { + user = options; } - var userObjectId = _.isString(target) ? target: target.id; + var userObjectId = _.isString(user) ? user : user.id; if(!userObjectId){ throw new Error('Invalid target user.'); } var route = 'users/' + this.id + '/friendship/' + userObjectId; - var request = AVRequest(route, null, null, 'DELETE', null, options); + var request = AVRequest(route, null, null, 'DELETE', null, authOptions); return request; }, @@ -593,7 +605,7 @@ module.exports = function(AV) { return AVRequest(`users/${this.id}/refreshSessionToken`, null, null, 'PUT', null, options) .then(response => { this._finishFetch(response); - return this; + return this._handleSaveResult(true).then(() => this); }); }, @@ -899,14 +911,21 @@ module.exports = function(AV) { * number associated with the user account. This sms code allows the user to * verify their mobile phone number by calling AV.User.verifyMobilePhone * - * @param {String} mobilePhone The mobile phone number associated with the + * @param {String} mobilePhoneNumber The mobile phone number associated with the * user that doesn't verify their mobile phone number. + * @param {AuthOptions} [options] AuthOptions plus: + * @param {String} [options.validateToken] a validate token returned by {@link AV.Cloud.verifyCaptcha} * @return {Promise} */ - requestMobilePhoneVerify: function(mobilePhone){ - var json = { mobilePhoneNumber: mobilePhone }; + requestMobilePhoneVerify: function(mobilePhoneNumber, options = {}){ + const data = { + mobilePhoneNumber, + } + if (options.validateToken) { + data.validate_token = options.validateToken + } var request = AVRequest("requestMobilePhoneVerify", null, null, "POST", - json); + data, options); return request; }, @@ -916,14 +935,21 @@ module.exports = function(AV) { * number associated with the user account. This sms code allows the user to * reset their account's password by calling AV.User.resetPasswordBySmsCode * - * @param {String} mobilePhone The mobile phone number associated with the + * @param {String} mobilePhoneNumber The mobile phone number associated with the * user that doesn't verify their mobile phone number. + * @param {AuthOptions} [options] AuthOptions plus: + * @param {String} [options.validateToken] a validate token returned by {@link AV.Cloud.verifyCaptcha} * @return {Promise} */ - requestPasswordResetBySmsCode: function(mobilePhone){ - var json = { mobilePhoneNumber: mobilePhone }; + requestPasswordResetBySmsCode: function(mobilePhoneNumber, options = {}){ + const data = { + mobilePhoneNumber, + } + if (options.validateToken) { + data.validate_token = options.validateToken + } var request = AVRequest("requestPasswordResetBySmsCode", null, null, "POST", - json); + data, options); return request; }, @@ -960,14 +986,21 @@ module.exports = function(AV) { * number associated with the user account. This sms code allows the user to * login by AV.User.logInWithMobilePhoneSmsCode function. * - * @param {String} mobilePhone The mobile phone number associated with the + * @param {String} mobilePhoneNumber The mobile phone number associated with the * user that want to login by AV.User.logInWithMobilePhoneSmsCode + * @param {AuthOptions} [options] AuthOptions plus: + * @param {String} [options.validateToken] a validate token returned by {@link AV.Cloud.verifyCaptcha} * @return {Promise} */ - requestLoginSmsCode: function(mobilePhone){ - var json = { mobilePhoneNumber: mobilePhone }; + requestLoginSmsCode: function(mobilePhoneNumber, options = {}){ + const data = { + mobilePhoneNumber, + } + if (options.validateToken) { + data.validate_token = options.validateToken + } var request = AVRequest("requestLoginSmsCode", null, null, "POST", - json); + data, options); return request; }, diff --git a/src/utils/index.js b/src/utils/index.js index bfb3cb1d6..515f0478f 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -13,6 +13,20 @@ const ensureArray = target => { return [target]; }; +const transformFetchOptions = ({ keys, include, includeACL } = {}) => { + const fetchOptions = {}; + if (keys) { + fetchOptions.keys = ensureArray(keys).join(','); + } + if (include) { + fetchOptions.include = ensureArray(include).join(','); + } + if (includeACL) { + fetchOptions.returnACL = includeACL; + } + return fetchOptions; +}; + const getSessionToken = (authOptions) => { if (authOptions.sessionToken) { return authOptions.sessionToken; @@ -29,6 +43,7 @@ const tap = interceptor => value => ((interceptor(value), value)); module.exports = { isNullOrUndefined, ensureArray, + transformFetchOptions, getSessionToken, tap, }; diff --git a/src/utils/localstorage-browser.js b/src/utils/localstorage-browser.js index deb8b5718..d6ece9754 100644 --- a/src/utils/localstorage-browser.js +++ b/src/utils/localstorage-browser.js @@ -36,7 +36,7 @@ try { // in browser, `localStorage.async = false` will excute `localStorage.setItem('async', false)` _(apiNames).each(function(apiName) { Storage[apiName] = function() { - return global.localStorage[apiName].apply(global.localStorage, arguments); + return localStorage[apiName].apply(localStorage, arguments); }; }); Storage.async = false; diff --git a/src/version.js b/src/version.js index ab7812ade..deef1b8be 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -module.exports = '2.1.2'; +module.exports = '2.5.4'; diff --git a/storage.d.ts b/storage.d.ts index 9c3291552..1faecc719 100644 --- a/storage.d.ts +++ b/storage.d.ts @@ -1,3 +1,7 @@ +interface IteratorResult { + done: boolean; + value: T; +} interface AsyncIterator { next(): Promise> } @@ -8,6 +12,12 @@ declare namespace AV { export var applicationKey: string; export var masterKey: string; + interface FetchOptions { + keys?: string | string[]; + include?: string | string[]; + includeACL?: boolean; + } + export interface AuthOptions { /** * In Cloud Code and Node only, causes the Master Key to be used for this request. @@ -17,6 +27,25 @@ declare namespace AV { user?: User; } + interface SMSAuthOptions extends AuthOptions { + validateToken?: string; + } + + interface CaptchaOptions { + size?: number; + width?: number; + height?: number; + ttl?: number; + } + + interface FileSaveOptions extends AuthOptions { + onprogress?: (event: { + loaded: number, + total: number, + percent: number, + }) => void; + } + export interface WaitOption { /** * Set to true to wait for the server to confirm success @@ -122,15 +151,15 @@ declare namespace AV { static withURL(name: string, url: string): File; static createWithoutData(objectId: string): File; - destroy(): Promise; - fetch(options?: AuthOptions): Promise; + destroy(): Promise; + fetch(fetchOptions?: FetchOptions, options?: AuthOptions): Promise; metaData(): any; metaData(metaKey: string): any; metaData(metaKey: string, metaValue: any): any; name(): string; ownerId(): string; url(): string; - save(options?: AuthOptions): Promise; + save(options?: FileSaveOptions): Promise; setACL(acl?: ACL): any; size(): any; thumbnailURL(width: number, height: number): string; @@ -250,7 +279,7 @@ declare namespace AV { destroy(options?: Object.DestroyOptions): Promise; dirty(attr: String): boolean; escape(attr: string): string; - fetch(fetchOptions?: any, options?: Object.FetchOptions): Promise; + fetch(fetchOptions?: FetchOptions, options?: AuthOptions): Promise; fetchWhenSave(enable: boolean): any; get(attr: string): any; getACL(): ACL; @@ -263,7 +292,8 @@ declare namespace AV { previousAttributes(): any; relation(attr: string): Relation; remove(attr: string, item: any): any; - save(options?: Object.SaveOptions, arg2?: any, arg3?: any): Promise; + save(attrs?: object | null, options?: Object.SaveOptions): Promise; + save(key: string, value: any, options?: Object.SaveOptions): Promise; set(key: string, value: any, options?: Object.SetOptions): boolean; setACL(acl: ACL, options?: Object.SetOptions): boolean; unset(attr: string, options?: Object.SetOptions): any; @@ -276,9 +306,10 @@ declare namespace AV { interface DestroyAllOptions extends AuthOptions { } - interface FetchOptions extends AuthOptions { } - - interface SaveOptions extends AuthOptions, SilentOption, WaitOption { } + interface SaveOptions extends AuthOptions, SilentOption, WaitOption { + fetchWhenSave?: boolean, + where?: Query, + } interface SaveAllOptions extends AuthOptions { } @@ -436,6 +467,7 @@ declare namespace AV { greaterThanOrEqualTo(key: string, value: any): Query; include(key: string): Query; include(keys: string[]): Query; + includeACL(value?: boolean): Query; lessThan(key: string, value: any): Query; lessThanOrEqualTo(key: string, value: any): Query; limit(n: number): Query; @@ -462,6 +494,8 @@ declare namespace AV { interface GetOptions extends AuthOptions { } } + class FriendShipQuery extends Query {} + /** * Represents a Role on the AV server. Roles represent groupings of * Users for the purposes of granting permissions (e.g. specifying an ACL @@ -497,28 +531,30 @@ declare namespace AV { export class User extends Object { static current(): User; - static signUp(username: string, password: string, attrs: any, options?: AuthOptions): Promise; - static logIn(username: string, password: string, options?: AuthOptions): Promise; - static logOut(): Promise; - static become(sessionToken: string, options?: AuthOptions): Promise; - - static loginWithWeapp(): Promise; - static logInWithMobilePhone(mobilePhone: string, password: string, options?: AuthOptions): Promise; - static logInWithMobilePhoneSmsCode(mobilePhone: string, smsCode: string, options?: AuthOptions): Promise; - static signUpOrlogInWithAuthData(data: any, platform: string, options?: AuthOptions): Promise; - static signUpOrlogInWithMobilePhone(mobilePhoneNumber: string, smsCode: string, attributes?: any, options?: AuthOptions): Promise; - static requestEmailVerify(email: string, options?: AuthOptions): Promise; - static requestLoginSmsCode(mobilePhone: string, options?: AuthOptions): Promise; - static requestMobilePhoneVerify(mobilePhone: string, options?: AuthOptions): Promise; - static requestPasswordReset(email: string, options?: AuthOptions): Promise; - static requestPasswordResetBySmsCode(mobilePhone: string, options?: AuthOptions): Promise; - static resetPasswordBySmsCode(code: string, password: string, options?: AuthOptions): Promise; - static verifyMobilePhone(code: string, options?: AuthOptions): Promise; - signUp(attrs?: any, options?: AuthOptions): Promise; - logIn(options?: AuthOptions): Promise; - linkWithWeapp(): Promise; - fetch(options?: AuthOptions): Promise; - save(arg1?: any, arg2?: any, arg3?: any): Promise; + static signUp(username: string, password: string, attrs: any, options?: AuthOptions): Promise; + static logIn(username: string, password: string, options?: AuthOptions): Promise; + static logOut(): Promise; + static become(sessionToken: string, options?: AuthOptions): Promise; + + static loginWithWeapp(): Promise; + static logInWithMobilePhone(mobilePhone: string, password: string, options?: AuthOptions): Promise; + static logInWithMobilePhoneSmsCode(mobilePhone: string, smsCode: string, options?: AuthOptions): Promise; + static signUpOrlogInWithAuthData(data: any, platform: string, options?: AuthOptions): Promise; + static signUpOrlogInWithMobilePhone(mobilePhoneNumber: string, smsCode: string, attributes?: any, options?: AuthOptions): Promise; + static requestEmailVerify(email: string, options?: AuthOptions): Promise; + static requestLoginSmsCode(mobilePhoneNumber: string, options?: SMSAuthOptions): Promise; + static requestMobilePhoneVerify(mobilePhoneNumber: string, options?: SMSAuthOptions): Promise; + static requestPasswordReset(email: string, options?: AuthOptions): Promise; + static requestPasswordResetBySmsCode(mobilePhoneNumber: string, options?: SMSAuthOptions): Promise; + static resetPasswordBySmsCode(code: string, password: string, options?: AuthOptions): Promise; + static verifyMobilePhone(code: string, options?: AuthOptions): Promise; + + static followerQuery(userObjectId: string): FriendShipQuery; + static followeeQuery(userObjectId: string): FriendShipQuery; + + signUp(attrs?: any, options?: AuthOptions): Promise; + logIn(options?: AuthOptions): Promise; + linkWithWeapp(): Promise; isAuthenticated(): Promise; isCurrent(): boolean; @@ -537,6 +573,59 @@ declare namespace AV { refreshSessionToken(options?: AuthOptions): Promise; getRoles(options?: AuthOptions): Promise; + + follow(user: User|string, authOptions?: AuthOptions): Promise; + follow(options: { user: User|string, attributes?: Object}, authOptions?: AuthOptions): Promise; + unfollow(user: User|string, authOptions?: AuthOptions): Promise; + unfollow(options: { user: User|string }, authOptions?: AuthOptions): Promise; + followerQuery(): FriendShipQuery; + followeeQuery(): FriendShipQuery; + } + + export class Captcha { + url: string; + captchaToken: string; + validateToken: string; + + static request(options?: CaptchaOptions, authOptions?: AuthOptions): Promise; + + refresh(): Promise; + verify(code: string): Promise; + bind(elements?: { + textInput?: string|HTMLInputElement, + image?: string|HTMLImageElement, + verifyButton?: string|HTMLElement, + }, callbacks?: { + success?: (validateToken: string) => any, + error?: (error: Error) => any, + }): void; + unbind(): void; + } + + /** + * @class AV.Conversation + *

An AV.Conversation is a local representation of a LeanCloud realtime's + * conversation. This class is a subclass of AV.Object, and retains the + * same functionality of an AV.Object, but also extends it with various + * conversation specific methods, like get members, creators of this conversation. + *

+ * + * @param {String} name The name of the Role to create. + * @param {Boolean} [options.isSystem] Set this conversation as system conversation. + * @param {Boolean} [options.isTransient] Set this conversation as transient conversation. + */ + export class Conversation extends Object { + constructor(name: string, options?: { isSytem?: boolean, isTransient?: boolean }); + getCreator(): string; + getLastMessageAt(): Date; + getMembers(): string[]; + addMember(member: string): Conversation; + getMutedMembers(): string[]; + getName(): string; + isTransient(): boolean; + isSystem(): boolean; + send(fromClient: string, message: string|object, options?: { transient?: boolean, pushData?: object, toClients?: string[] }, authOptions?: AuthOptions): Promise; + broadcast(fromClient: string, message: string|object, options?: { pushData?: object, validTill?: number|Date }, authOptions?: AuthOptions): Promise; } export class Error { @@ -673,9 +762,11 @@ declare namespace AV { } export namespace Cloud { - function run(name: string, data?: any, options?: AuthOptions): Promise; - function requestSmsCode(data: any, options?: AuthOptions): Promise; - function verifySmsCode(code: string, phone: string, options?: AuthOptions): Promise; + function run(name: string, data?: any, options?: AuthOptions): Promise; + function requestSmsCode(data: string|{ mobilePhoneNumber: string, template?: string, sign?: string }, options?: SMSAuthOptions): Promise; + function verifySmsCode(code: string, phone: string): Promise; + function requestCaptcha(options?: CaptchaOptions, authOptions?: AuthOptions): Promise; + function verifyCaptcha(code: string, captchaToken: string): Promise; } /** diff --git a/test/acl.js b/test/acl.js index 0097ebf75..f7e185b33 100644 --- a/test/acl.js +++ b/test/acl.js @@ -2,26 +2,42 @@ var GameScore = AV.Object.extend("GameScore"); describe("ObjectACL", function () { - describe("*", function () { - it("set * acl", function () { - var gameScore = new GameScore(); - gameScore.set("score", 2); - gameScore.set("playerName", "sdf"); - gameScore.set("cheatMode", false); + it("set and fetch acl", function () { + var gameScore = new GameScore(); + gameScore.set("score", 2); + gameScore.set("playerName", "sdf"); + gameScore.set("cheatMode", false); - var postACL = new AV.ACL(); - postACL.setPublicReadAccess(true); - postACL.setPublicWriteAccess(true); + var postACL = new AV.ACL(); + postACL.setPublicReadAccess(true); + postACL.setPublicWriteAccess(true); - postACL.setReadAccess("546", true); - postACL.setReadAccess("56238", true); - postACL.setWriteAccess("5a061", true); - postACL.setRoleWriteAccess("r6", true); - gameScore.setACL(postACL); - return gameScore.save().then(result => { - result.id.should.be.ok(); - return gameScore.destroy(); + postACL.setReadAccess("read-only", true); + postACL.setWriteAccess("write-only", true); + postACL.setRoleWriteAccess("write-only-role", true); + gameScore.setACL(postACL); + return gameScore.save().then(result => { + result.id.should.be.ok(); + return AV.Object.createWithoutData('GameScore', result.id).fetch({ + includeACL: true, }); - }); + }).then(fetchedGameScore => { + const acl = fetchedGameScore.getACL(); + acl.should.be.instanceOf(AV.ACL); + acl.getPublicReadAccess().should.eql(true); + acl.getPublicWriteAccess().should.eql(true); + acl.getReadAccess('read-only').should.eql(true); + acl.getWriteAccess('read-only').should.eql(false); + acl.getReadAccess('write-only').should.eql(false); + acl.getWriteAccess('write-only').should.eql(true); + acl.getRoleReadAccess('write-only-role').should.eql(false); + acl.getRoleWriteAccess('write-only-role').should.eql(true); + }).then( + () => gameScore.destroy(), + error => { + gameScore.destroy(); + throw error; + } + ); }); }); diff --git a/test/captcha.js b/test/captcha.js new file mode 100644 index 000000000..752ad3bf3 --- /dev/null +++ b/test/captcha.js @@ -0,0 +1,21 @@ +describe('Captcha', () => { + before(function () { + return AV.Captcha.request().then(captcha => { + this.captcha = captcha; + }); + }); + it('.request', function () { + this.captcha.should.be.instanceof(AV.Captcha); + this.captcha.url.should.be.a.String(); + this.captcha.captchaToken.should.be.a.String(); + }); + it('.refresh', function () { + const currentUrl = this.captcha.url; + return this.captcha.refresh().then(() => { + this.captcha.url.should.not.equalTo(currentUrl); + }); + }); + it('.refresh', function () { + return this.captcha.verify('fakecode').should.be.rejected(); + }); +}); diff --git a/test/conversation.js b/test/conversation.js new file mode 100644 index 000000000..35603b2e4 --- /dev/null +++ b/test/conversation.js @@ -0,0 +1,53 @@ +'use strict'; + +describe('Conversation', () => { + describe('.constructor', () => { + const conv = new AV.Conversation('test', { isSystem: true, isTransient: false }); + expect(conv.isTransient()).to.be(false); + expect(conv.isSystem()).to.be(true); + expect(conv.getName()).to.be('test'); + }); + describe('#save', () => { + it('should create a realtime conversation', () => { + const conv = new AV.Conversation('test'); + conv.addMember('test1'); + conv.addMember('test2'); + return conv.save(); + }); + }); + describe('#send', () => { + it('should send a realtime message to the conversation', () => { + const conv = new AV.Conversation('test'); + conv.addMember('test1'); + conv.addMember('test2'); + return conv.save().then(() => { + return conv.send('admin', 'test test test!', {}, { useMasterKey: true }); + }); + }); + + it('should send a realtime message to the system conversation', () => { + const conv = new AV.Conversation('system', { isSystem: true }); + return conv.save().then(() => { + return conv.send('admin', 'test system conversation !', { + toClients: ['user1', 'user2'] + }, { + useMasterKey: true, + }); + }); + }); + }); + describe('#broadcast', () => { + it('should broadcast a message to all clients with current conversation', () => { + const conv = new AV.Conversation('test', { isSystem: true }); + return conv.save().then(() => { + const options = { + validTill: new Date().getTime() / 1000 + 1000, + }; + const authOptions = { + useMasterKey: true, + }; + return conv.broadcast('admin', 'test broadcast!', options, authOptions); + }); + }); + }); +}); diff --git a/test/file.js b/test/file.js index 994034d59..5f78a0c91 100644 --- a/test/file.js +++ b/test/file.js @@ -124,7 +124,7 @@ describe('File', function() { }); describe('#fetch', function() { - var fileId = '52f9dd5ae4b019816c865985'; + const fileId = process.env.FILE_ID || '52f9dd5ae4b019816c865985'; it('createWithoutData() should return a File', function() { var file = AV.File.createWithoutData(fileId); expect(file).to.be.a(AV.File); diff --git a/test/index.js b/test/index.js index 6422cd05c..c529b272e 100644 --- a/test/index.js +++ b/test/index.js @@ -3,6 +3,7 @@ require('./test.js'); require('./av.js'); require('./file.js'); require('./error.js'); +// require('./captcha.js'); require('./object.js'); require('./user.js'); require('./query.js'); @@ -13,3 +14,4 @@ require('./status.js'); require('./sms.js'); require('./search.js'); require('./hooks.js'); +require('./conversation.js'); diff --git a/test/object.js b/test/object.js index 5a6c285a5..613b6b863 100644 --- a/test/object.js +++ b/test/object.js @@ -107,6 +107,19 @@ describe('Objects', function(){ parsedGameScore.get('score').should.eql(gameScore.get('score')); }); + it('toJSON and parse (User)', () => { + const user = new AV.Object.createWithoutData('_User', 'objectId'); + user.set('id', 'id'); + user.set('score', 20); + const json = user.toJSON(); + json.objectId.should.eql(user.id); + json.score.should.eql(user.get('score')); + const parsedUser = new AV.User(json, { parse: true }); + parsedUser.id.should.eql(user.id); + parsedUser.get('id').should.eql(user.get('id')); + parsedUser.get('score').should.eql(user.get('score')); + }); + it('should create a User',function(){ var User = AV.Object.extend("User"); var u = new User(); @@ -310,7 +323,6 @@ describe('Objects', function(){ var Person=AV.Object.extend("Person"); var p; - var posts=[]; it("should create a Person",function(){ var Person = AV.Object.extend("Person"); @@ -320,13 +332,12 @@ describe('Objects', function(){ }); it("should create many to many relations",function(){ - var query = new AV.Query(Person); - return query.first().then(function(result){ - var p=result; + return Promise.all([ + new AV.Query(Post).first(), + new AV.Query(Person).first(), + ]).then(function([post, p]){ var relation = p.relation("likes"); - for(var i=0;i { - const fileId = '52f9dd5ae4b019816c865985'; + const fileId = process.env.FILE_ID || '52f9dd5ae4b019816c865985'; query = new AV.Query(AV.File); query.equalTo('objectId', fileId); return query.find().then(([file]) => { @@ -208,6 +212,16 @@ describe('Queries', function () { }); }); + it('includeACL', function () { + return new AV.Query(GameScore) + .includeACL() + .equalTo('objectId', this.gameScore.id) + .find() + .then(([gameScore]) => { + gameScore.getACL().should.be.instanceOf(AV.ACL); + }); + }); + it('containsAll with an large array should not cause URI too long', () => { return new AV.Query(GameScore) .containsAll('arr', new Array(200).fill('contains-all-test')) @@ -277,12 +291,11 @@ describe('Queries', function () { var userQ = new AV.Query('Person'); - return userQ.get('52f9bea1e4b035debf88b730').then(function (p) { - p.relation('likes').query(); + return userQ.first().then(function (p) { + return p.relation('likes').query().count(); // p.relation('likes').query().count().then(function(c){ // debug(c) // }) - debug(p); }); // userQ.first().then(function(p){ diff --git a/test/role.js b/test/role.js index f017badab..58e7b6116 100644 --- a/test/role.js +++ b/test/role.js @@ -17,6 +17,16 @@ describe("Role", function() { } }); }); + it('no default ACL', () => { + expect(AV.Object.createWithoutData('_Role').getACL()).to.eql(undefined); + expect(AV._decode({ + __type: 'Pointer', + className: '_Role', + name: 'Admin', + objectId: '577e50c3165abd005549f210', + }).getACL()).to.eql(undefined); + expect((new AV.Object('_Role')).getACL()).not.to.eql(undefined); + }); it("type check", function() { expect(function() { new AV.Role('foo', {}); diff --git a/test/status.js b/test/status.js index 957b0646b..919412155 100644 --- a/test/status.js +++ b/test/status.js @@ -1,6 +1,7 @@ 'use strict'; describe("AV.Status",function(){ + var targetUser = process.env.STATUS_TARGET_USER_ID || '5627906060b22ef9c464cc99'; before(function() { var userName = this.userName = 'StatusTest' + Date.now(); return AV.User.signUp(userName, userName).then(user => { @@ -18,7 +19,7 @@ describe("AV.Status",function(){ it("should send private status to an user.",function(){ var status = new AV.Status('image url', 'message'); - return AV.Status.sendPrivateStatus(status, '5627906060b22ef9c464cc99'); + return AV.Status.sendPrivateStatus(status, targetUser); }); it("should send status to a female user.",function(){ @@ -30,7 +31,7 @@ describe("AV.Status",function(){ }); describe("Query statuses.", function(){ - const user = AV.Object.createWithoutData('_User', '5627906060b22ef9c464cc99'); + const user = AV.Object.createWithoutData('_User', targetUser); it("should return unread count.", function(){ return AV.Status.countUnreadStatuses().then(function(response){ expect(response.total).to.be.a('number'); @@ -59,22 +60,36 @@ describe("AV.Status",function(){ expect(response.unread).to.be.eql(0); }); }); + it('should not cause URI too long', () => { + return AV.Status.inboxQuery(user) + .containsAll('arr', new Array(50).fill(AV.Object.createWithoutData('Todo', '5850f138128fe1006978f766'))) + .find(); + }); }); describe("Status guide test.", function(){ - //follow 5627906060b22ef9c464cc99 - //unfolow 5627906060b22ef9c464cc99 - var targetUser = '5627906060b22ef9c464cc99'; it("should follow/unfollow successfully.", function(){ - return AV.User.current().follow(targetUser).then(function(){ + return AV.User.current().follow({ + user: targetUser, + attributes: { + group: 1, + position: new AV.GeoPoint(0,0), + }, + }).then(function(){ var query = AV.User.current().followeeQuery(); + query.equalTo('group', 1); query.include('followee'); return query.find(); }).then(function(followees){ debug(followees); expect(followees.length).to.be(1); - expect(followees[0].id).to.be('5627906060b22ef9c464cc99'); + expect(followees[0].id).to.be(targetUser); expect(followees[0].get('username')).to.be('leeyeh'); + var query = AV.User.current().followeeQuery(); + query.equalTo('group', 0); + return query.find(); + }).then(function(followees){ + expect(followees.length).to.be(0); return AV.User.current().unfollow(targetUser); }).then(function(){ var query = AV.User.current().followeeQuery(); diff --git a/test/test.html b/test/test.html index 304164ff6..a58bcf185 100644 --- a/test/test.html +++ b/test/test.html @@ -27,6 +27,7 @@ + diff --git a/test/test.js b/test/test.js index da6810c19..1070700d6 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,7 @@ 'use strict'; +if (!process) process = { env: {}}; + if (typeof require !== 'undefined') { global.debug = require('debug')('test'); global.expect = require('expect.js'); @@ -13,9 +15,10 @@ if (typeof require !== 'undefined') { // masterKey: 'l0n9wu3kwnrtf2cg1b6w2l87nphzpypgff6240d0lxui2mm4' // }); AV.init({ - appId: '95TNUaOSUd8IpKNW0RSqSEOm-9Nh9j0Va', - appKey: 'gNAE1iHowdQvV7cqpfCMGaGN', - masterKey: 'ue9M9nqwD4MQNXD3oiN5rAOv', - hookKey: '2iCbUZDgEF0siKxmCn2kVQXV' + appId: process.env.APPID || '95TNUaOSUd8IpKNW0RSqSEOm-9Nh9j0Va', + appKey: process.env.APPKEY || 'gNAE1iHowdQvV7cqpfCMGaGN', + masterKey: process.env.MASTERKEY || 'ue9M9nqwD4MQNXD3oiN5rAOv', + hookKey: process.env.HOOKKEY || '2iCbUZDgEF0siKxmCn2kVQXV', + region: process.env.REGION || 'cn', }); AV.setProduction(true); diff --git a/test/user.js b/test/user.js index 662704f85..c907742bf 100644 --- a/test/user.js +++ b/test/user.js @@ -122,7 +122,7 @@ describe("User", function() { it("should return conditoinal users", function() { var query = new AV.Query(AV.User); query.equalTo("gender", "female"); // find all the women - return query.find(); + return query.find({useMasterKey: true}); }); }); @@ -168,35 +168,6 @@ describe("User", function() { }); }); - describe("Follow/unfollow users", function() { - it("should follow/unfollow", function() { - var user = AV.User.current(); - return user.follow('53fb0fd6e4b074a0f883f08a').then(function() { - var query = user.followeeQuery(); - return query.find(); - }).then(function(results) { - expect(results.length).to.be(1); - debug(results); - expect(results[0].id).to.be('53fb0fd6e4b074a0f883f08a'); - var followerQuery = AV.User.followerQuery('53fb0fd6e4b074a0f883f08a'); - return followerQuery.find(); - }).then(function(results) { - expect(results.filter(function(result) { - return result.id === user.id; - })).not.to.be(0); - debug(results); - //unfollow - return user.unfollow('53fb0fd6e4b074a0f883f08a'); - }).then(function() { - //query should be emtpy - var query = user.followeeQuery(); - return query.find(); - }).then(function(results) { - expect(results.length).to.be(0); - }); - }); - }); - describe("User logInAnonymously", function() { it("should create anonymous user, and login with AV.User.signUpOrlogInWithAuthData()", function() { var getFixedId = function () { @@ -225,7 +196,7 @@ describe("User", function() { return AV.User.logIn(username, password); }).then(function (loginedUser) { return AV.User.associateWithAuthData(loginedUser, 'weixin', { - openid: 'aaabbbccc123123', + openid: 'aaabbbccc123123'+username, access_token: 'a123123aaabbbbcccc', expires_in: 1382686496, }); @@ -240,6 +211,10 @@ describe("User", function() { return user.refreshSessionToken().then(user => { user.getSessionToken().should.be.a.String(); user.getSessionToken().should.not.be.eql(prevSessionToken); + // cache refreshed + delete AV.User._currentUser; + AV.User._currentUserMatchesDisk = false; + user.getSessionToken().should.be.eql(AV.User.current().getSessionToken()); }) }); }) diff --git a/webpack/common.js b/webpack/common.js index bbea588e2..f9ab9e73c 100644 --- a/webpack/common.js +++ b/webpack/common.js @@ -8,7 +8,7 @@ module.exports = function() { filename: 'av.js', libraryTarget: "umd2", library: "AV", - path: './dist' + path: path.resolve(__dirname, '../dist') }, resolve: {}, devtool: 'source-map',