Skip to content

Commit

Permalink
chore(PPDSC-2625): fail build on cms issues (#564)
Browse files Browse the repository at this point in the history
* chore(PPDSC-2625): fail build on cms issues

* chore(PPDSC-2625): fix unit tests

* chore(PPDSC-2625): remove optional keys
  • Loading branch information
mstuartf committed Jan 19, 2023
1 parent 6dea464 commit 4a442e5
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 104 deletions.
96 changes: 49 additions & 47 deletions site/pages/about/roadmap.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,51 @@
import React from 'react';
import {
getSSRId,
InlineMessage,
LinkInline,
TextBlock,
UnorderedList,
getSSRId,
InlineMessage,
} from 'newskit';
import ReactMarkdown, {ReactMarkdownOptions} from 'react-markdown';
import {getSheets} from '../../utils/google-sheet';
import {formatSheetData, getCMSList} from '../../utils/google-sheet/utils';
import {
getSheets,
PageCMSPrefixedProps,
PageCMSRequiredProps,
} from '../../utils/google-sheet';
import {
getCMSPropsWithPrefix,
parseCMSResponse,
} from '../../utils/google-sheet/utils';
import {AboutPageTemplate} from '../../templates/about-page-template';
import {LayoutProps} from '../../components/layout';
import {
ContentSection,
ContentPrimary,
ContentSection,
} from '../../components/content-structure';
import {ComponentPageCell} from '../../components/layout-cells';

interface RoadmapContent {
intro_name: string;
intro_secondary: string;
intro_description: string;
intro_hero_illustration: string;
intro_date: string;
current_headline: string;
current_description: string;
comingup_headline: string;
comingup_description: string;
future_headline: string;
future_description: string;
[current_li_keys: `current_li${string}`]: string;
[comingup_li_keys: `comingup_li${string}`]: string;
[future_li_keys: `future_li${string}`]: string;
enum RequiredKeys {
intro_name = 'intro_name',
intro_secondary = 'intro_secondary',
intro_description = 'intro_description',
intro_hero_illustration = 'intro_hero_illustration',
intro_date = 'intro_date',
current_headline = 'current_headline',
current_description = 'current_description',
comingup_headline = 'comingup_headline',
comingup_description = 'comingup_description',
future_headline = 'future_headline',
future_description = 'future_description',
}

const roadmapFallbackContent: RoadmapContent = {
intro_name: 'Roadmap - fallback',
intro_description:
'Fallback - NewsKit’s Design System team is busy building and planning to help you build better products faster.',
intro_hero_illustration: 'components/hero-roadmap-illustration',
intro_secondary:
'The roadmap is a living document, and it is likely that priorities will change. See our Trello board for more details on the roadmap.',
intro_date: 'Last Updated: 6 December 2022',
current_headline: 'Current Quarter',
current_description: 'What we are working on:',
comingup_headline: 'Coming Up',
comingup_description: 'The focus for the next quarter:',
future_headline: 'Future',
future_description: 'Ideas we plan to look at:',
};
enum DynamicKeyPrefixes {
current_li_ = 'current_li_',
comingup_li_ = 'comingup_li_',
future_li_ = 'future_li_',
}

type RoadmapContent = PageCMSRequiredProps<RequiredKeys> &
PageCMSPrefixedProps<DynamicKeyPrefixes>;

const FormatMarkdown: React.FC<ReactMarkdownOptions> = ({children}) => (
/* eslint-disable @typescript-eslint/no-shadow */
Expand Down Expand Up @@ -85,16 +82,20 @@ const Roadmap = ({

const introSecondary = content.intro_secondary;

const currentList = getCMSList(content, 'current_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const currentList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'current_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

const comingupList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'comingup_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

const comingupList = getCMSList(content, 'comingup_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const futureList = getCMSList(content, 'future_li').map(entry => (
<FormatMarkdown key={entry[0]}>{entry[1]}</FormatMarkdown>
));
const futureList = getCMSPropsWithPrefix<typeof DynamicKeyPrefixes>(
content,
'future_li_',
).map(([k, v]) => <FormatMarkdown key={k}>{v}</FormatMarkdown>);

if (currentList.length && comingupList.length && futureList.length) {
return (
Expand Down Expand Up @@ -256,10 +257,11 @@ const Roadmap = ({
};
export default Roadmap;

// This function is called at build time and the response is passed to the page
// component as props.
export async function getStaticProps() {
const cmsData = await getSheets('Roadmap');
const content = {...roadmapFallbackContent, ...formatSheetData(cmsData)};
const content = parseCMSResponse(cmsData, {
required: RequiredKeys,
dynamic: DynamicKeyPrefixes,
});
return {props: {content}};
}
25 changes: 10 additions & 15 deletions site/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Layout, {LayoutProps} from '../components/layout';
import {IconFilledLaunch} from '../../src/icons';
import {GridLayoutProps} from '../../src/grid-layout/types';
import {fetchGitHubReleases} from '../utils/release-notes/functions';
import {getSheets} from '../utils/google-sheet';
import {formatSheetData} from '../utils/google-sheet/utils';
import {getSheets, PageCMSRequiredProps} from '../utils/google-sheet';
import {parseCMSResponse} from '../utils/google-sheet/utils';

const GRID_SECTION_OVERRIDES: GridLayoutProps['overrides'] = {
maxWidth: '1150px',
Expand All @@ -28,19 +28,14 @@ const GRID_SECTION_OVERRIDES: GridLayoutProps['overrides'] = {
},
};

interface HeroCardContent {
hero_card_title: string;
hero_card_description: string;
hero_card_link_text: string;
hero_card_link: string;
enum RequiredKeys {
hero_card_title = 'hero_card_title',
hero_card_description = 'hero_card_description',
hero_card_link_text = 'hero_card_link_text',
hero_card_link = 'hero_card_link',
}
// Content if the CMS fails - default to this
const heroCardFallbackContent: HeroCardContent = {
hero_card_title: `Latest blog`,
hero_card_description: `How an audio player component tells the story of NewsKit Design System's changing strategy.`,
hero_card_link_text: `Read on Medium`,
hero_card_link: `https://medium.com/newskit-design-system/how-an-audio-player-component-tells-the-story-of-newskit-design-systems-changing-strategy-8dc99d37ed67`,
};

type HeroCardContent = PageCMSRequiredProps<RequiredKeys>;

const Index = ({
releases,
Expand Down Expand Up @@ -159,6 +154,6 @@ export async function getStaticProps() {
releases = releasesOrError;
}

const content = {...heroCardFallbackContent, ...formatSheetData(data)};
const content = parseCMSResponse(data, {required: RequiredKeys});
return {props: {releases, content}};
}
35 changes: 18 additions & 17 deletions site/utils/google-sheet/__tests__/get-sheets.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import {google} from 'googleapis';
import {getSheets} from '../get-sheets';
import {CMSError} from '../utils';

const homePageContent = [
['hero_card_link_text', 'Read on Medium'],
['hero_card_link', 'https://medium.com/test'],
];

// The google.sheets() constructor returns a sheets function instance
const mockGoogleSheets = (values: string[][]) => () => ({
const mockGoogleSheets = (values?: string[][]) => () => ({
spreadsheets: {
values: {
get: jest.fn(() => ({
data: {
values: Promise.resolve(values),
},
})),
get: jest.fn(() =>
values
? Promise.resolve({
data: {
values,
},
})
: Promise.reject(new Error('Google Sheets error')),
),
},
},
});
Expand Down Expand Up @@ -55,22 +60,18 @@ describe('getSheets', () => {
process.env.GOOGLE_SHEETS_CLIENT_EMAIL = undefined;
process.env.GOOGLE_SHEETS_PRIVATE_KEY = undefined;
process.env.SPREADSHEET_ID = undefined;
const result = await getSheets('Homepage');
expect(googleSheetsSpy).not.toBeCalled();
expect(consoleErrorSpy).toBeCalled();
expect(result).toEqual([]);
await expect(async () => {
await getSheets('Homepage');
}).rejects.toThrowError(new CMSError('Missing environment variables'));
});

it('should return empty array on google failure', async () => {
process.env.GOOGLE_SHEETS_CLIENT_EMAIL = 'test';
process.env.GOOGLE_SHEETS_PRIVATE_KEY = 'test';
process.env.SPREADSHEET_ID = 'test';
googleSheetsSpy.mockImplementation(() => () =>
Promise.reject(new Error('Google error')),
);
const result = await getSheets('Homepage');
expect(googleSheetsSpy).toBeCalled();
expect(consoleErrorSpy).toBeCalled();
expect(result).toEqual([]);
googleSheetsSpy.mockImplementation(mockGoogleSheets());
await expect(async () => {
await getSheets('Homepage');
}).rejects.toThrowError(new CMSError('Google Sheets error'));
});
});
73 changes: 70 additions & 3 deletions site/utils/google-sheet/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {formatSheetData, getCMSList} from '../utils';
import {
CMSError,
formatSheetData,
getCMSPropsWithPrefix,
parseCMSResponse,
} from '../utils';

describe('formatSheetData', () => {
it('should convert google sheet data to an object', () => {
Expand All @@ -21,7 +26,7 @@ describe('formatSheetData', () => {
});
});

describe('getCMSList', () => {
describe('getCMSPropsWithPrefix', () => {
it('should return an array of items starting with a key', () => {
const values = [
['foo_li_1', 'foo1'],
Expand All @@ -31,11 +36,73 @@ describe('getCMSList', () => {
['baz', 'baz'],
];
const content = formatSheetData(values);
const result = getCMSList(content, 'foo_li');
const result = getCMSPropsWithPrefix(content, 'foo_li');
expect(result).toEqual([
['foo_li_1', 'foo1'],
['foo_li_2', 'foo2'],
['foo_li_3', 'foo3'],
]);
});
});

describe('parseCMSResponse', () => {
it('should throw an error if the response is empty', () => {
const response = null;
expect(() => {
parseCMSResponse(response, {required: {}});
}).toThrowError(new CMSError('No CMS data found'));
});
it('should throw an error if there are missing keys', () => {
const response: string[][] = [];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('MISSING_KEYS: key_1. '));
});
it('should throw an error if there are invalid keys', () => {
const response = [
['key_1', 'value_1'],
['key_2', 'value_2'],
];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('INVALID_KEYS: key_2. '));
});
it('should throw an error if there are missing and invalid keys', () => {
const response = [['key_2', 'value_2']];
expect(() => {
parseCMSResponse(response, {
required: {
key_1: 'key_1',
},
});
}).toThrowError(new CMSError('MISSING_KEYS: key_1. INVALID_KEYS: key_2. '));
});
it('should return the parsed content if all keys are valid', () => {
const response = [
['required_key', 'required_value'],
['dynamic_key_0', 'dynamic_value_0'],
['dynamic_key_1', 'dynamic_value_1'],
];
const parsed = parseCMSResponse(response, {
required: {
required_key: 'required_key',
},
dynamic: {
dynamic_key_: 'dynamic_key_',
},
});
expect(parsed).toEqual({
required_key: 'required_value',
dynamic_key_0: 'dynamic_value_0',
dynamic_key_1: 'dynamic_value_1',
});
});
});
14 changes: 3 additions & 11 deletions site/utils/google-sheet/get-sheets.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/* eslint-disable no-console */
import {google} from 'googleapis';
import {CMSError} from './utils';

require('dotenv').config();

// Define the required scopes. In our case we only need read access.
const SCOPE = ['https://www.googleapis.com/auth/spreadsheets.readonly'];

const handleError = (err: unknown) => {
console.error('>> ERROR: Cannot fetch data from googlesheet API.', err);
};

// Range is typically the sheet name
export async function getSheets(range: string) {
const {
Expand All @@ -22,9 +18,7 @@ export async function getSheets(range: string) {
!GOOGLE_SHEETS_PRIVATE_KEY &&
!SPREADSHEET_ID
) {
// Fail fast without a stacktrace
handleError('Have you added the .env file for local builds?');
return [];
throw new CMSError('Missing environment variables');
}

try {
Expand All @@ -46,8 +40,6 @@ export async function getSheets(range: string) {
});
return response.data.values;
} catch (err: unknown) {
handleError(err);
throw new CMSError(err instanceof Error ? err.message : 'Unknown error');
}

return [];
}
16 changes: 12 additions & 4 deletions site/utils/google-sheet/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export interface CMSDataProps {
export type PageCMSRequiredProps<T extends string> = {
[key in T]: string;
};

export type PageCMSPrefixedProps<T extends string> = {
[key in `${T}${number}`]: string;
};

export interface CMSProps {
[key: string]: string;
}

export interface ContentProps {
content: CMSDataProps;
}
export type CMSData = string[][];

export type CMSResponse = CMSData | null | undefined;

0 comments on commit 4a442e5

Please sign in to comment.