UNPKG

@netlify/content-engine

Version:
156 lines 8.47 kB
"use strict"; 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