Skip to content

Commit

Permalink
Merge pull request #17 from emtiajium/caching-guessing-game
Browse files Browse the repository at this point in the history
Sent current date's guessing game related date from the table avoiding random generation
  • Loading branch information
emtiajium authored Jun 2, 2024
2 parents 5ef6e0c + 2d7bdcd commit cce4c39
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 22 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@

## Change Logs

- `1.52.1`: Sent current date's guessing game related date from the table avoiding random generation
- `1.52.0`: API to get randomly chosen meanings (part of the guessing game)
- `1.51.0`: Created a separate DB (Docker container) for the automated test with `fsync=off`, `synchronous_commit=off` and `full_page_writes=off`
- `1.50.0`: Empty array when a vocab does not have a definition using the FILTER clause
Expand Down
51 changes: 51 additions & 0 deletions migrations/1717321421822-word-meaning-in-guessing-game-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class WordMeaningInGuessingGameTable1717321421822 implements MigrationInterface {
name = 'WordMeaningInGuessingGameTable1717321421822';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "GuessingGame"
ADD "meaning" character varying NULL
`);
await queryRunner.query(`
ALTER TABLE "GuessingGame"
ADD "word" character varying NULL
`);

await queryRunner.query(`
with "FilteredGuessingGame" as
(select "GuessingGame"."definitionId"
from "GuessingGame"),
"FilteredDefinition" as (select "Definition".id, "Definition".meaning, "Vocabulary".word
from "Definition"
inner join "Vocabulary" on "Definition"."vocabularyId" = "Vocabulary".id
where "Definition".id in (select "definitionId" from "FilteredGuessingGame"))
update "GuessingGame"
set word = "FilteredDefinition".word,
meaning = "FilteredDefinition".meaning
from "FilteredDefinition"
where "GuessingGame"."definitionId" = "FilteredDefinition".id;
`);

await queryRunner.query(`
ALTER TABLE "GuessingGame"
ALTER COLUMN "meaning" SET NOT NULL
`);
await queryRunner.query(`
ALTER TABLE "GuessingGame"
ALTER COLUMN "word" SET NOT NULL
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "GuessingGame"
DROP COLUMN "word"
`);
await queryRunner.query(`
ALTER TABLE "GuessingGame"
DROP COLUMN "meaning"
`);
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vocabulary-flashcard-backend",
"version": "1.52.0",
"version": "1.52.1",
"author": {
"name": "Emtiaj Hasan",
"email": "271emtiaj@gmail.com",
Expand Down
6 changes: 6 additions & 0 deletions src/vocabulary/domains/GuessingGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export default class GuessingGame {
@Column({ type: 'uuid', nullable: false })
userId: string;

@Column({ type: 'varchar', nullable: false })
meaning: string;

@Column({ type: 'varchar', nullable: false })
word: string;

@CreateDateColumn({ type: 'timestamp with time zone' })
createdAt: Date;
}
30 changes: 24 additions & 6 deletions src/vocabulary/repositories/GuessingGameRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,38 @@ export default class GuessingGameRepository extends Repository<GuessingGame> {
super(GuessingGame, dataSource.createEntityManager());
}

async insertMultiples(definitionIds: string[], userId: string): Promise<void> {
async insertMultiples(guessingGames: Partial<GuessingGame>[], userId: string): Promise<void> {
await this.insert(
definitionIds.map((definitionId) => {
guessingGames.map((guessingGame) => {
return {
userId,
definitionId,
definitionId: guessingGame.definitionId,
word: guessingGame.word,
meaning: guessingGame.meaning,
};
}),
);
}

async getDefinitionIdsByUserId(userId: string): Promise<string[]> {
const guessingGames = await this.find({ where: { userId }, select: ['definitionId'] });
return guessingGames.map(({ definitionId }) => definitionId);
getByUserId(userId: string): Promise<
{
definitionId: string;
word: string;
meaning: string;
isCreationDateToday: boolean;
}[]
> {
return this.query(
`
select "definitionId",
word,
meaning,
("createdAt"::date = now()::date) as "isCreationDateToday"
from "GuessingGame"
where "userId" = $1;
`,
[userId],
);
}

async deleteOlder(): Promise<void> {
Expand Down
48 changes: 35 additions & 13 deletions src/vocabulary/services/GuessingGameService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import DefinitionRepository from '@/vocabulary/repositories/DefinitionRepository';
import { shuffle } from 'lodash';
import { RandomlyChosenMeaningResponse } from '@/vocabulary/domains/RandomlyChosenMeaningResponse';
import {
RandomlyChosenMeaningQueryResponse,
RandomlyChosenMeaningResponse,
} from '@/vocabulary/domains/RandomlyChosenMeaningResponse';
import WordsApiAdapter from '@/vocabulary/adapters/WordsApiAdapter';
import GuessingGameRepository from '@/vocabulary/repositories/GuessingGameRepository';

Expand All @@ -14,16 +17,30 @@ export default class GuessingGameService {
) {}

async getRandomlyChosenMeanings(cohortId: string, userId: string): Promise<RandomlyChosenMeaningResponse[]> {
const [randomlyChosenMeaningResponsesFromExternalService, previousDefinitionIds] = await Promise.all([
const [randomlyChosenMeaningResponsesFromExternalService, previousDefinitions] = await Promise.all([
this.wordsApiAdapter.getRandomWords(),
this.guessingGameRepository.getDefinitionIdsByUserId(userId),
this.guessingGameRepository.getByUserId(userId),
]);

let randomlyChosenMeaningResponses = await this.definitionRepository.getRandomlyChosenMeanings(
cohortId,
userId,
previousDefinitionIds,
);
let definitionFromTodayExists = false;
const previousDefinitionIds: string[] = [];
const definitionsFromToday: RandomlyChosenMeaningQueryResponse[] = [];

previousDefinitions.forEach((previousDefinition) => {
previousDefinitionIds.push(previousDefinition.definitionId);
if (previousDefinition.isCreationDateToday) {
definitionFromTodayExists = true;
definitionsFromToday.push({
definitionId: previousDefinition.definitionId,
word: previousDefinition.word,
meaning: previousDefinition.meaning,
});
}
});

let randomlyChosenMeaningResponses = definitionFromTodayExists
? definitionsFromToday
: await this.definitionRepository.getRandomlyChosenMeanings(cohortId, userId, previousDefinitionIds);

if (randomlyChosenMeaningResponses.length === 0) {
randomlyChosenMeaningResponses = await this.definitionRepository.getAnyMeanings(
Expand All @@ -32,11 +49,9 @@ export default class GuessingGameService {
);
}

const definitionIds = randomlyChosenMeaningResponses.map((randomlyChosenMeaningResponse) => {
return randomlyChosenMeaningResponse.definitionId;
});

this.guessingGameRepository.insertMultiples(definitionIds, userId).finally();
if (!definitionFromTodayExists) {
this.save(randomlyChosenMeaningResponses, userId).finally();
}

return shuffle([
...randomlyChosenMeaningResponsesFromExternalService,
Expand All @@ -56,4 +71,11 @@ export default class GuessingGameService {
async deleteByUserId(userId: string): Promise<void> {
await this.guessingGameRepository.deleteByUserId(userId);
}

private async save(
randomlyChosenMeaningResponses: RandomlyChosenMeaningQueryResponse[],
userId: string,
): Promise<void> {
await this.guessingGameRepository.insertMultiples(randomlyChosenMeaningResponses, userId);
}
}
2 changes: 2 additions & 0 deletions test/e2e/user-controller-delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ describe('DELETE /v1/users/self', () => {
await app.get(GuessingGameRepository).insert({
userId: requester.id,
definitionId: uuid.v4(),
word: 'word',
meaning: 'meaning',
});

// Act
Expand Down
42 changes: 42 additions & 0 deletions test/e2e/vocabulary-controller-search-random-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { RandomlyChosenMeaningResponse } from '@/vocabulary/domains/RandomlyChos
import WordsApiAdapter from '@/vocabulary/adapters/WordsApiAdapter';
import GuessingGameRepository from '@/vocabulary/repositories/GuessingGameRepository';
import MomentUnit, { makeItOlder } from '@/common/utils/moment-util';
import DefinitionRepository from '@/vocabulary/repositories/DefinitionRepository';

describe('GET /v1/vocabularies/definitions/random-search', () => {
let app: INestApplication;
Expand Down Expand Up @@ -97,6 +98,8 @@ describe('GET /v1/vocabularies/definitions/random-search', () => {
userId: requester.id,
definitionId: previousVocabulary.definitions[0].id,
createdAt: makeItOlder(new Date(), MomentUnit.DAYS, 3),
word: previousVocabulary.word,
meaning: previousVocabulary.definitions[0].meaning,
});

// Act
Expand All @@ -114,5 +117,44 @@ describe('GET /v1/vocabularies/definitions/random-search', () => {
},
]);
});

it('SHOULD return 200 OK with definitions from today', async () => {
// Arrange
const getRandomlyChosenMeaningsMock = jest.spyOn(
DefinitionRepository.prototype,
'getRandomlyChosenMeanings',
);
const insertMultiplesMock = jest.spyOn(GuessingGameRepository.prototype, 'insertMultiples');
const previousVocabulary = await createVocabulary(getVocabularyWithDefinitions(), cohort.id);

await app.get(GuessingGameRepository).insert({
userId: requester.id,
definitionId: previousVocabulary.definitions[0].id,
createdAt: new Date(),
word: previousVocabulary.word,
meaning: previousVocabulary.definitions[0].meaning,
});

// Act
const { status, body } = await makeApiRequest(requester);

// Assert
expect(status).toBe(200);
expect(getRandomWordMock).not.toHaveBeenCalled();
expect(getRandomlyChosenMeaningsMock).not.toHaveBeenCalled();
expect(insertMultiplesMock).not.toHaveBeenCalled();
const response = body as RandomlyChosenMeaningResponse[];
expect(response).not.toHaveLength(0);
expect(response).toStrictEqual([
<RandomlyChosenMeaningResponse>{
word: previousVocabulary.word,
meaning: previousVocabulary.definitions[0].meaning,
},
]);

// Post-Assert
getRandomlyChosenMeaningsMock.mockRestore();
insertMultiplesMock.mockRestore();
});
});
});
4 changes: 4 additions & 0 deletions test/integration/delete-old-random-definitions-job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ describe('DeleteOldRandomDefinitionsJob', () => {
userId: uuid.v4(),
definitionId: uuid.v4(),
createdAt: makeItOlder(new Date(), MomentUnit.DAYS, 3),
word: 'word',
meaning: 'meaning',
});
const id = generatedMaps[0].id;

Expand All @@ -46,6 +48,8 @@ describe('DeleteOldRandomDefinitionsJob', () => {
userId: uuid.v4(),
definitionId: uuid.v4(),
createdAt: makeItOlder(new Date(), MomentUnit.DAYS, 16),
word: 'word',
meaning: 'meaning',
});
const id = generatedMaps[0].id;

Expand Down

0 comments on commit cce4c39

Please sign in to comment.