@websolutespa/payload-plugin-bowl
Version:
Bowl PayloadCms plugin of the BOM Repository
451 lines (450 loc) • 18.6 kB
JavaScript
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