Skip to content

Commit b4ea7f1

Browse files
authored
impr(dev): add endpoint to create test user/data (fehmer) (#5396)
!nuf
1 parent 57a6fd9 commit b4ea7f1

21 files changed

Lines changed: 994 additions & 134 deletions

File tree

backend/src/api/controllers/dev.ts

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
import { MonkeyResponse } from "../../utils/monkey-response";
2+
import * as UserDal from "../../dal/user";
3+
import FirebaseAdmin from "../../init/firebase-admin";
4+
import Logger from "../../utils/logger";
5+
import * as DateUtils from "date-fns";
6+
import { UTCDate } from "@date-fns/utc";
7+
import * as ResultDal from "../../dal/result";
8+
import { roundTo2 } from "../../utils/misc";
9+
import { ObjectId } from "mongodb";
10+
import * as LeaderboardDal from "../../dal/leaderboards";
11+
import { isNumber } from "lodash";
12+
import MonkeyError from "../../utils/error";
13+
14+
type GenerateDataOptions = {
15+
firstTestTimestamp: Date;
16+
lastTestTimestamp: Date;
17+
minTestsPerDay: number;
18+
maxTestsPerDay: number;
19+
};
20+
21+
const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = {
22+
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())),
23+
lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())),
24+
minTestsPerDay: 0,
25+
maxTestsPerDay: 50,
26+
};
27+
28+
export async function createTestData(
29+
req: MonkeyTypes.Request
30+
): Promise<MonkeyResponse> {
31+
const { username, createUser } = req.body;
32+
const user = await getOrCreateUser(username, "password", createUser);
33+
34+
const { uid, email } = user;
35+
36+
await createTestResults(user, req.body);
37+
await updateUser(uid);
38+
await updateLeaderboard();
39+
40+
return new MonkeyResponse("test data created", { uid, email }, 200);
41+
}
42+
43+
async function getOrCreateUser(
44+
username: string,
45+
password: string,
46+
createUser = false
47+
): Promise<MonkeyTypes.DBUser> {
48+
const existingUser = await UserDal.findByName(username);
49+
50+
if (existingUser !== undefined && existingUser !== null) {
51+
return existingUser;
52+
} else if (createUser === false) {
53+
throw new MonkeyError(404, `User ${username} does not exist.`);
54+
}
55+
56+
const email = username + "@example.com";
57+
Logger.success("create user " + username);
58+
const { uid } = await FirebaseAdmin().auth().createUser({
59+
displayName: username,
60+
password: password,
61+
email,
62+
emailVerified: true,
63+
});
64+
65+
await UserDal.addUser(username, email, uid);
66+
return UserDal.getUser(uid, "getOrCreateUser");
67+
}
68+
69+
async function createTestResults(
70+
user: MonkeyTypes.DBUser,
71+
configOptions: Partial<GenerateDataOptions>
72+
): Promise<void> {
73+
const config = {
74+
...CREATE_RESULT_DEFAULT_OPTIONS,
75+
...configOptions,
76+
};
77+
if (isNumber(config.firstTestTimestamp))
78+
config.firstTestTimestamp = toDate(config.firstTestTimestamp);
79+
if (isNumber(config.lastTestTimestamp))
80+
config.lastTestTimestamp = toDate(config.lastTestTimestamp);
81+
82+
const days = DateUtils.eachDayOfInterval({
83+
start: config.firstTestTimestamp,
84+
end: config.lastTestTimestamp,
85+
}).map((day) => ({
86+
timestamp: DateUtils.startOfDay(day),
87+
amount: Math.round(random(config.minTestsPerDay, config.maxTestsPerDay)),
88+
}));
89+
90+
for (const day of days) {
91+
Logger.success(
92+
`User ${user.name} insert ${day.amount} results on ${new Date(
93+
day.timestamp
94+
)}`
95+
);
96+
const results = createArray(day.amount, () =>
97+
createResult(user, day.timestamp)
98+
);
99+
if (results.length > 0)
100+
await ResultDal.getResultCollection().insertMany(results);
101+
}
102+
}
103+
104+
function toDate(value: number): Date {
105+
return new UTCDate(value);
106+
}
107+
108+
function random(min: number, max: number): number {
109+
return roundTo2(Math.random() * (max - min) + min);
110+
}
111+
112+
function createResult(
113+
user: MonkeyTypes.DBUser,
114+
timestamp: Date //evil, we modify this value
115+
): MonkeyTypes.DBResult {
116+
const mode: SharedTypes.Config.Mode = randomValue(["time", "words"]);
117+
const mode2: number =
118+
mode === "time"
119+
? randomValue([15, 30, 60, 120])
120+
: randomValue([10, 25, 50, 100]);
121+
const testDuration = mode2;
122+
123+
timestamp = DateUtils.addSeconds(timestamp, testDuration);
124+
return {
125+
_id: new ObjectId(),
126+
uid: user.uid,
127+
wpm: random(80, 120),
128+
rawWpm: random(80, 120),
129+
charStats: [131, 0, 0, 0],
130+
acc: random(80, 100),
131+
language: "english",
132+
mode: mode as SharedTypes.Config.Mode,
133+
mode2: mode2 as unknown as never,
134+
timestamp: timestamp.valueOf(),
135+
testDuration: testDuration,
136+
consistency: random(80, 100),
137+
keyConsistency: 33.18,
138+
chartData: {
139+
wpm: createArray(testDuration, () => random(80, 120)),
140+
raw: createArray(testDuration, () => random(80, 120)),
141+
err: createArray(testDuration, () => (Math.random() < 0.1 ? 1 : 0)),
142+
},
143+
keySpacingStats: {
144+
average: 113.88,
145+
sd: 77.3,
146+
},
147+
keyDurationStats: {
148+
average: 107.13,
149+
sd: 39.86,
150+
},
151+
isPb: Math.random() < 0.1,
152+
name: user.name,
153+
};
154+
}
155+
156+
async function updateUser(uid: string): Promise<void> {
157+
//update timetyping and completedTests
158+
const stats = await ResultDal.getResultCollection()
159+
.aggregate([
160+
{
161+
$match: {
162+
uid,
163+
},
164+
},
165+
{
166+
$group: {
167+
_id: {
168+
language: "$language",
169+
mode: "$mode",
170+
mode2: "$mode2",
171+
},
172+
timeTyping: {
173+
$sum: "$testDuration",
174+
},
175+
completedTests: {
176+
$count: {},
177+
},
178+
},
179+
},
180+
])
181+
.toArray();
182+
183+
const timeTyping = stats.reduce((a, c) => a + c["timeTyping"], 0);
184+
const completedTests = stats.reduce((a, c) => a + c["completedTests"], 0);
185+
186+
//update PBs
187+
const lbPersonalBests: MonkeyTypes.LbPersonalBests = {
188+
time: {
189+
15: {},
190+
60: {},
191+
},
192+
};
193+
194+
const personalBests: SharedTypes.PersonalBests = {
195+
time: {},
196+
custom: {},
197+
words: {},
198+
zen: {},
199+
quote: {},
200+
};
201+
const modes = stats.map((it) => it["_id"]);
202+
for (const mode of modes) {
203+
const best = (
204+
await ResultDal.getResultCollection()
205+
.find({
206+
uid,
207+
language: mode.language,
208+
mode: mode.mode,
209+
mode2: mode.mode2,
210+
})
211+
.sort({ wpm: -1, timestamp: 1 })
212+
.limit(1)
213+
.toArray()
214+
)[0] as MonkeyTypes.DBResult;
215+
216+
if (personalBests[mode.mode] === undefined) personalBests[mode.mode] = {};
217+
if (personalBests[mode.mode][mode.mode2] === undefined)
218+
personalBests[mode.mode][mode.mode2] = [];
219+
220+
const entry = {
221+
acc: best.acc,
222+
consistency: best.consistency,
223+
difficulty: best.difficulty ?? "normal",
224+
lazyMode: best.lazyMode,
225+
language: mode.language,
226+
punctuation: best.punctuation,
227+
raw: best.rawWpm,
228+
wpm: best.wpm,
229+
numbers: best.numbers,
230+
timestamp: best.timestamp,
231+
} as SharedTypes.PersonalBest;
232+
233+
personalBests[mode.mode][mode.mode2].push(entry);
234+
235+
if (mode.mode === "time") {
236+
if (lbPersonalBests[mode.mode][mode.mode2] === undefined)
237+
lbPersonalBests[mode.mode][mode.mode2] = {};
238+
239+
lbPersonalBests[mode.mode][mode.mode2][mode.language] = entry;
240+
}
241+
242+
//update testActivity
243+
await updateTestActicity(uid);
244+
}
245+
246+
//update the user
247+
await UserDal.getUsersCollection().updateOne(
248+
{ uid },
249+
{
250+
$set: {
251+
timeTyping: timeTyping,
252+
completedTests: completedTests,
253+
startedTests: Math.round(completedTests * 1.25),
254+
personalBests: personalBests as SharedTypes.PersonalBests,
255+
lbPersonalBests: lbPersonalBests,
256+
},
257+
}
258+
);
259+
}
260+
261+
async function updateLeaderboard(): Promise<void> {
262+
await LeaderboardDal.update("time", "15", "english");
263+
await LeaderboardDal.update("time", "60", "english");
264+
}
265+
266+
function randomValue<T>(values: T[]): T {
267+
const rnd = Math.round(Math.random() * (values.length - 1));
268+
return values[rnd] as T;
269+
}
270+
271+
function createArray<T>(size: number, builder: () => T): T[] {
272+
return new Array(size).fill(0).map((it) => builder());
273+
}
274+
275+
async function updateTestActicity(uid: string): Promise<void> {
276+
await ResultDal.getResultCollection()
277+
.aggregate(
278+
[
279+
{
280+
$match: {
281+
uid,
282+
},
283+
},
284+
{
285+
$project: {
286+
_id: 0,
287+
timestamp: -1,
288+
uid: 1,
289+
},
290+
},
291+
{
292+
$addFields: {
293+
date: {
294+
$toDate: "$timestamp",
295+
},
296+
},
297+
},
298+
{
299+
$replaceWith: {
300+
uid: "$uid",
301+
year: {
302+
$year: "$date",
303+
},
304+
day: {
305+
$dayOfYear: "$date",
306+
},
307+
},
308+
},
309+
{
310+
$group: {
311+
_id: {
312+
uid: "$uid",
313+
year: "$year",
314+
day: "$day",
315+
},
316+
count: {
317+
$sum: 1,
318+
},
319+
},
320+
},
321+
{
322+
$group: {
323+
_id: {
324+
uid: "$_id.uid",
325+
year: "$_id.year",
326+
},
327+
days: {
328+
$addToSet: {
329+
day: "$_id.day",
330+
tests: "$count",
331+
},
332+
},
333+
},
334+
},
335+
{
336+
$replaceWith: {
337+
uid: "$_id.uid",
338+
days: {
339+
$function: {
340+
lang: "js",
341+
args: ["$days", "$_id.year"],
342+
body: `function (days, year) {
343+
var max = Math.max(
344+
...days.map((it) => it.day)
345+
)-1;
346+
var arr = new Array(max).fill(null);
347+
for (day of days) {
348+
arr[day.day-1] = day.tests;
349+
}
350+
let result = {};
351+
result[year] = arr;
352+
return result;
353+
}`,
354+
},
355+
},
356+
},
357+
},
358+
{
359+
$group: {
360+
_id: "$uid",
361+
testActivity: {
362+
$mergeObjects: "$days",
363+
},
364+
},
365+
},
366+
{
367+
$addFields: {
368+
uid: "$_id",
369+
},
370+
},
371+
{
372+
$project: {
373+
_id: 0,
374+
},
375+
},
376+
{
377+
$merge: {
378+
into: "users",
379+
on: "uid",
380+
whenMatched: "merge",
381+
whenNotMatched: "discard",
382+
},
383+
},
384+
],
385+
{ allowDiskUse: true }
386+
)
387+
.toArray();
388+
}

0 commit comments

Comments
 (0)