import { Express } from 'express'; import { snakeCase } from 'lodash'; import { getCustomRepository, getManager } from 'typeorm'; import { v4 as uuidv4 } from 'uuid'; import { PageRepository } from '@/repositories'; import logger from '@/logger'; import { Page, Product } from '@/entities'; import { setupSwagger } from './swagger'; import { BatchUpdatePage } from './types'; import { errorHandler, notFoundHandler, rateLimiter } from './utils'; import v3 from './v3'; const queryObjToQueryString = (queryObj: any) => { return Object.keys(queryObj) .map((key) => key + '=' + encodeURIComponent(queryObj[key])) .join('&'); }; const snakeCaseProperties = (record: any) => { return record ? { ...Object.keys(record).reduce((prev, key) => { return { ...prev, [snakeCase(key)]: record[key], }; }, {}), ...record, } : record; }; export const setupRoutes = (app: Express): void => { app.use(rateLimiter); app.use((req, _res, next) => { logger.info(`${req.method} ${req.path}`); next(); }); // v2 API calls app.get('/v2/page/:url', async (req, res) => { const url = decodeURIComponent(req.params.url); const params = req.query; const pages = await Page.find({ url, ...(Object.keys(params).length !== 0 ? { urlQuery: queryObjToQueryString(params), } : {}), }); if (pages.length === 1) { const page = pages[0]; // Check if queryString exists but wasn't given... if (page.urlQuery && Object.keys(params).length == 0) { res.json({ error: { msg: 'Variant (urlQuery) needed but was not given.', type: 'error', }, }); } else { const pages = await Page.find({ where: { productFirebaseId: page.productFirebaseId, }, }); res.status(200).json({ page: snakeCaseProperties(page), pages: pages.map(snakeCaseProperties), productID: page.productFirebaseId, }); } } if (pages.length == 0) { res.json({ error: { msg: 'No page found.', type: 'warning' }, }); } if (pages.length > 1) { // Too many products, need urlQuery res.json({ error: { msg: 'Duplicates detected. Variant (urlQuery) data need.', type: 'error', }, }); } }); app.post('/v2/page', async (req, res) => { const data = req.body; let productID = data.product_id; const clientPage = data.clientPage as { availability: boolean; distributor?: string; distributor_sku?: string; img?: string; name?: string; product_sku?: string; price?: number; retailer?: string; url?: string; urlQuery?: string; asin?: string; dimensions?: string; color_code?: string; }; const serverPage = null; const pages = await Page.find({ url: data.url, ...(data.urlQuery ? { ulrQuery: data.urlQuery, } : {}), }); // Check if "connected" Page already exists in database if (pages.length > 0) { res.json({ status: 'Page already exists', error: true, }); } else { if (!productID) { const id = uuidv4(); const product = await Product.create({ id, firebaseId: id, }).save(); // Create a new Product productID = product.id; // Create a new "root" Page const rootPageId = uuidv4(); await Page.create({ id: rootPageId, firebaseId: rootPageId, productFirebaseId: productID, availability: clientPage.availability, distributor: clientPage.distributor, distributorSKU: clientPage.distributor_sku, img: clientPage.img, name: clientPage.name, productSKU: clientPage.product_sku, price: clientPage.price, retailer: clientPage.retailer, url: clientPage.url, urlQuery: clientPage.urlQuery, asin: clientPage.asin, dimensions: clientPage.dimensions, colorCode: clientPage.color_code, }).save(); } // Create a new "connected" Page const pageId = uuidv4(); const page = await Page.create({ id: pageId, firebaseId: pageId, url: data.url, urlOriginal: data.urlOriginal, retailer: data.retailer, price: data.price, productFirebaseId: productID, ...(data.urlQuery ? { urlQuery: data.urlQuery, } : {}), }).save(); res.json({ status: 'Successfully added page', error: false, newPage: page, productID: productID, serverPage: serverPage, clientPage: clientPage, }); } }); app.post('/v2/page/update', async (req, res) => { const productId = req.body.product_id; const pageId = req.body.page_id; if (pageId && productId !== null) { const pageInput: { retailer?: string; url?: string; urlOriginal?: string; urlQuery?: string; availability?: boolean; name?: string; price?: number; img?: string; product_sku?: string; distributor?: string; distributor_sku?: string; dimensions?: string; featured?: boolean; } | null = req.body.page ?? {}; // Update main page if necessary const page = await Page.findOne(pageId); if (!page) { res.status(404).json({ status: 'page not found. no update made' }); return; } if (pageInput) { if (productId) page.productFirebaseId = productId; if (pageInput.retailer) page.retailer = pageInput.retailer; if (pageInput.url) page.url = pageInput.url; if (pageInput.urlOriginal) page.urlOriginal = pageInput.urlOriginal; if (pageInput.urlQuery) page.urlQuery = pageInput.urlQuery; if (pageInput.availability !== undefined) page.availability = pageInput.availability; if (pageInput.name) page.name = pageInput.name; if (pageInput.price) page.price = pageInput.price; if (pageInput.img) page.img = pageInput.img; if (pageInput.product_sku) page.productSKU = pageInput.product_sku; if (pageInput.distributor) page.distributor = pageInput.distributor; if (pageInput.distributor_sku) page.distributorSKU = pageInput.distributor_sku; if (pageInput.dimensions) page.dimensions = pageInput.dimensions; if (pageInput.featured !== undefined) page.featured = pageInput.featured; logger.info(`Updating page ${page.id}`); await page.save(); } // Update lowest_price main page const subpages = await Page.find({ where: { // Only consider discoverable pages discoverable: true, productFirebaseId: page.productFirebaseId, }, }); const minObj = subpages.reduce( (prev, curr) => (prev.price < curr.price ? prev : curr), subpages[0] ); if (minObj) { await Page.update( { firebaseId: pageId }, { lowestPricePageId: minObj.id, } ); // Getting up-to-date document page.lowestPricePageId = minObj.id; } res.json({ page: snakeCaseProperties(page), pages: subpages.map(snakeCaseProperties), }); return; } res.status(404).json({ status: 'no update made' }); }); app.post('/v2/page/batch-update', async (req, res) => { /** @todo schema validation */ const inputPages = req.body.pages as BatchUpdatePage[]; const updatedPages: Page[] = []; const failedPages: BatchUpdatePage[] = []; for (const inputPage of inputPages) { try { await getManager().transaction(async (manager) => { const page = await getCustomRepository(PageRepository).getByUrl( inputPage.url, {}, manager ); page.price = inputPage.price; await manager.save(page); const subpages = await manager.find(Page, { where: { // Only consider discoverable pages discoverable: true, productFirebaseId: page.productFirebaseId, }, }); const minObj = subpages.reduce( (prev, curr) => (prev.price < curr.price ? prev : curr), subpages[0] ); subpages.forEach((subpage) => { subpage.lowestPricePageId = minObj.id; }); await manager.save(subpages); const updatedPage = await manager.findOne(Page, page.id); if (updatedPage) { updatedPages.push(updatedPage); } }); } catch (e) { failedPages.push(inputPage); } } res.json({ successfulUpdates: updatedPages.map(snakeCaseProperties), failedPages: failedPages.map(snakeCaseProperties), }); }); app.post('/v2/shopping/add', async (req, res) => { // EXAMPLE let items = req.body.items; // Run through all new inventory ==> items.forEach( async (item: { pageID: string; price: number; googleShoppingID: string; productID: string; annotations: string; }) => { // If new inventory has page_id, update Page (- name) if (item.pageID) { await Page.update( { firebaseId: item.pageID }, { price: item.price, googleShoppingIDs: [item.googleShoppingID], ...(item.annotations ? { annotations: item.annotations, } : {}), } ); } else { // If new inventory has product_id, create new Page if (item.productID) { const page = await Page.create({ productFirebaseId: item.productID, ...item, }).save(); item.pageID = page.firebaseId; } else { // If new inventory doesn't have page_id or product_id, create // Product and Page, add product_id to all similar (same GID) items // in new inventory // Create a new Product const id = uuidv4(); const product = await Product.create({ id, firebaseId: id, }).save(); const productID = product.id; // Create a new "root" Page const pageId = uuidv4(); await Page.create({ id: pageId, firebaseId: pageId, productFirebaseId: productID, ...item, }).save(); // Is this good practice? // Add Product ID to items with the same Google Shopping ID items = items.map( (el: { googleShoppingID: string; productID: string }) => { if (el.googleShoppingID === item.googleShoppingID) { el.productID = productID; } return el; } ); } } } ); res.json({ items: items.map(snakeCaseProperties), }); }); app.use('/v3', v3()); // @todo determine who should see these docs if (process.env.NODE_ENV === 'development') { setupSwagger(app); } app.use(notFoundHandler); app.use(errorHandler); };