Skip to content

Commit

Permalink
feat(api): implement news prediction api with mock ml function
Browse files Browse the repository at this point in the history
  • Loading branch information
swalahamani committed Nov 12, 2023
1 parent b31bcd1 commit a447fd2
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 3 deletions.
55 changes: 54 additions & 1 deletion src/api/routes/newsRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import logger from "@loaders/logger";

import expressUtil from "@util/expressUtil";

import {iRequest, iResponse, RouteType} from "@customTypes/expressTypes";
import {
iRequest,
iRequestParams,
iResponse,
RouteType,
} from "@customTypes/expressTypes";
import {iNewsSubmissionDTO} from "customTypes/appDataTypes/newsTypes";
import {celebrate, Segments} from "celebrate";
import {newsSubmissionBodySchema} from "validations/newsRouteSchemas";
Expand Down Expand Up @@ -69,6 +74,54 @@ const newsRoute: RouteType = (apiRouter) => {
}
}
);

route.get(
"/:newsId/predict",
async (
req: iRequestParams<{
newsId: string;
}>,
res: iResponse<{annotationIds: string[]}>,
next: NextFunction
) => {
const uniqueRequestId = expressUtil.parseUniqueRequestId(req);

logger.debug(
uniqueRequestId,
"Calling GET:/news/:newsId/predict endpoint with params:",
null,
{
requestParams: req.params,
}
);

try {
const {newsId} = req.params;

const result = await newsService.predictAnnotation(
uniqueRequestId,
newsId
);

logger.debug(
uniqueRequestId,
"GET:/news/:newsId/predict :: Completed newsService.predictAnnotation & sending result to client:",
null,
result
);

return res.status(200).json(result);
} catch (error) {
logger.error(
uniqueRequestId,
"Error on GET:/news/:newsId/predict :",
error
);

return next(error);
}
}
);
};

export default newsRoute;
13 changes: 13 additions & 0 deletions src/constants/errors/newsServiceErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {asTypeIServiceError} from "@customTypes/commonServiceTypes";

const newsServiceError = asTypeIServiceError({
predictAnnotations: {
NewsDoesNotExists: {
error: "NewsDoesNotExists",

message: "News doesn't exists",
},
},
});

export {newsServiceError};
2 changes: 2 additions & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
EmailVerificationRequestLogsRepository,
AnnotationsRepository,
NewsRepository,
NewsAnnotationRepository,
} from "@db/repositories/index";

import {Diagnostics} from "@db/diagnostics"; // optional diagnostics
Expand Down Expand Up @@ -59,6 +60,7 @@ const initOptions: IInitOptions<iDBInterfaceExtensions> = {
new EmailVerificationRequestLogsRepository(obj, pgp);
obj.annotations = new AnnotationsRepository(obj, pgp);
obj.news = new NewsRepository(obj, pgp);
obj.newsAnnotationMap = new NewsAnnotationRepository(obj, pgp);
},
};

Expand Down
8 changes: 8 additions & 0 deletions src/db/models/newsAnnotationMap.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface iNewsAnnotationMapModel {
id: string;
news_id: string;
annotation_id: string;
annotated_by: string;
user_id: string;
created_at: string;
}
70 changes: 70 additions & 0 deletions src/db/repositories/NewsAnnotationRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {IDatabase, IMain} from "pg-promise";

import {iNewsAnnotationMapModel} from "db/models/newsAnnotationMap.model";

import {newsAnnotationMap as sql} from "@db/sql";
import {NullableString} from "customTypes/commonTypes";

/*
This repository mixes hard-coded and dynamic SQL, just to show how to use both.
*/
export default class NewsAnnotationRepository {
/**
* @param db
* Automated database connection context/interface.
*
* If you ever need to access other repositories from this one,
* you will have to replace type 'IDatabase<any>' with 'any'.
*
* @param pgp
* Library's root, if ever needed, like to access 'helpers'
* or other namespaces available from the root.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(private db: IDatabase<any>, private pgp: IMain) {
/*
If your repository needs to use helpers like ColumnSet,
you should create it conditionally, inside the constructor,
i.e. only once, as a singleton.
*/
}

/**
* Creates the news_annotation_map table.
*
* @returns null
*/
async create(): Promise<null> {
return this.db.none(sql.create);
}

// Returns all news_annotation_map records;
async all(): Promise<iNewsAnnotationMapModel[]> {
return this.db.any("SELECT * FROM news_annotation_map");
}

// Finds a news_annotation_map record by its ID;
async findById(id: string): Promise<iNewsAnnotationMapModel | null> {
return this.db.oneOrNone(
"SELECT * FROM news_annotation_map WHERE id = $1",
id
);
}

// Adds a new news_annotation_map record, and returns the new object;
async add(
id: string,
newsId: string,
annotationId: string,
annotatedBy: string,
userId: NullableString
): Promise<iNewsAnnotationMapModel> {
return this.db.one(sql.add, {
id,
newsId,
annotationId,
annotatedBy,
userId,
});
}
}
5 changes: 5 additions & 0 deletions src/db/repositories/NewsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export default class NewsRepository {
return this.db.any("SELECT * FROM news");
}

