Skip to content

Commit

Permalink
Merge pull request #33 from contentful/fix/live-updates
Browse files Browse the repository at this point in the history
fix: live updates [TOL-1000, TOL-1022]
  • Loading branch information
YvesRijckaert authored Mar 23, 2023
2 parents f8c5d74 + 0d84573 commit 4f93df9
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 35 deletions.
19 changes: 16 additions & 3 deletions src/graphql/__tests__/entries.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EntryProps } from 'contentful-management/types';
import { describe, it, expect, vi, afterEach } from 'vitest';

import { SysProps } from '../../types';
import { updateEntry } from '../entries';
import contentType from './fixtures/contentType.json';
import entry from './fixtures/entry.json';
Expand All @@ -18,7 +19,7 @@ describe('Update GraphQL Entry', () => {
update = entry as EntryProps,
locale = EN,
}: {
data: Record<string, unknown>;
data: Record<string, unknown> & { sys: SysProps };
update?: EntryProps;
locale?: string;
}) => {
Expand All @@ -27,7 +28,7 @@ describe('Update GraphQL Entry', () => {

it('keeps __typename unchanged', () => {
const warn = vi.spyOn(console, 'warn');
const data = { __typename: 'CT', shortText: 'text' };
const data = { __typename: 'CT', shortText: 'text', sys: { id: 'abc' } };

const update = updateFn({ data });

Expand All @@ -40,7 +41,7 @@ describe('Update GraphQL Entry', () => {
});

it('warns but keeps unknown fields', () => {
const data = { unknownField: 'text' };
const data = { unknownField: 'text', sys: { id: 'abc' } };
const warn = vi.spyOn(console, 'warn');

const update = updateFn({ data });
Expand All @@ -65,6 +66,9 @@ describe('Update GraphQL Entry', () => {
json: {
test: 'oldValue',
},
sys: {
id: 'abc',
},
};

expect(updateFn({ data })).toEqual({
Expand All @@ -77,18 +81,27 @@ describe('Update GraphQL Entry', () => {
dateTime: entry.fields.dateTime[EN],
location: entry.fields.location[EN],
json: entry.fields.json[EN],
sys: {
id: 'abc',
},
});
});

it('falls back to null for empty fields', () => {
const data = {
shortText: 'oldValue',
sys: {
id: 'abc',
},
};

const update = updateFn({ data, locale: 'n/a' });

expect(update).toEqual({
shortText: null,
sys: {
id: 'abc',
},
});
});
});
2 changes: 1 addition & 1 deletion src/graphql/__tests__/fixtures/entry.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"id": "80q6jb175og9"
}
},
"id": "4g1Xg1YnUCD5GUgndA7NLD",
"id": "abc",
"type": "Entry",
"createdAt": "2023-03-14T18:36:40.364Z",
"updatedAt": "2023-03-15T09:20:31.607Z",
Expand Down
3 changes: 2 additions & 1 deletion src/graphql/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ContentFields,
} from 'contentful-management/types';

import { SysProps } from '../types';
import { updateEntry } from './entries';

const field = (name: string, type = 'Symbol'): ContentFields => ({
Expand Down Expand Up @@ -44,7 +45,7 @@ const AssetContentType = {
* @param locale locale code
*/
export function updateAsset(
data: Record<string, unknown>,
data: Record<string, unknown> & { sys: SysProps },
update: AssetProps,
locale: string
): Record<string, unknown> {
Expand Down
79 changes: 68 additions & 11 deletions src/graphql/entries.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,96 @@
import { ContentTypeProps, EntryProps } from 'contentful-management/types';

import { CollectionItem, SysProps } from '../types';
import { isPrimitiveField, logUnrecognizedFields } from './utils';

/**
* Updates GraphQL response data based on CMA entry object
*
* @param contentType entity
* @param data the GraphQL response to be updated
* @param update CMA entry object containing the update
* @param locale code
* @param contentType ContentTypeProps
* @param data Record<string, unknown> - The GraphQL response to be updated
* @param update EntryProps - CMA entry object containing the update
* @param locale string - Locale code
* @returns Record<string, unknown> - Updated GraphQL response data
*/
export function updateEntry(
contentType: ContentTypeProps,
data: Record<string, unknown>,
data: Record<string, unknown> & { sys: SysProps },
update: EntryProps,
locale: string
): Record<string, unknown> {
): Record<string, unknown> & { sys: SysProps } {
const modified = { ...data };
const { fields } = contentType;

// Warn about unrecognized fields
logUnrecognizedFields(
fields.map((f) => f.apiName ?? f.name),
data
);

if (modified.sys.id !== update.sys.id) {
return modified;
}

for (const field of fields) {
const name = field.apiName ?? field.name;

if (isPrimitiveField(field) && name in data) {
// Falling back to 'null' as it's what GraphQL users would expect
// FIXME: handle locale fallbacks
modified[name] = update.fields?.[name]?.[locale] ?? null;
if (isPrimitiveField(field)) {
updatePrimitiveField(modified, update, name, locale);
} else if (field.type === 'RichText') {
updateRichTextField(modified, update, name, locale);
} else if (field.type === 'Array' && field.items?.type === 'Link') {
updateMultiRefField(modified, update, name, locale);
}
}

return modified;
}

function updatePrimitiveField(
modified: Record<string, unknown>,
update: EntryProps,
name: string,
locale: string
) {
if (name in modified) {
modified[name] = update.fields?.[name]?.[locale] ?? null;
}
}

function updateRichTextField(
modified: Record<string, unknown>,
update: EntryProps,
name: string,
locale: string
) {
if (name in modified) {
(modified[name] as { json: unknown }).json = update?.fields?.[name]?.[locale] ?? null;
}
}

function updateMultiRefField(
modified: Record<string, unknown>,
update: EntryProps,
name: string,
locale: string
) {
if (name in modified) {
// Listen to sorting
(modified[`${name}Collection`] as { items: CollectionItem[] }).items.sort((a, b) => {
const aIndex = update?.fields?.[name]?.[locale].findIndex(
(item: CollectionItem) => item.sys.id === a.sys.id
);
const bIndex = update?.fields?.[name]?.[locale].findIndex(
(item: CollectionItem) => item.sys.id === b.sys.id
);
return aIndex - bIndex;
});

// Listen to removal
const updateRefIds = update?.fields?.[name]?.[locale].map(
(item: CollectionItem) => item.sys.id
);
(modified[`${name}Collection`] as { items: CollectionItem[] }).items = (
modified[`${name}Collection`] as { items: CollectionItem[] }
).items.filter((item: CollectionItem) => updateRefIds.includes(item.sys.id));
}
}
18 changes: 0 additions & 18 deletions src/graphql/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,3 @@ export function isPrimitiveField(field: ContentFields): boolean {

return false;
}

export function isComplexField(field: ContentFields): boolean {
const types = new Set(['Link', 'ResourceLink']);

if (types.has(field.type)) {
return true;
}

// Array of Links or ResourceLinks
if (
(field.type === 'Array' && field.items?.type === 'Link') ||
(field.type === 'Array' && field.items?.type === 'ResourceLink')
) {
return true;
}

return false;
}
11 changes: 10 additions & 1 deletion src/live-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Subscription {
*/
export class LiveUpdates {
private subscriptions = new Map<string, Subscription>();
private updatedEntriesCache = new Map<string, Record<string, unknown>>();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
private mergeGraphQL(
Expand All @@ -26,8 +27,16 @@ export class LiveUpdates {
return gql.updateAsset(initial as any, updated as any, locale);
}

const entryId = (initial as any).sys.id;
const cachedData = this.updatedEntriesCache.get(entryId) || initial;

//@ts-expect-error -- ..
return gql.updateEntry(contentType, initial, updated, locale);
const updatedData = gql.updateEntry(contentType, cachedData, updated, locale);

// Cache the updated data for future updates
this.updatedEntriesCache.set(entryId, updatedData);

return updatedData;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ export enum TagAttributes {
export type Entity = Record<string, unknown>;
export type Argument = Entity | Entity[];
export type SubscribeCallback = (data: Argument) => void;

export interface SysProps {
id: string;
[key: string]: unknown;
}

export interface CollectionItem {
sys: SysProps;
}

0 comments on commit 4f93df9

Please sign in to comment.