From e7d992442ad530221fb8730968a3388a4d6d78c0 Mon Sep 17 00:00:00 2001 From: Aman Desai <39585600+amandesai01@users.noreply.github.com> Date: Sat, 7 Sep 2024 15:19:52 +0530 Subject: [PATCH] perf(posting): serve postings from cache (#124) --- server/api/posting/index.delete.ts | 22 ++++++++---- server/api/posting/index.get.ts | 28 +++++++++------ server/api/posting/index.post.ts | 12 +++++-- server/api/posting/index.put.ts | 25 +++++++++---- server/api/postings/index.get.ts | 58 +++++++++++++----------------- server/api/postings/lite.get.ts | 12 +++---- server/api/public/posting.get.ts | 31 ++++++---------- server/api/public/postings.get.ts | 25 ++++++------- server/utils/tasks/seed-cache.ts | 13 +++++-- 9 files changed, 121 insertions(+), 105 deletions(-) diff --git a/server/api/posting/index.delete.ts b/server/api/posting/index.delete.ts index e23f423..4d26439 100644 --- a/server/api/posting/index.delete.ts +++ b/server/api/posting/index.delete.ts @@ -1,11 +1,10 @@ import { eq } from 'drizzle-orm'; -import { jobPostingsTable } from '../../db/schema'; +import { type JobPosting, jobPostingsTable } from '../../db/schema'; import authenticateAdminRequest from '../../utils/admin'; import { deleteJobPostingSchema } from '~~/shared/schemas/posting'; export default defineEventHandler(async (event) => { - const session = await authenticateAdminRequest(event); - + await authenticateAdminRequest(event); const q = await getValidatedQuery(event, deleteJobPostingSchema.parse); const jobPostingId = q.id; @@ -17,11 +16,20 @@ export default defineEventHandler(async (event) => { .where(eq(jobPostingsTable.id, jobPostingId)) .returning({ id: jobPostingsTable.id }); - if (!(Array.isArray(jobPostingsResult) && jobPostingsResult.length == 1)) { + if (jobPostingsResult.length == 0) { throw createError({ - statusCode: 400, - message: - 'Invalid posting id. Contact support if you think this is a mistake.', + statusCode: 404, + statusMessage: 'Job posting not found', }); } + + const deletedPostingId = jobPostingsResult[0]?.id; + + const postings = + (await general_memoryStorage.getItem('postings')) || []; + + await general_memoryStorage.setItem( + 'postings', + postings.filter((p) => p.id !== deletedPostingId) + ); }); diff --git a/server/api/posting/index.get.ts b/server/api/posting/index.get.ts index d197fc5..12bbb09 100644 --- a/server/api/posting/index.get.ts +++ b/server/api/posting/index.get.ts @@ -1,20 +1,26 @@ -import { eq } from 'drizzle-orm'; -import { jobPostingsTable } from '../../db/schema'; +import { type JobPosting } from '../../db/schema'; import authenticateAdminRequest from '../../utils/admin'; import { fetchJobPostingFilterSchema } from '~~/shared/schemas/posting'; +/** + * In future, if totalApplicants needed on posting page from admin side, + * either revamp caching or use database straight away for applicants. + */ export default defineEventHandler(async (event) => { await authenticateAdminRequest(event); const q = await getValidatedQuery(event, fetchJobPostingFilterSchema.parse); - const database = await useDatabase(); + const postings = + (await general_memoryStorage.getItem('postings')) || []; - return ( - await database - .select() - .from(jobPostingsTable) - .where(eq(jobPostingsTable.id, q.id)) - .orderBy(jobPostingsTable.createdAt) - .limit(1) - )[0]; + const posting = postings.find((p) => p.id === q.id); + + if (!posting) { + throw createError({ + statusCode: 404, + statusMessage: 'Posting not found', + }); + } + + return posting; }); diff --git a/server/api/posting/index.post.ts b/server/api/posting/index.post.ts index e4b18e9..a5b0314 100644 --- a/server/api/posting/index.post.ts +++ b/server/api/posting/index.post.ts @@ -1,6 +1,6 @@ import authenticateAdminRequest from '../../utils/admin'; import { createJobPostingSchema } from '~~/shared/schemas/posting'; -import { jobPostingsTable } from '../../db/schema'; +import { type JobPosting, jobPostingsTable } from '../../db/schema'; export default defineEventHandler(async (event) => { const session = await authenticateAdminRequest(event); @@ -15,10 +15,16 @@ export default defineEventHandler(async (event) => { const database = await useDatabase(); - return ( + const newPosting = ( await database .insert(jobPostingsTable) .values({ ...jobPostingRequest, owner: session.user.id }) .returning() - )[0]; + )[0] as JobPosting; + + const postings = + (await general_memoryStorage.getItem('postings')) || []; + + postings.unshift(newPosting); + await general_memoryStorage.setItem('postings', postings); }); diff --git a/server/api/posting/index.put.ts b/server/api/posting/index.put.ts index 255be76..86c6b43 100644 --- a/server/api/posting/index.put.ts +++ b/server/api/posting/index.put.ts @@ -1,4 +1,4 @@ -import { jobPostingsTable } from '../../db/schema'; +import { type JobPosting, jobPostingsTable } from '../../db/schema'; import authenticateAdminRequest from '../../utils/admin'; import { eq } from 'drizzle-orm'; import { updateJobPostingSchema } from '~~/shared/schemas/posting'; @@ -18,11 +18,22 @@ export default defineEventHandler(async (event) => { updatedAt: new Date(), }; - const updatedJobPosting = await database - .update(jobPostingsTable) - .set(updateQuery) - .where(eq(jobPostingsTable.id, q.id)) - .returning(); + const updatedJobPosting = ( + await database + .update(jobPostingsTable) + .set(updateQuery) + .where(eq(jobPostingsTable.id, q.id)) + .returning() + )[0] as JobPosting; - return updatedJobPosting; + const postings = + (await general_memoryStorage.getItem('postings')) || []; + + await general_memoryStorage.setItem( + 'postings', + postings.map((p) => { + if (p.id == updatedJobPosting.id) return updatedJobPosting; + return p; + }) + ); }); diff --git a/server/api/postings/index.get.ts b/server/api/postings/index.get.ts index 2871f1f..8160036 100644 --- a/server/api/postings/index.get.ts +++ b/server/api/postings/index.get.ts @@ -1,41 +1,33 @@ -import { jobPostingsTable } from '../../db/schema'; +import { type JobPosting, jobPostingsTable } from '../../db/schema'; import authenticateAdminRequest from '../../utils/admin'; -import { and, eq, getTableColumns, or } from 'drizzle-orm'; -import { listJobPostingsFilterSchema } from '~~/shared/schemas/posting'; +import { desc } from 'drizzle-orm'; +/** + * DB lookup for totalApplicants and cache lookup for everything else. + */ export default defineEventHandler(async (event) => { - const database = await useDatabase(); - - const session = await authenticateAdminRequest(event); - const q = await getValidatedQuery(event, listJobPostingsFilterSchema.parse); + await authenticateAdminRequest(event); - const { contents, ...columns } = getTableColumns(jobPostingsTable); + const database = await useDatabase(); - const conditions = []; + const totalApplicantsRecord = await database + .select({ + id: jobPostingsTable.id, + totalApplicants: jobPostingsTable.totalApplicants, + }) + .from(jobPostingsTable) + .orderBy(desc(jobPostingsTable.createdAt)); - if (q && q.id) { - conditions.push(eq(jobPostingsTable.id, q.id)); - } else if (q && q.ownerId) { - // If owner specified, just return published postings. - conditions.push( - and( - eq(jobPostingsTable.owner, q.ownerId), - eq(jobPostingsTable.isPublished, true) - ) - ); - } else { - // If owner not specified, just return published postings + postings by admin himself - conditions.push( - or( - eq(jobPostingsTable.isPublished, true), - eq(jobPostingsTable.owner, session.user.id) - ) - ); - } + const totalApplicantsById: Record = {}; + totalApplicantsRecord.forEach( + (tar) => (totalApplicantsById[tar.id] = tar.totalApplicants) + ); - return database - .select(columns) - .from(jobPostingsTable) - .where(conditions.length ? and(...conditions) : undefined) - .orderBy(jobPostingsTable.createdAt); + const postings = + (await general_memoryStorage.getItem('postings')) || []; + return postings.map((p) => ({ + ...p, + contents: null, + totalApplicants: totalApplicantsById[p.id] || 0, + })); }); diff --git a/server/api/postings/lite.get.ts b/server/api/postings/lite.get.ts index 294f9eb..63eff85 100644 --- a/server/api/postings/lite.get.ts +++ b/server/api/postings/lite.get.ts @@ -1,19 +1,15 @@ -import { desc } from 'drizzle-orm'; -import { jobPostingsTable } from '~~/server/db/schema'; +import { type JobPosting } from '~~/server/db/schema'; import authenticateAdminRequest from '~~/server/utils/admin'; export default defineEventHandler(async (event) => { await authenticateAdminRequest(event); - const db = await useDatabase(); + const postings = + (await general_memoryStorage.getItem('postings')) || []; - const postings = await db - .select({ id: jobPostingsTable.id, title: jobPostingsTable.title }) - .from(jobPostingsTable) - .orderBy(desc(jobPostingsTable.createdAt)); if (IS_DEV) { console.log('[/api/postings/lite] found', postings.length, 'postings'); } - return postings; + return postings.map((p) => ({ id: p.id, title: p.title })); }); diff --git a/server/api/public/posting.get.ts b/server/api/public/posting.get.ts index c9b9448..dabc31f 100644 --- a/server/api/public/posting.get.ts +++ b/server/api/public/posting.get.ts @@ -1,9 +1,6 @@ -import { and, eq } from 'drizzle-orm'; -import { jobPostingsTable } from '~~/server/db/schema'; +import { type JobPosting } from '~~/server/db/schema'; export default defineEventHandler(async (event) => { - const database = await useDatabase(); - const query = getQuery<{ id: string }>(event); if (IS_DEV) { @@ -18,23 +15,17 @@ export default defineEventHandler(async (event) => { } const postings = ( - await database - .select() - .from(jobPostingsTable) - .where( - and( - eq(jobPostingsTable.isPublished, true), - eq(jobPostingsTable.id, query.id) - ) - ) - ).map((p) => { - return { - ...p, - totalApplicants: 0, - }; - }); + (await general_memoryStorage.getItem('postings')) || [] + ) + .filter((p) => p.id === query.id && p.isPublished) + .map((p) => { + return { + ...p, + owner: null, + }; + }); - if (!(Array.isArray(postings) && postings.length == 1)) { + if (postings.length != 1) { throw createError({ statusCode: 404, statusMessage: 'Posting not found', diff --git a/server/api/public/postings.get.ts b/server/api/public/postings.get.ts index 5a24e56..3e56df7 100644 --- a/server/api/public/postings.get.ts +++ b/server/api/public/postings.get.ts @@ -1,22 +1,19 @@ -import { eq, getTableColumns } from 'drizzle-orm'; -import { type JobPosting, jobPostingsTable } from '~~/server/db/schema'; +import { type JobPosting } from '~~/server/db/schema'; export default defineEventHandler(async (_) => { - const database = await useDatabase(); - - const { contents, owner, isPublished, totalApplicants, ...requiredColumns } = - getTableColumns(jobPostingsTable); - - const postings = await database - .select({ - ...requiredColumns, - }) - .from(jobPostingsTable) - .where(eq(jobPostingsTable.isPublished, true)); + const postings = ( + (await general_memoryStorage.getItem('postings')) || [] + ) + .filter((p) => p.isPublished) + .map((p) => ({ + ...p, + contents: null, + owner: null, + })); if (IS_DEV) { console.log('[PUBLIC] postings page found', postings.length, 'postings.'); } - return postings as JobPosting[]; + return postings; }); diff --git a/server/utils/tasks/seed-cache.ts b/server/utils/tasks/seed-cache.ts index b9285d9..1064b93 100644 --- a/server/utils/tasks/seed-cache.ts +++ b/server/utils/tasks/seed-cache.ts @@ -1,6 +1,6 @@ -import { inArray } from 'drizzle-orm'; +import { desc, inArray } from 'drizzle-orm'; import type { GeneralSettings } from '~~/shared/schemas/setting'; -import { metaDataTable } from '~~/server/db/schema'; +import { jobPostingsTable, metaDataTable } from '~~/server/db/schema'; export async function seedCache() { console.log('Seeding Cache'); @@ -18,6 +18,14 @@ export async function seedCache() { ]) ); + // Do not save totalApplicants in cache + const jobPostings = ( + await db + .select() + .from(jobPostingsTable) + .orderBy(desc(jobPostingsTable.createdAt)) + ).map((p) => ({ ...p, totalApplicants: -1 })); + const settings: GeneralSettings = { careerSite: {}, seo: {}, @@ -40,6 +48,7 @@ export async function seedCache() { settings_memoryStorage.setItem('seoConfig', settings.seo), general_memoryStorage.setItem('firstSetupAccessKey', firstSetupAccessKey), general_memoryStorage.setItem('remoteAssetBase', remoteAssetBase), + general_memoryStorage.setItem('postings', jobPostings), ]); return { result: true };