Skip to content

Commit

Permalink
Merge pull request #17 from hmif-itb/feat/unsubscribe-settings-api
Browse files Browse the repository at this point in the history
Info Subscribe Settings API 86epb9bua
  • Loading branch information
fawwazabrials authored May 20, 2024
2 parents 68945ec + 26ccae3 commit db69d9a
Show file tree
Hide file tree
Showing 9 changed files with 852 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/controllers/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { pushRouter } from './push.controller';
import { commentRouter } from './comment.controller';
import { courseRouter } from './course.controller';
import { reactionRouter } from './reaction.controller';
import { userUnsubscribeRouter } from './user-unsubscribe.controller';

const unprotectedApiRouter = new OpenAPIHono();
unprotectedApiRouter.route('/', loginRouter);
Expand All @@ -20,6 +21,7 @@ protectedApiRouter.route('/', loginProtectedRouter);
protectedApiRouter.route('/', commentRouter);
protectedApiRouter.route('/', courseRouter);
protectedApiRouter.route('/', reactionRouter);
protectedApiRouter.route('/', userUnsubscribeRouter);

export const apiRouter = new OpenAPIHono();
apiRouter.route('/', unprotectedApiRouter);
Expand Down
317 changes: 317 additions & 0 deletions src/controllers/user-unsubscribe.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import {
deleteUserUnsubscribeCategory,
getListUserUnsubscribeCategory,
getUserUnsubscribeCategory,
postListUserUnsubcribeCategory,
postUserUnsubcribeCategory,
} from '~/repositories/user-unsubscribe.repo';
import { createAuthRouter } from './router-factory';
import { db } from '~/db/drizzle';
import {
getUserUnsubscribeCategoryRoute,
getListUserUnsubscribeCategoryRoute,
postUserUnsubscribeCategoryRoute,
postListUserUnsubscribeCategoryRoute,
deleteListUserUnsubscribeRoute,
deleteUserUnsubscribeCategoryRoute,
} from '~/routes/user-unsubscribe.route';
import {
DeleteListUserUnsubscribeCategoryResponseSchema,
DeleteUserUnsubscribeCategorySchema,
GetUserUnsubscribeCategoryResponseSchema,
PostUserUnsubscribeCategorySchema,
} from '~/types/user-unsubscribe.types';
import { z } from 'zod';
import { checkRequired, isCategoryExists } from '~/repositories/category.repo';
import { CategoryNotFoundSchema, CategorySchema } from '~/types/category.types';

export const userUnsubscribeRouter = createAuthRouter();

