@last-rev/graphql-contentful-core
TypeScript icon, indicating that this package has built-in type declarations

0.5.17 • Public • Published

Installation

NPM:

npm install @last-rev/graphql-contentful-core

Yarn:

yarn add @last-rev/graphql-contentful-core

Usage

import { getServer, getLocalizedField, getFieldDataFetcher } from '@last-rev/graphql-contentful-core';

exports

getServer

Runs the last-rev graphql server

getLocalizedField

A utility which can be used in mappers to get the localized value of the field from the current item

getFieldDataFetcher

A utility which returns an async function that will fetch the field data for the current field. This function will return an object with two parameters: fieldValue, the value of the resolved field, and fieldName the name of the initial field that this value came from.

Extension file format

The extension file should be a standard javascript file which exports any of these 4 properties: typeDefs, mappers, resolvers and typeMappings.

typeDefs

This should be a string with any additional type definitions or extensions that are different from what the CMS provides.

For example, if in your CMS, there is a type simplePage in which there are two single entry reference fields, but your components expect a single array of references called content, you might add the following to typeDefs (note, the schema type is a capitalized version of the content type ID):

const typeDefs = `
extend type SimplePage {
  content: [Content]
}
`;

module.exports = {
  typeDefs
};

mappers

Mappers are where the main extensibility of the last-rev graphql system comes into play. This is where you can map fields to other field names or display types.

Mappers are an object with three levels. The first is a set of keys of schema types. The second is displayTypes, representing what to do if a certain content type is being resolved where a different type is expected. The third level is the field names, which are the actual mappers.

A mapper can either be a string, in which case it maps the value of the named field to the keyed field name, or it can be a function that takes the standard apollo graphql resolver arguments, parent, args, context, info. Please see the below documentation to see what is available in the context parameter.

Additionally, if a field is being mapped to afield with a different name, adding the property __fieldName__ to the returned object will ensure that the sidekickLookup knows the right field. For primitive types, you will have to use an object creator instead of the primitive type.

String example:

const mappers = {
  // type
  SomeItem: {
    // display type
    SomeItem: {
      //field name
      description: 'subTitle'
    }
  }
};

module.exports = {
  mappers
};

In the above example, SomeItem, when resolved where it is expected to be someItem, will have a field description which points to the value subTitle of same item.

Function example: (taking the SimplePage example from typeDefs)

const _ = require('lodash');
const _ = require('@last-rev/graphql-contentful-core');

const mappers = {
  SimplePage: {
    SimplePage: {
      content: async (parent, _args, ctx, _info) => {
        const heroModule = getLocalizedField(parent.fields, 'hero', ctx);
        const secondaryModule = getLocalizedField(parent.fields, 'secondaryModule', ctx);
        // nothing we can do here for the sidekickLookup, since there are two fields to point back to.
        return _.compact([heroModule, secondaryModule]);
      }
    }
  }
};

module.exports = {
  mappers
};

Below is another example where you want to map a SomePage item into a Card, which has 3 fields, title, subTitle, image;

const _ = require('lodash');
const _ = require('@last-rev/graphql-contentful-core');
const shorten = require('../utils/shorten');

const mappers = {
  SomePage: {
    Card: {
      // title field is same, so no mapping needed
      //title: 'title'
      subTitle: async (parent, _args, ctx, _info) => {
        const fieldName = 'description';
        const description = getLocalizedField(parent.fields, fieldName, ctx);
        // using string constructor in order to add __fieldName__ prop
        // this will be used by the sidekickLookupResolver to resolve the sidekickLookup for this field
        const subTitle = new String(`${shorten(description, 80)}`);
        // will ensure that the last-rev sidekick knows that this field is derived from the 'description' field
        subTitle.__fieldName__ = fieldName;
        return subTitle;
      },
      image: 'mainImage'
    }
  }
};

module.exports = {
  mappers
};

resolvers

In addition to mappers, standard Apollo Resolvers can be used. This should not be necessary in most scenarios, unless loading data from a different source.

typeMappings

TypeMappings are a simple key value pair which map a contentTypeId from the cms, to a different typeId that is expected by the component (for example, when using a last-rev out-of-the-box component). This will affect both the generated schemas and mappers.

const typeMappings = {
  promoCard: 'card'
};

module.exports = {
  typeMappings
};

In the example above, even though the content type is promoCard, the schema would create type Card and mappers would refer to this type:

const mappers = {
  Card: {
    Card: {
      //...
    }
  }
};

pathsConfigs

PathsConfigs allow you to define how a path is constructed from a specific content type. There are two ways to define these:

  1. A mapping from a contentType name to a string representing the root of the path. This will append the slug field of the content item to the root in order to construct the path for a specific content item:
const pathsConfigs = {
  // pageLanding item with slug="about" would render this path: "/about"
  'pageLanding': '/'
  // pageBlog item with slug="my-trip-to-italy" would render this path: "/blog/my-trip-to-italy"
  'pageBlog': '/blog'
}
  1. The second way is to provide a function. This function takes four arguments: the content item itself, our file system loaders, the default locale, and the list of available locales. The function should return a mapping of path to content ID, or path to an object consisting of content ID and an array of blocked locales:
// in this example, courses live at '/courses', and any topics that belong to
// those courses live under the specific course slug: '/courses/course-1/topic-1
// all topics also live at '/topics'
const pathsConfigs = {
  // this function will handle returning all the path mappings for the 'topic' content type
  topic: async (topic, loaders, defaultLocale, locales) => {
    const topicId = topic.sys.id;
    const topicSlug = get(topic, ['fields', 'slug', defaultLocale]);
    if (!topicSlug) return {};
    // all topics will have a corresponding location under '/topics'
    const mapping = {
      // for example '/topics/topic-1'
      [`/topics/${topicSlug}`]: topicId
    };
    // Here we load all courses, in order to find the topics that are listed below them
    const courses = await loaders.entriesByContentTypeLoader.load('course');
    for (const course of courses) {
      const courseSlug = get(course, ['fields', 'slug', defaultLocale]);
      if (!courseSlug) continue;
      const blockedLocales = [];
      for (const locale of locales) {
        // here we check whether the field 'topics' in the course exists for the current locale, or for the default locale
        if (!courseHasTopicForLocale(course.fields, topicId, locale, defaultLocale)) {
          // if it does not exist, push it to blockedLocales.
          // This topic will not generate a path under this course slug
          // for this locale
          blockedLocales.push(locale);
        }
      }
      if (blockedLocales.length === locales.length) {
        // all locales blocked (content not targeted), continue
        continue;
      }
      if (!blockedLocales.length) {
        // all locales are targeted, just return the ID string
        mapping[`/courses/${courseSlug}/${topicSlug}`] = topicId;
      } else {
        // some locales are blocked, return an object with the ID string
        // and the list of blockedLocales
        mapping[`/courses/${courseSlug}/${topicSlug}`] = {
          id: topicId,
          blockedLocales
        };
      }
    }
    return mapping;
  },
  // this will simply target all courses to '/courses': '/courses/course-1'
  course: '/courses'
};

const courseHasTopicForLocale = (courseFields, topicId, locale, defaultLocale) => {
  const topics = get(courseFields, ['topics', locale], get(courseFields, ['topics', defaultLocale], null));
  if (!topics) return null;
  return some(topics, (topic) => topic.sys.id === topicId);
};

Resolver Context

The 3rd argument passed to resolver functions represents the Apollo Resolver Context. We pass a number of things to this context to be used in resolvers and mappers.

resolve = async (_parent, args, context, info) => {
  const {
    // locale
    // after the initial root resolution, the queried locale (or the default locale
    locale,
    defaultLocale,
    loaders,
    mappers,
    typeMappings,
    pathToIdMapping
  } = context;

  // load an entry
  const id = '12345';
  const entry = await loaders.entries(id, locale);
  // load a page
  const slug = 'my-page';
  const page = await loaders.pages(slug, locale);
  // load an asset
  const asset = await loaders.assets(id);
  // find an item by path
  const mapped = pathToIdMapping[path];
  if (isString(mapped)) {
    const entry = await loaders.entries(mapped, locale);
  } else {
    const { id, blockedLocales } = mapped;
    if (blockedLocales.indexOf(locale) === -1) {
      const entry = await loaders.entries(id, locale);
    }
  }

  // if needed, mappers, and typeMappings are also available in the context
};

Readme

Keywords

none

Package Sidebar

Install

npm i @last-rev/graphql-contentful-core

Weekly Downloads

249

Version

0.5.17

License

ISC

Unpacked Size

170 kB

Total Files

98

Last publish

Collaborators

  • maxtechera.lastrev
  • tharris
  • justinlastrev
  • jaimelastrev
  • bradtaylorsf