diff --git a/app/api_types.ts b/app/api_types.ts index 86187695d0342ba2a668b4ffc58a51199e5420e3..0ce20d02bc4c40abdb4ee4c18feda11a5f68e3c4 100644 --- a/app/api_types.ts +++ b/app/api_types.ts @@ -1,31 +1,31 @@ export type GoodNewsCategory = - | "ANIMAL_RIGHTS" - | "CLIMATE" - | "DIGITIZATION" - | "EDUCATION" - | "GLOBAL_HEALTH" - | "HUMAN_RIGHTS" - | "SUSTAINABILITY"; + | 'ANIMAL_RIGHTS' + | 'CLIMATE' + | 'DIGITIZATION' + | 'EDUCATION' + | 'GLOBAL_HEALTH' + | 'HUMAN_RIGHTS' + | 'SUSTAINABILITY' export type GoodNewsLanguage = - | "de" - | "en"; + | 'de' + | 'en' export type GoodNewsArticle = { - id: string; - date: string; - headline: string; - teaserText: string; - pictureUrl: string; - pictureCredit: string | undefined; - articleUrl: string; - source: string | undefined; - sortKey: number; - category: GoodNewsCategory | undefined; - language: GoodNewsLanguage; -}; + id: string + date: string + headline: string + teaserText: string + pictureUrl: string + pictureCredit: string | undefined + articleUrl: string + source: string | undefined + sortKey: number + category: GoodNewsCategory | undefined + language: GoodNewsLanguage +} export type GoodNewsArticleQueryResponse = { - articles: GoodNewsArticle[]; - previousDateTeaser: GoodNewsArticle | undefined; -}; + articles: GoodNewsArticle[] + previousDateTeaser: GoodNewsArticle | undefined +} diff --git a/app/common_test.ts b/app/common_test.ts index ddbf135ffc9d7859ded40537c03cc63eb7db13d9..22067c4347c2001527f4a369f6ddf0fd690d66e2 100644 --- a/app/common_test.ts +++ b/app/common_test.ts @@ -1,42 +1,40 @@ -import { returnsNext, stub } from "./dev_deps.ts"; -import { GraphQLServer } from "./server.ts"; +import { returnsNext, stub } from './dev_deps.ts' +import { GraphQLServer } from './server.ts' -export type ResponsePayload = Record<string, unknown> | Error; +export type ResponsePayload = Record<string, unknown> | Error export const stubFetch = (response: ResponsePayload) => { return stub( globalThis, - "fetch", + 'fetch', returnsNext([Promise.resolve(new Response(JSON.stringify(response)))]), - ); -}; + ) +} -export type UrlPrefix = string; +export type UrlPrefix = string export const stubFetchByUrlPrefix = ( responses: Record<UrlPrefix, ResponsePayload>, ) => { - return stub(globalThis, "fetch", (input) => { - const url = input.toString(); + return stub(globalThis, 'fetch', (input) => { + const url = input.toString() for (const [urlPrefix, response] of Object.entries(responses)) { if (url.startsWith(urlPrefix)) { if (response instanceof Error) { - return Promise.reject(response); + return Promise.reject(response) } else { - return Promise.resolve(new Response(JSON.stringify(response))); + return Promise.resolve(new Response(JSON.stringify(response))) } } } - const prefixes = Object.keys(responses); + const prefixes = Object.keys(responses) throw Promise.reject( new Error( - `Unexpected request URL "${url}", expected prefixes are:\n ${ - prefixes.join("\n ") - }`, + `Unexpected request URL "${url}", expected prefixes are:\n ${prefixes.join('\n ')}`, ), - ); - }); -}; + ) + }) +} export const processGqlRequest = ( graphQLServer: GraphQLServer, @@ -44,11 +42,11 @@ export const processGqlRequest = ( variables: Record<string, unknown> = {}, headers: Record<string, unknown> = {}, ): Promise<Record<string, unknown> | undefined | null> => { - const request = new Request("http://localhost:8002/graphql", { - method: "POST", + const request = new Request('http://localhost:8002/graphql', { + method: 'POST', headers: { - "content-type": "application/json", - "accept": "*/*", + 'content-type': 'application/json', + 'accept': '*/*', ...headers, }, body: JSON.stringify({ @@ -57,10 +55,10 @@ export const processGqlRequest = ( query: query, extensions: { headers }, }), - }); + }) return graphQLServer.handleRequest(request) // deno-lint-ignore no-explicit-any .then((response: any) => response.json()) // deno-lint-ignore no-explicit-any - .then((response: any) => response.data); -}; + .then((response: any) => response.data) +} diff --git a/app/contentful_types.ts b/app/contentful_types.ts index 42e51b133c3a5bafa8696a764571ee4b525f0755..c7c556bbcd443caeb32dec445216e5cd0b6beda4 100644 --- a/app/contentful_types.ts +++ b/app/contentful_types.ts @@ -1,83 +1,83 @@ -export type ContentfulLanguage = "de" | "en"; +export type ContentfulLanguage = 'de' | 'en' -export type ContentfulItemBildSource = string; +export type ContentfulItemBildSource = string export type ContentfulItemBildManuell = { - sys: { type: string; linkType: string; id: string }; -}; + sys: { type: string; linkType: string; id: string } +} /** items contain either bildManuell OR bildSource but not both */ export type ContentfulItemBild = | { bildSource?: ContentfulItemBildSource } - | { bildManuell?: ContentfulItemBildManuell }; + | { bildManuell?: ContentfulItemBildManuell } export type ContentfulItemCategory = { categoryID: - | "animal_rights" - | "climate" - | "digitization" - | "education" - | "global_health" - | "human_rights" - | "sustainability" - | undefined; -}; + | 'animal_rights' + | 'climate' + | 'digitization' + | 'education' + | 'global_health' + | 'human_rights' + | 'sustainability' + | undefined +} export type ContentfulItemFields = & Record<string, unknown> & ContentfulItemCategory & ContentfulItemBild & { - datum: string; - headline: string; - layout: string; - photoCredit: string; - teaserText: string; - quelle: string; - articleUrl: string; - sortKey: number; - language: ContentfulLanguage; - }; + datum: string + headline: string + layout: string + photoCredit: string + teaserText: string + quelle: string + articleUrl: string + sortKey: number + language: ContentfulLanguage + } // only covers the fields we're interested in export type ContentfulItem = Record<string, unknown> & { sys: Record<string, unknown> & { - id: string; - }; - fields: ContentfulItemFields; -}; + id: string + } + fields: ContentfulItemFields +} export type ContentfulAssetFields = { - title: string; - description: string; + title: string + description: string file: { - url: string; + url: string details: { - size: number; + size: number image: { - width: number; - height: number; - }; - }; - fileName: string; - contentType: string; - }; -}; + width: number + height: number + } + } + fileName: string + contentType: string + } +} // only covers the fields we're interested in export type ContentfulAsset = Record<string, unknown> & { sys: Record<string, unknown> & { - id: string; - }; - fields: ContentfulAssetFields; -}; + id: string + } + fields: ContentfulAssetFields +} export type ContentfulEntriesResponse = Record<string, unknown> & { - total: number; - skip: number; - limit: number; - items: ContentfulItem[]; + total: number + skip: number + limit: number + items: ContentfulItem[] includes: { - Asset: ContentfulAsset[]; - }; -}; + Asset: ContentfulAsset[] + } +} diff --git a/app/deps.ts b/app/deps.ts index 9427cd92f6a2ab2800863328eccf7603685eb13a..0f8301900f8809b98d1ccdc4faf64d8b0ba36642 100644 --- a/app/deps.ts +++ b/app/deps.ts @@ -1,7 +1,4 @@ -export { serve } from "https://deno.land/std@0.165.0/http/server.ts"; -export { - createSchema, - createYoga, -} from "https://cdn.skypack.dev/graphql-yoga@3.5.1"; -export { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts"; -export { useResponseCache } from "https://cdn.skypack.dev/@graphql-yoga/plugin-response-cache@1.0.3"; +export { serve } from 'https://deno.land/std@0.165.0/http/server.ts' +export { createSchema, createYoga } from 'https://cdn.skypack.dev/graphql-yoga@3.5.1' +export { gql } from 'https://deno.land/x/graphql_tag@0.0.1/mod.ts' +export { useResponseCache } from 'https://cdn.skypack.dev/@graphql-yoga/plugin-response-cache@1.0.3' diff --git a/app/dev_deps.ts b/app/dev_deps.ts index 5bbc7cfb90f6f8aa8e402903a8ba59daba936c99..53cbb265f3e980cb1a9307fff244da656a342970 100644 --- a/app/dev_deps.ts +++ b/app/dev_deps.ts @@ -1,21 +1,7 @@ -export { - afterEach, - beforeEach, - describe, - it, -} from "https://deno.land/std@0.165.0/testing/bdd.ts"; +export { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.165.0/testing/bdd.ts' -export { - assert, - assertEquals, - assertExists, - assertRejects, -} from "https://deno.land/std@0.165.0/testing/asserts.ts"; +export { assert, assertEquals, assertExists, assertRejects } from 'https://deno.land/std@0.165.0/testing/asserts.ts' -export { - assertSpyCall, - returnsNext, - stub, -} from "https://deno.land/std@0.165.0/testing/mock.ts"; +export { assertSpyCall, returnsNext, stub } from 'https://deno.land/std@0.165.0/testing/mock.ts' -export type { Stub } from "https://deno.land/std@0.155.0/testing/mock.ts"; +export type { Stub } from 'https://deno.land/std@0.155.0/testing/mock.ts' diff --git a/app/goodnews.ts b/app/goodnews.ts index b0564b883b2a53ba14b6eb44b5738e110b7bc1ed..edd89fb84f66fdcfcae9057c0acb8ccdaedbbd24 100644 --- a/app/goodnews.ts +++ b/app/goodnews.ts @@ -1,9 +1,5 @@ -import { - GoodNewsArticle, - GoodNewsArticleQueryResponse, - GoodNewsCategory, -} from "./api_types.ts"; -import { ContentfulConfig } from "./server.ts"; +import { GoodNewsArticle, GoodNewsArticleQueryResponse, GoodNewsCategory } from './api_types.ts' +import { ContentfulConfig } from './server.ts' import { ContentfulAsset, ContentfulEntriesResponse, @@ -11,37 +7,37 @@ import { ContentfulItemBildManuell, ContentfulItemFields, ContentfulLanguage, -} from "./contentful_types.ts"; -import { logger } from "./logging.ts"; +} from './contentful_types.ts' +import { logger } from './logging.ts' -export const SUPPORTED_LANGUAGES = ["en", "de"]; -export const DEFAULT_LANGUAGE: ContentfulLanguage = "en"; +export const SUPPORTED_LANGUAGES = ['en', 'de'] +export const DEFAULT_LANGUAGE: ContentfulLanguage = 'en' export const resolveCategory = ( fields: ContentfulItemFields, ): GoodNewsCategory | undefined => { switch (fields.categoryID) { - case "animal_rights": - return "ANIMAL_RIGHTS"; - case "climate": - return "CLIMATE"; - case "digitization": - return "DIGITIZATION"; - case "education": - return "EDUCATION"; - case "global_health": - return "GLOBAL_HEALTH"; - case "human_rights": - return "HUMAN_RIGHTS"; - case "sustainability": - return "SUSTAINABILITY"; + case 'animal_rights': + return 'ANIMAL_RIGHTS' + case 'climate': + return 'CLIMATE' + case 'digitization': + return 'DIGITIZATION' + case 'education': + return 'EDUCATION' + case 'global_health': + return 'GLOBAL_HEALTH' + case 'human_rights': + return 'HUMAN_RIGHTS' + case 'sustainability': + return 'SUSTAINABILITY' case undefined: - return undefined; + return undefined default: - logger.error(`Received unknown category "${fields.categoryID}"`); - return undefined; + logger.error(`Received unknown category "${fields.categoryID}"`) + return undefined } -}; +} export const resolvePictureUrlFromAssets = ( item: ContentfulItem, @@ -51,20 +47,20 @@ export const resolvePictureUrlFromAssets = ( return ( asset.sys.id === (item.fields.bildManuell as ContentfulItemBildManuell).sys.id - ); - }); - return "https:" + asset?.fields.file.url; -}; + ) + }) + return 'https:' + asset?.fields.file.url +} export const resolvePictureUrl = ( item: ContentfulItem, assets: ContentfulAsset[], ): string => { - if ("bildSource" in item.fields) return item.fields.bildSource as string; - else if ("bildManuell" in item.fields) { - return resolvePictureUrlFromAssets(item, assets); - } else throw Error("Item missing both bildSource and bildManuell"); -}; + if ('bildSource' in item.fields) return item.fields.bildSource as string + else if ('bildManuell' in item.fields) { + return resolvePictureUrlFromAssets(item, assets) + } else throw Error('Item missing both bildSource and bildManuell') +} export const toGoodNewsArticle = ( item: ContentfulItem, @@ -83,99 +79,92 @@ export const toGoodNewsArticle = ( sortKey: item.fields.sortKey, category: resolveCategory(item.fields), language: item.fields.language, - }; + } } catch (error) { - logger.error(`Failed to transform article: ${error}`); - return undefined; + logger.error(`Failed to transform article: ${error}`) + return undefined } -}; +} const fetchPage = - (config: ContentfulConfig) => - async (skip: number, limit: number): Promise<ContentfulEntriesResponse> => { + (config: ContentfulConfig) => async (skip: number, limit: number): Promise<ContentfulEntriesResponse> => { // baseUrl used for logging to not leak access_token - const baseUrl = - `${config.baseUrl}/${config.spaceId}/environments/${config.environment}/entries`; - const url = new URL(baseUrl); + const baseUrl = `${config.baseUrl}/${config.spaceId}/environments/${config.environment}/entries` + const url = new URL(baseUrl) - url.searchParams.set("skip", skip.toString()); - url.searchParams.set("limit", limit.toString()); - url.searchParams.set("access_token", config.deliveryApiAccessToken); + url.searchParams.set('skip', skip.toString()) + url.searchParams.set('limit', limit.toString()) + url.searchParams.set('access_token', config.deliveryApiAccessToken) - logger.info(`fetching articles ${skip} to ${skip + limit} from ${baseUrl}`); + logger.info(`fetching articles ${skip} to ${skip + limit} from ${baseUrl}`) - const start = Date.now(); + const start = Date.now() try { - const fetchResult = await fetch(url); - const resultJson = await fetchResult.json(); - return resultJson; + const fetchResult = await fetch(url) + const resultJson = await fetchResult.json() + return resultJson } catch (err) { - logger.error("fetching articles failed: " + err.message); - throw err; + logger.error('fetching articles failed: ' + err.message) + throw err } finally { - const duration = Date.now() - start; - logger.info(`fetching articles took ${duration} ms`); + const duration = Date.now() - start + logger.info(`fetching articles took ${duration} ms`) } - }; + } -type ContentfulItemFilter = (item: ContentfulItem) => boolean; +type ContentfulItemFilter = (item: ContentfulItem) => boolean -const olderThan = - (date: string): ContentfulItemFilter => (item: ContentfulItem) => - item.fields.datum.localeCompare(date) === -1; +const olderThan = (date: string): ContentfulItemFilter => (item: ContentfulItem) => + item.fields.datum.localeCompare(date) === -1 -const sameLanguage = - (language: ContentfulLanguage): ContentfulItemFilter => - (item: ContentfulItem) => item.fields.language === language; +const sameLanguage = (language: ContentfulLanguage): ContentfulItemFilter => (item: ContentfulItem) => + item.fields.language === language const containsMandatoryFields = (item: ContentfulItem) => { - const { fields } = item; + const { fields } = item return ( !!fields.datum && !!fields.headline && !!fields.teaserText && !!fields.articleUrl && !!fields.language && - typeof fields.sortKey === "number" - ); -}; + typeof fields.sortKey === 'number' + ) +} -const sortKeyComparator = (a: ContentfulItem, b: ContentfulItem) => - a.fields.sortKey - b.fields.sortKey; +const sortKeyComparator = (a: ContentfulItem, b: ContentfulItem) => a.fields.sortKey - b.fields.sortKey export const executeArticlesQuery = (config: ContentfulConfig) => async ( parameters: { lastDate?: string }, validatedLocale: ContentfulLanguage, ): Promise<GoodNewsArticleQueryResponse> => { - let responseDate: string | undefined; - const responseItems: ContentfulItem[] = []; - const responseAssets: ContentfulAsset[] = []; - let skip = 0; - const limit = config.pageSize; - let fetchMore = true; + let responseDate: string | undefined + const responseItems: ContentfulItem[] = [] + const responseAssets: ContentfulAsset[] = [] + let skip = 0 + const limit = config.pageSize + let fetchMore = true while (fetchMore) { - const contentfulResponse = await fetchPage(config)(skip, limit); + const contentfulResponse = await fetchPage(config)(skip, limit) responseItems.push( ...contentfulResponse.items .filter(sameLanguage(validatedLocale)) .filter(containsMandatoryFields), - ); - responseItems.sort(sortKeyComparator); - responseAssets.push(...contentfulResponse.includes.Asset); + ) + responseItems.sort(sortKeyComparator) + responseAssets.push(...contentfulResponse.includes.Asset) if (parameters.lastDate && !responseDate) { responseDate = responseItems.find(olderThan(parameters.lastDate)) - ?.fields.datum; + ?.fields.datum } else if (!responseDate) { - responseDate = responseItems.length - ? responseItems[0].fields.datum - : undefined; + responseDate = responseItems.length ? responseItems[0].fields.datum : undefined } if (responseDate && responseItems.find(olderThan(responseDate!))) { - fetchMore = false; + fetchMore = false } - skip = skip + limit; + skip = skip + limit } const responseArticles: GoodNewsArticle[] = responseItems @@ -185,20 +174,18 @@ async ( item.fields.language === validatedLocale, ) .map((item) => toGoodNewsArticle(item, responseAssets)) - .filter((article) => !!article) as GoodNewsArticle[]; - - const previousDateTeaser: GoodNewsArticle | undefined = !responseDate - ? undefined - : responseItems - .filter( - (item) => - item.fields.datum < responseDate! && - item.fields.language === validatedLocale, - ) - .map((item) => toGoodNewsArticle(item, responseAssets))[0]; + .filter((article) => !!article) as GoodNewsArticle[] + + const previousDateTeaser: GoodNewsArticle | undefined = !responseDate ? undefined : responseItems + .filter( + (item) => + item.fields.datum < responseDate! && + item.fields.language === validatedLocale, + ) + .map((item) => toGoodNewsArticle(item, responseAssets))[0] return { articles: responseArticles, previousDateTeaser: previousDateTeaser, - }; -}; + } +} diff --git a/app/goodnews_test.ts b/app/goodnews_test.ts index 0a771b5ada4d0d297ee3d0a5bfda4aec38039e89..74cf0d3ff440fe3801a13b4717deebba46051f49 100644 --- a/app/goodnews_test.ts +++ b/app/goodnews_test.ts @@ -1,12 +1,4 @@ -import { - afterEach, - assert, - assertEquals, - assertExists, - describe, - it, - Stub, -} from "./dev_deps.ts"; +import { afterEach, assert, assertEquals, assertExists, describe, it, Stub } from './dev_deps.ts' import { contentfulResponse, item0, @@ -28,35 +20,27 @@ import { item4_id, item5, item5_id, -} from "./goodnews_test_data.ts"; -import { executeArticlesQuery } from "./goodnews.ts"; -import { - ContentfulConfig, - createGraphQLServer, - GraphQLServer, - ServerConfig, -} from "./server.ts"; -import { GoodNewsArticle, GoodNewsArticleQueryResponse } from "./api_types.ts"; -import { processGqlRequest, stubFetchByUrlPrefix } from "./common_test.ts"; -import { - ContentfulEntriesResponse, - ContentfulItem, -} from "./contentful_types.ts"; +} from './goodnews_test_data.ts' +import { executeArticlesQuery } from './goodnews.ts' +import { ContentfulConfig, createGraphQLServer, GraphQLServer, ServerConfig } from './server.ts' +import { GoodNewsArticle, GoodNewsArticleQueryResponse } from './api_types.ts' +import { processGqlRequest, stubFetchByUrlPrefix } from './common_test.ts' +import { ContentfulEntriesResponse, ContentfulItem } from './contentful_types.ts' const mockConfig = (pageSize: number): ContentfulConfig => ({ - baseUrl: "https://test.com/spaces", - spaceId: "testSpaceId", - environment: "master", - deliveryApiAccessToken: "testAccessToken", + baseUrl: 'https://test.com/spaces', + spaceId: 'testSpaceId', + environment: 'master', + deliveryApiAccessToken: 'testAccessToken', pageSize, fake: false, -}); +}) type MockPage = { - skip: number; - limit: number; - items: ContentfulItem[]; -}; + skip: number + limit: number + items: ContentfulItem[] +} const withMockedResponses = async ( config: ContentfulConfig, @@ -64,50 +48,46 @@ const withMockedResponses = async ( testFn: () => unknown, ) => { const urlFor = (skip: number, limit: number): string => - `https://test.com/spaces/${config.spaceId}/environments/${config.environment}/entries?skip=${skip}&limit=${limit}&access_token=${config.deliveryApiAccessToken}`; + `https://test.com/spaces/${config.spaceId}/environments/${config.environment}/entries?skip=${skip}&limit=${limit}&access_token=${config.deliveryApiAccessToken}` const responses: Record<string, ContentfulEntriesResponse> = pages.map( (page) => { // deno-lint-ignore no-explicit-any - const obj: any = {}; - const url = urlFor(page.skip, page.limit); - obj[url] = contentfulResponse(page.skip, page.limit, page.items); - return obj as Record<string, ContentfulEntriesResponse>; + const obj: any = {} + const url = urlFor(page.skip, page.limit) + obj[url] = contentfulResponse(page.skip, page.limit, page.items) + return obj as Record<string, ContentfulEntriesResponse> }, - ).reduce((prev, curr) => ({ ...prev, ...curr })); + ).reduce((prev, curr) => ({ ...prev, ...curr })) - const fetchStub = stubFetchByUrlPrefix(responses); + const fetchStub = stubFetchByUrlPrefix(responses) try { - await testFn(); + await testFn() } finally { - fetchStub.restore(); + fetchStub.restore() } -}; +} const assertContains = (result: GoodNewsArticleQueryResponse, id: string) => { - const articlesIds = result.articles.map((article) => article.id); + const articlesIds = result.articles.map((article) => article.id) assertEquals( articlesIds.includes(id), true, - `Article with ID "${id}" is NOT contained in result ([${ - articlesIds.join(",") - }])"`, - ); -}; + `Article with ID "${id}" is NOT contained in result ([${articlesIds.join(',')}])"`, + ) +} const assertNotContains = ( result: GoodNewsArticleQueryResponse, id: string, ) => { - const articlesIds = result.articles.map((article) => article.id); + const articlesIds = result.articles.map((article) => article.id) assertEquals( articlesIds.includes(id), false, - `Article with ID "${id}" is contained in result ([${ - articlesIds.join(",") - }]) but SHOULD NOT BE"`, - ); -}; + `Article with ID "${id}" is contained in result ([${articlesIds.join(',')}]) but SHOULD NOT BE"`, + ) +} const assertPreviousDateTeaser = ( result: GoodNewsArticleQueryResponse, @@ -116,331 +96,325 @@ const assertPreviousDateTeaser = ( assertExists( result.previousDateTeaser, `previousDateTeaser is not contained in result but it should be`, - ); + ) assertEquals( result.previousDateTeaser!.id, id, - `previousDateTeaser does not have expected ID "${id}" (actual ID: ${ - result.previousDateTeaser!.id - }`, - ); -}; - -describe("goodnews", () => { - it("should fetch articles of one day when within the page", () => { - const config = mockConfig(3); + `previousDateTeaser does not have expected ID "${id}" (actual ID: ${result.previousDateTeaser!.id}`, + ) +} + +describe('goodnews', () => { + it('should fetch articles of one day when within the page', () => { + const config = mockConfig(3) return withMockedResponses(config, [ { skip: 0, limit: 3, items: [ - item0("2022-11-29", "de"), - item1("2022-11-29", "de"), - item2("2022-11-28", "de"), + item0('2022-11-29', 'de'), + item1('2022-11-29', 'de'), + item2('2022-11-28', 'de'), ], }, { skip: 3, limit: 3, items: [ - item3("2022-11-28", "de"), - item4("2022-11-27", "de"), - item5("2022-11-27", "de"), + item3('2022-11-28', 'de'), + item4('2022-11-27', 'de'), + item5('2022-11-27', 'de'), ], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); + const result = await executeArticlesQuery(config)({}, 'de') - assertContains(result, item0_id); - assertContains(result, item1_id); + assertContains(result, item0_id) + assertContains(result, item1_id) - assertNotContains(result, item2_id); - assertNotContains(result, item3_id); - assertNotContains(result, item4_id); - assertNotContains(result, item5_id); + assertNotContains(result, item2_id) + assertNotContains(result, item3_id) + assertNotContains(result, item4_id) + assertNotContains(result, item5_id) - assertPreviousDateTeaser(result, item2_id); - }); - }); + assertPreviousDateTeaser(result, item2_id) + }) + }) - it("should fetch articles of one day when distributed over multiple pages", () => { - const config = mockConfig(2); + it('should fetch articles of one day when distributed over multiple pages', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "de"), item3("2022-11-28", "de")], + items: [item2('2022-11-29', 'de'), item3('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); + const result = await executeArticlesQuery(config)({}, 'de') - assertContains(result, item0_id); - assertContains(result, item1_id); - assertContains(result, item2_id); + assertContains(result, item0_id) + assertContains(result, item1_id) + assertContains(result, item2_id) - assertNotContains(result, item3_id); + assertNotContains(result, item3_id) - assertPreviousDateTeaser(result, item3_id); - }); - }); + assertPreviousDateTeaser(result, item3_id) + }) + }) - it("should fetch articles of an older day when within the page", () => { - const config = mockConfig(3); + it('should fetch articles of an older day when within the page', () => { + const config = mockConfig(3) return withMockedResponses(config, [ { skip: 0, limit: 3, items: [ - item0("2022-11-29", "de"), - item1("2022-11-29", "de"), - item2("2022-11-29", "de"), + item0('2022-11-29', 'de'), + item1('2022-11-29', 'de'), + item2('2022-11-29', 'de'), ], }, { skip: 3, limit: 3, items: [ - item3("2022-11-28", "de"), - item4("2022-11-28", "de"), - item5("2022-11-27", "de"), + item3('2022-11-28', 'de'), + item4('2022-11-28', 'de'), + item5('2022-11-27', 'de'), ], }, ], async () => { const result = await executeArticlesQuery(config)({ - lastDate: "2022-11-29", - }, "de"); + lastDate: '2022-11-29', + }, 'de') - assertNotContains(result, item0_id); - assertNotContains(result, item1_id); - assertNotContains(result, item2_id); + assertNotContains(result, item0_id) + assertNotContains(result, item1_id) + assertNotContains(result, item2_id) - assertContains(result, item3_id); - assertContains(result, item4_id); + assertContains(result, item3_id) + assertContains(result, item4_id) - assertNotContains(result, item5_id); + assertNotContains(result, item5_id) - assertPreviousDateTeaser(result, item5_id); - }); - }); + assertPreviousDateTeaser(result, item5_id) + }) + }) - it("should fetch articles of an older day when fills the page and check next page for more", () => { - const config = mockConfig(2); + it('should fetch articles of an older day when fills the page and check next page for more', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-28", "de"), item3("2022-11-28", "de")], + items: [item2('2022-11-28', 'de'), item3('2022-11-28', 'de')], }, { skip: 4, limit: 2, - items: [item4("2022-11-27", "de"), item5("2022-11-27", "de")], + items: [item4('2022-11-27', 'de'), item5('2022-11-27', 'de')], }, ], async () => { const result = await executeArticlesQuery(config)({ - lastDate: "2022-11-29", - }, "de"); + lastDate: '2022-11-29', + }, 'de') - assertNotContains(result, item0_id); - assertNotContains(result, item1_id); + assertNotContains(result, item0_id) + assertNotContains(result, item1_id) - assertContains(result, item2_id); - assertContains(result, item3_id); + assertContains(result, item2_id) + assertContains(result, item3_id) - assertNotContains(result, item4_id); - assertNotContains(result, item5_id); + assertNotContains(result, item4_id) + assertNotContains(result, item5_id) - assertPreviousDateTeaser(result, item4_id); - }); - }); + assertPreviousDateTeaser(result, item4_id) + }) + }) - it("should fetch articles of an older day when distributed over multiple pages", () => { - const config = mockConfig(2); + it('should fetch articles of an older day when distributed over multiple pages', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-28", "de"), item3("2022-11-28", "de")], + items: [item2('2022-11-28', 'de'), item3('2022-11-28', 'de')], }, { skip: 4, limit: 2, - items: [item4("2022-11-28", "de"), item5("2022-11-27", "de")], + items: [item4('2022-11-28', 'de'), item5('2022-11-27', 'de')], }, ], async () => { const result = await executeArticlesQuery(config)({ - lastDate: "2022-11-29", - }, "de"); + lastDate: '2022-11-29', + }, 'de') - assertNotContains(result, item0_id); - assertNotContains(result, item1_id); + assertNotContains(result, item0_id) + assertNotContains(result, item1_id) - assertContains(result, item2_id); - assertContains(result, item3_id); - assertContains(result, item4_id); + assertContains(result, item2_id) + assertContains(result, item3_id) + assertContains(result, item4_id) - assertNotContains(result, item5_id); + assertNotContains(result, item5_id) - assertPreviousDateTeaser(result, item5_id); - }); - }); + assertPreviousDateTeaser(result, item5_id) + }) + }) - it("should deliver articles according to sortKey order", () => { - const config = mockConfig(2); + it('should deliver articles according to sortKey order', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-28", "de", 3), item3("2022-11-28", "de", 2)], + items: [item2('2022-11-28', 'de', 3), item3('2022-11-28', 'de', 2)], }, { skip: 4, limit: 2, - items: [item4("2022-11-28", "de", 1), item5("2022-11-27", "de")], + items: [item4('2022-11-28', 'de', 1), item5('2022-11-27', 'de')], }, ], async () => { const result = await executeArticlesQuery(config)({ - lastDate: "2022-11-29", - }, "de"); + lastDate: '2022-11-29', + }, 'de') assertEquals( result.articles.map((article) => article.id), [item4_id, item3_id, item2_id], - ); - }); - }); + ) + }) + }) - it("should filter out articles with a different language (same language first in response)", () => { - const config = mockConfig(2); + it('should filter out articles with a different language (same language first in response)', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "en"), item3("2022-11-28", "de")], + items: [item2('2022-11-29', 'en'), item3('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); + const result = await executeArticlesQuery(config)({}, 'de') - assertContains(result, item0_id); - assertContains(result, item1_id); + assertContains(result, item0_id) + assertContains(result, item1_id) - assertNotContains(result, item2_id); - assertNotContains(result, item3_id); + assertNotContains(result, item2_id) + assertNotContains(result, item3_id) - assertPreviousDateTeaser(result, item3_id); - }); - }); + assertPreviousDateTeaser(result, item3_id) + }) + }) - it("should filter out articles with a different language (other language first in response)", () => { - const config = mockConfig(2); + it('should filter out articles with a different language (other language first in response)', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "de"), item3("2022-11-29", "en")], + items: [item2('2022-11-29', 'de'), item3('2022-11-29', 'en')], }, { skip: 4, limit: 2, - items: [item4("2022-11-29", "en"), item5("2022-11-28", "en")], + items: [item4('2022-11-29', 'en'), item5('2022-11-28', 'en')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "en"); + const result = await executeArticlesQuery(config)({}, 'en') - assertNotContains(result, item0_id); - assertNotContains(result, item1_id); - assertNotContains(result, item2_id); + assertNotContains(result, item0_id) + assertNotContains(result, item1_id) + assertNotContains(result, item2_id) - assertContains(result, item3_id); - assertContains(result, item4_id); + assertContains(result, item3_id) + assertContains(result, item4_id) - assertNotContains(result, item5_id); + assertNotContains(result, item5_id) - assertPreviousDateTeaser(result, item5_id); - }); - }); + assertPreviousDateTeaser(result, item5_id) + }) + }) - it("includes image url from bildSource", () => { - const config = mockConfig(2); + it('includes image url from bildSource', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "en"), item3("2022-11-28", "de")], + items: [item2('2022-11-29', 'en'), item3('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); - - const article = result.articles.find((article) => - article.id === item0_id - ); - assert(article); - assertEquals(article?.pictureUrl, item0_bildSource); - }); - }); - - it("includes image url from bildManuell (asset resolution)", () => { - const config = mockConfig(2); + const result = await executeArticlesQuery(config)({}, 'de') + + const article = result.articles.find((article) => article.id === item0_id) + assert(article) + assertEquals(article?.pictureUrl, item0_bildSource) + }) + }) + + it('includes image url from bildManuell (asset resolution)', () => { + const config = mockConfig(2) return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "de")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "de"), item3("2022-11-28", "de")], + items: [item2('2022-11-29', 'de'), item3('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); - - const article = result.articles.find((article) => - article.id === item2_id - ); - assert(article); - assertEquals(article?.pictureUrl, item2_bild_url); - }); - }); - - it("filters articles without valid image url", () => { - const config = mockConfig(2); - const itemWithImages = item0("2022-11-29", "de"); + const result = await executeArticlesQuery(config)({}, 'de') + + const article = result.articles.find((article) => article.id === item2_id) + assert(article) + assertEquals(article?.pictureUrl, item2_bild_url) + }) + }) + + it('filters articles without valid image url', () => { + const config = mockConfig(2) + const itemWithImages = item0('2022-11-29', 'de') const itemWithoutImages = { ...itemWithImages, fields: { @@ -448,87 +422,85 @@ describe("goodnews", () => { bildSource: undefined, bildManuell: undefined, }, - }; + } return withMockedResponses(config, [ { skip: 0, limit: 2, - items: [itemWithoutImages, item1("2022-11-29", "de")], + items: [itemWithoutImages, item1('2022-11-29', 'de')], }, { skip: 2, limit: 2, - items: [item2("2022-11-29", "de"), item3("2022-11-28", "de")], + items: [item2('2022-11-29', 'de'), item3('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); + const result = await executeArticlesQuery(config)({}, 'de') // Article with id item0_id should not be part of the result - const articleIds = result.articles.map((article) => article.id); - assertEquals(result.articles.length, 2); - assertEquals(articleIds, [item2_id, item1_id]); - }); - }); - - it("should include all expected data", () => { - const config = mockConfig(1); + const articleIds = result.articles.map((article) => article.id) + assertEquals(result.articles.length, 2) + assertEquals(articleIds, [item2_id, item1_id]) + }) + }) + + it('should include all expected data', () => { + const config = mockConfig(1) return withMockedResponses(config, [ { skip: 0, limit: 1, - items: [item0("2022-11-29", "de", 13)], + items: [item0('2022-11-29', 'de', 13)], }, { skip: 1, limit: 1, - items: [item1("2022-11-28", "de")], + items: [item1('2022-11-28', 'de')], }, ], async () => { - const result = await executeArticlesQuery(config)({}, "de"); - - const article = result.articles.find((article) => - article.id === item0_id - ); - assert(article); - assertEquals(article?.date, "2022-11-29"); - assertEquals(article?.headline, item0_headline); - assertEquals(article?.teaserText, item0_teaserText); - assertEquals(article?.pictureUrl, item0_bildSource); - assertEquals(article?.pictureCredit, item0_photoCredit); - assertEquals(article?.articleUrl, item0_articleUrl); - assertEquals(article?.source, item0_quelle); - assertEquals(article?.sortKey, 13); - assertEquals(article?.category, "CLIMATE"); - assertEquals(article?.language, "de"); - }); - }); -}); - -describe("GoodNews server", () => { - let fetchStub: Stub; - const contentfulConfig = mockConfig(2); + const result = await executeArticlesQuery(config)({}, 'de') + + const article = result.articles.find((article) => article.id === item0_id) + assert(article) + assertEquals(article?.date, '2022-11-29') + assertEquals(article?.headline, item0_headline) + assertEquals(article?.teaserText, item0_teaserText) + assertEquals(article?.pictureUrl, item0_bildSource) + assertEquals(article?.pictureCredit, item0_photoCredit) + assertEquals(article?.articleUrl, item0_articleUrl) + assertEquals(article?.source, item0_quelle) + assertEquals(article?.sortKey, 13) + assertEquals(article?.category, 'CLIMATE') + assertEquals(article?.language, 'de') + }) + }) +}) + +describe('GoodNews server', () => { + let fetchStub: Stub + const contentfulConfig = mockConfig(2) const config: ServerConfig = { cacheEnabled: false, port: 8002, cacheTtlMs: 0, contentful: contentfulConfig, - }; + } const responses = [ { skip: 0, limit: 2, - items: [item0("2022-11-29", "de"), item1("2022-11-29", "en")], + items: [item0('2022-11-29', 'de'), item1('2022-11-29', 'en')], }, { skip: 2, limit: 2, - items: [item2("2022-11-28", "de"), item3("2022-11-28", "en")], + items: [item2('2022-11-28', 'de'), item3('2022-11-28', 'en')], }, - ]; + ] interface TestGoodNewsResponse { - articles: Partial<GoodNewsArticle>[]; - previousDateTeaser: Partial<GoodNewsArticle> | undefined; + articles: Partial<GoodNewsArticle>[] + previousDateTeaser: Partial<GoodNewsArticle> | undefined } const queryArticles = ( graphQLServer: GraphQLServer, @@ -546,57 +518,57 @@ describe("GoodNews server", () => { language } } - }`; + }` return processGqlRequest(graphQLServer, query, {}, headers).then( (result) => result?.articles as TestGoodNewsResponse, - ); - }; + ) + } afterEach(() => { - fetchStub?.restore(); - }); + fetchStub?.restore() + }) - it("correctly extracts language from locale", () => { + it('correctly extracts language from locale', () => { return withMockedResponses(contentfulConfig, responses, async () => { - const graphQLServer = createGraphQLServer(config); + const graphQLServer = createGraphQLServer(config) const result = await queryArticles(graphQLServer, { - "accept-language": "de-DE", - }); + 'accept-language': 'de-DE', + }) assertEquals(result, { articles: [ { id: item0_id, - language: "de", + language: 'de', }, ], previousDateTeaser: { id: item2_id, - language: "de", + language: 'de', }, - }); - }); - }); + }) + }) + }) - it("uses correct language as default if requested language is not supported", () => { + it('uses correct language as default if requested language is not supported', () => { return withMockedResponses(contentfulConfig, responses, async () => { - const graphQLServer = createGraphQLServer(config); + const graphQLServer = createGraphQLServer(config) const result = await queryArticles(graphQLServer, { - "accept-language": "es", - }); + 'accept-language': 'es', + }) assertEquals(result, { articles: [ { id: item1_id, - language: "en", + language: 'en', }, ], previousDateTeaser: { id: item3_id, - language: "en", + language: 'en', }, - }); - }); - }); -}); + }) + }) + }) +}) diff --git a/app/goodnews_test_data.ts b/app/goodnews_test_data.ts index 95adc2bb59a1801b086774bdad938c012573b5fc..c54922de3e296d328fbd2d2b2e4f561b0693dbef 100644 --- a/app/goodnews_test_data.ts +++ b/app/goodnews_test_data.ts @@ -3,867 +3,857 @@ import { ContentfulItem, ContentfulItemBild, ContentfulLanguage, -} from "./contentful_types.ts"; +} from './contentful_types.ts' -export const item0_id = "item0_id"; -export const item0_headline = "Canada increases the price of pollution"; -export const item0_bildSource = - "https://www.ecowatch.com/wp-content/uploads/2022/11/prince-edward-island.jpg"; +export const item0_id = 'item0_id' +export const item0_headline = 'Canada increases the price of pollution' +export const item0_bildSource = 'https://www.ecowatch.com/wp-content/uploads/2022/11/prince-edward-island.jpg' export const item0_teaserText = - "Canada has updated its carbon pollution pricing system introduced in 2019 to ensure major polluters will pay more for their emissions over time. The changes will also increase funding for families participating in the Climate Action Incentive."; -export const item0_photoCredit = - "Image: shaunl / E+ / Getty Images via ecowatch.com"; -export const item0_articleUrl = - "https://www.ecowatch.com/canada-pollution-pricing.html"; -export const item0_quelle = "ECOWATCH"; + 'Canada has updated its carbon pollution pricing system introduced in 2019 to ensure major polluters will pay more for their emissions over time. The changes will also increase funding for families participating in the Climate Action Incentive.' +export const item0_photoCredit = 'Image: shaunl / E+ / Getty Images via ecowatch.com' +export const item0_articleUrl = 'https://www.ecowatch.com/canada-pollution-pricing.html' +export const item0_quelle = 'ECOWATCH' export const item0 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item0_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.542Z", - "updatedAt": "2022-11-29T16:32:03.542Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item0_headline, - "layout": "Article", - "bildSource": item0_bildSource, - "photoCredit": item0_photoCredit, - "teaserText": item0_teaserText, - "quelle": item0_quelle, - "articleUrl": item0_articleUrl, - "sortKey": sortKey || 1, - "language": language, - "categoryID": "climate", - }, -}); + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item0_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.542Z', + 'updatedAt': '2022-11-29T16:32:03.542Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item0_headline, + 'layout': 'Article', + 'bildSource': item0_bildSource, + 'photoCredit': item0_photoCredit, + 'teaserText': item0_teaserText, + 'quelle': item0_quelle, + 'articleUrl': item0_articleUrl, + 'sortKey': sortKey || 1, + 'language': language, + 'categoryID': 'climate', + }, +}) -export const item1_id = "item1_id"; -export const item1_headline = "One man's mission against trash"; +export const item1_id = 'item1_id' +export const item1_headline = "One man's mission against trash" export const item1_bild: ContentfulItemBild = { - "bildSource": - "https://i.guim.co.uk/img/media/3516e35f66c5e1204abeeabc3b6c225f9d153cc8/0_0_6000_4000/master/6000.jpg?width=1010&quality=45&auto=format&fit=max&dpr=2&s=a8301f7352061989679af2a8c24d0784", -}; + 'bildSource': + 'https://i.guim.co.uk/img/media/3516e35f66c5e1204abeeabc3b6c225f9d153cc8/0_0_6000_4000/master/6000.jpg?width=1010&quality=45&auto=format&fit=max&dpr=2&s=a8301f7352061989679af2a8c24d0784', +} export const item1 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item1_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.532Z", - "updatedAt": "2022-11-29T16:32:03.532Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item1_headline, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item1_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.532Z', + 'updatedAt': '2022-11-29T16:32:03.532Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item1_headline, + 'layout': 'Article', ...item1_bild, - "photoCredit": "Image: Plastic Man via theguardian.com", - "teaserText": - "On a beach in Senegal that has so much garbage accumulated on it that most of the sand is covered, one man is trying to raise awareness about the dangers of plastic pollution. Plastic man started his mission on World Environment Day in 2011", - "quelle": "THE GUARDIAN", - "articleUrl": - "https://www.theguardian.com/artanddesign/gallery/2022/nov/21/plastic-man-in-senegal-on-mission-against-trash-in-pictures", - "sortKey": sortKey || 6, - "language": language, - "categoryID": "climate", - }, -}); + 'photoCredit': 'Image: Plastic Man via theguardian.com', + 'teaserText': + 'On a beach in Senegal that has so much garbage accumulated on it that most of the sand is covered, one man is trying to raise awareness about the dangers of plastic pollution. Plastic man started his mission on World Environment Day in 2011', + 'quelle': 'THE GUARDIAN', + 'articleUrl': + 'https://www.theguardian.com/artanddesign/gallery/2022/nov/21/plastic-man-in-senegal-on-mission-against-trash-in-pictures', + 'sortKey': sortKey || 6, + 'language': language, + 'categoryID': 'climate', + }, +}) -export const item2_id = "item2_id"; -export const item2_headline = "Fetal monitoring app for remote areas"; +export const item2_id = 'item2_id' +export const item2_headline = 'Fetal monitoring app for remote areas' export const item2_bild_url = - "https://images.test.com/evvo9u5z5a9q/2GlEQvbXVValp0jkqKZEqh/0271e2aadeb28bb559393bdd5c088b92/Screenshot__37_.png"; + 'https://images.test.com/evvo9u5z5a9q/2GlEQvbXVValp0jkqKZEqh/0271e2aadeb28bb559393bdd5c088b92/Screenshot__37_.png' export const item2_bild: ContentfulItemBild = { - "bildManuell": { - "sys": { - "type": "Link", - "linkType": "Asset", - "id": "2GlEQvbXVValp0jkqKZEqh", + 'bildManuell': { + 'sys': { + 'type': 'Link', + 'linkType': 'Asset', + 'id': '2GlEQvbXVValp0jkqKZEqh', }, }, -}; +} export const item2 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item2_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.518Z", - "updatedAt": "2022-11-29T16:32:03.518Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item2_headline, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item2_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.518Z', + 'updatedAt': '2022-11-29T16:32:03.518Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item2_headline, + 'layout': 'Article', ...item2_bild, - "photoCredit": "Image: screenshot via africanews.com", - "teaserText": - "It is not always easy in rural Africa to get regular pregnancy check-ups, and developing countries often struggle with staff shortages. This simple app-linked device can bring fetal monitoring to wherever it is needed.", - "quelle": "AFRICA NEWS", - "articleUrl": - "https://www.africanews.com/2022/11/24/wekebere-the-fetal-monitoring-app-saving-mothers-and-babies/", - "sortKey": sortKey || 2, - "language": language, - "categoryID": "global_health", - }, -}); + 'photoCredit': 'Image: screenshot via africanews.com', + 'teaserText': + 'It is not always easy in rural Africa to get regular pregnancy check-ups, and developing countries often struggle with staff shortages. This simple app-linked device can bring fetal monitoring to wherever it is needed.', + 'quelle': 'AFRICA NEWS', + 'articleUrl': 'https://www.africanews.com/2022/11/24/wekebere-the-fetal-monitoring-app-saving-mothers-and-babies/', + 'sortKey': sortKey || 2, + 'language': language, + 'categoryID': 'global_health', + }, +}) -export const item3_id = "item3_id"; -export const item3_headline = "Permanent 4-day work week, same pay?"; +export const item3_id = 'item3_id' +export const item3_headline = 'Permanent 4-day work week, same pay?' export const item3_bildSource = - "https://images.unsplash.com/photo-1557425493-6f90ae4659fc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80"; + 'https://images.unsplash.com/photo-1557425493-6f90ae4659fc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80' export const item3 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item3_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.510Z", - "updatedAt": "2022-11-29T16:32:03.510Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item3_headline, - "layout": "Article", - "bildSource": item3_bildSource, - "photoCredit": "Image: Jason Goodman/Unsplash", - "teaserText": - "In what is considered a transformative move in the UK labor market, one hundred British companies have signed up for a permanent four-day working week for thousands of employees without reducing their salary. ", - "quelle": "THE GUARDIAN", - "articleUrl": - "https://www.theguardian.com/business/2022/nov/27/a-hundred-uk-companies-sign-up-for-four-day-week-with-no-loss-of-pay", - "sortKey": sortKey || 3, - "language": language, - "categoryID": "global_health", - }, -}); + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item3_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.510Z', + 'updatedAt': '2022-11-29T16:32:03.510Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item3_headline, + 'layout': 'Article', + 'bildSource': item3_bildSource, + 'photoCredit': 'Image: Jason Goodman/Unsplash', + 'teaserText': + 'In what is considered a transformative move in the UK labor market, one hundred British companies have signed up for a permanent four-day working week for thousands of employees without reducing their salary. ', + 'quelle': 'THE GUARDIAN', + 'articleUrl': + 'https://www.theguardian.com/business/2022/nov/27/a-hundred-uk-companies-sign-up-for-four-day-week-with-no-loss-of-pay', + 'sortKey': sortKey || 3, + 'language': language, + 'categoryID': 'global_health', + }, +}) -export const item4_id = "item4_id"; -export const item4_headline = "New solar panels from panel production dust"; +export const item4_id = 'item4_id' +export const item4_headline = 'New solar panels from panel production dust' export const item4_bild: ContentfulItemBild = { - "bildSource": - "https://scx2.b-cdn.net/gfx/news/hires/2022/new-solar-panels-from.jpg", -}; + 'bildSource': 'https://scx2.b-cdn.net/gfx/news/hires/2022/new-solar-panels-from.jpg', +} export const item4 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item4_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.501Z", - "updatedAt": "2022-11-29T16:32:03.501Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item4_headline, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item4_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.501Z', + 'updatedAt': '2022-11-29T16:32:03.501Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item4_headline, + 'layout': 'Article', ...item4_bild, - "photoCredit": "Image: Thor Nielsen via techxplore.com", - "teaserText": + 'photoCredit': 'Image: Thor Nielsen via techxplore.com', + 'teaserText': 'Almost one-third of the silicon crystals used to make solar panels is lost as sawdust during manifacture. A researcher from the Norwegian University of Science and Technology is using this "black gold" to create new panels.', - "quelle": "TECH XPLORE", - "articleUrl": "https://techxplore.com/news/2022-11-solar-panels-panel.html", - "sortKey": sortKey || 4, - "language": language, - "categoryID": "sustainability", + 'quelle': 'TECH XPLORE', + 'articleUrl': 'https://techxplore.com/news/2022-11-solar-panels-panel.html', + 'sortKey': sortKey || 4, + 'language': language, + 'categoryID': 'sustainability', }, -}); +}) -export const item5_id = "item5_id"; -export const item5_headline = "New tool turns handwriting into code"; +export const item5_id = 'item5_id' +export const item5_headline = 'New tool turns handwriting into code' export const item5_bild: ContentfulItemBild = { - "bildSource": - "https://scx2.b-cdn.net/gfx/news/2022/new-programming-tool-t.jpg", -}; + 'bildSource': 'https://scx2.b-cdn.net/gfx/news/2022/new-programming-tool-t.jpg', +} export const item5 = ( datum: string, language: ContentfulLanguage, sortKey: number | undefined = undefined, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item5_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.493Z", - "updatedAt": "2022-11-29T16:32:03.493Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item5_headline, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item5_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.493Z', + 'updatedAt': '2022-11-29T16:32:03.493Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item5_headline, + 'layout': 'Article', ...item5_bild, - "photoCredit": "Image: via techxplore.com", - "teaserText": - "An innovative pen-based interface that bridges handwritten and textual programming lets users of digital notebooks use drawing canvases or handwritten diagrams to create traditional, digitized computer code.", - "quelle": "TECH XPLORE", - "articleUrl": "https://techxplore.com/news/2022-11-tool-code.html", - "sortKey": sortKey || 5, - "language": language, - "categoryID": "digitization", - }, -}); + 'photoCredit': 'Image: via techxplore.com', + 'teaserText': + 'An innovative pen-based interface that bridges handwritten and textual programming lets users of digital notebooks use drawing canvases or handwritten diagrams to create traditional, digitized computer code.', + 'quelle': 'TECH XPLORE', + 'articleUrl': 'https://techxplore.com/news/2022-11-tool-code.html', + 'sortKey': sortKey || 5, + 'language': language, + 'categoryID': 'digitization', + }, +}) -export const item6_id = "item6_id"; -export const item6_headline = "Support us ✨and help us spread optimism!"; +export const item6_id = 'item6_id' +export const item6_headline = 'Support us ✨and help us spread optimism!' export const item6_bild: ContentfulItemBild = { - "bildManuell": { - "sys": { - "type": "Link", - "linkType": "Asset", - "id": "2ykn7DSH46uUg1TJc51iJ8", + 'bildManuell': { + 'sys': { + 'type': 'Link', + 'linkType': 'Asset', + 'id': '2ykn7DSH46uUg1TJc51iJ8', }, }, -}; +} export const item6 = ( datum: string, language: ContentfulLanguage, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item6_id, - "type": "Entry", - "createdAt": "2022-11-29T16:32:03.465Z", - "updatedAt": "2022-11-29T16:32:03.465Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "donationAd", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item6_headline, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item6_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T16:32:03.465Z', + 'updatedAt': '2022-11-29T16:32:03.465Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'donationAd', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item6_headline, + 'layout': 'Article', ...item6_bild, - "photoCredit": "Image: Chris Lawton/Unsplash", - "teaserText": - "Thank you for all your support! As days are getting shorter we all need as much good news as we can get! If you can, please support our work with a monthly donation, every little bit helps! Spread the word, and share optimism! Thank you 🧡", - "quelle": "GOOD NEWS", - "articleUrl": "https://donorbox.org/good-news-en", - "sortKey": 7, - "language": language, - "categoryID": undefined, - }, -}); + 'photoCredit': 'Image: Chris Lawton/Unsplash', + 'teaserText': + 'Thank you for all your support! As days are getting shorter we all need as much good news as we can get! If you can, please support our work with a monthly donation, every little bit helps! Spread the word, and share optimism! Thank you 🧡', + 'quelle': 'GOOD NEWS', + 'articleUrl': 'https://donorbox.org/good-news-en', + 'sortKey': 7, + 'language': language, + 'categoryID': undefined, + }, +}) -export const item7_id = "item7_id"; -export const item7_headline = "Slowenien stärkt die Pressefreiheit"; +export const item7_id = 'item7_id' +export const item7_headline = 'Slowenien stärkt die Pressefreiheit' export const item7_bildManuell = { - "sys": { - "type": "Link", - "linkType": "Asset", - "id": "4DQZB6GeZutdteJSy07Mnt", + 'sys': { + 'type': 'Link', + 'linkType': 'Asset', + 'id': '4DQZB6GeZutdteJSy07Mnt', }, -}; +} export const item7 = ( datum: string, language: ContentfulLanguage, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item7_id, - "type": "Entry", - "createdAt": "2022-11-29T14:01:27.982Z", - "updatedAt": "2022-11-29T14:01:27.982Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item7_headline, - "layout": "Article", - "bildManuell": item7_bildManuell, - "photoCredit": "Bild: Unsplash / AbsolutVision", - "teaserText": - "Es ist nicht gerade gut bestellt um Sloweniens Pressefreiheit: Laut Reporter ohne Grenzen liegt das Land im internationalen Vergleich auf Platz 54. Nun hat die slowenische Bevölkerung in einem Referendum einem Gesetz zugestimmt, das die Pressefreiheit stärken und Redaktionen unabhängig machen soll von politischer Einflussnahme.", - "quelle": "DEUTSCHLANDFUNK NOVA", - "articleUrl": - "https://www.deutschlandfunknova.de/nachrichten/slowenien-gesetz-fuer-freiere-berichterstattung-kann-kommen", - "sortKey": 1, - "language": language, - "categoryID": "education", - }, -}); + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item7_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T14:01:27.982Z', + 'updatedAt': '2022-11-29T14:01:27.982Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item7_headline, + 'layout': 'Article', + 'bildManuell': item7_bildManuell, + 'photoCredit': 'Bild: Unsplash / AbsolutVision', + 'teaserText': + 'Es ist nicht gerade gut bestellt um Sloweniens Pressefreiheit: Laut Reporter ohne Grenzen liegt das Land im internationalen Vergleich auf Platz 54. Nun hat die slowenische Bevölkerung in einem Referendum einem Gesetz zugestimmt, das die Pressefreiheit stärken und Redaktionen unabhängig machen soll von politischer Einflussnahme.', + 'quelle': 'DEUTSCHLANDFUNK NOVA', + 'articleUrl': + 'https://www.deutschlandfunknova.de/nachrichten/slowenien-gesetz-fuer-freiere-berichterstattung-kann-kommen', + 'sortKey': 1, + 'language': language, + 'categoryID': 'education', + }, +}) -export const item8_id = "item8_id"; -export const item8_title = "Tuvalu gründet digitalen Staat"; +export const item8_id = 'item8_id' +export const item8_title = 'Tuvalu gründet digitalen Staat' export const item8_bild: ContentfulItemBild = { - "bildManuell": { - "sys": { - "type": "Link", - "linkType": "Asset", - "id": "4EQK5W49r7UAZPMG3y15RV", + 'bildManuell': { + 'sys': { + 'type': 'Link', + 'linkType': 'Asset', + 'id': '4EQK5W49r7UAZPMG3y15RV', }, }, -}; +} export const item8 = ( datum: string, language: ContentfulLanguage, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item8_id, - "type": "Entry", - "createdAt": "2022-11-29T14:01:27.967Z", - "updatedAt": "2022-11-29T14:01:27.967Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item8_title, - "layout": "Article", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item8_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T14:01:27.967Z', + 'updatedAt': '2022-11-29T14:01:27.967Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item8_title, + 'layout': 'Article', ...item8_bild, - "photoCredit": "Bild: IMAGO / Nature Picture Library", - "teaserText": - "Wegen des steigendes Meerespiegels versinkt der Inselstaat Tuvalu im Meer – keine gute Nachricht. Um den Staat jedoch funktionsfähig zu halten, will die Regierung das Land nun digitalisieren. So soll es den Bürger:innen künftig etwa möglich sein, im Metaverse Verwaltungs- und Konsulatsdienste in Anspruch zu nehmen.", - "quelle": "DEUTSCHE WELLE", - "articleUrl": - "https://www.dw.com/de/inselstaat-tuvalu-gr%C3%BCndet-erste-digitale-nation/a-63803995", - "sortKey": 3, - "language": language, - "categoryID": "digitization", - }, -}); + 'photoCredit': 'Bild: IMAGO / Nature Picture Library', + 'teaserText': + 'Wegen des steigendes Meerespiegels versinkt der Inselstaat Tuvalu im Meer – keine gute Nachricht. Um den Staat jedoch funktionsfähig zu halten, will die Regierung das Land nun digitalisieren. So soll es den Bürger:innen künftig etwa möglich sein, im Metaverse Verwaltungs- und Konsulatsdienste in Anspruch zu nehmen.', + 'quelle': 'DEUTSCHE WELLE', + 'articleUrl': 'https://www.dw.com/de/inselstaat-tuvalu-gr%C3%BCndet-erste-digitale-nation/a-63803995', + 'sortKey': 3, + 'language': language, + 'categoryID': 'digitization', + }, +}) -export const item9_id = "item9_id"; -export const item9_headline = "Fair Entspannung"; +export const item9_id = 'item9_id' +export const item9_headline = 'Fair Entspannung' export const item9_bild: ContentfulItemBild = { - "bildManuell": { - "sys": { - "type": "Link", - "linkType": "Asset", - "id": "3q1EpSvM7Db1h8nclZdI0u", + 'bildManuell': { + 'sys': { + 'type': 'Link', + 'linkType': 'Asset', + 'id': '3q1EpSvM7Db1h8nclZdI0u', }, }, -}; +} export const item9 = ( datum: string, language: ContentfulLanguage, ): ContentfulItem => ({ - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": item9_id, - "type": "Entry", - "createdAt": "2022-11-29T14:01:27.952Z", - "updatedAt": "2022-11-29T14:01:27.952Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "contentType": { - "sys": { - "type": "Link", - "linkType": "ContentType", - "id": "newsTeaser", - }, - }, - "locale": "de-DE", - }, - "fields": { - "datum": datum, - "headline": item9_headline, - "layout": "Article", - "label": "GOOD FAMILY ANZEIGE", + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': item9_id, + 'type': 'Entry', + 'createdAt': '2022-11-29T14:01:27.952Z', + 'updatedAt': '2022-11-29T14:01:27.952Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'contentType': { + 'sys': { + 'type': 'Link', + 'linkType': 'ContentType', + 'id': 'newsTeaser', + }, + }, + 'locale': 'de-DE', + }, + 'fields': { + 'datum': datum, + 'headline': item9_headline, + 'layout': 'Article', + 'label': 'GOOD FAMILY ANZEIGE', ...item9_bild, - "photoCredit": "Bild: GoodBuy ", - "teaserText": - "Entspanne Körper und Geist durch Akupressur. Die Produkte von Shakti werden von Frauen in Indien hergestellt. Sie profitieren von überdurchschnittlicher Bezahlung, Gesundheitsversorgung und Stipendienprogrammen für ihre Töchter.", - "quelle": "GoodBuy", - "articleUrl": - "https://www.goodbuy.eu/collections/shaktimat?utm_medium=referral&utm_source=goodnewsapp&utm_campaign=goodnews-29.11.2022&utm_content=collection", - "sortKey": 4, - "language": language, - "categoryID": undefined, - }, -}); + 'photoCredit': 'Bild: GoodBuy ', + 'teaserText': + 'Entspanne Körper und Geist durch Akupressur. Die Produkte von Shakti werden von Frauen in Indien hergestellt. Sie profitieren von überdurchschnittlicher Bezahlung, Gesundheitsversorgung und Stipendienprogrammen für ihre Töchter.', + 'quelle': 'GoodBuy', + 'articleUrl': + 'https://www.goodbuy.eu/collections/shaktimat?utm_medium=referral&utm_source=goodnewsapp&utm_campaign=goodnews-29.11.2022&utm_content=collection', + 'sortKey': 4, + 'language': language, + 'categoryID': undefined, + }, +}) const asset_item2 = { - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": "2GlEQvbXVValp0jkqKZEqh", - "type": "Asset", - "createdAt": "2022-11-29T16:27:44.895Z", - "updatedAt": "2022-11-29T16:27:44.895Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "locale": "de-DE", - }, - "fields": { - "title": "Screenshot (37)", - "description": "", - "file": { - "url": - "//images.test.com/evvo9u5z5a9q/2GlEQvbXVValp0jkqKZEqh/0271e2aadeb28bb559393bdd5c088b92/Screenshot__37_.png", - "details": { - "size": 3135141, - "image": { - "width": 2251, - "height": 1594, + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': '2GlEQvbXVValp0jkqKZEqh', + 'type': 'Asset', + 'createdAt': '2022-11-29T16:27:44.895Z', + 'updatedAt': '2022-11-29T16:27:44.895Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'locale': 'de-DE', + }, + 'fields': { + 'title': 'Screenshot (37)', + 'description': '', + 'file': { + 'url': + '//images.test.com/evvo9u5z5a9q/2GlEQvbXVValp0jkqKZEqh/0271e2aadeb28bb559393bdd5c088b92/Screenshot__37_.png', + 'details': { + 'size': 3135141, + 'image': { + 'width': 2251, + 'height': 1594, }, }, - "fileName": "Screenshot__37_.png", - "contentType": "image/png", + 'fileName': 'Screenshot__37_.png', + 'contentType': 'image/png', }, }, -}; +} const asset_item6 = { - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": "2ykn7DSH46uUg1TJc51iJ8", - "type": "Asset", - "createdAt": "2022-11-23T14:30:59.752Z", - "updatedAt": "2022-11-23T14:31:29.863Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 2, - "locale": "de-DE", - }, - "fields": { - "title": "photo-1506903836541-e18b4ba42343", - "description": "", - "file": { - "url": - "//images.test.com/evvo9u5z5a9q/2ykn7DSH46uUg1TJc51iJ8/e5921073d184dfa6ca56555e1a220bc4/ryan-christodoulou-XvLLcF_mt7c-unsplash.jpg", - "details": { - "size": 364507, - "image": { - "width": 1920, - "height": 2880, + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': '2ykn7DSH46uUg1TJc51iJ8', + 'type': 'Asset', + 'createdAt': '2022-11-23T14:30:59.752Z', + 'updatedAt': '2022-11-23T14:31:29.863Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 2, + 'locale': 'de-DE', + }, + 'fields': { + 'title': 'photo-1506903836541-e18b4ba42343', + 'description': '', + 'file': { + 'url': + '//images.test.com/evvo9u5z5a9q/2ykn7DSH46uUg1TJc51iJ8/e5921073d184dfa6ca56555e1a220bc4/ryan-christodoulou-XvLLcF_mt7c-unsplash.jpg', + 'details': { + 'size': 364507, + 'image': { + 'width': 1920, + 'height': 2880, }, }, - "fileName": "ryan-christodoulou-XvLLcF_mt7c-unsplash.jpg", - "contentType": "image/jpeg", + 'fileName': 'ryan-christodoulou-XvLLcF_mt7c-unsplash.jpg', + 'contentType': 'image/jpeg', }, }, -}; +} const asset_item9 = { - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": "3q1EpSvM7Db1h8nclZdI0u", - "type": "Asset", - "createdAt": "2022-11-29T13:49:56.201Z", - "updatedAt": "2022-11-29T13:49:56.201Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "locale": "de-DE", - }, - "fields": { - "title": "24", - "description": "", - "file": { - "url": - "//images.test.com/evvo9u5z5a9q/3q1EpSvM7Db1h8nclZdI0u/a329363446885437794e8be4aad4d288/24.png", - "details": { - "size": 1747534, - "image": { - "width": 1080, - "height": 1080, + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': '3q1EpSvM7Db1h8nclZdI0u', + 'type': 'Asset', + 'createdAt': '2022-11-29T13:49:56.201Z', + 'updatedAt': '2022-11-29T13:49:56.201Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'locale': 'de-DE', + }, + 'fields': { + 'title': '24', + 'description': '', + 'file': { + 'url': '//images.test.com/evvo9u5z5a9q/3q1EpSvM7Db1h8nclZdI0u/a329363446885437794e8be4aad4d288/24.png', + 'details': { + 'size': 1747534, + 'image': { + 'width': 1080, + 'height': 1080, }, }, - "fileName": "24.png", - "contentType": "image/png", + 'fileName': '24.png', + 'contentType': 'image/png', }, }, -}; +} const asset_item7 = { - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": "4DQZB6GeZutdteJSy07Mnt", - "type": "Asset", - "createdAt": "2022-11-29T13:45:55.828Z", - "updatedAt": "2022-11-29T13:45:55.828Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "locale": "de-DE", - }, - "fields": { - "title": "2", - "description": "", - "file": { - "url": - "//images.test.com/evvo9u5z5a9q/4DQZB6GeZutdteJSy07Mnt/33cdadcac7a4458f6edd8f5dd42d9328/2.png", - "details": { - "size": 1341043, - "image": { - "width": 1080, - "height": 1080, + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': '4DQZB6GeZutdteJSy07Mnt', + 'type': 'Asset', + 'createdAt': '2022-11-29T13:45:55.828Z', + 'updatedAt': '2022-11-29T13:45:55.828Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'locale': 'de-DE', + }, + 'fields': { + 'title': '2', + 'description': '', + 'file': { + 'url': '//images.test.com/evvo9u5z5a9q/4DQZB6GeZutdteJSy07Mnt/33cdadcac7a4458f6edd8f5dd42d9328/2.png', + 'details': { + 'size': 1341043, + 'image': { + 'width': 1080, + 'height': 1080, }, }, - "fileName": "2.png", - "contentType": "image/png", + 'fileName': '2.png', + 'contentType': 'image/png', }, }, -}; +} const asset_item8 = { - "metadata": { - "tags": [], - }, - "sys": { - "space": { - "sys": { - "type": "Link", - "linkType": "Space", - "id": "evvo9u5z5a9q", - }, - }, - "id": "4EQK5W49r7UAZPMG3y15RV", - "type": "Asset", - "createdAt": "2022-11-29T13:48:07.295Z", - "updatedAt": "2022-11-29T13:48:07.295Z", - "environment": { - "sys": { - "id": "master", - "type": "Link", - "linkType": "Environment", - }, - }, - "revision": 1, - "locale": "de-DE", - }, - "fields": { - "title": "3", - "description": "", - "file": { - "url": - "//images.test.com/evvo9u5z5a9q/4EQK5W49r7UAZPMG3y15RV/c50b2637a37e769558a42d738281741c/3.png", - "details": { - "size": 1347111, - "image": { - "width": 1080, - "height": 1080, + 'metadata': { + 'tags': [], + }, + 'sys': { + 'space': { + 'sys': { + 'type': 'Link', + 'linkType': 'Space', + 'id': 'evvo9u5z5a9q', + }, + }, + 'id': '4EQK5W49r7UAZPMG3y15RV', + 'type': 'Asset', + 'createdAt': '2022-11-29T13:48:07.295Z', + 'updatedAt': '2022-11-29T13:48:07.295Z', + 'environment': { + 'sys': { + 'id': 'master', + 'type': 'Link', + 'linkType': 'Environment', + }, + }, + 'revision': 1, + 'locale': 'de-DE', + }, + 'fields': { + 'title': '3', + 'description': '', + 'file': { + 'url': '//images.test.com/evvo9u5z5a9q/4EQK5W49r7UAZPMG3y15RV/c50b2637a37e769558a42d738281741c/3.png', + 'details': { + 'size': 1347111, + 'image': { + 'width': 1080, + 'height': 1080, }, }, - "fileName": "3.png", - "contentType": "image/png", + 'fileName': '3.png', + 'contentType': 'image/png', }, }, -}; +} export const contentfulResponse = ( skip: number, limit: number, items: ContentfulItem[], ): ContentfulEntriesResponse => ({ - "sys": { - "type": "Array", - }, - "total": 13999, - "skip": skip, - "limit": limit, - "items": items, - "includes": { - "Asset": [ + 'sys': { + 'type': 'Array', + }, + 'total': 13999, + 'skip': skip, + 'limit': limit, + 'items': items, + 'includes': { + 'Asset': [ asset_item2, asset_item6, asset_item9, @@ -871,4 +861,4 @@ export const contentfulResponse = ( asset_item8, ], }, -}); +}) diff --git a/app/logging.ts b/app/logging.ts index dcd266055c37924d48e605d73952e821f0386a28..dbd14d224c7e6bfb584f1c659b39297532979e27 100644 --- a/app/logging.ts +++ b/app/logging.ts @@ -1,14 +1,14 @@ // Google Cloud LogSeverity levels https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity export enum LogSeverity { - DEFAULT = "DEFAULT", - DEBUG = "DEBUG", - INFO = "INFO", - NOTICE = "NOTICE", - WARNING = "WARNING", - ERROR = "ERROR", - CRITICAL = "CRITICAL", - ALERT = "ALERT", - EMERGENCY = "EMERGENCY", + DEFAULT = 'DEFAULT', + DEBUG = 'DEBUG', + INFO = 'INFO', + NOTICE = 'NOTICE', + WARNING = 'WARNING', + ERROR = 'ERROR', + CRITICAL = 'CRITICAL', + ALERT = 'ALERT', + EMERGENCY = 'EMERGENCY', } const sortedLevels = [ @@ -21,20 +21,20 @@ const sortedLevels = [ LogSeverity.CRITICAL, LogSeverity.ALERT, LogSeverity.EMERGENCY, -]; +] /** * The Logger class requires a setUpLogger method to be run during the app's initialization in order get the current environment. */ class Logger { - environment = "development"; + environment = 'development' // minimum level to log - level = LogSeverity.DEFAULT; + level = LogSeverity.DEFAULT setUpLogger(environment: string, level: LogSeverity) { - this.environment = environment; - this.level = level; + this.environment = environment + this.level = level } /** @@ -46,50 +46,48 @@ class Logger { * In other environments it should follow Google Cloud's Structured Logging formatting: https://cloud.google.com/logging/docs/structured-logging */ log(severity: LogSeverity, message: string, options = {}) { - if (!shouldLogLevel(this.level, severity)) return; + if (!shouldLogLevel(this.level, severity)) return - const date = new Date().toISOString(); - const input = this.environment === "development" - ? `${date} ${severity} ${message}` - : JSON.stringify({ - environment: this.environment, - severity, - message, - ...options, - }); + const date = new Date().toISOString() + const input = this.environment === 'development' ? `${date} ${severity} ${message}` : JSON.stringify({ + environment: this.environment, + severity, + message, + ...options, + }) switch (severity) { case LogSeverity.DEFAULT: - console.log(input); - break; + console.log(input) + break case LogSeverity.DEBUG: - console.debug(input); - break; + console.debug(input) + break case LogSeverity.INFO: case LogSeverity.NOTICE: - console.info(input); - break; + console.info(input) + break case LogSeverity.WARNING: - console.warn(input); - break; + console.warn(input) + break case LogSeverity.ERROR: case LogSeverity.CRITICAL: case LogSeverity.ALERT: case LogSeverity.EMERGENCY: - console.error(input); + console.error(input) } } debug(message: string, options = {}) { - this.log(LogSeverity.DEBUG, message, options); + this.log(LogSeverity.DEBUG, message, options) } info(message: string, options = {}) { - this.log(LogSeverity.INFO, message, options); + this.log(LogSeverity.INFO, message, options) } error(message: string, options = {}) { - this.log(LogSeverity.ERROR, message, options); + this.log(LogSeverity.ERROR, message, options) } } @@ -99,9 +97,9 @@ class Logger { * @returns Boolean, true if the current level is accepted and false if it is rejected by the minLevel */ const shouldLogLevel = (minLevel: LogSeverity, level: LogSeverity) => { - const minLevelIndex = sortedLevels.indexOf(minLevel); - const currentLevelIndex = sortedLevels.indexOf(level); - return currentLevelIndex >= minLevelIndex; -}; + const minLevelIndex = sortedLevels.indexOf(minLevel) + const currentLevelIndex = sortedLevels.indexOf(level) + return currentLevelIndex >= minLevelIndex +} -export const logger = new Logger(); +export const logger = new Logger() diff --git a/app/main.ts b/app/main.ts index 1e1fb1f0ebcbd8bc2fc9d9fe52d758636ea70f35..2768f6c2ee65861232a40fa5c237e4977a2dfede 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,4 +1,4 @@ -import { logger, LogSeverity } from "./logging.ts"; +import { logger, LogSeverity } from './logging.ts' import { DEFAULT_CACHE_ENABLED, DEFAULT_CACHE_TTL_MS, @@ -7,70 +7,70 @@ import { DEFAULT_PORT, ServerConfig, startServer, -} from "./server.ts"; +} from './server.ts' -const environment = Deno.env.get("ENVIRONMENT") || "development"; +const environment = Deno.env.get('ENVIRONMENT') || 'development' logger.setUpLogger( environment, - environment === "development" ? LogSeverity.DEFAULT : LogSeverity.INFO, -); + environment === 'development' ? LogSeverity.DEFAULT : LogSeverity.INFO, +) const requiredEnv = <T>( name: string, typeFn: (s: string) => T, fallback?: T, ): T => { - const env = Deno.env.get(name); + const env = Deno.env.get(name) if (env === undefined && fallback === undefined) { - throw Error(`Environment variable "${name}" is required`); + throw Error(`Environment variable "${name}" is required`) } else { - return env !== undefined ? typeFn(env) : fallback!; + return env !== undefined ? typeFn(env) : fallback! } -}; +} -const asBoolean = (str: string) => /^true$/i.test(str); +const asBoolean = (str: string) => /^true$/i.test(str) const serverConfigFromEnv = (): ServerConfig => { - const fake = requiredEnv("FAKE", asBoolean, false); // For local development. If set, the API returns dummy data + const fake = requiredEnv('FAKE', asBoolean, false) // For local development. If set, the API returns dummy data return { - port: requiredEnv("PORT", Number, DEFAULT_PORT), + port: requiredEnv('PORT', Number, DEFAULT_PORT), cacheEnabled: requiredEnv( - "CACHE_ENABLED", + 'CACHE_ENABLED', asBoolean, DEFAULT_CACHE_ENABLED, ), cacheTtlMs: requiredEnv( - "CACHE_TTL_MS", + 'CACHE_TTL_MS', Number, DEFAULT_CACHE_TTL_MS, ), contentful: { baseUrl: requiredEnv( - "CONTENTFUL_BASE_URL", + 'CONTENTFUL_BASE_URL', String, - fake ? "dummy value" : undefined, + fake ? 'dummy value' : undefined, ), spaceId: requiredEnv( - "CONTENTFUL_SPACE_ID", + 'CONTENTFUL_SPACE_ID', String, - fake ? "dummy value" : undefined, + fake ? 'dummy value' : undefined, ), deliveryApiAccessToken: requiredEnv( - "CONTENTFUL_DELIVERY_API_ACCESS_TOKEN", + 'CONTENTFUL_DELIVERY_API_ACCESS_TOKEN', String, - fake ? "dummy value" : undefined, + fake ? 'dummy value' : undefined, ), environment: requiredEnv( - "CONTENTFUL_ENVIRONMENT", + 'CONTENTFUL_ENVIRONMENT', String, DEFAULT_CONTENTFUL_ENVIRONMENT, ), pageSize: DEFAULT_PAGE_SIZE, fake, }, - }; -}; + } +} -const config = serverConfigFromEnv(); -await startServer(config); +const config = serverConfigFromEnv() +await startServer(config) diff --git a/app/server.ts b/app/server.ts index 600b36950a140b7e465b4c77a7d614b028d138a0..fdc5ea791996113969121c802c95974915c2cfb8 100644 --- a/app/server.ts +++ b/app/server.ts @@ -1,17 +1,7 @@ -import { GoodNewsArticleQueryResponse, GoodNewsLanguage } from "./api_types.ts"; -import { - createSchema, - createYoga, - gql, - serve, - useResponseCache, -} from "./deps.ts"; -import { - DEFAULT_LANGUAGE, - executeArticlesQuery, - SUPPORTED_LANGUAGES, -} from "./goodnews.ts"; -import { logger } from "./logging.ts"; +import { GoodNewsArticleQueryResponse, GoodNewsLanguage } from './api_types.ts' +import { createSchema, createYoga, gql, serve, useResponseCache } from './deps.ts' +import { DEFAULT_LANGUAGE, executeArticlesQuery, SUPPORTED_LANGUAGES } from './goodnews.ts' +import { logger } from './logging.ts' const typeDefs = gql` enum Category { @@ -48,7 +38,7 @@ const typeDefs = gql` # returns the articles of the day, starting *after* lastDate (if given) or today (if not given) articles(lastDate: String): GoodNewsArticlesQueryResponse } -`; +` const createResolvers = (config: ContentfulConfig) => { return ({ @@ -64,44 +54,44 @@ const createResolvers = (config: ContentfulConfig) => { : executeArticlesQuery(config)( parameters, getLanguage(context?.request?.headers), - ); + ) }, }, - }); -}; + }) +} const getLanguage = (headers?: Headers) => { - const languages: string = headers?.get("accept-language") || DEFAULT_LANGUAGE; + const languages: string = headers?.get('accept-language') || DEFAULT_LANGUAGE return (languages - .split(",") // languages are comma separated - .map((language) => language.split("-")[0]) // languages may include country code 'de-DE' e.g. - .map((language) => language.split(";")[0]) // languages may include a 'de;q=0.6' e.g. + .split(',') // languages are comma separated + .map((language) => language.split('-')[0]) // languages may include country code 'de-DE' e.g. + .map((language) => language.split(';')[0]) // languages may include a 'de;q=0.6' e.g. .map((language) => language.trim()) .find((language) => SUPPORTED_LANGUAGES.includes(language)) || - DEFAULT_LANGUAGE) as GoodNewsLanguage; -}; + DEFAULT_LANGUAGE) as GoodNewsLanguage +} -export const DEFAULT_PORT = 8002; -export const DEFAULT_CACHE_ENABLED = true; -export const DEFAULT_CACHE_TTL_MS = 60_000; -export const DEFAULT_PAGE_SIZE = 100; +export const DEFAULT_PORT = 8002 +export const DEFAULT_CACHE_ENABLED = true +export const DEFAULT_CACHE_TTL_MS = 60_000 +export const DEFAULT_PAGE_SIZE = 100 -export const DEFAULT_CONTENTFUL_ENVIRONMENT = "master"; +export const DEFAULT_CONTENTFUL_ENVIRONMENT = 'master' export interface ContentfulConfig { - baseUrl: string; - spaceId: string; - environment: string; // default: master - deliveryApiAccessToken: string; - pageSize: number; - fake: boolean; // For local development. If set, the API returns dummy data + baseUrl: string + spaceId: string + environment: string // default: master + deliveryApiAccessToken: string + pageSize: number + fake: boolean // For local development. If set, the API returns dummy data } export interface ServerConfig { - port: number; // default: 8002 - cacheEnabled: boolean; // default: true - cacheTtlMs: number; // default: 60 seconds - contentful: ContentfulConfig; + port: number // default: 8002 + cacheEnabled: boolean // default: true + cacheTtlMs: number // default: 60 seconds + contentful: ContentfulConfig } export const createGraphQLServer = (config: ServerConfig): GraphQLServer => { @@ -111,48 +101,46 @@ export const createGraphQLServer = (config: ServerConfig): GraphQLServer => { // global cache per language, shared by all users session: (request: Request) => getLanguage(request.headers), ttlPerSchemaCoordinate: { - "Query.articles": config.cacheTtlMs || DEFAULT_CACHE_TTL_MS, + 'Query.articles': config.cacheTtlMs || DEFAULT_CACHE_TTL_MS, }, }), ] - : []; - const resolvers = createResolvers(config.contentful); + : [] + const resolvers = createResolvers(config.contentful) return createYoga({ schema: createSchema({ resolvers, typeDefs }), graphiql: true, plugins, - }); -}; + }) +} // deno-lint-ignore no-explicit-any -export type GraphQLServer = any; +export type GraphQLServer = any export type GraphQLContext = { - request?: { headers: Headers }; + request?: { headers: Headers } params?: { extensions?: { headers?: { - [key: string]: string; - }; - }; - }; -}; + [key: string]: string + } + } + } +} export const startServer = (config: ServerConfig): Promise<void> => { - const graphQLServer: GraphQLServer = createGraphQLServer(config); + const graphQLServer: GraphQLServer = createGraphQLServer(config) return serve(graphQLServer, { port: config.port || DEFAULT_PORT, onListen({ port, hostname }) { logger.info( - `Server started at http://${ - hostname === "0.0.0.0" ? "localhost" : hostname - }:${port}/graphql`, - ); + `Server started at http://${hostname === '0.0.0.0' ? 'localhost' : hostname}:${port}/graphql`, + ) if (config.contentful.fake) { logger.info( `Server is serving fake data due to FAKE env var set to true`, - ); + ) } }, - }); -}; + }) +} diff --git a/deno.json b/deno.json index c8261fc9565924a5e09d8d2abec89b5c3b7830b0..e78627f679fcd645637ad4e81c0d799b26d68eb0 100644 --- a/deno.json +++ b/deno.json @@ -19,6 +19,11 @@ } }, "fmt": { - "exclude": ["*.md"] + "lineWidth": 120, + "singleQuote": true, + "semiColons": false, + "exclude": [ + "*.md" + ] } }