diff --git a/README.md b/README.md index d5ac2eeded1327acfc412bd861b1cf94fb0c1301..6fc2cdb80127378434c765f45b2ff356b9e2bd09 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ if "you know what you're doing". | PORT | 8002 | the port to listen on | | CACHE_ENABLED | true | wether or not to enable caching | | CACHE_TTL_MS | 60 seconds | time-to-live in ms | +| CONTENTFUL_BASE_URL | undefined | Contentful Base Url - defines the base url of the Contentful API | | CONTENTFUL_SPACE_ID | undefined | Contentful Space ID - identifies the GoodNews account on Contentful | | CONTENTFUL_DELIVERY_API_ACCESS_TOKEN | undefined | Contentful Delivery API Access Token - needed to access the articles from the Contentful API | | CONTENTFUL_ENVIRONMENT | master | Contentful Environment - currently always "master" | diff --git a/app/goodnews.ts b/app/goodnews.ts index aa2d80e92433332ee759058bde4a52351ea10c78..d83dd0edaa18f39f57183d46eed45ca2c00517a6 100644 --- a/app/goodnews.ts +++ b/app/goodnews.ts @@ -3,7 +3,7 @@ import { GoodNewsArticleQueryResponse, GoodNewsCategory, } from "./api_types.ts"; -import { ContentfulConfig, GraphQLContext } from "./server.ts"; +import { ContentfulConfig } from "./server.ts"; import { ContentfulAsset, ContentfulEntriesResponse, @@ -136,12 +136,9 @@ const sortKeyComparator = (a: ContentfulItem, b: ContentfulItem) => export const executeArticlesQuery = (config: ContentfulConfig) => async ( - // deno-lint-ignore no-explicit-any - _parent: any, parameters: { lastDate?: string }, - context: GraphQLContext, + validatedLocale: ContentfulLanguage, ): Promise<GoodNewsArticleQueryResponse> => { - const validatedLocale = context.language as ContentfulLanguage; let responseDate: string | undefined; const responseItems: ContentfulItem[] = []; const responseAssets: ContentfulAsset[] = []; @@ -166,7 +163,6 @@ async ( ? responseItems[0].fields.datum : undefined; } - if (responseDate && responseItems.find(olderThan(responseDate!))) { fetchMore = false; } diff --git a/app/goodnews_test.ts b/app/goodnews_test.ts index dfcac17cb0190ba84049f47c03102433f394d397..c1d8bcb4adce937a3441c43b9c9fdfc30c9efe3d 100644 --- a/app/goodnews_test.ts +++ b/app/goodnews_test.ts @@ -49,6 +49,7 @@ const mockConfig = (pageSize: number): ContentfulConfig => ({ environment: "master", deliveryApiAccessToken: "testAccessToken", pageSize, + fake: false, }); type MockPage = { @@ -148,9 +149,7 @@ describe("goodnews", () => { ], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); assertContains(result, item0_id); assertContains(result, item1_id); @@ -178,9 +177,7 @@ describe("goodnews", () => { items: [item2("2022-11-29", "de"), item3("2022-11-28", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); assertContains(result, item0_id); assertContains(result, item1_id); @@ -214,9 +211,9 @@ describe("goodnews", () => { ], }, ], async () => { - const result = await executeArticlesQuery(config)(null, { + const result = await executeArticlesQuery(config)({ lastDate: "2022-11-29", - }, { language: "de" }); + }, "de"); assertNotContains(result, item0_id); assertNotContains(result, item1_id); @@ -250,9 +247,9 @@ describe("goodnews", () => { items: [item4("2022-11-27", "de"), item5("2022-11-27", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, { + const result = await executeArticlesQuery(config)({ lastDate: "2022-11-29", - }, { language: "de" }); + }, "de"); assertNotContains(result, item0_id); assertNotContains(result, item1_id); @@ -286,9 +283,9 @@ describe("goodnews", () => { items: [item4("2022-11-28", "de"), item5("2022-11-27", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, { + const result = await executeArticlesQuery(config)({ lastDate: "2022-11-29", - }, { language: "de" }); + }, "de"); assertNotContains(result, item0_id); assertNotContains(result, item1_id); @@ -322,9 +319,9 @@ describe("goodnews", () => { items: [item4("2022-11-28", "de", 1), item5("2022-11-27", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, { + const result = await executeArticlesQuery(config)({ lastDate: "2022-11-29", - }, { language: "de" }); + }, "de"); assertEquals( result.articles.map((article) => article.id), @@ -347,9 +344,7 @@ describe("goodnews", () => { items: [item2("2022-11-29", "en"), item3("2022-11-28", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); assertContains(result, item0_id); assertContains(result, item1_id); @@ -380,9 +375,7 @@ describe("goodnews", () => { items: [item4("2022-11-29", "en"), item5("2022-11-28", "en")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "en", - }); + const result = await executeArticlesQuery(config)({}, "en"); assertNotContains(result, item0_id); assertNotContains(result, item1_id); @@ -411,9 +404,7 @@ describe("goodnews", () => { items: [item2("2022-11-29", "en"), item3("2022-11-28", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); const article = result.articles.find((article) => article.id === item0_id @@ -437,9 +428,7 @@ describe("goodnews", () => { items: [item2("2022-11-29", "de"), item3("2022-11-28", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); const article = result.articles.find((article) => article.id === item2_id @@ -463,9 +452,7 @@ describe("goodnews", () => { items: [item1("2022-11-28", "de")], }, ], async () => { - const result = await executeArticlesQuery(config)(null, {}, { - language: "de", - }); + const result = await executeArticlesQuery(config)({}, "de"); const article = result.articles.find((article) => article.id === item0_id diff --git a/app/main.ts b/app/main.ts index a62c22938b56b6f40a5877069dfdc537cc18b778..1e1fb1f0ebcbd8bc2fc9d9fe52d758636ea70f35 100644 --- a/app/main.ts +++ b/app/main.ts @@ -16,45 +16,58 @@ logger.setUpLogger( environment === "development" ? LogSeverity.DEFAULT : LogSeverity.INFO, ); +const requiredEnv = <T>( + name: string, + typeFn: (s: string) => T, + fallback?: T, +): T => { + const env = Deno.env.get(name); + if (env === undefined && fallback === undefined) { + throw Error(`Environment variable "${name}" is required`); + } else { + return env !== undefined ? typeFn(env) : fallback!; + } +}; + +const asBoolean = (str: string) => /^true$/i.test(str); + const serverConfigFromEnv = (): ServerConfig => { - const required = <T>(name: string, t?: T): T => { - if (!t) { - throw Error(`Environment variable "${name}" is required`); - } else { - return t!; - } - }; - const asNumber = (str?: string) => (str ? Number(str) : undefined); - const asBoolean = (str?: string) => (str ? Boolean(str) : undefined); + const fake = requiredEnv("FAKE", asBoolean, false); // For local development. If set, the API returns dummy data return { - port: required("PORT", asNumber(Deno.env.get("PORT")) || DEFAULT_PORT), - cacheEnabled: required( + port: requiredEnv("PORT", Number, DEFAULT_PORT), + cacheEnabled: requiredEnv( "CACHE_ENABLED", - asBoolean(Deno.env.get("CACHE_ENABLED")) || DEFAULT_CACHE_ENABLED, + asBoolean, + DEFAULT_CACHE_ENABLED, ), - cacheTtlMs: required( + cacheTtlMs: requiredEnv( "CACHE_TTL_MS", - asNumber(Deno.env.get("CACHE_TTL_MS")) || DEFAULT_CACHE_TTL_MS, + Number, + DEFAULT_CACHE_TTL_MS, ), contentful: { - baseUrl: required( + baseUrl: requiredEnv( "CONTENTFUL_BASE_URL", - Deno.env.get("CONTENTFUL_BASE_URL"), + String, + fake ? "dummy value" : undefined, ), - spaceId: required( + spaceId: requiredEnv( "CONTENTFUL_SPACE_ID", - Deno.env.get("CONTENTFUL_SPACE_ID"), + String, + fake ? "dummy value" : undefined, ), - deliveryApiAccessToken: required( + deliveryApiAccessToken: requiredEnv( "CONTENTFUL_DELIVERY_API_ACCESS_TOKEN", - Deno.env.get("CONTENTFUL_DELIVERY_API_ACCESS_TOKEN"), + String, + fake ? "dummy value" : undefined, ), - environment: required( + environment: requiredEnv( "CONTENTFUL_ENVIRONMENT", - Deno.env.get("CONTENTFUL_ENVIRONMENT") || - DEFAULT_CONTENTFUL_ENVIRONMENT, + String, + DEFAULT_CONTENTFUL_ENVIRONMENT, ), pageSize: DEFAULT_PAGE_SIZE, + fake, }, }; }; diff --git a/app/server.ts b/app/server.ts index a71b6c04724a37aa48ce9c257fdd9773d815d936..e8020be379a014c7a491768caffc7d96b60517ed 100644 --- a/app/server.ts +++ b/app/server.ts @@ -1,4 +1,4 @@ -import { GoodNewsLanguage } from "./api_types.ts"; +import { GoodNewsArticleQueryResponse, GoodNewsLanguage } from "./api_types.ts"; import { createSchema, createYoga, @@ -50,11 +50,22 @@ const typeDefs = gql` } `; -const createResolvers = (config: ContentfulConfig) => ({ - Query: { - articles: executeArticlesQuery(config), - }, -}); +const createResolvers = (config: ContentfulConfig) => { + return ({ + Query: { + articles: ( + // deno-lint-ignore no-explicit-any + _parent: any, + parameters: { lastDate?: string }, + context: GraphQLContext, + ): Promise<GoodNewsArticleQueryResponse> => { + return config.fake + ? Promise.resolve({ articles: [], previousDateTeaser: undefined }) + : executeArticlesQuery(config)(parameters, context.language); + }, + }, + }); +}; const getLanguage = (languages = "") => languages @@ -76,6 +87,7 @@ export interface ContentfulConfig { environment: string; // default: master deliveryApiAccessToken: string; pageSize: number; + fake: boolean; // For local development. If set, the API returns dummy data } export interface ServerConfig { @@ -141,6 +153,11 @@ export const startServer = (config: ServerConfig): Promise<void> => { 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`, + ); + } }, }); };