// Finds a news record by its ID;
async findById(id: string): Promise<iNewsModel | null> {
return this.db.oneOrNone("SELECT * FROM news WHERE id = $1", id);
}

// Adds a new news record, and returns the new object;
async add(
id: string,
Expand Down
3 changes: 3 additions & 0 deletions src/db/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ResetPasswordRepository from "./ResetPasswordRepository";
import EmailVerificationRequestLogsRepository from "./EmailVerificationRequestLogsRepository";
import AnnotationsRepository from "./AnnotationsRepository";
import NewsRepository from "./NewsRepository";
import NewsAnnotationRepository from "./NewsAnnotationRepository";

/**
* Database Interface Extensions:
Expand All @@ -17,6 +18,7 @@ interface iDBInterfaceExtensions {
emailVerificationRequestLogs: EmailVerificationRequestLogsRepository;
annotations: AnnotationsRepository;
news: NewsRepository;
newsAnnotationMap: NewsAnnotationRepository;
}

type DBTaskType = pgPromise.ITask<iDBInterfaceExtensions> &
Expand All @@ -32,6 +34,7 @@ export {
EmailVerificationRequestLogsRepository,
AnnotationsRepository,
NewsRepository,
NewsAnnotationRepository,
};

export type {DBTaskType, NullableDBTaskType};
5 changes: 5 additions & 0 deletions src/db/sql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,8 @@ export const news = {
create: sql("news/create.sql"),
add: sql("news/add.sql"),
};

export const newsAnnotationMap = {
create: sql("newsAnnotationMap/create.sql"),
add: sql("newsAnnotationMap/add.sql"),
};
6 changes: 6 additions & 0 deletions src/db/sql/newsAnnotationMap/add.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Inserts a new news_annotation_map record.
*/
INSERT INTO news_annotations_map(id, news_id, annotation_id, annotated_by, user_id, created_at)
VALUES(${id}, ${newsId}, ${annotationId}, ${annotatedBy}, ${userId}, CURRENT_TIMESTAMP)
RETURNING *;
9 changes: 9 additions & 0 deletions src/db/sql/newsAnnotationMap/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE news_annotations_map
(
id UUID PRIMARY KEY,
news_id UUID NOT NULL,
annotation_id UUID NOT NULL,
annotated_by VARCHAR(10) NOT NULL,
user_id UUID, -- This column will be filled with the user's ID when annotated_by is 'user'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
73 changes: 71 additions & 2 deletions src/services/NewsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import serviceUtil from "@util/serviceUtil";
import {iGenericServiceResult} from "@customTypes/commonServiceTypes";
import {httpStatusCodes} from "@customTypes/networkTypes";

import {NullableString} from "@customTypes/commonTypes";
import {NullableString, StringArray} from "@customTypes/commonTypes";
import {iNewsSubmissionDTO} from "@customTypes/appDataTypes/newsTypes";
import securityUtil from "util/securityUtil";
import securityUtil from "@util/securityUtil";
import {newsServiceError} from "@constants/errors/newsServiceErrors";
import {DBTaskType} from "@db/repositories";

export default class NewsService {
public async addNews(
Expand Down Expand Up @@ -40,4 +42,71 @@ export default class NewsService {
);
});
}

public async predictAnnotation(
uniqueRequestId: NullableString,
newsId: string
): Promise<iGenericServiceResult<{annotationIds: StringArray} | null>> {
return db.task("predict-annotation", async (task) => {
const newsRecord = await task.news.findById(newsId);
console.log(
"🚀 ~ file: NewsService.ts:52 ~ NewsService ~ returndb.task ~ newsRecord:",
newsRecord
);

if (!newsRecord) {
return serviceUtil.buildResult(
false,
httpStatusCodes.CLIENT_ERROR_BAD_REQUEST,
uniqueRequestId,
newsServiceError.predictAnnotations.NewsDoesNotExists
);
}

// TODO: Call ML model to predict annotations
// Now calling a dummy function which queries the database to get all annotations and return a few of them randomly
const annotationIds: StringArray = await this.getRandomAnnotationIds(
task
);

// Insert predicted annotations to news_annotation_map table if annotationIds is not empty
if (annotationIds.length > 0) {
await task.newsAnnotationMap.add(
securityUtil.generateUUID(),
newsId,
annotationIds[0],
"AI",
null
);
}

return serviceUtil.buildResult(
true,
httpStatusCodes.SUCCESS_OK,
uniqueRequestId,
null,
{
annotationIds,
}
);
});
}

private async getRandomAnnotationIds(dbTask: DBTaskType): Promise<string[]> {
const annotations = await dbTask.annotations.all();

const annotationIds = annotations.map((annotation) => {
return annotation.id;
});

const randomAnnotationIds = [];

while (randomAnnotationIds.length < 3 && annotationIds.length > 0) {
const randomIndex = Math.floor(Math.random() * annotationIds.length);
const randomAnnotationId = annotationIds.splice(randomIndex, 1)[0];
randomAnnotationIds.push(randomAnnotationId);
}

return randomAnnotationIds;
}
}

0 comments on commit a447fd2

Please sign in to comment.