Skip to content

Commit 6c2774f

Browse files
Merge pull request #9 from numericgraphics/feature/02_use-typescript-2
improve schema
2 parents 94cc968 + 22eea5e commit 6c2774f

File tree

5 files changed

+172
-108
lines changed

5 files changed

+172
-108
lines changed

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/controllers/Global.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import MongoDB from "../services/MongoDB";
22
import Users, { User } from "../services/Users";
33
import Scores from "../services/Scores";
44
import EVENTS from "../constants/events";
5-
import type { Db } from "mongodb";
5+
import type { Db, ObjectId } from "mongodb";
66

7-
// Narrow message types to exactly the events this method can return
7+
// Results using literal event types
88
type AddUserResult =
99
| { message: typeof EVENTS.USER_ALREADY_EXIST }
1010
| { message: typeof EVENTS.USER_CREATED; list: User[] };
@@ -38,28 +38,47 @@ export default class Global {
3838
}
3939
}
4040

41-
async addUser(user: Omit<User, "_id">): Promise<AddUserResult> {
42-
const { email, score } = user;
41+
/**
42+
* Create a user aligned with the mobile schema: { userName, password }
43+
* Optionally seed an initial score in the scores collection.
44+
*/
45+
async addUser(payload: {
46+
userName: string;
47+
password: string;
48+
score?: number;
49+
}): Promise<AddUserResult> {
50+
const { userName, password, score } = payload;
4351

44-
if (typeof score !== "number") {
45-
throw new Error("Invalid score");
52+
if (!userName || !password) {
53+
throw new Error("Invalid payload: userName and password are required");
4654
}
4755

48-
const oldUser = await this.users.isUserAlreadyExist(email);
49-
if (oldUser) {
56+
const exists = await this.users.findByUserName(userName);
57+
if (exists) {
5058
return { message: EVENTS.USER_ALREADY_EXIST };
5159
}
5260

5361
try {
54-
const list = await this.users.addUser(user);
62+
const created = await this.users.addUser({ userName, password });
63+
64+
// create an initial score if provided
65+
if (typeof score === "number" && !Number.isNaN(score)) {
66+
await this.scores.addScore(created._id as ObjectId, score);
67+
}
68+
69+
const list = await this.users.list();
5570
return { message: EVENTS.USER_CREATED, list };
56-
} catch {
71+
} catch (e) {
5772
// eslint-disable-next-line no-console
5873
console.log("Global Controller - addUser failed !!!");
5974
throw new Error("Add user failed");
6075
}
6176
}
6277

78+
/**
79+
* Check a raw score against the global minimum across the scores collection.
80+
* Does not store the score.
81+
*/
6382
async checkScore(score: number): Promise<CheckScoreResult> {
6483
try {
6584
const ok = await this.scores.checkScores(score);
@@ -70,4 +89,30 @@ export default class Global {
7089
throw e;
7190
}
7291
}
92+
93+
/**
94+
* Add a score to a given user by userName.
95+
*/
96+
async addScoreForUser(
97+
userName: string,
98+
value: number
99+
): Promise<{ userId?: ObjectId; created?: boolean } & CheckScoreResult> {
100+
const user = await this.users.findByUserName(userName);
101+
if (!user || !user._id) {
102+
return { message: EVENTS.SCORE_REJECTED };
103+
}
104+
105+
// optional: enforce same acceptance rule as checkScore
106+
const acceptable = await this.scores.checkScores(value);
107+
if (!acceptable) {
108+
return {
109+
userId: user._id,
110+
created: false,
111+
message: EVENTS.SCORE_REJECTED,
112+
};
113+
}
114+
115+
await this.scores.addScore(user._id, value);
116+
return { userId: user._id, created: true, message: EVENTS.SCORED };
117+
}
73118
}

src/index.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import EVENTS from "./constants/events";
66

77
const app = express();
88
const server = http.createServer(app);
9+
910
const PORT = Number(process.env.API_PORT) || 3000;
1011

1112
const globalController = new Global();
@@ -20,10 +21,10 @@ app.get("/", (_req, res) => {
2021
res.send("<h1>Hello world</h1>");
2122
});
2223

23-
type ScoreRequestBody = { score: number };
24+
// --- Score check endpoint (schema-agnostic): checks against global min, does not persist ---
2425
app.post(
2526
"/score",
26-
async (req: Request<{}, {}, ScoreRequestBody>, res: Response) => {
27+
async (req: Request<{}, {}, { score: number }>, res: Response) => {
2728
try {
2829
const { score } = req.body;
2930
const result = await globalController.checkScore(score);
@@ -37,22 +38,23 @@ app.post(
3738
}
3839
);
3940

40-
type AddUserRequestBody = {
41-
username: string;
42-
score: number;
43-
email: string;
44-
password: string;
45-
};
41+
// --- Create user aligned with mobile schema: { userName, password, score? } ---
4642
app.post(
4743
"/adduser",
48-
async (req: Request<{}, {}, AddUserRequestBody>, res: Response) => {
44+
async (
45+
req: Request<
46+
{},
47+
{},
48+
{ userName: string; password: string; score?: number }
49+
>,
50+
res: Response
51+
) => {
4952
try {
50-
const { username, score, email, password } = req.body;
53+
const { userName, password, score } = req.body;
5154
const result = await globalController.addUser({
52-
username,
53-
score,
54-
email,
55+
userName,
5556
password,
57+
score,
5658
});
5759

5860
if (result.message === EVENTS.USER_ALREADY_EXIST) {
@@ -69,6 +71,32 @@ app.post(
6971
}
7072
);
7173

74+
// --- Add a score for a specific user ---
75+
app.post(
76+
"/users/:userName/scores",
77+
async (
78+
req: Request<{ userName: string }, {}, { value: number }>,
79+
res: Response
80+
) => {
81+
try {
82+
const { userName } = req.params;
83+
const { value } = req.body;
84+
85+
if (typeof value !== "number") {
86+
return res.status(400).send("value must be a number");
87+
}
88+
89+
const result = await globalController.addScoreForUser(userName, value);
90+
if (result.message === EVENTS.SCORED && result.created) {
91+
return res.status(201).json({ userId: result.userId, value });
92+
}
93+
return res.status(409).send();
94+
} catch (e) {
95+
return res.status(406).send(e);
96+
}
97+
}
98+
);
99+
72100
server.listen(PORT, () => {
73101
console.log(`Listening on ${PORT}`);
74102
});

src/services/Scores.ts

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { Collection, Db } from "mongodb";
2-
import { getCollectionPropertyValue } from "../utils/array";
3-
import type { User } from "./Users";
1+
import { Collection, Db, ObjectId } from "mongodb";
2+
3+
export interface Score {
4+
_id?: ObjectId;
5+
userId: ObjectId;
6+
value: number;
7+
createdAt?: number;
8+
}
49

510
export default class Scores {
6-
private collection!: Collection<User>;
11+
private collection!: Collection<Score>;
712

813
constructor() {
914
// eslint-disable-next-line no-console
@@ -13,47 +18,41 @@ export default class Scores {
1318
init(db: Db): void {
1419
// eslint-disable-next-line no-console
1520
console.log("Scores Class - init");
16-
this.collection = db.collection<User>("users");
21+
this.collection = db.collection<Score>("scores");
22+
}
23+
24+
async addScore(userId: ObjectId, value: number): Promise<Score> {
25+
const doc: Omit<Score, "_id"> = { userId, value, createdAt: Date.now() };
26+
const res = await this.collection.insertOne(doc);
27+
return { ...doc, _id: res.insertedId };
28+
}
29+
30+
async getMinScore(): Promise<number | null> {
31+
const doc = await this.collection.find().sort({ value: 1 }).limit(1).next();
32+
return doc ? doc.value : null;
1733
}
1834

19-
async getSmallerScores(): Promise<number> {
20-
const collection = await this.collection.find().toArray();
21-
const scores = getCollectionPropertyValue(collection, "score") as number[];
22-
return Math.min(...scores);
35+
async getMaxScore(): Promise<number | null> {
36+
const doc = await this.collection
37+
.find()
38+
.sort({ value: -1 })
39+
.limit(1)
40+
.next();
41+
return doc ? doc.value : null;
2342
}
2443

25-
async getHigherScores(): Promise<number> {
26-
const collection = await this.collection.find().toArray();
27-
const scores = getCollectionPropertyValue(collection, "score") as number[];
28-
return Math.max(...scores);
44+
async topScoreForUser(userId: ObjectId): Promise<number | null> {
45+
const doc = await this.collection
46+
.find({ userId })
47+
.sort({ value: -1 })
48+
.limit(1)
49+
.next();
50+
return doc ? doc.value : null;
2951
}
3052

3153
async checkScores(score: number): Promise<boolean> {
32-
try {
33-
const collection = await this.collection.find().toArray();
34-
const scores = getCollectionPropertyValue(
35-
collection,
36-
"score"
37-
) as number[];
38-
39-
// eslint-disable-next-line no-console
40-
console.log("User score --> ", score);
41-
// eslint-disable-next-line no-console
42-
console.log("-- checkScores - collection User --------------");
43-
// eslint-disable-next-line no-console
44-
console.log("length", collection.length);
45-
// eslint-disable-next-line no-console
46-
console.log("min", Math.min(...scores));
47-
// eslint-disable-next-line no-console
48-
console.log("max", Math.max(...scores));
49-
// eslint-disable-next-line no-console
50-
console.log("---------------------------");
51-
52-
return score > Math.min(...scores);
53-
} catch (error) {
54-
// eslint-disable-next-line no-console
55-
console.log("checkScores - ERROR", error);
56-
throw error;
57-
}
54+
const min = await this.getMinScore();
55+
if (min === null) return true; // no scores yet -> accept
56+
return score > min;
5857
}
5958
}

0 commit comments

Comments
 (0)