Skip to content

Commit

Permalink
Extract out logic to create sentence scores
Browse files Browse the repository at this point in the history
Signed-off-by: Abhishek Kumar <[email protected]>
  • Loading branch information
abhi-kr-2100 committed Mar 30, 2024
1 parent f5a883a commit 1d0e611
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 91 deletions.
93 changes: 2 additions & 91 deletions express-backend/src/middlewares/createUserSpecificScores.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Request, Response, NextFunction } from 'express';

import { Types } from 'mongoose';

import Sentence, { SentenceType } from '../sentence';
import SentenceScore from '../sentence/sentenceScore';
import Word, { WordType } from '../word/word';
import WordScore from '../word/wordScore';
import { UserProfile } from '../user-profile';

import { getLanguageModel } from '../language-models';
import createScoresForUser from '../scripts/ScoreCreator';

export default async function createUserSpecificScores(
req: Request,
Expand All @@ -20,90 +14,7 @@ export default async function createUserSpecificScores(
}

const user = await UserProfile.findOne({ userId: req.auth.payload.sub });
if (
user.configuredSentenceLists.includes(new Types.ObjectId(req.params.id))
) {
return next();
}

user.configuredSentenceLists.push(new Types.ObjectId(req.params.id));
const updateUserPromise = user.save();

const sentences = await Sentence.find({
'sentenceList._id': req.params.id,
});
const createSentenceScoresPromise = SentenceScore.insertMany(
sentences.map((sentence) => ({
sentence,
owner: user,
level: 0,
score: {},
})),
{
ordered: false,
},
);

const words = await getUniqueWordsFromSentences(sentences);
const isWordNew = await Promise.all(
words.map((w) => isANewWordForUser(w, user._id.toString())),
);
const newWords = words.filter((w, idx) => isWordNew[idx]);

const createWordScorePromise = WordScore.insertMany(
newWords.map((word) => ({
owner: user,
score: {},
word,
})),
{
ordered: false,
},
);

await Promise.all([
updateUserPromise,
createSentenceScoresPromise,
createWordScorePromise,
]);
await createScoresForUser(user._id, new Types.ObjectId(req.params.id));

return next();
}

async function getUniqueWordsFromSentences(sentences: SentenceType[]) {
const uniqueWordSchemas = [
...new Set(
sentences
.map((sentence) => {
const lm = getLanguageModel(sentence.textLanguageCode);
const wordTexts = lm.getWords(sentence.text);
return wordTexts.map((wordText) => ({
wordText,
languageCode: sentence.textLanguageCode,
}));
})
.flat()
.map((wordSchema) => JSON.stringify(wordSchema)),
),
];

return Promise.all(
uniqueWordSchemas.map((wordSchemaStr) => {
const wordSchema = JSON.parse(wordSchemaStr);
return Word.findOne({
wordText: wordSchema.wordText,
languageCode: wordSchema.languageCode,
});
}),
);
}

async function isANewWordForUser(word: WordType, userId: string) {
const foundWords = await WordScore.find({
'word.wordText': word.wordText,
'word.languageCode': word.languageCode,
'owner._id': userId,
});

return foundWords.length == 0;
}
93 changes: 93 additions & 0 deletions express-backend/src/scripts/ScoreCreator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Types } from 'mongoose';

import Sentence, { SentenceType } from '../../sentence';
import SentenceScore from '../../sentence/sentenceScore';
import Word, { WordType } from '../../word/word';
import WordScore from '../../word/wordScore';
import { UserProfile } from '../../user-profile';

import { getLanguageModel } from '../../language-models';

export default async function createScoresForUser(
userProfileId: Types.ObjectId,
sentenceListId: Types.ObjectId,
) {
const user = await UserProfile.findById(userProfileId);
if (user.configuredSentenceLists.includes(sentenceListId)) {
return;
}

const sentences = await Sentence.find({
'sentenceList._id': sentenceListId,
});
const createSentenceScoresPromise = SentenceScore.insertMany(
sentences.map((sentence) => ({
sentence,
owner: user,
level: 0,
score: {},
})),
{
ordered: false,
},
);

const words = await getUniqueWordsFromSentences(sentences);
const isWordNew = await Promise.all(
words.map((w) => isANewWordForUser(w, user._id.toString())),
);
const newWords = words.filter((w, idx) => isWordNew[idx]);

const createWordScorePromise = WordScore.insertMany(
newWords.map((word) => ({
owner: user,
score: {},
word,
})),
{
ordered: false,
},
);

await Promise.all([createSentenceScoresPromise, createWordScorePromise]);

user.configuredSentenceLists.push(sentenceListId);
await user.save();
}

async function getUniqueWordsFromSentences(sentences: SentenceType[]) {
const uniqueWords = [
...new Set(
sentences
.map((sentence) => {
const lm = getLanguageModel(sentence.textLanguageCode);
const wordTexts = lm.getWords(sentence.text);
return wordTexts.map((wordText) => ({
wordText,
languageCode: sentence.textLanguageCode,
}));
})
.flat()
.map((wordSchema) => JSON.stringify(wordSchema)),
),
].map((serialized) => JSON.parse(serialized) as WordType);

return Promise.all(
uniqueWords.map((word) => {
return Word.findOne({
wordText: word.wordText,
languageCode: word.languageCode,
});
}),
);
}