userUnsubscribeRouter.openapi(getUserUnsubscribeCategoryRoute, async (c) => {
const { id } = c.var.user;
const { categoryId } = c.req.valid('param');

try {
const categoryExist = await isCategoryExists(db, categoryId);
if (!categoryExist) {
return c.json(
{ error: `Category with id ${categoryId} does not exist!` },
400,
);
}

const category = await getUserUnsubscribeCategory(db, {
userId: id,
categoryId,
});

let response: z.infer<typeof GetUserUnsubscribeCategoryResponseSchema> = {
userId: id,
categoryId,
unsubscribed: false,
};

if (category) {
response = {
...category,
unsubscribed: true,
};
}

return c.json(response, 200);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
});

userUnsubscribeRouter.openapi(
getListUserUnsubscribeCategoryRoute,
async (c) => {
const { id } = c.var.user;

try {
const categories = await getListUserUnsubscribeCategory(db, id);
const categoriesArray = categories.map((category) => category.categoryId);

const data = {
userId: id,
categoryId: categoriesArray,
};
return c.json(data, 200);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
},
);

userUnsubscribeRouter.openapi(postUserUnsubscribeCategoryRoute, async (c) => {
const { id } = c.var.user;
const { categoryId } = c.req.valid('json');
const data = {
userId: id,
categoryId,
};

try {
const { requiredPush } = await checkRequired(db, categoryId);
if (requiredPush === null) {
return c.json(
{ error: `Category with id ${categoryId} does not exist!` },
400,
);
}

if (requiredPush) {
return c.json(
{
error: `Subscription to category with id '${categoryId}' is required!`,
},
400,
);
}

let res = await postUserUnsubcribeCategory(db, data);
if (!res) {
// If unsubscription is already in DB
res = {
userId: id,
categoryId,
};
}
return c.json(res, 201);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
});

userUnsubscribeRouter.openapi(
postListUserUnsubscribeCategoryRoute,
async (c) => {
const { id } = c.var.user;
const categoryIds = c.req.valid('json').categoryId;

const subsNotRequired: string[] = [];
const subsRequired: string[] = [];
const categoriesNotFound: string[] = [];

const checkRequiredPromises: Array<
Promise<z.infer<typeof CategoryNotFoundSchema | typeof CategorySchema>>
> = [];

categoryIds.forEach((categoryId) => {
checkRequiredPromises.push(checkRequired(db, categoryId));
});

const res = await Promise.allSettled(checkRequiredPromises);
res.forEach((result) => {
if (result.status === 'fulfilled') {
// Category was not found
if (result.value.requiredPush === null) {
categoriesNotFound.push(result.value.id);
} else if (result.value.requiredPush) {
subsRequired.push(result.value.id);
} else {
subsNotRequired.push(result.value.id);
}
}
});

if (subsNotRequired.length === 0) {
return c.json({ error: 'All categories are required!' }, 400);
}

const categoriesToUnsubscribe: Array<
z.infer<typeof PostUserUnsubscribeCategorySchema>
> = [];

subsNotRequired.forEach((categoryId) => {
categoriesToUnsubscribe.push({
userId: id,
categoryId,
});
});

try {
const res = await postListUserUnsubcribeCategory(
db,
categoriesToUnsubscribe,
);

const categoriesAlreadyUnsubscribed: string[] = [];
const insertedCategoriesSet = new Set(res.map((data) => data.categoryId));

categoriesToUnsubscribe.forEach((data) => {
if (!insertedCategoriesSet.has(data.categoryId)) {
categoriesAlreadyUnsubscribed.push(data.categoryId);
}
});

const returnObj = {
userId: id,
categoryId: res.map((data) => data.categoryId),
requiredSubscriptions: subsRequired,
categoriesNotFound,
categoriesAlreadyUnsubscribed,
};
return c.json(returnObj, 201);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
},
);

userUnsubscribeRouter.openapi(deleteUserUnsubscribeCategoryRoute, async (c) => {
const { id } = c.var.user;
const { categoryId } = c.req.valid('json');

try {
const categoryExist = await isCategoryExists(db, categoryId);
if (!categoryExist) {
return c.json(
{ error: `Category with id ${categoryId} does not exist!` },
400,
);
}

const res = await deleteUserUnsubscribeCategory(
db,
{
userId: id,
categoryId,
},
id,
);

if (!res) {
return c.json(
{ error: 'User is already subscribed to that category!' },
400,
);
}

return c.json(res, 201);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
});

userUnsubscribeRouter.openapi(deleteListUserUnsubscribeRoute, async (c) => {
const { id } = c.var.user;
const categoryIds = c.req.valid('json').categoryId;

try {
const existingCategories: string[] = [];
const checkExistingCategoryPromises: Array<
Promise<false | z.infer<typeof CategorySchema>>
> = [];

categoryIds.forEach((categoryId) => {
checkExistingCategoryPromises.push(isCategoryExists(db, categoryId));
});

const res = await Promise.allSettled(checkExistingCategoryPromises);
res.forEach((result) => {
if (result.status === 'fulfilled') {
if (result.value) {
existingCategories.push(result.value.id);
}
}
});

if (existingCategories.length === 0) {
return c.json({ error: 'All categories does not exist!' }, 400);
}

const notFoundCategories = categoryIds.filter(
(categoryId) => !existingCategories.includes(categoryId),
);

const deletePromises: Array<
Promise<z.infer<typeof DeleteUserUnsubscribeCategorySchema>>
> = [];

existingCategories.forEach((categoryId) => {
deletePromises.push(
deleteUserUnsubscribeCategory(
db,
{
userId: id,
categoryId,
},
id,
),
);
});

const resDelete = await Promise.allSettled(deletePromises);
const deletedCategories: string[] = [];
const alreadySubscribedCategories: string[] = [];

resDelete.forEach((result) => {
if (result.status === 'fulfilled') {
if (result.value) {
deletedCategories.push(result.value.categoryId);
}
}
});

existingCategories.forEach((categoryId) => {
if (!deletedCategories.includes(categoryId)) {
alreadySubscribedCategories.push(categoryId);
}
});

const returnObj: z.infer<
typeof DeleteListUserUnsubscribeCategoryResponseSchema
> = {
userId: id,
categoryId: deletedCategories,
categoriesNotFound: notFoundCategories,
categoriesAlreadySubscribed: alreadySubscribedCategories,
};

return c.json(returnObj, 201);
} catch (e) {
return c.json({ error: 'Something went wrong!' }, 500);
}
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ app.doc('/doc', {
{ name: 'comment', description: 'Comment API' },
{ name: 'reaction', description: 'Comment API' },
{ name: 'open-graph', description: 'Scrape Open Graph API' },
{ name: 'unsubscribe', description: 'Unsubscribe API' },
],
});
app.get('/swagger', swaggerUI({ url: '/doc' }));
Expand Down
44 changes: 44 additions & 0 deletions src/repositories/category.repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Database } from '~/db/drizzle';
import { eq } from 'drizzle-orm';
import { categories } from '~/db/schema';

/**
* Check if a category with the given categoryId exists
* @param db Database to be used
* @param categoryId Id of the category to check
* @returns false if category doesn't exist, {id, name, and requiredPush} if it exists
*/
export async function isCategoryExists(db: Database, categoryId: string) {
const q = await db.query.categories.findFirst({
where: eq(categories.id, categoryId),
});

if (!q) {
return false;
}

return q;
}

/**
* Check if a given category is required to be subscribed
* @param db Database to be used
* @param categoryId Id of the category to check
* @returns \{id, name=null, requiredPush=null} if category doesn't exist, \{id, name, requiredPush} if it exists
*/
export async function checkRequired(db: Database, categoryId: string) {
const category = await isCategoryExists(db, categoryId);
if (!category) {
return {
id: categoryId,
name: null,
requiredPush: null,
};
}

return {
id: category.id,
name: category.name,
requiredPush: category.requiredPush,
};
}
Loading

0 comments on commit db69d9a

Please sign in to comment.