diff --git a/app/api_types.ts b/app/api_types.ts index d66756788cee4a172a605a9eb3f0de1ea4191159..6e7f0c141d625dd7fb6fc39b119cc250ea186d37 100644 --- a/app/api_types.ts +++ b/app/api_types.ts @@ -1,12 +1,12 @@ export type ApiTranslationReslut = { detectedLanguage: { - confidence: number; - language: string; - }; - translatedText: string; -}; + confidence: number + language: string + } + translatedText: string +} export type ApiLanguageDetectionReslut = Array<{ - confidence: number; - language: string; -}>; + confidence: number + language: string +}> diff --git a/app/deps.ts b/app/deps.ts index 7b042e380f650a7ae207e37dbad7d10dcea7ae8e..a48a348b4d8d1af3cbb0c7bc8b4d39057eec3360 100644 --- a/app/deps.ts +++ b/app/deps.ts @@ -1,6 +1,6 @@ -export { serve } from "https://deno.land/std@0.156.0/http/server.ts"; -export { createSchema, createYoga } from "npm:graphql-yoga@5.0.1"; -export { useResponseCache } from "npm:@graphql-yoga/plugin-response-cache@1.0.0"; -export { GraphQLError } from "npm:graphql@16.8.1"; -export { Parser } from "https://deno.land/x/html_parser@v0.1.3/src/mod.ts"; -export { createHash } from "https://deno.land/std@0.129.0/hash/mod.ts"; +export { serve } from 'https://deno.land/std@0.156.0/http/server.ts' +export { createSchema, createYoga } from 'npm:graphql-yoga@5.0.1' +export { useResponseCache } from 'npm:@graphql-yoga/plugin-response-cache@1.0.0' +export { GraphQLError } from 'npm:graphql@16.8.1' +export { Parser } from 'https://deno.land/x/html_parser@v0.1.3/src/mod.ts' +export { createHash } from 'https://deno.land/std@0.129.0/hash/mod.ts' diff --git a/app/dev_deps.ts b/app/dev_deps.ts index c3f570c4b1e7d7b0cabfc39a70bd95db3ea55cd9..cd82e58119121edb6283f04dad8164f79317c27a 100644 --- a/app/dev_deps.ts +++ b/app/dev_deps.ts @@ -1,18 +1,5 @@ -export { - assertSpyCall, - returnsNext, - 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"; -export { - assertEquals, - assertExists, - assertRejects, -} from "https://deno.land/std@0.155.0/testing/asserts.ts"; -export { - afterEach, - beforeEach, - describe, - it, -} from "https://deno.land/std@0.155.0/testing/bdd.ts"; -export { FakeTime } from "https://deno.land/std@0.198.0/testing/time.ts"; +export { assertSpyCall, returnsNext, 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' +export { assertEquals, assertExists, assertRejects } from 'https://deno.land/std@0.155.0/testing/asserts.ts' +export { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.155.0/testing/bdd.ts' +export { FakeTime } from 'https://deno.land/std@0.198.0/testing/time.ts' diff --git a/app/libretranslate.ts b/app/libretranslate.ts index 870791520ac1a44f70094b2d4e1567a275de3b69..de5bc68bc3072286160220a494847b5f4d72323c 100644 --- a/app/libretranslate.ts +++ b/app/libretranslate.ts @@ -1,71 +1,64 @@ -import { - DetectLanguageQueryParameters, - TranslationFormat, - TranslationQueryParameters, -} from "./types.ts"; -import { - ApiLanguageDetectionReslut, - ApiTranslationReslut, -} from "./api_types.ts"; -import { logger } from "./logging.ts"; -import { client } from "./redis.ts"; -import { createHash, GraphQLError } from "./deps.ts"; +import { DetectLanguageQueryParameters, TranslationFormat, TranslationQueryParameters } from './types.ts' +import { ApiLanguageDetectionReslut, ApiTranslationReslut } from './api_types.ts' +import { logger } from './logging.ts' +import { client } from './redis.ts' +import { createHash, GraphQLError } from './deps.ts' -const BASE_URL = Deno.env.get("HOLI_LIBRETRANSLATE_BASE_URL"); +const BASE_URL = Deno.env.get('HOLI_LIBRETRANSLATE_BASE_URL') if (!BASE_URL) { throw new Error( - "HOLI_LIBRETRANSLATE_BASE_URL environment variable is not set", - ); + 'HOLI_LIBRETRANSLATE_BASE_URL environment variable is not set', + ) } const transformTranslationResult = ( translation: ApiTranslationReslut, -): string => translation.translatedText; +): string => translation.translatedText const transformLanguageDetectionResult = ( detections: ApiLanguageDetectionReslut, ): string => { - return detections.sort((a, b) => b.confidence - a.confidence)[0].language; -}; + return detections.sort((a, b) => b.confidence - a.confidence)[0].language +} export const translate = async ( query: TranslationQueryParameters, ): Promise<string> => { - const cacheKey = createHash("sha256").update( + const cacheKey = createHash('sha256').update( `translate:${query.text}:${query.targetLanguage}`, - ).toString(); + ).toString() - const cachedResult = await client.get(cacheKey); + const cachedResult = await client.get(cacheKey) if (cachedResult) { - logger.debug(`cache hit: ${JSON.stringify({ key: cacheKey })}`); - return cachedResult; + logger.debug(`cache hit: ${JSON.stringify({ key: cacheKey })}`) + return cachedResult } - const url = `${BASE_URL}/translate`; - logger.info(`Starting translation...`); - const start = Date.now(); + const url = `${BASE_URL}/translate` + logger.info(`Starting translation...`) + const start = Date.now() try { const response = await fetch(url, { - method: "POST", + method: 'POST', body: JSON.stringify({ q: query.text, - source: "auto", + source: 'auto', target: query.targetLanguage, format: TranslationFormat.TEXT, }), - headers: { "Content-Type": "application/json" }, - }); + headers: { 'Content-Type': 'application/json' }, + }) if (response.status >= 400) { - throw new GraphQLError("Translation failed"); + throw new GraphQLError('Translation failed') } - const json = await response.json(); - const result = transformTranslationResult(json); + const json = await response.json() + const result = transformTranslationResult(json) - await client.set(cacheKey, result); + await client.set(cacheKey, result) - return result; + return result } catch (e) { logger.error( `Error performing request to ${url}: ${ @@ -74,33 +67,33 @@ export const translate = async ( cause: e.cause, }) }`, - ); - throw e; + ) + throw e } finally { - const duration = Date.now() - start; - logger.debug(`Translation took ${duration} ms`); + const duration = Date.now() - start + logger.debug(`Translation took ${duration} ms`) } -}; +} export const detectLanguage = async ( query: DetectLanguageQueryParameters, ): Promise<string> => { - const url = `${BASE_URL}/detect`; - logger.info(`Starting language detection...`); - const start = Date.now(); + const url = `${BASE_URL}/detect` + logger.info(`Starting language detection...`) + const start = Date.now() try { const response = await fetch(url, { - method: "POST", + method: 'POST', body: JSON.stringify({ q: query.text, }), - headers: { "Content-Type": "application/json" }, - }); + headers: { 'Content-Type': 'application/json' }, + }) if (response.status >= 400) { - throw new GraphQLError("Language detection failed"); + throw new GraphQLError('Language detection failed') } - const json = await response.json(); - return transformLanguageDetectionResult(json); + const json = await response.json() + return transformLanguageDetectionResult(json) } catch (e) { logger.error( `Error performing request to ${url}: ${ @@ -109,10 +102,10 @@ export const detectLanguage = async ( cause: e.cause, }) }`, - ); - throw e; + ) + throw e } finally { - const duration = Date.now() - start; - logger.debug(`Language detection took ${duration} ms`); + const duration = Date.now() - start + logger.debug(`Language detection took ${duration} ms`) } -}; +} 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 50519187308c8777cf8860b889ef46e48a0ff21e..77d629c18d3a00c42e55e89237f4731b4b60011f 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,48 +1,43 @@ -import { logger, LogSeverity } from "./logging.ts"; -import { - DEFAULT_CACHE_ENABLED, - DEFAULT_CACHE_TTL_MS_LIBRE_TRANSLATE, - DEFAULT_PORT, - startServer, -} from "./server.ts"; +import { logger, LogSeverity } from './logging.ts' +import { DEFAULT_CACHE_ENABLED, DEFAULT_CACHE_TTL_MS_LIBRE_TRANSLATE, DEFAULT_PORT, startServer } 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 = () => { return { - port: requiredEnv("PORT", Number, DEFAULT_PORT), + port: requiredEnv('PORT', Number, DEFAULT_PORT), cacheEnabled: requiredEnv( - "CACHE_ENABLED", + 'CACHE_ENABLED', asBoolean, DEFAULT_CACHE_ENABLED, ), cacheTtlMsLibreTranslate: requiredEnv( - "CACHE_TTL_MS_LIBRE_TRANSLATE", + 'CACHE_TTL_MS_LIBRE_TRANSLATE', Number, DEFAULT_CACHE_TTL_MS_LIBRE_TRANSLATE, ), - fake: requiredEnv("FAKE", asBoolean, false), // For local development. If set, the API returns dummy data - }; -}; + fake: requiredEnv('FAKE', asBoolean, false), // For local development. If set, the API returns dummy data + } +} -await startServer(serverConfigFromEnv()); +await startServer(serverConfigFromEnv()) diff --git a/app/redis.ts b/app/redis.ts index 0149d7bedacb7f53b7cafd149609d08727be639d..8ade53aa180f97b55019b04e4c3c2a79dcb3a240 100644 --- a/app/redis.ts +++ b/app/redis.ts @@ -1,23 +1,21 @@ -import { createClient } from "npm:redis@^4.6"; +import { createClient } from 'npm:redis@^4.6' -const missingEnvVars = []; +const missingEnvVars = [] -const REDIS_HOST = Deno.env.get("REDIS_HOST"); -const REDIS_PORT = Deno.env.get("REDIS_PORT"); -const REDIS_PASSWORD = Deno.env.get("REDIS_PASSWORD"); -const REDIS_DB = Deno.env.get("REDIS_DB"); +const REDIS_HOST = Deno.env.get('REDIS_HOST') +const REDIS_PORT = Deno.env.get('REDIS_PORT') +const REDIS_PASSWORD = Deno.env.get('REDIS_PASSWORD') +const REDIS_DB = Deno.env.get('REDIS_DB') -if (!REDIS_HOST) missingEnvVars.push("REDIS_HOST"); -if (!REDIS_PORT) missingEnvVars.push("REDIS_PORT"); -if (!REDIS_PASSWORD) missingEnvVars.push("REDIS_PASSWORD"); -if (!REDIS_DB) missingEnvVars.push("REDIS_DB"); +if (!REDIS_HOST) missingEnvVars.push('REDIS_HOST') +if (!REDIS_PORT) missingEnvVars.push('REDIS_PORT') +if (!REDIS_PASSWORD) missingEnvVars.push('REDIS_PASSWORD') +if (!REDIS_DB) missingEnvVars.push('REDIS_DB') if (missingEnvVars.length > 0) { throw new Error( - `The following Redis environment variables are missing: ${ - missingEnvVars.join(", ") - }`, - ); + `The following Redis environment variables are missing: ${missingEnvVars.join(', ')}`, + ) } export const client = await createClient({ @@ -27,4 +25,4 @@ export const client = await createClient({ }, password: REDIS_PASSWORD, database: parseInt(REDIS_DB), -}).connect(); +}).connect() diff --git a/app/server.ts b/app/server.ts index e2b074d5013ebc09d2b150e64ef2097667028971..a5855166cb8cdb6f1c077429603da88e6b65d5bb 100644 --- a/app/server.ts +++ b/app/server.ts @@ -1,31 +1,28 @@ -import { createSchema, createYoga, serve, useResponseCache } from "./deps.ts"; -import { detectLanguage, translate } from "./libretranslate.ts"; -import { logger } from "./logging.ts"; -import { - DetectLanguageQueryParameters, - TranslationQueryParameters, -} from "./types.ts"; +import { createSchema, createYoga, serve, useResponseCache } from './deps.ts' +import { detectLanguage, translate } from './libretranslate.ts' +import { logger } from './logging.ts' +import { DetectLanguageQueryParameters, TranslationQueryParameters } from './types.ts' const typeDefs = ` type Query { translate(text: String!, targetLanguage: String!): String! detectLanguage(text: String!): String! } -`; +` -const fakeTranslation = "This is a fake translation"; -const fakeDetectLanguage = "zz"; +const fakeTranslation = 'This is a fake translation' +const fakeDetectLanguage = 'zz' type GraphQLContext = { - request?: { headers: Headers }; + request?: { headers: Headers } params?: { extensions?: { headers?: { - [key: string]: string; - }; - }; - }; -}; + [key: string]: string + } + } + } +} const createResolvers = (_config: ServerConfig) => ({ Query: { @@ -46,17 +43,17 @@ const createResolvers = (_config: ServerConfig) => ({ parameters, ), }, -}); +}) -export const DEFAULT_PORT = 8089; -export const DEFAULT_CACHE_ENABLED = true; -export const DEFAULT_CACHE_TTL_MS_LIBRE_TRANSLATE = 60_000; +export const DEFAULT_PORT = 8089 +export const DEFAULT_CACHE_ENABLED = true +export const DEFAULT_CACHE_TTL_MS_LIBRE_TRANSLATE = 60_000 export interface ServerConfig { - port: number; // default: 8089 - cacheEnabled: boolean; // default: true - cacheTtlMsLibreTranslate: number; // default: 60 seconds - fake: boolean; // For local development. If set, the API returns dummy data + port: number // default: 8089 + cacheEnabled: boolean // default: true + cacheTtlMsLibreTranslate: number // default: 60 seconds + fake: boolean // For local development. If set, the API returns dummy data } export const createGraphQLServer = (config: ServerConfig): GraphQLServer => { @@ -65,38 +62,36 @@ export const createGraphQLServer = (config: ServerConfig): GraphQLServer => { useResponseCache({ session: () => null, // global cache, shared by all users ttlPerSchemaCoordinate: { - "Query.translate": config.cacheTtlMsLibreTranslate, - "Query.detectLanguage": config.cacheTtlMsLibreTranslate, + 'Query.translate': config.cacheTtlMsLibreTranslate, + 'Query.detectLanguage': config.cacheTtlMsLibreTranslate, }, }), ] - : []; - const resolvers = createResolvers(config); + : [] + const resolvers = createResolvers(config) return createYoga({ schema: createSchema({ resolvers, typeDefs }), graphiql: true, plugins, - }); -}; + }) +} // deno-lint-ignore no-explicit-any -export type GraphQLServer = any; +export type GraphQLServer = any export const startServer = (config: ServerConfig): Promise<void> => { - const graphQLServer: GraphQLServer = createGraphQLServer(config); + const graphQLServer: GraphQLServer = createGraphQLServer(config) return serve(graphQLServer, { port: config.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.fake) { logger.info( `Server is serving fake data due to FAKE env var set to true`, - ); + ) } }, - }); -}; + }) +} diff --git a/app/types.ts b/app/types.ts index d30c885d75ebafa04e6a691e76335ed2fe234cdd..7c058597719579c13e32a92082da4e24a5df1acf 100644 --- a/app/types.ts +++ b/app/types.ts @@ -1,13 +1,13 @@ export type TranslationQueryParameters = { - text: string; - targetLanguage: string; -}; + text: string + targetLanguage: string +} export type DetectLanguageQueryParameters = { - text: string; -}; + text: string +} export enum TranslationFormat { - TEXT = "text", - HTML = "html", + TEXT = 'text', + HTML = 'html', } diff --git a/deno.json b/deno.json index 6b91d23475942968f58393b997eb73fff5795cd8..db0d792f0caed9a169e7f35260bb7542d397032c 100644 --- a/deno.json +++ b/deno.json @@ -17,6 +17,11 @@ } }, "fmt": { - "exclude": ["*.md"] + "lineWidth": 120, + "singleQuote": true, + "semiColons": false, + "exclude": [ + "*.md" + ] } } diff --git a/terraform/environments/scripts/destroy-env.sh b/terraform/environments/scripts/destroy-env.sh index d8137694745e9a6bcded13142bf6a5e666b55e6c..aac6dea64eafc13c326e5b4819351c32365e9b3c 100755 --- a/terraform/environments/scripts/destroy-env.sh +++ b/terraform/environments/scripts/destroy-env.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # exit when any command fails set -ex @@ -12,7 +12,7 @@ export TF_LOG=DEBUG # * GCP stuff not allowing our resources to be deleted. # Most of the time, retrying a destroy fixes these causes. retry() { - for i in {1..3}; do + for i in $(seq 1 3); do set +e "$@" retval=$?