UNPKG

@websolutespa/payload-plugin-bowl

Version:

Bowl PayloadCms plugin of the BOM Repository

451 lines (450 loc) 18.6 kB
import { asCategoryId, collectRoutes, eachMarketLocale, getCategorySegments, getCategorySegmentsByCategory, getRootCategory, getRouteHref, hasMarket, isObject, localize } from '@websolutespa/bom-core'; import { ResponseBadRequest, ResponseError, ResponseNotFound, ResponseSuccess } from '@websolutespa/payload-utils/server'; import { addDataAndFileToRequest } from 'payload'; import { options } from '../../options'; import { sortCollection, whereCollection } from '../api/where.service'; import { InMemoryCache } from './cache.service'; import { getNewCategoriesFromChanges } from './category.service'; import { getCollectionItems, getGlobalItems } from './collection.service'; import { getPagination } from './pagination.service'; import { getNumericParam } from './utils'; const USE_CACHE = true; const CACHE_ = new InMemoryCache(); export async function getRoutes(req) { const { user } = req; // console.log('getRoutes.user', user); if (!user) { throw { status: 403, message: 'Unauthorized' }; } const roles = user.roles || []; const tenants = Array.isArray(user.tenants) ? user.tenants.map((x)=>isObject(x) ? x.id : x) : []; const keys = [ ...roles, ...tenants ]; keys.sort(); // console.log('getRoutes.roles', roles); const { market = 'all', locale = 'all', draft = 'published' } = req.query; // !!! todo add isActive to route to handle temporary parent redirect const key = `route-${market}-${locale}-${draft}-${keys.join('-')}`; if (USE_CACHE && CACHE_.has(key)) { return CACHE_.get(key); } console.log('RouteService.getRoutes', key); const subRequest = { ...req, query: { market, locale, draft }, params: {} }; const unlocalizedRequest = { ...req, query: { market, locale: 'all', draft }, params: {} }; let routes = []; // console.log('getRoutes', market, locale, draft); let markets = await getCollectionItems(subRequest, options.slug.market, 0); markets = markets ? markets.filter((x)=>x.isActive) : []; // console.log('getRoutes.markets', markets.length); let locales = await getGlobalItems(subRequest, options.slug.locale, 0); locales = locales ? locales.filter((x)=>x.isActive) : []; // console.log('getRoutes.locales', locales.length); if (locales.length > 0 && markets.length > 0) { const store = await getPages(unlocalizedRequest); // console.log('getRoutes.store'); const categories = await getCollectionItems(unlocalizedRequest, options.slug.category, 1); // console.log('getRoutes.categories'); const medias = await getCollectionItems(unlocalizedRequest, options.slug.media, 1); // console.log('getRoutes.medias'); routes = collectRoutes(store, categories, markets, locales, medias); } CACHE_.set(key, routes); return routes; } export async function getRoute(req, id) { const routes = await getRoutes(req); const route = routes.find((x)=>x.id === id); return route || null; } export async function getRouteByItemAndLocale(req, item, localeId, slug) { // console.log('getRouteByItemAndLocale'); const routes = []; const categories = await getCollectionItems(req, options.slug.category, 1); const rootCategory = getRootCategory(categories); const segments = getCategorySegments(categories, item); const visibleSegments = segments.filter((x)=>x.id !== rootCategory?.id && Boolean(x.isHidden) === false); const visibleCategory = visibleSegments.length > 0 ? visibleSegments[visibleSegments.length - 1] : undefined; if (visibleCategory) { const slugs = visibleSegments.map((x)=>({ slug: x.slug })); if (Boolean(item.isDefault) === false) { slugs.push({ slug: item.slug }); } await getEachMarketLocale(req, (market, locale, markets, locales)=>{ // const defaultLocale = locales.find(x => x.isDefault) || locales[0]; // const defaultMarket = markets.find(x => x.isDefault) || markets[0]; // const defaultMarketLocale = defaultMarket && defaultMarket.defaultLanguage ? asEntityId(defaultMarket.defaultLanguage) as string : defaultLocale.id; const isRootCategory = rootCategory && item.category === rootCategory.id && item.isDefault; if (hasMarket(item, market) && locale.id === localeId) { const title = localize(item.title, locale.id); const id = getRouteHref(slugs, market, locale, markets, locales); /* const isDefaultRootCategory = isRootCategory && market.id === defaultMarket.id && locale.id === defaultMarketLocale; */ const { _status, category, isDefault, media, order, template, updatedAt, useSplat } = item; const route = { _status, category, media, template, updatedAt, resolvedCategory: visibleCategory.id, id, locale: locale.id, market: market.id, page: item.id, schema: slug, title }; if (isDefault) { route.isDefault = true; } if (isRootCategory) { route.isRoot = true; } const noindex = (item.meta?.robots || '').includes('noindex'); if (noindex) { route.noindex = true; } if (order != null) { route.order = order; } if (useSplat) { route.useSplat = true; } routes.push(route); } }); } return routes; } export async function getRouteByCategoryAndLocale(req, item, localeId, slug) { const routes = []; const categories = await getCollectionItems(req, options.slug.category, 1); const rootCategory = getRootCategory(categories); const segments = getCategorySegmentsByCategory(categories, item); const visibleSegments = segments.filter((x)=>x.id !== rootCategory?.id && Boolean(x.isHidden) === false); const visibleCategory = visibleSegments.length > 0 ? visibleSegments[visibleSegments.length - 1] : undefined; if (visibleCategory) { await getEachMarketLocale(req, (market, locale, markets, locales)=>{ if (locale.id === localeId) { const title = localize(item.title, locale.id); const id = getRouteHref(visibleSegments, market, locale, markets, locales); const route = { category: asCategoryId(item.category) || '', order: item.order, resolvedCategory: visibleCategory.id, id, locale: locale.id, market: market.id, page: item.id, schema: slug, title }; routes.push(route); } }); } return routes; } export const routeGet = { path: '/route', method: 'get', handler: async (req)=>{ try { const routes = await getRoutes(req); const { query = {} } = req; const { where, sort, pagination, page, limit } = query; const usePagination = pagination === 'true'; let items = await whereCollection(routes, where); items = await sortCollection(items, sort); if (usePagination) { const result = await getPagination(items, getNumericParam(page), getNumericParam(limit)); return ResponseSuccess(result); } else { return ResponseSuccess(items); } } catch (error) { console.error('RouteService.routeGet.error', error); return ResponseError(error); } } }; export const routePost = { path: '/route', method: 'post', handler: routePostHandler }; export const routeChangesPost = { path: '/route/changes', method: 'post', handler: async (req)=>{ let routes = []; try { await addDataAndFileToRequest(req); const changes = req.data; // console.log('routeChangesPost.changes', changes); const { market, locale } = req.query; const subRequest = { ...req, query: { market, locale, draft: true }, params: {} }; const unlocalizedRequest = { ...req, query: { market, locale: 'all', draft: true }, params: {} }; let markets = await getCollectionItems(subRequest, options.slug.market, 0); markets = markets ? markets.filter((x)=>x.isActive) : []; // console.log('getRoutes.markets'); let locales = await getGlobalItems(subRequest, options.slug.locale, 0); locales = locales ? locales.filter((x)=>x.isActive) : []; // console.log('getRoutes.locales'); if (locales.length > 0 && markets.length > 0) { const categories = await getCollectionItems(unlocalizedRequest, options.slug.category, 1); // console.log('getRoutes.categories'); const store = await getPages(unlocalizedRequest); // console.log('getRoutes.store'); const newCategories = changes && Object.keys(changes).length > 0 ? getNewCategoriesFromChanges(categories, changes) : categories; routes = collectRoutes(store, newCategories, markets, locales); } if (typeof req.query.where === 'object') { const filteredRoutes = await whereCollection(routes, req.query.where); return ResponseSuccess(filteredRoutes); } else { return ResponseSuccess(routes); } } catch (error) { console.log('RouteService.routeChangesPost.error', error); return ResponseSuccess(routes); } } }; const trimTerminalSlash = (path)=>{ return path.replace(/\/+$/, ''); }; const sanitizeHref = (url, urlBeforeRedirect)=>{ if (urlBeforeRedirect) { const sanitizedUrl = new URL(urlBeforeRedirect); sanitizedUrl.protocol = url.protocol; return trimTerminalSlash(sanitizedUrl.href); } else { return trimTerminalSlash(url.href); } }; const sanitizeRedirectFrom = (path, url, urlBeforeRedirect)=>{ path = trimTerminalSlash(path); if (urlBeforeRedirect) { return path.replace(/^(\^?)((https?:\/\/)+)/, `$1${url.protocol}//`); } else { return path.replace(/^(\^?)((https?:\/\/)+(\w|\.|:)+)/, `$1${url.origin}`); } }; const sanitizeRedirectTo = (path)=>{ return path ? trimTerminalSlash(path) : undefined; }; const sanitizeRedirectUrl = (path, url)=>{ if (path != null) { path = trimTerminalSlash(path); return path.match(/^((https?:\/\/)+)/) ? path : `${url.origin}${path}`; } else { return path; } }; // route post resolver export async function routePostHandler(req) { try { await addDataAndFileToRequest(req); const { pathname, href, hrefBeforeRedirect } = req.data; if (pathname && href) { /* console.log('pathname', pathname); console.log('href', href); console.log('hrefBeforeRedirect', hrefBeforeRedirect); */ const routes = await getRoutes(req); const route = routes.find((x)=>x.id === pathname); // console.log('route', route); if (route !== undefined) { return ResponseSuccess(route); } else { // exact route match not found // let's try if match splat routes const splatRoutes = routes.filter((x)=>x.useSplat === true); // console.log('splatRoutes', splatRoutes); // find best match splat route const splatRoute = splatRoutes.reduce((p, c)=>{ const match = pathname.indexOf(c.id) === 0; if (match && (!p || p.id.length < c.id.length)) { return { ...c, splat: pathname.substring(c.id.length, pathname.length) }; } else { return p; } }, undefined); if (splatRoute) { return ResponseSuccess(splatRoute); } const url = new URL(href); const urlBeforeRedirect = hrefBeforeRedirect ? new URL(hrefBeforeRedirect) : null; const sanitizedHref = sanitizeHref(url, urlBeforeRedirect); // collect redirects const { payload } = req; const payloadResponse = await payload.find({ collection: options.slug.redirect, where: { isActive: { equals: true } }, sort: 'order', depth: 0, limit: 10000, pagination: false, req, overrideAccess: true }); let redirects = payloadResponse.docs ? payloadResponse.docs : []; redirects = redirects.map((x)=>({ ...x, from: sanitizeRedirectFrom(x.from, url, urlBeforeRedirect), to: sanitizeRedirectTo(x.to) })); // redirects.sort((a, b) => a.order - b.order); // exact match redirect resolver const exactMatchResolver = (redirect)=>{ const status = parseInt(redirect.status) || 307; if (status < 400) { const redirectUrl = sanitizeRedirectUrl(redirect.to, url); // console.log('>>>>>>>>>> findRoute exactMatchResolver', status, redirectUrl, redirect.to); return Response.json({ redirectUrl }, { status }); } else { return Response.json('gone', { status }); } }; // check exact match redirects const exactMatchRedirect = redirects.find((x)=>x.from === sanitizedHref); if (exactMatchRedirect !== undefined) { return exactMatchResolver(exactMatchRedirect); } // pattern match redirect resolver const patternMatchResolver = (redirect)=>{ const status = parseInt(redirect.status) || 307; if (status < 400) { const regexp = new RegExp(redirect.from); let i = 0; let redirectUrl = redirect.to.replace(/\*/g, (...rest)=>{ return `$${++i}`; }); redirectUrl = sanitizedHref.replace(regexp, redirectUrl); // console.log('>>>>>>>>>> findRoute patternMatchResolver', status, redirectUrl, redirect.to); return Response.json({ redirectUrl }, { status }); } else { return Response.json('gone', { status }); } }; // check redirects by pattern redirects.forEach((x)=>{ let expression = x.from; // wrapping catchall in (.*) expression = expression.replace(/\*(?!\))/g, '(.*)'); // wrapping expression in ^...$ expression = `^${expression.replace(/(^\^)|(\$$)/g, '')}$`; x.from = expression; }); // check if a regex pattern redirect does match const firstMatch = redirects.find((x)=>sanitizedHref.match(x.from)); // console.log('>>>>>>>>>> findRoute firstMatch', firstMatch); if (firstMatch) { return patternMatchResolver(firstMatch); } // console.error('not found', collection); return ResponseNotFound(); } } else { return ResponseBadRequest(); } } catch (error) { console.error('RouteService.routePostHandler.error', error); return ResponseError(error); } } export async function getEachMarketLocale(req, callback) { let markets = await getCollectionItems(req, options.slug.market, 0); markets = markets ? markets.filter((x)=>x.isActive) : []; let locales = await getGlobalItems(req, options.slug.locale, 0); locales = locales ? locales.filter((x)=>x.isActive) : []; return eachMarketLocale(markets, locales, callback); } export async function getPages(req) { const store = {}; /* const where = typeof req.query.where === 'object' ? req.query.where : {}; const subRequest: PayloadRequest = { ...req, query: { ...req.query, where: { ...where, _status: { equals: 'published', }, }, }, } as unknown as PayloadRequest; */ const keys = options.pages; for (const key of keys){ // console.log('getPages', key, req.user); const items = await getCollectionItems(req, key, 0); store[key] = items; } return store; } //# sourceMappingURL=route.service.js.map