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', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy