Skip to content

Commit

Permalink
fix(graphql): providing the query no longer prevents updates [TOL-140…
Browse files Browse the repository at this point in the history
…9] (#288)

* fix(graphql): not updating content below collections if the query is provided

* chore(examples): update nextjs-graphql example to also provide the query
  • Loading branch information
chrishelgert authored Aug 28, 2023
1 parent ffd70d8 commit b24df04
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 84 deletions.
130 changes: 67 additions & 63 deletions examples/nextjs-graphql/lib/api-graphql.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type { DocumentNode } from 'graphql';
import { GraphQLClient } from 'graphql-request';
import gql from 'graphql-tag';

interface Sys {
id: string;
}
Expand All @@ -15,9 +19,7 @@ interface PostCollection {
}

interface FetchResponse {
data?: {
postCollection?: PostCollection;
};
postCollection?: PostCollection;
}

const POST_GRAPHQL_FIELDS = `
Expand All @@ -30,90 +32,92 @@ title
description
`;

async function fetchGraphQL(query: string, draftMode = false): Promise<FetchResponse> {
return fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`,
export const POST_QUERY = gql`
query postCollection($slug: String!, $preview: Boolean!) {
postCollection(where: { slug: $slug }, preview: $preview, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}
`;

export const ALL_POSTS_SLUGGED_QUERY = gql`
query postCollectionSlugged($preview: Boolean!) {
postCollection(where: { slug_exists: true }, preview: $preview) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}
`;

export const ALL_POSTS_HOMEPAGE_QUERY = gql`
query postCollectionHomepage($preview: Boolean!) {
postCollection(preview: $preview) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}
`;

const client = new GraphQLClient(
`https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`
);

async function fetchGraphQL(
query: DocumentNode,
variables: Record<string, string | number | boolean>,
draftMode = false
) {
return client.request<{ postCollection?: PostCollection }>(
query,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${
draftMode
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
},
body: JSON.stringify({ query }),
...variables,
preview: draftMode,
},
{
Authorization: `Bearer ${
draftMode
? process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN
: process.env.CONTENTFUL_ACCESS_TOKEN
}`,
}
).then((response) => response.json());
);
}

function extractPost(fetchResponse: FetchResponse): Post | undefined {
return fetchResponse?.data?.postCollection?.items?.[0];
return fetchResponse?.postCollection?.items?.[0];
}

function extractPostEntries(fetchResponse: FetchResponse): Post[] | undefined {
return fetchResponse?.data?.postCollection?.items;
return fetchResponse?.postCollection?.items;
}

export async function getPreviewPostBySlug(slug: string): Promise<Post | undefined> {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: true, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
true
);
const entry = await fetchGraphQL(POST_QUERY, { slug }, true);

return extractPost(entry);
}

export async function getAllPostsWithSlug(): Promise<Post[] | undefined> {
const entries = await fetchGraphQL(
`query {
postCollection(where: { slug_exists: true }) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`
);
const entries = await fetchGraphQL(ALL_POSTS_SLUGGED_QUERY, {}, false);

return extractPostEntries(entries);
}

export async function getAllPostsForHome(draftMode: boolean): Promise<Post[] | undefined> {
const entries = await fetchGraphQL(
`query {
postCollection(preview: ${draftMode ? 'true' : 'false'}) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
draftMode
);
const entries = await fetchGraphQL(ALL_POSTS_HOMEPAGE_QUERY, {}, draftMode);

return extractPostEntries(entries);
}

export async function getPost(
slug: string,
draftMode: boolean
): Promise<{ post: Post | undefined }> {
const entry = await fetchGraphQL(
`query {
postCollection(where: { slug: "${slug}" }, preview: ${
draftMode ? 'true' : 'false'
}, limit: 1) {
items {
${POST_GRAPHQL_FIELDS}
}
}
}`,
draftMode
);
return {
post: extractPost(entry),
};
const entry = await fetchGraphQL(POST_QUERY, { slug }, draftMode);

return { post: extractPost(entry) };
}
3 changes: 2 additions & 1 deletion examples/nextjs-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"start": "next start"
},
"dependencies": {
"@contentful/live-preview": "^2.4.3",
"@contentful/live-preview": "latest",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"graphql-request": "6.1.0",
"next": "13.4.3",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
1 change: 1 addition & 0 deletions examples/nextjs-graphql/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function App({ Component, pageProps }: AppProps) {
locale="en-US"
enableInspectorMode={pageProps.draftMode}
enableLiveUpdates={pageProps.draftMode}
debugMode
>
<Component {...pageProps} />
</ContentfulLivePreviewProvider>
Expand Down
11 changes: 9 additions & 2 deletions examples/nextjs-graphql/pages/posts/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import {
import { useRouter } from 'next/router';
import Head from 'next/head';
import ErrorPage from 'next/error';
import { Post as PostType, getAllPostsWithSlug, getPost } from '../../lib/api-graphql';
import {
ALL_POSTS_SLUGGED_QUERY,
Post as PostType,
getAllPostsWithSlug,
getPost,
} from '../../lib/api-graphql';

interface PostProps {
post: PostType | null;
}

const Post: NextPage<PostProps> = ({ post }) => {
const router = useRouter();
const updatedPost = useContentfulLiveUpdates(post);
const updatedPost = useContentfulLiveUpdates(post, {
query: ALL_POSTS_SLUGGED_QUERY,
});
const inspectorProps = useContentfulInspectorMode({ entryId: post?.sys.id });

if (!router.isFallback && !updatedPost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const query = gql`
}
topSectionCollection {
items {
__typename
sys {
id
}
Expand Down Expand Up @@ -48,7 +49,7 @@ describe('parseGraphQLParams', () => {
['sys', { alias: new Map(), fields: new Set(['id']) }],
['content', { alias: new Map(), fields: new Set(['json']) }],
['topSectionCollection', { alias: new Map(), fields: new Set(['items']) }],
['items', { alias: new Map(), fields: new Set(['sys']) }],
['TopSection', { alias: new Map(), fields: new Set(['sys', '__typename']) }],
])
);
});
Expand Down
11 changes: 2 additions & 9 deletions packages/live-preview-sdk/src/graphql/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@ import { BLOCKS, INLINES } from '@contentful/rich-text-types';
import { Asset, Entry } from 'contentful';
import type { SetOptional } from 'type-fest';

import {
isPrimitiveField,
updatePrimitiveField,
resolveReference,
clone,
debug,
generateTypeName,
} from '../helpers';
import { isPrimitiveField, updatePrimitiveField, resolveReference, clone, debug } from '../helpers';
import { SUPPORTED_RICHTEXT_EMBEDS, isAsset, isRichText } from '../helpers/entities';
import {
CollectionItem,
Expand All @@ -24,7 +17,7 @@ import {
} from '../types';
import { updateAsset } from './assets';
import { isRelevantField, updateAliasedInformation } from './queryUtils';
import { buildCollectionName } from './utils';
import { buildCollectionName, generateTypeName } from './utils';

/**
* Updates GraphQL response data based on CMA entry object
Expand Down
26 changes: 22 additions & 4 deletions packages/live-preview-sdk/src/graphql/queryUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import type { DocumentNode, SelectionNode } from 'graphql';
import type { DocumentNode, FieldNode, SelectionNode } from 'graphql';

import { buildCollectionName, extractNameFromCollectionName } from './utils';
import { debug } from '../helpers';
import type { GraphQLParams } from '../types';
import { buildCollectionName } from './utils';

interface GeneratedGraphQLStructure {
name: string;
alias?: string;
__typename: string;
}

/**
* Generates the typename for the next node
* If it's inside a collection it will provide the typename from the collection name
*/
function getTypeName(selection: FieldNode, prevTypeName: string): string {
if (selection.name.value === 'items') {
return extractNameFromCollectionName(prevTypeName) || selection.name.value;
}

return selection.name.value;
}

/**
* Takes a list of graphql fields and extract the field names and aliases for the live update processing
*/
Expand All @@ -28,7 +41,10 @@ function gatherFieldInformation(

if (selection.selectionSet?.selections) {
generated.push(
...gatherFieldInformation(selection.selectionSet.selections, selection.name.value)
...gatherFieldInformation(
selection.selectionSet.selections,
getTypeName(selection, typename)
)
);
}
}
Expand Down Expand Up @@ -100,7 +116,9 @@ export function isRelevantField(
return false;
}

return queryInformation.fields.has(name) || queryInformation.fields.has(buildCollectionName(name));
return (
queryInformation.fields.has(name) || queryInformation.fields.has(buildCollectionName(name))
);
}

/**
Expand Down
16 changes: 16 additions & 0 deletions packages/live-preview-sdk/src/graphql/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
const COLLECTION_SUFFIX = 'Collection';

export function generateTypeName(contentTypeId: string): string {
return contentTypeId.charAt(0).toUpperCase() + contentTypeId.slice(1);
}

/** Generates the name of the field for a collection */
export function buildCollectionName(name: string): string {
return `${name}${COLLECTION_SUFFIX}`;
}

/**
* Extract the name of an entry from the collection (e.g. "postCollection" => "Post")
* Returns undefined if the name doesn't has the collection suffix.
*/
export function extractNameFromCollectionName(collection: string): string | undefined {
if (!collection.endsWith(COLLECTION_SUFFIX)) {
return undefined;
}

return generateTypeName(collection.replace(COLLECTION_SUFFIX, ''));
}
5 changes: 1 addition & 4 deletions packages/live-preview-sdk/src/helpers/resolveReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import type { Asset, Entry } from 'contentful';

import { ASSET_TYPENAME, EntityReferenceMap } from '../types';
import { sendMessageToEditor } from './utils';
import { generateTypeName } from '../graphql/utils';

const store: Record<string, EditorEntityStore> = {};

export function generateTypeName(contentTypeId: string): string {
return contentTypeId.charAt(0).toUpperCase() + contentTypeId.slice(1);
}

function getStore(locale: string): EditorEntityStore {
if (!store[locale]) {
store[locale] = new EditorEntityStore({
Expand Down

0 comments on commit b24df04

Please sign in to comment.