Skip to content

Commit

Permalink
feat(api): implement news submission api (without date min validation)
Browse files Browse the repository at this point in the history
  • Loading branch information
swalahamani committed Nov 12, 2023
1 parent ef3cee1 commit b31bcd1
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 1 deletion.
4 changes: 3 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {Router} from "express";
import authRoute from "@api/routes/authRoute";
import userRoute from "@api/routes/usersRoute";
import verificationRoute from "@api/routes/verificationRoute";
import annotationsRoute from "./routes/annotationsRoute";
import annotationsRoute from "@api/routes/annotationsRoute";
import newsRoute from "@api/routes/newsRoute";

const getRouter = (): Router => {
const apiRouter = Router();
Expand All @@ -13,6 +14,7 @@ const getRouter = (): Router => {
userRoute(apiRouter);
verificationRoute(apiRouter);
annotationsRoute(apiRouter);
newsRoute(apiRouter);

return apiRouter;
};
Expand Down
74 changes: 74 additions & 0 deletions src/api/routes/newsRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {NextFunction, Router} from "express";

import logger from "@loaders/logger";

// import middlewares from "@api/middlewares";

import expressUtil from "@util/expressUtil";

import {iRequest, iResponse, RouteType} from "@customTypes/expressTypes";
import {iNewsSubmissionDTO} from "customTypes/appDataTypes/newsTypes";
import {celebrate, Segments} from "celebrate";
import {newsSubmissionBodySchema} from "validations/newsRouteSchemas";
import NewsService from "services/NewsService";

const route = Router();
const newsService = new NewsService();

const newsRoute: RouteType = (apiRouter) => {
apiRouter.use("/news", route);

/*
Registering isAuthorized middleware to the entire /users route
as all the endpoint in this route needs authorization.
*/
// route.use(middlewares.isAuthorized);

route.post(
"/",
celebrate({
[Segments.BODY]: newsSubmissionBodySchema,
}),
async (
req: iRequest<iNewsSubmissionDTO>,
res: iResponse<{id: string}>,
next: NextFunction
) => {
const uniqueRequestId = expressUtil.parseUniqueRequestId(req);

logger.debug(
uniqueRequestId,
"Calling POST:/news endpoint with body:",
null,
{
requestBody: req.body,
}
);

try {
const {body} = req;

const result = await newsService.addNews(uniqueRequestId, body);

logger.debug(
uniqueRequestId,
"POST:/news :: Completed newsService.addNews & sending result to client:",
null,
{
result,
}
);

const {httpStatusCode} = result;

return res.status(httpStatusCode).json(result);
} catch (error) {
logger.error(uniqueRequestId, "Error on POST:/news :", error);

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

export default newsRoute;
27 changes: 27 additions & 0 deletions src/customTypes/appDataTypes/newsTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {StringArray} from "customTypes/commonTypes";

export interface iNewsSubmissionDTO {
title: string;
content: string;
url: string;
publishedDate: string;
}

interface iNews {
id: string;
publishedDate: string;
url: string;
title: string;
content: string;
createdAt: string;
}

type MultiNews = {
ids: StringArray;

items: {
[key in string]: iNews;
};
};

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

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

Expand Down
8 changes: 8 additions & 0 deletions src/db/models/news.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface iNewsModel {
id: string;
published_date: string;
url: string;
title: string;
content: string;
created_at: string;
}
61 changes: 61 additions & 0 deletions src/db/repositories/NewsRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {IDatabase, IMain} from "pg-promise";

import {iNewsModel} from "db/models/news.model";

import {news as sql} from "@db/sql";

/*
This repository mixes hard-coded and dynamic SQL, just to show how to use both.
*/
export default class NewsRepository {
/**
* @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 annotations table.
*
* @returns null
*/
async create(): Promise<null> {
return this.db.none(sql.create);
}

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

// Adds a new news record, and returns the new object;
async add(
id: string,
publishedDate: string,
url: string,
title: string,
content: string
): Promise<iNewsModel> {
return this.db.one(sql.add, {
id,
publishedDate,
url,
title,
content,
});
}
}
3 changes: 3 additions & 0 deletions src/db/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import EmailLogsRepository from "./EmailLogsRepository";
import ResetPasswordRepository from "./ResetPasswordRepository";
import EmailVerificationRequestLogsRepository from "./EmailVerificationRequestLogsRepository";
import AnnotationsRepository from "./AnnotationsRepository";
import NewsRepository from "./NewsRepository";

/**
* Database Interface Extensions:
Expand All @@ -15,6 +16,7 @@ interface iDBInterfaceExtensions {
resetPasswordLogs: ResetPasswordRepository;
emailVerificationRequestLogs: EmailVerificationRequestLogsRepository;
annotations: AnnotationsRepository;
news: NewsRepository;
}

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

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 @@ -81,3 +81,8 @@ export const emailVerificationRequestLogs = {
export const annotations = {
create: sql("annotations/create.sql"),
};

export const news = {
create: sql("news/create.sql"),
add: sql("news/add.sql"),
};
6 changes: 6 additions & 0 deletions src/db/sql/news/add.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
Inserts a new news record.
*/
INSERT INTO news(id, published_date, url, title, content, created_at)
VALUES(${id}, ${publishedDate}, ${url}, ${title}, ${content}, CURRENT_TIMESTAMP)
RETURNING *
12 changes: 12 additions & 0 deletions src/db/sql/news/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Creates table news.
*/
CREATE TABLE news
(
id UUID PRIMARY KEY,
published_date TIMESTAMPTZ,
url TEXT NOT NULL,
title VARCHAR(225) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ
)
43 changes: 43 additions & 0 deletions src/services/NewsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logger from "@loaders/logger";

import {db} from "@db/index";

import serviceUtil from "@util/serviceUtil";

import {iGenericServiceResult} from "@customTypes/commonServiceTypes";
import {httpStatusCodes} from "@customTypes/networkTypes";

import {NullableString} from "@customTypes/commonTypes";
import {iNewsSubmissionDTO} from "@customTypes/appDataTypes/newsTypes";
import securityUtil from "util/securityUtil";

export default class NewsService {
public async addNews(
uniqueRequestId: NullableString,
news: iNewsSubmissionDTO
): Promise<iGenericServiceResult<{id: string} | null>> {
return db.tx("add-news", async (task) => {
logger.silly("Inserting new news record to news table");

const uuid = securityUtil.generateUUID();

const newNewsRecord = await task.news.add(
uuid,
news.publishedDate,
news.url,
news.title,
news.content
);

return serviceUtil.buildResult(
true,
httpStatusCodes.SUCCESS_OK,
uniqueRequestId,
null,
{
id: newNewsRecord.id,
}
);
});
}
}
14 changes: 14 additions & 0 deletions src/validations/newsRouteSchemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Joi} from "celebrate";

import {uuidv4Schema} from "@validations/genericSchemas";

const newsIdSchema = uuidv4Schema.required();

const newsSubmissionBodySchema = Joi.object({
title: Joi.string().min(1).required(),
content: Joi.string().min(1).required(),
url: Joi.string().uri().required(),
date: Joi.string().isoDate().required(),
});

export {newsIdSchema, newsSubmissionBodySchema};

0 comments on commit b31bcd1

Please sign in to comment.