@netlify/content-engine
Version:
156 lines • 8.47 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.mutateGraphQLContextWithFieldLocalizations = mutateGraphQLContextWithFieldLocalizations;
exports.findAncestorLocaleCode = findAncestorLocaleCode;
// This whole file adds tree specific context (gql context is global to the request usually) so that we can find any parent fields of the current resolver where @locale() was used - since @locale() cascades to all descendant fields
// This fn is called in every resolver to check for both @locale (on the current field or in an ancestor)
// as well as @localized(codes:x,codesProxyFrom:y) per-request.
// GraphQL path info is used to check for ancestor @locale codes of the current field
// GraphQL context stores a map of paths to queried @locales
// mutateGraphQLContextWithFieldLocalizations then checks in context for an ancestor @locale and mutates
// the @proxy directive using info from @localized() to properly resolve localized data for each field.
function mutateGraphQLContextWithFieldLocalizations(
// TODO: add types
context, info) {
// TODO: _fields is internal to gql and we're not supposed to use it, however
// I wasn't able to access the same info with the public getFields() method.
// There is a better way to do this but I don't have the time to figure it out now :)
// this works well so the only downside is using something we're not officially supposed to use.
const serverExtensions = info.parentType?._fields?.[info.fieldName]
?.directives;
const localizedServerDirective = serverExtensions?.find((e) => {
return e.name === "localized";
})?.args;
// if there's no @localized() set for the current field
if (!localizedServerDirective?.codesProxyFrom) {
// only check for @locale(code:) and pass the locale down to descendant resolvers via gql context
maybeSetQueriedFieldLocaleOnContext(context, info);
}
// otherwise the current field has @localized() set, so check for @locale(code:) on the current field and all ancestors
// and resolve the current @localized field by mutating @proxy(from:x) to use the proxy path set in @localized(codesProxyFrom:x) for the current locale
else {
const localeInfo = maybeSetQueriedFieldLocaleOnContext(context, info) ||
findAncestorLocaleCode(context, info);
if (localeInfo?.localeCode) {
const localeProxyFrom = localizedServerDirective?.codesProxyFrom?.[localizedServerDirective?.codes?.findIndex?.((s) => s === localeInfo.localeCode)];
// since @locale(code: x) is a String, not an enum (for simplicity),
// we throw an error if the client requests @locale(code: "anything-thats-not-a-known-locale")
if (localizedServerDirective?.codes?.length && !localeProxyFrom) {
throw new Error(`Received @locale(code: "${localeInfo.localeCode}"), but this locale code is not available.\nAvailable locale codes: ${localizedServerDirective?.codes?.join(", ")}`);
}
info.from = localeProxyFrom;
}
}
}
const flatten = (path) => {
return Object.keys(path).reduce((prev, element) => {
const pathValue = path[element];
const typeofPathValue = typeof pathValue;
if (typeofPathValue === "object" && !Array.isArray(pathValue)) {
return [...prev, ...flatten(pathValue)];
}
if (typeofPathValue === "number")
return prev;
if (typeofPathValue === "undefined")
return prev;
return [...prev, pathValue];
}, []);
};
function infoPathToArray(path, operation) {
if (typeof operation !== "undefined" && operation.operation) {
return [operation.operation.toString(), ...flatten(path)];
}
return flatten(path);
}
function infoPathIsAncestorToInfoPath(currentInfoPath, compareInfoPath) {
// if the compared path is longer, it can't be an ancestor
if (currentInfoPath.length < compareInfoPath.length)
return false;
// compare each item in the path, and short circuit early if the paths don't match anywhere along the way (via .every)
const isAnscestor = compareInfoPath.every((segment, index) => segment === currentInfoPath[index]);
return isAnscestor;
}
// checks for @locale and sets the locale code in context for the current path if it's found.
function maybeSetQueriedFieldLocaleOnContext(context, info) {
// shortcut checking fields that have definitely don't have @locale set to save cpu cycles at scale
if (info.fieldNodes?.length === 1 &&
// no directives
(info.fieldNodes[0]?.directives?.length === 0 ||
// or one directive but it's not @locale
(info.fieldNodes[0]?.directives?.length === 1 &&
info.fieldNodes[0].directives[0].name?.value !== `locale`))) {
return;
}
const selectedLocaleCodes = new Set();
for (const fieldNode of info.fieldNodes || []) {
if (fieldNode.directives?.length) {
const localeDirective = fieldNode.directives.find((d) => d.name.value === `locale`);
if (localeDirective) {
// @ts-ignore todo fix this - value.value only exists for some kinds of arguments. need to type localeDirective
if (localeDirective?.arguments?.[0]?.value?.value) {
selectedLocaleCodes.add(
// @ts-ignore
localeDirective?.arguments?.[0]?.value?.value);
}
else {
// check if locale is a variable
const variableName =
// @ts-ignore
localeDirective.arguments?.[0]?.value?.name?.value;
const localeCode = info.variableValues?.[variableName];
if (localeCode) {
selectedLocaleCodes.add(localeCode);
}
}
}
}
}
switch (selectedLocaleCodes.size) {
case 0:
// no locale codes selected at this level
return;
case 1: {
// one locale selected
// set context as having atleast one locale directive so child resolvers only need to check for localizations if @locale was provided in the query, to save cpu cycles
context._queryHasLocaleDirective = true;
// add a locale path map if there's no map
context._selectedLocaleAtPaths ||= new Map();
const infoPath = infoPathToArray(info.path, info.operation);
// store this selected locale code and path to compare against in child resolvers
// child resolvers will see that _selectedLocale === true and use findAncestorLocaleCode() to determine if they are a descendent of a field that had @locale() set on it. They do this by comparing the info.path values to check that the beginning of their flattened info.path matches up with an existing @locale() anscestor field.
// TODO: we should probably use an array instead of a map. We need to sort the localePaths from longest to shortest, as longer matches are more specific than shorter ones.
const localeInfo = {
infoPath,
localeCode: Array.from(selectedLocaleCodes.keys())?.[0],
};
context._selectedLocaleAtPaths.set(infoPath.join(`.`), localeInfo);
return localeInfo;
}
default:
throw new Error(`Found multiple conflicting field locales: ${Array.from(selectedLocaleCodes.keys()).join(`, `)}. On field ${info.fieldName}`);
}
}
function findAncestorLocaleCode(context, info) {
if (context._queryHasLocaleDirective) {
const currentPath = infoPathToArray(info.path, info.operation);
let ancestorLocale = undefined;
let ancestorPath = undefined;
context._selectedLocaleAtPaths.forEach((value) => {
if (ancestorLocale)
return;
if (infoPathIsAncestorToInfoPath(currentPath, value.infoPath)) {
ancestorLocale = value.localeCode;
ancestorPath = value.infoPath;
}
});
if (ancestorLocale && ancestorPath) {
return {
infoPath: ancestorPath,
localeCode: ancestorLocale,
};
}
return null;
}
return null;
}
//# sourceMappingURL=localization.js.map
;