import { chunk } from 'lodash'; import { addJob, JobKind } from '@spokenio/jobqueue-producer'; import { subHours } from 'date-fns'; import express, { Express, Request } from 'express'; import { getRepository, In, IsNull, LessThan } from 'typeorm'; import logger from '@/logger'; import { batchProcessEntityStream } from '@/entities/utils'; import { Page } from '@/entities'; const scrapableRetailers = [ 'urbanoutfitters', 'homedepot', 'houzz', 'amazon', 'franceandson', 'luluandgeorgia', 'pier1', 'burkedecor', 'bedbathandbeyond', 'walmart', 'dotandbo', 'onekingslane', 'amara', 'worldmarket', 'kathykuohome', 'afastores', 'crateandbarrel', 'etsy', 'wayfair', 'allmodern', 'jossandmain', 'birchlane', 'perigold', ]; const setup = (): Express => { const app = express(); app.put('/update', async (req: Request<{ force?: string }>, res) => { const forceQuery = req.query.force; if (forceQuery && typeof forceQuery !== 'string') { return res .status(400) .send("Query param 'force' has to be of type string"); } const forceAsString = forceQuery?.toLowerCase().trim(); const force = forceAsString && forceAsString !== 'false' && forceAsString !== '0'; batchProcessEntityStream( Page, async (pages) => { logger.info( `Queuing bulk scrape latest page for ${pages.length} pages.` ); const urls = pages .map( ({ urlOriginal: url, id: pageId, productFirebaseId: productId, }) => ({ url, pageId, productId }) ) .filter( ( page ): page is { url: string; pageId: string; productId: string } => { return !!(page.url && page.pageId && page.productId); } ); await Promise.all( chunk(urls, 10).map(async (urlsChunk) => { await addJob(JobKind.BULK_FETCH_LATEST_PAGE_1, { pages: urlsChunk, }); }) ); await getRepository(Page).update( { id: In(pages.map(({ id }) => id)) }, { lastScrapedAt: new Date(), } ); }, { where: force ? { retailer: In(scrapableRetailers), } : [ { // @TODO: we should probably run this once per hour and check // last 23 hours. // Instead today, we run this once per day. lastScrapedAt: LessThan(subHours(new Date(), 23)), retailer: In(scrapableRetailers), }, { lastScrapedAt: IsNull(), retailer: In(scrapableRetailers), }, ], } ); return res.status(200).json({ status: 'queued', }); }); return app; }; export default setup;