Skip to content

Commit 429b8ee

Browse files
committed
feat: add AV.Leaderboard
1 parent 7d9cc66 commit 429b8ee

File tree

6 files changed

+334
-9
lines changed

6 files changed

+334
-9
lines changed

src/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* The LeanCloud JavaScript SDK is freely distributable under the MIT license.
77
*/
88
require('./polyfills');
9+
const _ = require('underscore');
910

1011
const AV = require('./av');
1112

12-
AV._ = require('underscore');
13+
AV._ = _;
1314
AV.version = require('./version');
1415
AV.Promise = require('./promise');
1516
AV.localStorage = require('./localstorage');
@@ -36,7 +37,7 @@ require('./search')(AV);
3637
require('./insight')(AV);
3738

3839
AV.Conversation = require('./conversation');
39-
40+
_.extend(AV, require('./leaderboard'));
4041
module.exports = AV;
4142

4243
/**

src/leaderboard.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
const _ = require('underscore');
2+
const Promise = require('./promise');
3+
const { request } = require('./request');
4+
const { ensureArray, parseDate } = require('./utils');
5+
const AV = require('./av');
6+
7+
const LeaderboardVersionChangeInterval = {
8+
NEVER: 'never',
9+
HOUR: 'hour',
10+
DAY: 'day',
11+
WEEK: 'week',
12+
MONTH: 'month',
13+
};
14+
15+
const LeaderboardOrder = {
16+
ASCENDING: 'ascending',
17+
DESCENDING: 'descending',
18+
};
19+
20+
function Statistic({ user, name, value, position, version }) {
21+
this.name = name;
22+
this.value = value;
23+
this.user = user;
24+
this.position = position;
25+
this.version = version;
26+
}
27+
28+
function Leaderboard(statisticName) {
29+
this.statisticName = statisticName;
30+
this.versionChangeInterval = undefined;
31+
this.version = undefined;
32+
this.nextResetAt = undefined;
33+
}
34+
35+
Leaderboard.createWithoutData = statisticName => new Leaderboard(statisticName);
36+
Leaderboard.createLeaderboard = (
37+
{ statisticName, order, versionChangeInterval },
38+
authOptions
39+
) =>
40+
request({
41+
method: 'POST',
42+
path: '/play/leaderboards',
43+
data: {
44+
statisticName,
45+
order,
46+
versionChangeInterval,
47+
},
48+
authOptions,
49+
}).then(data => {
50+
const leaderboard = new Leaderboard(statisticName);
51+
return leaderboard._finishFetch(data);
52+
});
53+
Leaderboard.getLeaderboard = (statisticName, authOptions) =>
54+
Leaderboard.createWithoutData(statisticName).fetch(authOptions);
55+
Leaderboard.getStatistics = (user, { statisticNames } = {}, authOptions) =>
56+
Promise.resolve().then(() => {
57+
if (!(user && user.id)) throw new Error('user must be an AV.User');
58+
return request({
59+
method: 'GET',
60+
path: `/play/users/${user.id}/statistics`,
61+
query: {
62+
statistics: statisticNames ? ensureArray(statisticNames) : undefined,
63+
},
64+
authOptions,
65+
}).then(({ results }) =>
66+
results.map(statisticData => {
67+
const {
68+
statisticName: name,
69+
statisticValue: value,
70+
version,
71+
} = AV._decode(statisticData);
72+
return new Statistic({ user, name, value, version });
73+
})
74+
);
75+
});
76+
Leaderboard.updateStatistics = (user, statistics, authOptions) =>
77+
Promise.resolve().then(() => {
78+
if (!(user && user.id)) throw new Error('user must be an AV.User');
79+
const data = _.map(statistics, (value, key) => ({
80+
statisticName: key,
81+
statisticValue: value,
82+
}));
83+
return request({
84+
method: 'POST',
85+
path: `/play/users/${user.id}/statistics`,
86+
data,
87+
authOptions,
88+
}).then(({ results }) =>
89+
results.map(statisticData => {
90+
const {
91+
statisticName: name,
92+
statisticValue: value,
93+
version,
94+
} = AV._decode(statisticData);
95+
return new Statistic({ user, name, value, version });
96+
})
97+
);
98+
});
99+
100+
_.extend(Leaderboard.prototype, {
101+
_finishFetch(data) {
102+
_.forEach(data, (value, key) => {
103+
if (key === 'updatedAt' || key === 'objectId') return;
104+
if (key === 'expiredAt') {
105+
key = 'nextResetAt';
106+
}
107+
if (value.__type === 'Date') {
108+
value = parseDate(value.iso);
109+
}
110+
this[key] = value;
111+
});
112+
return this;
113+
},
114+
fetch(authOptions) {
115+
return request({
116+
method: 'GET',
117+
path: `/play/leaderboards/${this.statisticName}`,
118+
authOptions,
119+
}).then(data => this._finishFetch(data));
120+
},
121+
_getResults({ skip, limit, includeUserKeys }, authOptions, self) {
122+
return request({
123+
method: 'GET',
124+
path: `/play/leaderboards/${this.statisticName}/positions${
125+
self ? '/self' : ''
126+
}`,
127+
query: {
128+
skip,
129+
limit,
130+
includeUser: includeUserKeys
131+
? ensureArray(includeUserKeys).join(',')
132+
: undefined,
133+
},
134+
authOptions,
135+
}).then(({ results }) =>
136+
results.map(statisticData => {
137+
const {
138+
user,
139+
statisticName: name,
140+
statisticValue: value,
141+
position,
142+
} = AV._decode(statisticData);
143+
return new Statistic({ user, name, value, position });
144+
})
145+
);
146+
},
147+
getResults({ skip, limit, includeUserKeys } = {}, authOptions) {
148+
return this._getResults({ skip, limit, includeUserKeys }, authOptions);
149+
},
150+
getResultsAroundUser({ limit, includeUserKeys } = {}, authOptions) {
151+
return this._getResults({ limit, includeUserKeys }, authOptions, true);
152+
},
153+
updateVersionChangeInterval(versionChangeInterval, authOptions) {
154+
return request({
155+
method: 'PUT',
156+
path: `/play/leaderboards/${this.statisticName}`,
157+
data: {
158+
versionChangeInterval,
159+
},
160+
authOptions,
161+
}).then(data => this._finishFetch(data));
162+
},
163+
reset(authOptions) {
164+
return request({
165+
method: 'PUT',
166+
path: `/play/leaderboards/${this.statisticName}/incrementVersion`,
167+
authOptions,
168+
}).then(data => this._finishFetch(data));
169+
},
170+
});
171+
172+
module.exports = {
173+
Leaderboard,
174+
LeaderboardOrder,
175+
LeaderboardVersionChangeInterval,
176+
};

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ require('./search.js');
1717
require('./cloud.js');
1818
require('./hooks.js');
1919
require('./conversation.js');
20+
require('./leaderboard.js');

test/leaderboard.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
const statisticName = `score_${Date.now()}`;
2+
3+
describe('Leaderboard', () => {
4+
before(function setUpLeaderboard() {
5+
return AV.Leaderboard.createLeaderboard(
6+
{
7+
statisticName,
8+
order: AV.LeaderboardOrder.ASCENDING,
9+
versionChangeInterval: AV.LeaderboardVersionChangeInterval.WEEK,
10+
},
11+
{
12+
useMasterKey: true,
13+
}
14+
).then(leaderboard => {
15+
this.leaderboard = leaderboard;
16+
});
17+
});
18+
19+
function validateLeaderboard(leaderboard) {
20+
leaderboard.should.be.instanceof(AV.Leaderboard);
21+
leaderboard.statisticName.should.be.eql(statisticName);
22+
leaderboard.versionChangeInterval.should.be.eql(
23+
AV.LeaderboardVersionChangeInterval.WEEK
24+
);
25+
leaderboard.order.should.be.eql(AV.LeaderboardOrder.ASCENDING);
26+
leaderboard.version.should.be.eql(0);
27+
leaderboard.nextResetAt.should.be.a.Date();
28+
}
29+
30+
it('shoud have properties', function() {
31+
validateLeaderboard(this.leaderboard);
32+
});
33+
34+
it('query', () =>
35+
AV.Leaderboard.getLeaderboard(statisticName).then(validateLeaderboard));
36+
37+
it('mutation by client should be rejected', function() {
38+
return this.leaderboard
39+
.updateVersionChangeInterval(AV.LeaderboardVersionChangeInterval.NEVER)
40+
.should.be.rejected();
41+
});
42+
it('mutation with masterKey', function() {
43+
return this.leaderboard
44+
.updateVersionChangeInterval(AV.LeaderboardVersionChangeInterval.DAY, {
45+
useMasterKey: true,
46+
})
47+
.then(leaderboard => {
48+
leaderboard.versionChangeInterval.should.be.eql(
49+
AV.LeaderboardVersionChangeInterval.DAY
50+
);
51+
});
52+
});
53+
54+
describe('Statistics', function() {
55+
let users;
56+
let currentUser;
57+
let stats;
58+
before(() =>
59+
Promise.all(
60+
['0', '1', '2', '3'].map(value =>
61+
AV.User.signUp(Date.now() + value, Date.now() + value)
62+
)
63+
)
64+
.then(result => {
65+
users = result;
66+
currentUser = users[2];
67+
return Promise.all(
68+
users.map((user, index) =>
69+
AV.Leaderboard.updateStatistics(
70+
user,
71+
{ [statisticName]: index },
72+
{
73+
user,
74+
}
75+
)
76+
)
77+
);
78+
})
79+
.then(result => {
80+
stats = result[2];
81+
})
82+
);
83+
84+
after(() =>
85+
Promise.all(users.map(user => user.destroy({ useMasterKey: true })))
86+
);
87+
88+
function validateStatistic(statistic) {
89+
statistic.name.should.eql(statisticName);
90+
statistic.value.should.eql(2);
91+
statistic.user.id.should.eql(currentUser.id);
92+
}
93+
94+
it('shoud have properties', () => {
95+
const statistic = stats[0];
96+
validateStatistic(statistic);
97+
statistic.version.should.eql(0);
98+
});
99+
100+
it('get statistics', () =>
101+
AV.Leaderboard.getStatistics(currentUser, undefined, {
102+
user: currentUser,
103+
}).then(statistics => {
104+
statistics.should.be.an.Array();
105+
validateStatistic(statistics[0]);
106+
statistics[0].version.should.eql(0);
107+
}));
108+
109+
it('getResults', function() {
110+
const leaderboard = this.leaderboard;
111+
return leaderboard
112+
.getResults({ includeUserKeys: ['username'] })
113+
.then(statistics => {
114+
statistics.should.be.an.Array();
115+
statistics.should.be.length(4);
116+
statistics
117+
.map(statistic => statistic.position)
118+
.should.eql([0, 1, 2, 3]);
119+
validateStatistic(statistics[2]);
120+
statistics[2].user
121+
.get('username')
122+
.should.be.eql(currentUser.get('username'));
123+
});
124+
});
125+
it('getResultsAroundUser', function() {
126+
const leaderboard = this.leaderboard;
127+
return leaderboard
128+
.getResultsAroundUser({ limit: 3 }, { user: currentUser })
129+
.then(statistics => {
130+
statistics.should.be.an.Array();
131+
statistics.should.be.length(3);
132+
validateStatistic(statistics[1]);
133+
statistics.map(statistic => statistic.position).should.eql([1, 2, 3]);
134+
});
135+
});
136+
});
137+
138+
after(() =>
139+
AV.request({
140+
method: 'DELETE',
141+
path: `/play/leaderboards/${statisticName}`,
142+
authOptions: {
143+
useMasterKey: true,
144+
},
145+
})
146+
);
147+
});

test/test.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
<script src="search.js"></script>
4040
<script src="cloud.js"></script>
4141
<script src="hooks.js"></script>
42+
<script src="conversation.js"></script>
43+
<script src="leaderboard.js"></script>
4244
<script>
4345
onload = function(){
4446
mocha.checkLeaks();

test/test.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
'use strict';
2-
3-
if (!process) process = { env: {} };
1+
if (typeof process === 'undefined') process = { env: {} };
42

53
if (typeof require !== 'undefined') {
64
global.debug = require('debug')('test');
75
global.expect = require('expect.js');
86
global.AV = require('../src');
97
}
108

11-
// AV._config.APIServerURL = 'https://cn-stg1.avoscloud.com';
129
// AV.init({
13-
// appId: 'mxrb5nn3qz7drek0etojy5lh4yrwjnk485lqajnsgjwfxrb5',
14-
// appKey: 'd7sbus0d81mrum4tko4t8gl74b27vl0rh762ff7ngrb6ymmq',
15-
// masterKey: 'l0n9wu3kwnrtf2cg1b6w2l87nphzpypgff6240d0lxui2mm4'
10+
// appId: 'Vpe1RqHgS5VGWBlhB6pdiiow-null',
11+
// appKey: 'OxKVgM0izOIckMi9WiT0pBSf',
12+
// masterKey: 'RCLNNJ6l51YJXzv7YG4fHA5v',
13+
// serverURLs: 'https://cn-stg1.leancloud.cn',
1614
// });
1715
AV.init({
1816
appId: process.env.APPID || '95TNUaOSUd8IpKNW0RSqSEOm-9Nh9j0Va',

0 commit comments

Comments
 (0)