async function isANewWordForUser(word: WordType, userId: string) {
const foundWords = await WordScore.find({
'word.wordText': word.wordText,
'word.languageCode': word.languageCode,
'owner._id': userId,
});

return foundWords.length == 0;
}
153 changes: 153 additions & 0 deletions express-backend/src/tests/scripts/ScoreCreator/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
beforeEach,
afterEach,
describe,
it,
beforeAll,
afterAll,
expect,
} from '@jest/globals';

import createScoresForUser from '../../../scripts/ScoreCreator';

import { MongoMemoryServer } from 'mongodb-memory-server';
import mongoose, { Document, Types } from 'mongoose';
import { UserProfile, UserProfileType } from '../../../user-profile';
import SentenceList, { SentenceListType } from '../../../sentence-list';
import WordScore from '../../../word/wordScore';
import Sentence, { SentenceType } from '../../../sentence';
import Word, { WordType } from '../../../word/word';
import SentenceScore from '../../../sentence/sentenceScore';

describe('create user specific scores middleware', () => {
let mongoDB: MongoMemoryServer;

let testUser: Document<Types.ObjectId, {}, UserProfileType> & UserProfileType,
testSentenceList: Document<Types.ObjectId, {}, SentenceListType> &
SentenceListType,
testSentenceList2: Document<Types.ObjectId, {}, SentenceListType> &
SentenceListType,
testSentence: Document<Types.ObjectId, {}, SentenceType> & SentenceType,
testSentence2: Document<Types.ObjectId, {}, SentenceType> & SentenceType,
testWords = [] as (Document<Types.ObjectId, {}, WordType> & WordType)[];

beforeAll(async () => {
mongoDB = await MongoMemoryServer.create();
const uri = mongoDB.getUri();
await mongoose.connect(uri);

testUser = await UserProfile.create({
userId: 'google-oauth2|113671952727045600873',
});

testSentenceList = await SentenceList.create({
title: 'Test Sentence List',
owner: testUser,
});

testSentenceList2 = await SentenceList.create({
title: 'Test Sentence List 2',
owner: testUser,
});

testSentence = await Sentence.create({
sentenceList: testSentenceList,
text: 'Test',
textLanguageCode: 'en',
});

testSentence2 = await Sentence.create({
sentenceList: testSentenceList2,
text: 'Test',
textLanguageCode: 'en',
});

testWords.push(
await Word.create({
wordText: 'test',
languageCode: 'en',
}),
);
});

afterEach(async () => {
await WordScore.deleteMany({});
await SentenceScore.deleteMany({});

testUser.configuredSentenceLists = [];
await testUser.save();
});

afterAll(async () => {
await mongoose.disconnect();
await mongoDB.stop();
});

it("should create sentence and word scores for a user if they don't exist", async () => {
await createScoresForUser(testUser._id, testSentenceList._id);

testUser = await UserProfile.findById(testUser._id);

expect(testUser.configuredSentenceLists.length).toBe(1);
expect(testUser.configuredSentenceLists[0].toString()).toBe(
testSentenceList._id.toString(),
);

const sentenceScores = await SentenceScore.find({});
expect(sentenceScores.length).toBe(1);
expect(sentenceScores[0].sentence.text).toBe(testSentence.text);
expect(sentenceScores[0].owner.userId).toBe(testUser.userId);
expect(sentenceScores[0].score.easinessFactor).toBe(2.5);
expect(sentenceScores[0].score.interRepetitionIntervalInDays).toBe(1);
expect(sentenceScores[0].score.repetitionNumber).toBe(0);
expect(sentenceScores[0].level).toBe(0);

const wordScores = await WordScore.find({});
expect(wordScores.length).toBe(1);
expect(wordScores[0].word.wordText).toBe(testWords[0].wordText);
expect(wordScores[0].owner.userId).toBe(testUser.userId);
expect(wordScores[0].score.easinessFactor).toBe(2.5);
expect(wordScores[0].score.interRepetitionIntervalInDays).toBe(1);
expect(wordScores[0].score.repetitionNumber).toBe(0);
});

it('should not create sentence or word scores for a user if they already exist', async () => {
testUser.configuredSentenceLists.push(testSentenceList._id);
await testUser.save();

await createScoresForUser(testUser._id, testSentenceList._id);

const sentenceScores = await SentenceScore.find({});
const wordScores = await WordScore.find({});

expect(testUser.configuredSentenceLists.length).toBe(1);
expect(sentenceScores.length).toBe(0);
expect(wordScores.length).toBe(0);
});

it('should not create duplicate word scores', async () => {
await createScoresForUser(testUser._id, testSentenceList._id);
await createScoresForUser(testUser._id, testSentenceList2._id);

testUser = await UserProfile.findById(testUser._id);

expect(testUser.configuredSentenceLists.length).toBe(2);
expect(
testUser.configuredSentenceLists.map((sl) => sl.toString()),
).toContain(testSentenceList._id.toString());
expect(
testUser.configuredSentenceLists.map((sl) => sl.toString()),
).toContain(testSentenceList2._id.toString());

const sentenceScores = await SentenceScore.find({});
expect(sentenceScores.length).toBe(2);

const wordScores = await WordScore.find({});
expect(wordScores.length).toBe(1);
expect(wordScores[0].word.wordText).toBe(testWords[0].wordText);
expect(wordScores[0].owner.userId).toBe(testUser.userId);
expect(wordScores[0].score.easinessFactor).toBe(2.5);
expect(wordScores[0].score.interRepetitionIntervalInDays).toBe(1);
expect(wordScores[0].score.repetitionNumber).toBe(0);
});
});

0 comments on commit 1d0e611

Please sign in to comment.