From b24d0499d8b5a5886810d11741ce6bb7b6761aae Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Thu, 3 Mar 2022 01:15:11 +0000 Subject: [PATCH 01/13] chore: create api-languages --- apps/api-gateway/supergraph.yml | 4 + apps/api-languages/.env | 2 + apps/api-languages/.env.example | 2 + apps/api-languages/.eslintrc.json | 21 +++++ apps/api-languages/Dockerfile | 8 ++ apps/api-languages/db/db.ts | 13 +++ apps/api-languages/db/seed.ts | 26 ++++++ apps/api-languages/index.ts | 1 + apps/api-languages/jest.config.js | 15 ++++ apps/api-languages/project.json | 90 +++++++++++++++++++ apps/api-languages/schema.graphql | 11 +++ apps/api-languages/src/app/.gitkeep | 0 .../src/app/__generated__/graphql.ts | 19 ++++ apps/api-languages/src/app/app.module.ts | 19 ++++ .../src/app/modules/language/language.graphql | 7 ++ .../app/modules/language/language.module.ts | 11 +++ .../app/modules/language/language.resolver.ts | 15 ++++ .../app/modules/language/language.service.ts | 8 ++ apps/api-languages/src/assets/.gitkeep | 0 .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 3 + apps/api-languages/src/generate-typings.ts | 20 +++++ apps/api-languages/src/main.ts | 15 ++++ apps/api-languages/tsconfig.app.json | 12 +++ apps/api-languages/tsconfig.json | 13 +++ apps/api-languages/tsconfig.spec.json | 9 ++ workspace.json | 1 + 27 files changed, 348 insertions(+) create mode 100644 apps/api-languages/.env create mode 100644 apps/api-languages/.env.example create mode 100644 apps/api-languages/.eslintrc.json create mode 100644 apps/api-languages/Dockerfile create mode 100644 apps/api-languages/db/db.ts create mode 100644 apps/api-languages/db/seed.ts create mode 100644 apps/api-languages/index.ts create mode 100644 apps/api-languages/jest.config.js create mode 100644 apps/api-languages/project.json create mode 100644 apps/api-languages/schema.graphql create mode 100644 apps/api-languages/src/app/.gitkeep create mode 100644 apps/api-languages/src/app/__generated__/graphql.ts create mode 100644 apps/api-languages/src/app/app.module.ts create mode 100644 apps/api-languages/src/app/modules/language/language.graphql create mode 100644 apps/api-languages/src/app/modules/language/language.module.ts create mode 100644 apps/api-languages/src/app/modules/language/language.resolver.ts create mode 100644 apps/api-languages/src/app/modules/language/language.service.ts create mode 100644 apps/api-languages/src/assets/.gitkeep create mode 100644 apps/api-languages/src/environments/environment.prod.ts create mode 100644 apps/api-languages/src/environments/environment.ts create mode 100644 apps/api-languages/src/generate-typings.ts create mode 100644 apps/api-languages/src/main.ts create mode 100644 apps/api-languages/tsconfig.app.json create mode 100644 apps/api-languages/tsconfig.json create mode 100644 apps/api-languages/tsconfig.spec.json diff --git a/apps/api-gateway/supergraph.yml b/apps/api-gateway/supergraph.yml index 46649d623c1..b729d0e0efd 100644 --- a/apps/api-gateway/supergraph.yml +++ b/apps/api-gateway/supergraph.yml @@ -7,3 +7,7 @@ subgraphs: routing_url: http://127.0.0.1:4002/graphql schema: file: ../api-users/schema.graphql + languages: + routing_url: http://127.0.0.1:4003/graphql + schema: + file: ../api-languages/schema.graphql diff --git a/apps/api-languages/.env b/apps/api-languages/.env new file mode 100644 index 00000000000..c7ab440c167 --- /dev/null +++ b/apps/api-languages/.env @@ -0,0 +1,2 @@ +DATABASE_URL="arangodb://arangodb:8529" +GOOGLE_APPLICATION_JSON= \ No newline at end of file diff --git a/apps/api-languages/.env.example b/apps/api-languages/.env.example new file mode 100644 index 00000000000..c7ab440c167 --- /dev/null +++ b/apps/api-languages/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL="arangodb://arangodb:8529" +GOOGLE_APPLICATION_JSON= \ No newline at end of file diff --git a/apps/api-languages/.eslintrc.json b/apps/api-languages/.eslintrc.json new file mode 100644 index 00000000000..a8ae95b9ee0 --- /dev/null +++ b/apps/api-languages/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "parserOptions": { + "project": ["apps/api-languages/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/api-languages/Dockerfile b/apps/api-languages/Dockerfile new file mode 100644 index 00000000000..12496372dc0 --- /dev/null +++ b/apps/api-languages/Dockerfile @@ -0,0 +1,8 @@ +FROM node:14-alpine +WORKDIR /app +COPY ./dist/apps/api-languages . +EXPOSE 4002 +# dependencies that nestjs needs +RUN npm install --production --silent +RUN npm install tslib apollo-server-express @nestjs/mapped-types +CMD node ./main.js \ No newline at end of file diff --git a/apps/api-languages/db/db.ts b/apps/api-languages/db/db.ts new file mode 100644 index 00000000000..4a12769acc5 --- /dev/null +++ b/apps/api-languages/db/db.ts @@ -0,0 +1,13 @@ +import { Database } from 'arangojs' + +export function ArangoDB(): Database { + let db: Database + if (process.env.DATABASE_DB != null) + db = new Database({ + url: process.env.DATABASE_URL, + databaseName: process.env.DATABASE_DB + }) + else db = new Database({ url: process.env.DATABASE_URL }) + db.useBasicAuth(process.env.DATABASE_USER, process.env.DATABASE_PASS) + return db +} diff --git a/apps/api-languages/db/seed.ts b/apps/api-languages/db/seed.ts new file mode 100644 index 00000000000..1ebb5e82a3e --- /dev/null +++ b/apps/api-languages/db/seed.ts @@ -0,0 +1,26 @@ +import { ArangoDB } from './db' + +const db = ArangoDB() + +async function main(): Promise { + try { + await db.createCollection('languages', { keyOptions: { type: 'uuid' } }) + } catch {} + await db.collection('languages').ensureIndex({ + type: 'persistent', + fields: ['bcp47'], + name: 'bcp47', + unique: true + }) + await db.collection('languages').ensureIndex({ + type: 'persistent', + fields: ['iso3'], + name: 'iso3', + unique: true + }) +} + +main().catch((e) => { + console.error(e) + process.exit(1) +}) diff --git a/apps/api-languages/index.ts b/apps/api-languages/index.ts new file mode 100644 index 00000000000..da70d960f6c --- /dev/null +++ b/apps/api-languages/index.ts @@ -0,0 +1 @@ +export * from './src/app/__generated__/graphql' diff --git a/apps/api-languages/jest.config.js b/apps/api-languages/jest.config.js new file mode 100644 index 00000000000..c6905415b9d --- /dev/null +++ b/apps/api-languages/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'api-languages', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json' + } + }, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/apps/api-languages' +} diff --git a/apps/api-languages/project.json b/apps/api-languages/project.json new file mode 100644 index 00000000000..35dbcd2fd7f --- /dev/null +++ b/apps/api-languages/project.json @@ -0,0 +1,90 @@ +{ + "root": "apps/api-languages", + "sourceRoot": "apps/api-languages/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@nrwl/node:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/api-languages", + "main": "apps/api-languages/src/main.ts", + "tsConfig": "apps/api-languages/tsconfig.app.json", + "assets": [ + "apps/api-languages/src/assets", + { + "glob": "**/*.graphql", + "input": "apps/api-languages/src/app/", + "output": "./assets" + } + ], + "generatePackageJson": true + }, + "configurations": { + "production": { + "optimization": true, + "extractLicenses": true, + "inspect": false, + "fileReplacements": [ + { + "replace": "apps/api-languages/src/environments/environment.ts", + "with": "apps/api-languages/src/environments/environment.prod.ts" + } + ] + } + } + }, + "seed": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "npx ts-node apps/api-languages/db/seed.ts" + } + }, + "serve-nowatch": { + "executor": "@nrwl/node:execute", + "options": { + "buildTarget": "api-languages:build" + } + }, + "serve": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "nx serve-nowatch api-languages" + }, + { + "command": "npx ts-node apps/api-languages/src/generate-typings.ts" + } + ], + "parallel": true + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["apps/api-languages/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/apps/api-languages"], + "options": { + "jestConfig": "apps/api-languages/jest.config.js", + "passWithNoTests": true + } + }, + "generate-graphql": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "commands": [ + { + "command": "rover subgraph introspect http://localhost:4002/graphql > apps/api-languages/schema.graphql" + } + ] + } + } + }, + "tags": [] +} diff --git a/apps/api-languages/schema.graphql b/apps/api-languages/schema.graphql new file mode 100644 index 00000000000..c909a9e1abf --- /dev/null +++ b/apps/api-languages/schema.graphql @@ -0,0 +1,11 @@ +type User @key(fields: "id") { + id: ID! + firstName: String! + lastName: String + email: String! + imageUrl: String +} + +extend type Query { + me: User +} diff --git a/apps/api-languages/src/app/.gitkeep b/apps/api-languages/src/app/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/api-languages/src/app/__generated__/graphql.ts b/apps/api-languages/src/app/__generated__/graphql.ts new file mode 100644 index 00000000000..9fd4f8f2c9f --- /dev/null +++ b/apps/api-languages/src/app/__generated__/graphql.ts @@ -0,0 +1,19 @@ + +/* + * ------------------------------------------------------- + * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) + * ------------------------------------------------------- + */ + +/* tslint:disable */ +/* eslint-disable */ +export class Language { + __typename?: 'Language'; + id: string; +} + +export abstract class IQuery { + abstract languages(): Language[] | Promise; +} + +type Nullable = T | null; diff --git a/apps/api-languages/src/app/app.module.ts b/apps/api-languages/src/app/app.module.ts new file mode 100644 index 00000000000..b6fe8e0ac3c --- /dev/null +++ b/apps/api-languages/src/app/app.module.ts @@ -0,0 +1,19 @@ +import { join } from 'path' +import { Module } from '@nestjs/common' +import { GraphQLFederationModule } from '@nestjs/graphql' +import { LanguageModule } from './modules/language/language.module' + +@Module({ + imports: [ + LanguageModule, + GraphQLFederationModule.forRoot({ + typePaths: [ + join(process.cwd(), 'apps/api-languages/src/app/**/*.graphql'), + join(process.cwd(), 'assets/**/*.graphql') + ], + cors: true, + context: ({ req }) => ({ headers: req.headers }) + }) + ] +}) +export class AppModule {} diff --git a/apps/api-languages/src/app/modules/language/language.graphql b/apps/api-languages/src/app/modules/language/language.graphql new file mode 100644 index 00000000000..4423f6b7e1b --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.graphql @@ -0,0 +1,7 @@ +type Language @key(fields: "id") { + id: ID! +} + +extend type Query { + languages: [Language!]! +} diff --git a/apps/api-languages/src/app/modules/language/language.module.ts b/apps/api-languages/src/app/modules/language/language.module.ts new file mode 100644 index 00000000000..9a1887e99b6 --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { DatabaseModule } from '@core/nest/database' +import { LanguageResolver } from './language.resolver' +import { LanguageService } from './language.service' + +@Module({ + imports: [DatabaseModule], + providers: [LanguageResolver, LanguageService], + exports: [LanguageService] +}) +export class LanguageModule {} diff --git a/apps/api-languages/src/app/modules/language/language.resolver.ts b/apps/api-languages/src/app/modules/language/language.resolver.ts new file mode 100644 index 00000000000..1eb6a9a7ffc --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.resolver.ts @@ -0,0 +1,15 @@ +import { Resolver, Query } from '@nestjs/graphql' +import { KeyAsId } from '@core/nest/decorators' +import { Language } from '../../__generated__/graphql' +import { LanguageService } from './language.service' + +@Resolver('Language') +export class LanguageResolver { + constructor(private readonly languageService: LanguageService) {} + + @Query() + @KeyAsId() + async languages(): Promise { + return await this.languageService.getAll() + } +} diff --git a/apps/api-languages/src/app/modules/language/language.service.ts b/apps/api-languages/src/app/modules/language/language.service.ts new file mode 100644 index 00000000000..452b77da1cb --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.service.ts @@ -0,0 +1,8 @@ +import { BaseService } from '@core/nest/database' +import { Injectable } from '@nestjs/common' +import { DocumentCollection } from 'arangojs/collection' + +@Injectable() +export class LanguageService extends BaseService { + collection: DocumentCollection = this.db.collection('languages') +} diff --git a/apps/api-languages/src/assets/.gitkeep b/apps/api-languages/src/assets/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/api-languages/src/environments/environment.prod.ts b/apps/api-languages/src/environments/environment.prod.ts new file mode 100644 index 00000000000..bc0327dbebd --- /dev/null +++ b/apps/api-languages/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +} diff --git a/apps/api-languages/src/environments/environment.ts b/apps/api-languages/src/environments/environment.ts new file mode 100644 index 00000000000..b676dc71d19 --- /dev/null +++ b/apps/api-languages/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + production: false +} diff --git a/apps/api-languages/src/generate-typings.ts b/apps/api-languages/src/generate-typings.ts new file mode 100644 index 00000000000..9d9ef20d879 --- /dev/null +++ b/apps/api-languages/src/generate-typings.ts @@ -0,0 +1,20 @@ +import { join } from 'path' +import { GraphQLDefinitionsFactory } from '@nestjs/graphql' + +const definitionsFactory = new GraphQLDefinitionsFactory() +definitionsFactory + .generate({ + federation: true, + typePaths: [join(process.cwd(), 'apps/api-languages/src/app/**/*.graphql')], + path: join( + process.cwd(), + 'apps/api-languages/src/app/__generated__/graphql.ts' + ), + outputAs: 'class', + watch: true, + emitTypenameField: true, + customScalarTypeMapping: { + DateTime: 'String' + } + }) + .catch((err) => console.log(err)) diff --git a/apps/api-languages/src/main.ts b/apps/api-languages/src/main.ts new file mode 100644 index 00000000000..f48950768fa --- /dev/null +++ b/apps/api-languages/src/main.ts @@ -0,0 +1,15 @@ +import { Logger } from '@nestjs/common' +import { NestFactory } from '@nestjs/core' +import { AppModule } from './app/app.module' + +async function bootstrap(): Promise { + const port = process.env.PORT ?? '4003' + const app = await NestFactory.create(AppModule) + await app.listen(port, () => { + Logger.log('Listening at http://localhost:' + port + '/graphql') + }) +} + +bootstrap().catch((err) => { + console.log(err) +}) diff --git a/apps/api-languages/tsconfig.app.json b/apps/api-languages/tsconfig.app.json new file mode 100644 index 00000000000..0a067b3bb5c --- /dev/null +++ b/apps/api-languages/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["node"], + "emitDecoratorMetadata": true, + "target": "es2015" + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/apps/api-languages/tsconfig.json b/apps/api-languages/tsconfig.json new file mode 100644 index 00000000000..63dbe35fb28 --- /dev/null +++ b/apps/api-languages/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/api-languages/tsconfig.spec.json b/apps/api-languages/tsconfig.spec.json new file mode 100644 index 00000000000..29efa430b2b --- /dev/null +++ b/apps/api-languages/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/workspace.json b/workspace.json index 19c52b4c185..b3eb8be84ad 100644 --- a/workspace.json +++ b/workspace.json @@ -3,6 +3,7 @@ "projects": { "api-gateway": "apps/api-gateway", "api-journeys": "apps/api-journeys", + "api-languages": "apps/api-languages", "api-users": "apps/api-users", "apollo-logging": "libs/apollo/logging", "journeys": "apps/journeys", From a62b23717990a877c7bc1756a36c2a357d42da54 Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Thu, 3 Mar 2022 02:37:12 +0000 Subject: [PATCH 02/13] feat: fetch data from arclight api --- apps/api-languages/db/seed.ts | 77 +++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/apps/api-languages/db/seed.ts b/apps/api-languages/db/seed.ts index 1ebb5e82a3e..1fa6e4a889e 100644 --- a/apps/api-languages/db/seed.ts +++ b/apps/api-languages/db/seed.ts @@ -1,23 +1,76 @@ +import { aql } from 'arangojs' +import { isEmpty } from 'lodash' +import fetch from 'node-fetch' import { ArangoDB } from './db' +interface Language { + _key: string +} + const db = ArangoDB() async function main(): Promise { try { await db.createCollection('languages', { keyOptions: { type: 'uuid' } }) } catch {} - await db.collection('languages').ensureIndex({ - type: 'persistent', - fields: ['bcp47'], - name: 'bcp47', - unique: true - }) - await db.collection('languages').ensureIndex({ - type: 'persistent', - fields: ['iso3'], - name: 'iso3', - unique: true - }) + const collection = db.collection('languages') + const data = await ( + await fetch( + 'https://api.arclight.org/v2/media-languages?page=1&limit=5000&filter=default&apiKey=50105582a2e5a6.72068725' + ) + ).json() + + async function getLanguage( + languageId: number + ): Promise { + const rst = await db.query(aql` + FOR item IN ${collection} + FILTER item._key == ${languageId.toString()} + LIMIT 1 + RETURN item`) + return await rst.next() + } + + async function getLanguageByBcp47( + bcp47: string + ): Promise { + const rst = await db.query(aql` + FOR item IN ${collection} + FILTER item.bcp47 == ${bcp47} + LIMIT 1 + RETURN item`) + return await rst.next() + } + + for (const mediaLanguage of data._embedded.mediaLanguages) { + const { languageId, bcp47, iso3, nameNative } = mediaLanguage + const body = { + bcp47: isEmpty(bcp47) ? undefined : bcp47, + iso3: isEmpty(iso3) ? undefined : iso3, + nameNative + } + const language = await getLanguage(languageId) + if (language != null) { + await collection.update(languageId.toString(), body) + } else { + await collection.save({ _key: languageId.toString(), ...body }) + } + } + + for (const mediaLanguage of data._embedded.mediaLanguages) { + const { languageId, metadataLanguageTag, name } = mediaLanguage + const metadataLanguage = await getLanguageByBcp47(metadataLanguageTag) + if (metadataLanguage == null) continue + const body = { + names: [ + { + name, + languageId: metadataLanguage._key + } + ] + } + await collection.update(languageId.toString(), body) + } } main().catch((e) => { From 7b897a921ae8e2dfb18ccab45e5991eef01ca529 Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Mon, 7 Mar 2022 00:06:23 +0000 Subject: [PATCH 03/13] chore: match translation object --- apps/api-languages/db/seed.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/api-languages/db/seed.ts b/apps/api-languages/db/seed.ts index 1fa6e4a889e..1613edbb150 100644 --- a/apps/api-languages/db/seed.ts +++ b/apps/api-languages/db/seed.ts @@ -62,10 +62,11 @@ async function main(): Promise { const metadataLanguage = await getLanguageByBcp47(metadataLanguageTag) if (metadataLanguage == null) continue const body = { - names: [ + name: [ { - name, - languageId: metadataLanguage._key + value: name, + languageId: metadataLanguage._key, + primary: true } ] } From 158838f0bcb7334396a387c28ba877df01a3cefc Mon Sep 17 00:00:00 2001 From: John Geronimo Date: Mon, 7 Mar 2022 22:37:38 +0000 Subject: [PATCH 04/13] feat: implement language module --- apps/api-gateway/project.json | 3 ++ apps/api-gateway/schema.graphql | 20 +++++++ apps/api-languages/project.json | 2 +- apps/api-languages/schema.graphql | 19 ++++--- .../src/app/__generated__/graphql.ts | 13 +++++ .../src/app/modules/language/language.graphql | 5 ++ .../app/modules/language/language.module.ts | 3 +- .../language/language.resolver.spec.ts | 53 +++++++++++++++++++ .../app/modules/language/language.resolver.ts | 20 ++++++- .../modules/translation/translation.graphql | 5 ++ .../translation/translation.resolver.spec.ts | 32 +++++++++++ .../translation/translation.resolver.ts | 13 +++++ 12 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 apps/api-languages/src/app/modules/language/language.resolver.spec.ts create mode 100644 apps/api-languages/src/app/modules/translation/translation.graphql create mode 100644 apps/api-languages/src/app/modules/translation/translation.resolver.spec.ts create mode 100644 apps/api-languages/src/app/modules/translation/translation.resolver.ts diff --git a/apps/api-gateway/project.json b/apps/api-gateway/project.json index 30704091044..f0e0b4a1e15 100644 --- a/apps/api-gateway/project.json +++ b/apps/api-gateway/project.json @@ -44,6 +44,9 @@ }, { "command": "nx serve api-gateway" + }, + { + "command": "nx serve api-languages" } ], "parallel": true diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 0c7bb7172b8..0ad21e212ac 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -289,6 +289,7 @@ scalar join__FieldSet enum join__Graph { JOURNEYS @join__graph(name: "journeys" url: "http://127.0.0.1:4001/graphql") + LANGUAGES @join__graph(name: "languages" url: "http://127.0.0.1:4003/graphql") USERS @join__graph(name: "users" url: "http://127.0.0.1:4002/graphql") } @@ -349,6 +350,17 @@ input JourneyUpdateInput { title: String } +type Language + @join__owner(graph: LANGUAGES) + @join__type(graph: LANGUAGES, key: "id") +{ + bcp47: String @join__field(graph: LANGUAGES) + id: ID! @join__field(graph: LANGUAGES) + iso3: String @join__field(graph: LANGUAGES) + name(languageId: ID): [Translation]! @join__field(graph: LANGUAGES) + nameNative: String @join__field(graph: LANGUAGES) +} + type LinkAction implements Action { gtmEventName: String parentBlockId: ID! @@ -443,6 +455,8 @@ type Query { adminJourneys: [Journey!]! @join__field(graph: JOURNEYS) journey(id: ID!, idType: IdType): Journey @join__field(graph: JOURNEYS) journeys: [Journey!]! @join__field(graph: JOURNEYS) + language(id: ID!): Language @join__field(graph: LANGUAGES) + languages: [Language!]! @join__field(graph: LANGUAGES) me: User @join__field(graph: USERS) } @@ -595,6 +609,12 @@ enum ThemeName { base } +type Translation { + language: Language! + primary: Boolean! + value: String! +} + enum TypographyAlign { center left diff --git a/apps/api-languages/project.json b/apps/api-languages/project.json index 35dbcd2fd7f..921e3d2c538 100644 --- a/apps/api-languages/project.json +++ b/apps/api-languages/project.json @@ -80,7 +80,7 @@ "options": { "commands": [ { - "command": "rover subgraph introspect http://localhost:4002/graphql > apps/api-languages/schema.graphql" + "command": "rover subgraph introspect http://localhost:4003/graphql > apps/api-languages/schema.graphql" } ] } diff --git a/apps/api-languages/schema.graphql b/apps/api-languages/schema.graphql index c909a9e1abf..0299cbe4595 100644 --- a/apps/api-languages/schema.graphql +++ b/apps/api-languages/schema.graphql @@ -1,11 +1,18 @@ -type User @key(fields: "id") { +type Translation { + value: String! + language: Language! + primary: Boolean! +} + +type Language @key(fields: "id") { id: ID! - firstName: String! - lastName: String - email: String! - imageUrl: String + bcp47: String + iso3: String + nameNative: String + name(languageId: ID): [Translation]! } extend type Query { - me: User + languages: [Language!]! + language(id: ID!): Language } diff --git a/apps/api-languages/src/app/__generated__/graphql.ts b/apps/api-languages/src/app/__generated__/graphql.ts index 9fd4f8f2c9f..01ed634ecec 100644 --- a/apps/api-languages/src/app/__generated__/graphql.ts +++ b/apps/api-languages/src/app/__generated__/graphql.ts @@ -10,10 +10,23 @@ export class Language { __typename?: 'Language'; id: string; + bcp47?: Nullable; + iso3?: Nullable; + nameNative?: Nullable; + name: Nullable[]; +} + +export class Translation { + __typename?: 'Translation'; + value: string; + language: Language; + primary: boolean; } export abstract class IQuery { abstract languages(): Language[] | Promise; + + abstract language(id: string): Nullable | Promise>; } type Nullable = T | null; diff --git a/apps/api-languages/src/app/modules/language/language.graphql b/apps/api-languages/src/app/modules/language/language.graphql index 4423f6b7e1b..06a532d8c69 100644 --- a/apps/api-languages/src/app/modules/language/language.graphql +++ b/apps/api-languages/src/app/modules/language/language.graphql @@ -1,7 +1,12 @@ type Language @key(fields: "id") { id: ID! + bcp47: String + iso3: String + nameNative: String + name(languageId: ID): [Translation]! } extend type Query { languages: [Language!]! + language(id: ID!): Language } diff --git a/apps/api-languages/src/app/modules/language/language.module.ts b/apps/api-languages/src/app/modules/language/language.module.ts index 9a1887e99b6..cd6ee3efe5f 100644 --- a/apps/api-languages/src/app/modules/language/language.module.ts +++ b/apps/api-languages/src/app/modules/language/language.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common' import { DatabaseModule } from '@core/nest/database' +import { TranslationResolver } from '../translation/translation.resolver' import { LanguageResolver } from './language.resolver' import { LanguageService } from './language.service' @Module({ imports: [DatabaseModule], - providers: [LanguageResolver, LanguageService], + providers: [LanguageResolver, LanguageService, TranslationResolver], exports: [LanguageService] }) export class LanguageModule {} diff --git a/apps/api-languages/src/app/modules/language/language.resolver.spec.ts b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts new file mode 100644 index 00000000000..6c317d2e837 --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts @@ -0,0 +1,53 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { LanguageResolver } from './language.resolver' +import { LanguageService } from './language.service' + +describe('LangaugeResolver', () => { + let resolver: LanguageResolver + + const language = { + id: '1', + bscp47: 'TEC', + iso3: 'TEC', + nameNative: 'Teke, Central', + name: [ + { + value: 'Teke, Central', + languageId: '529', + primary: true + } + ] + } + + beforeEach(async () => { + const languageService = { + provide: LanguageService, + useFactory: () => ({ + get: jest.fn(() => language), + getAll: jest.fn(() => [language, language]) + }) + } + const module: TestingModule = await Test.createTestingModule({ + providers: [LanguageResolver, languageService] + }).compile() + resolver = module.get(LanguageResolver) + }) + + describe('languages', () => { + it('returns Languages', async () => { + expect(await resolver.languages()).toEqual([language, language]) + }) + }) + + describe('language', () => { + it('should return language', async () => { + expect(await resolver.language(language.id)).toEqual(language) + }) + }) + + describe('name', () => { + it('should return translation', () => { + expect(resolver.name(language, '529')).toEqual([...language.name]) + }) + }) +}) diff --git a/apps/api-languages/src/app/modules/language/language.resolver.ts b/apps/api-languages/src/app/modules/language/language.resolver.ts index 1eb6a9a7ffc..f6bcd45a8a1 100644 --- a/apps/api-languages/src/app/modules/language/language.resolver.ts +++ b/apps/api-languages/src/app/modules/language/language.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Query } from '@nestjs/graphql' +import { Resolver, Query, Args, ResolveField, Parent } from '@nestjs/graphql' import { KeyAsId } from '@core/nest/decorators' import { Language } from '../../__generated__/graphql' import { LanguageService } from './language.service' @@ -12,4 +12,22 @@ export class LanguageResolver { async languages(): Promise { return await this.languageService.getAll() } + + @Query() + @KeyAsId() + async language(@Args('id') _key: string): Promise { + return await this.languageService.get(_key) + } + + @ResolveField() + name( + @Parent() language, + @Args('languageId') languageId: string + ): Array<{ value: string; languageId: string; primary: boolean }> { + if (languageId == null) return language.name + + return language.name.filter( + (translation) => translation.languageId === languageId + ) + } } diff --git a/apps/api-languages/src/app/modules/translation/translation.graphql b/apps/api-languages/src/app/modules/translation/translation.graphql new file mode 100644 index 00000000000..8472ada3077 --- /dev/null +++ b/apps/api-languages/src/app/modules/translation/translation.graphql @@ -0,0 +1,5 @@ +type Translation { + value: String! + language: Language! + primary: Boolean! +} diff --git a/apps/api-languages/src/app/modules/translation/translation.resolver.spec.ts b/apps/api-languages/src/app/modules/translation/translation.resolver.spec.ts new file mode 100644 index 00000000000..bd687b4a9a9 --- /dev/null +++ b/apps/api-languages/src/app/modules/translation/translation.resolver.spec.ts @@ -0,0 +1,32 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { LanguageService } from '../language/language.service' +import { TranslationResolver } from './translation.resolver' + +describe('Translation Resolver', () => { + let resolver: TranslationResolver + + const translation = [ + { + value: 'Teke, Central', + languageId: '529', + primary: true + } + ] + + beforeEach(async () => { + const languageService = { + provide: LanguageService, + useFactory: () => ({ + get: jest.fn(() => translation) + }) + } + const module: TestingModule = await Test.createTestingModule({ + providers: [TranslationResolver, languageService] + }).compile() + resolver = module.get(TranslationResolver) + }) + + it('should return the translation', async () => { + expect(await resolver.language(translation)).toEqual(translation) + }) +}) diff --git a/apps/api-languages/src/app/modules/translation/translation.resolver.ts b/apps/api-languages/src/app/modules/translation/translation.resolver.ts new file mode 100644 index 00000000000..588008ea245 --- /dev/null +++ b/apps/api-languages/src/app/modules/translation/translation.resolver.ts @@ -0,0 +1,13 @@ +import { Resolver, ResolveField, Parent } from '@nestjs/graphql' +import { Language } from '../../__generated__/graphql' +import { LanguageService } from '../language/language.service' + +@Resolver('Translation') +export class TranslationResolver { + constructor(private readonly languageService: LanguageService) {} + + @ResolveField() + async language(@Parent() translation): Promise { + return await this.languageService.get(translation.languageId) + } +} From 46f5a6cbcc4c31edef8dde7248a134b599d80f9e Mon Sep 17 00:00:00 2001 From: John Geronimo Date: Tue, 8 Mar 2022 21:17:08 +0000 Subject: [PATCH 05/13] refactor: language and translation module --- apps/api-gateway/schema.graphql | 2 +- apps/api-languages/schema.graphql | 14 ++-- .../src/app/__generated__/graphql.ts | 2 +- apps/api-languages/src/app/app.module.ts | 2 + .../src/app/modules/language/language.graphql | 2 +- .../app/modules/language/language.module.ts | 3 +- .../language/language.resolver.spec.ts | 6 +- .../app/modules/language/language.resolver.ts | 7 +- .../modules/language/language.service.spec.ts | 83 +++++++++++++++++++ .../app/modules/language/language.service.ts | 10 +++ .../modules/translation/translation.module.ts | 11 +++ .../translation/translation.resolver.ts | 2 + 12 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 apps/api-languages/src/app/modules/language/language.service.spec.ts create mode 100644 apps/api-languages/src/app/modules/translation/translation.module.ts diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 0ad21e212ac..07d928c570d 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -456,7 +456,7 @@ type Query { journey(id: ID!, idType: IdType): Journey @join__field(graph: JOURNEYS) journeys: [Journey!]! @join__field(graph: JOURNEYS) language(id: ID!): Language @join__field(graph: LANGUAGES) - languages: [Language!]! @join__field(graph: LANGUAGES) + languages(limit: Int, page: Int): [Language!]! @join__field(graph: LANGUAGES) me: User @join__field(graph: USERS) } diff --git a/apps/api-languages/schema.graphql b/apps/api-languages/schema.graphql index 0299cbe4595..43e9308ca61 100644 --- a/apps/api-languages/schema.graphql +++ b/apps/api-languages/schema.graphql @@ -1,9 +1,3 @@ -type Translation { - value: String! - language: Language! - primary: Boolean! -} - type Language @key(fields: "id") { id: ID! bcp47: String @@ -12,7 +6,13 @@ type Language @key(fields: "id") { name(languageId: ID): [Translation]! } +type Translation { + value: String! + language: Language! + primary: Boolean! +} + extend type Query { - languages: [Language!]! + languages(page: Int, limit: Int): [Language!]! language(id: ID!): Language } diff --git a/apps/api-languages/src/app/__generated__/graphql.ts b/apps/api-languages/src/app/__generated__/graphql.ts index 01ed634ecec..f7b1c2ae5fd 100644 --- a/apps/api-languages/src/app/__generated__/graphql.ts +++ b/apps/api-languages/src/app/__generated__/graphql.ts @@ -24,7 +24,7 @@ export class Translation { } export abstract class IQuery { - abstract languages(): Language[] | Promise; + abstract languages(page?: Nullable, limit?: Nullable): Language[] | Promise; abstract language(id: string): Nullable | Promise>; } diff --git a/apps/api-languages/src/app/app.module.ts b/apps/api-languages/src/app/app.module.ts index b6fe8e0ac3c..89d01c445ba 100644 --- a/apps/api-languages/src/app/app.module.ts +++ b/apps/api-languages/src/app/app.module.ts @@ -2,10 +2,12 @@ import { join } from 'path' import { Module } from '@nestjs/common' import { GraphQLFederationModule } from '@nestjs/graphql' import { LanguageModule } from './modules/language/language.module' +import { TranslationModule } from './modules/translation/translation.module' @Module({ imports: [ LanguageModule, + TranslationModule, GraphQLFederationModule.forRoot({ typePaths: [ join(process.cwd(), 'apps/api-languages/src/app/**/*.graphql'), diff --git a/apps/api-languages/src/app/modules/language/language.graphql b/apps/api-languages/src/app/modules/language/language.graphql index 06a532d8c69..ffd67af0909 100644 --- a/apps/api-languages/src/app/modules/language/language.graphql +++ b/apps/api-languages/src/app/modules/language/language.graphql @@ -7,6 +7,6 @@ type Language @key(fields: "id") { } extend type Query { - languages: [Language!]! + languages(page: Int, limit: Int): [Language!]! language(id: ID!): Language } diff --git a/apps/api-languages/src/app/modules/language/language.module.ts b/apps/api-languages/src/app/modules/language/language.module.ts index cd6ee3efe5f..9a1887e99b6 100644 --- a/apps/api-languages/src/app/modules/language/language.module.ts +++ b/apps/api-languages/src/app/modules/language/language.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common' import { DatabaseModule } from '@core/nest/database' -import { TranslationResolver } from '../translation/translation.resolver' import { LanguageResolver } from './language.resolver' import { LanguageService } from './language.service' @Module({ imports: [DatabaseModule], - providers: [LanguageResolver, LanguageService, TranslationResolver], + providers: [LanguageResolver, LanguageService], exports: [LanguageService] }) export class LanguageModule {} diff --git a/apps/api-languages/src/app/modules/language/language.resolver.spec.ts b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts index 6c317d2e837..df001bcef7b 100644 --- a/apps/api-languages/src/app/modules/language/language.resolver.spec.ts +++ b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts @@ -3,7 +3,7 @@ import { LanguageResolver } from './language.resolver' import { LanguageService } from './language.service' describe('LangaugeResolver', () => { - let resolver: LanguageResolver + let resolver: LanguageResolver, service: LanguageService const language = { id: '1', @@ -31,11 +31,13 @@ describe('LangaugeResolver', () => { providers: [LanguageResolver, languageService] }).compile() resolver = module.get(LanguageResolver) + service = await module.resolve(LanguageService) }) describe('languages', () => { it('returns Languages', async () => { - expect(await resolver.languages()).toEqual([language, language]) + expect(await resolver.languages(1, 2)).toEqual([language, language]) + expect(service.getAll).toHaveBeenCalledWith(1, 2) }) }) diff --git a/apps/api-languages/src/app/modules/language/language.resolver.ts b/apps/api-languages/src/app/modules/language/language.resolver.ts index f6bcd45a8a1..aca090fa0e4 100644 --- a/apps/api-languages/src/app/modules/language/language.resolver.ts +++ b/apps/api-languages/src/app/modules/language/language.resolver.ts @@ -9,8 +9,11 @@ export class LanguageResolver { @Query() @KeyAsId() - async languages(): Promise { - return await this.languageService.getAll() + async languages( + @Args('page') page: number, + @Args('limit') limit: number + ): Promise { + return await this.languageService.getAll(page, limit) } @Query() diff --git a/apps/api-languages/src/app/modules/language/language.service.spec.ts b/apps/api-languages/src/app/modules/language/language.service.spec.ts new file mode 100644 index 00000000000..d6b8b4e37db --- /dev/null +++ b/apps/api-languages/src/app/modules/language/language.service.spec.ts @@ -0,0 +1,83 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { Database } from 'arangojs' +import { DeepMockProxy, mockDeep } from 'jest-mock-extended' +import { + mockCollectionSaveResult, + mockCollectionRemoveResult, + mockDbQueryResult +} from '@core/nest/database' +import { DocumentCollection } from 'arangojs/collection' +import { LanguageService } from './language.service' + +describe('LanguageService', () => { + let service: LanguageService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + LanguageService, + { + provide: 'DATABASE', + useFactory: () => mockDeep() + } + ] + }).compile() + + service = module.get(LanguageService) + service.collection = mockDeep() + }) + + const language = { + _key: '1', + bscp47: 'TEC', + iso3: 'TEC', + nameNative: 'Teke, Central', + name: [ + { + value: 'Teke, Central', + languageId: '529', + primary: true + } + ] + } + + describe('getAll', () => { + beforeEach(() => { + ;(service.db as DeepMockProxy).query.mockReturnValue( + mockDbQueryResult(service.db, [language, language]) + ) + }) + + it('should retun an array of languages', async () => { + expect(await service.getAll(1, 2)).toEqual([language, language]) + }) + }) + + describe('save', () => { + beforeEach(() => { + ;( + service.collection as DeepMockProxy + ).save.mockReturnValue( + mockCollectionSaveResult(service.collection, language) + ) + }) + + it('should return a language', async () => { + expect(await service.save(language)).toEqual(language) + }) + }) + + describe('remove', () => { + beforeEach(() => { + ;( + service.collection as DeepMockProxy + ).remove.mockReturnValue( + mockCollectionRemoveResult(service.collection, language) + ) + }) + + it('should return a language', async () => { + expect(await service.remove('1')).toEqual(language) + }) + }) +}) diff --git a/apps/api-languages/src/app/modules/language/language.service.ts b/apps/api-languages/src/app/modules/language/language.service.ts index 452b77da1cb..58748b1b647 100644 --- a/apps/api-languages/src/app/modules/language/language.service.ts +++ b/apps/api-languages/src/app/modules/language/language.service.ts @@ -1,8 +1,18 @@ import { BaseService } from '@core/nest/database' import { Injectable } from '@nestjs/common' +import { aql } from 'arangojs' import { DocumentCollection } from 'arangojs/collection' @Injectable() export class LanguageService extends BaseService { collection: DocumentCollection = this.db.collection('languages') + + async getAll(offset = 0, limit = 1000): Promise { + const res = await this.db.query(aql` + FOR item IN ${this.collection} + LIMIT ${offset}, ${limit} + RETURN item + `) + return await res.all() + } } diff --git a/apps/api-languages/src/app/modules/translation/translation.module.ts b/apps/api-languages/src/app/modules/translation/translation.module.ts new file mode 100644 index 00000000000..970de434235 --- /dev/null +++ b/apps/api-languages/src/app/modules/translation/translation.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { DatabaseModule } from '@core/nest/database' +import { LanguageService } from '../language/language.service' +import { TranslationResolver } from './translation.resolver' + +@Module({ + imports: [DatabaseModule], + providers: [LanguageService, TranslationResolver], + exports: [TranslationResolver] +}) +export class TranslationModule {} diff --git a/apps/api-languages/src/app/modules/translation/translation.resolver.ts b/apps/api-languages/src/app/modules/translation/translation.resolver.ts index 588008ea245..706ded13ff8 100644 --- a/apps/api-languages/src/app/modules/translation/translation.resolver.ts +++ b/apps/api-languages/src/app/modules/translation/translation.resolver.ts @@ -1,3 +1,4 @@ +import { KeyAsId } from '@core/nest/decorators' import { Resolver, ResolveField, Parent } from '@nestjs/graphql' import { Language } from '../../__generated__/graphql' import { LanguageService } from '../language/language.service' @@ -7,6 +8,7 @@ export class TranslationResolver { constructor(private readonly languageService: LanguageService) {} @ResolveField() + @KeyAsId() async language(@Parent() translation): Promise { return await this.languageService.get(translation.languageId) } From 215702102cded29cd695a0630c32a759477cd4be Mon Sep 17 00:00:00 2001 From: John Geronimo Date: Wed, 9 Mar 2022 20:16:23 +0000 Subject: [PATCH 06/13] fix: language getAll logic --- .../api-languages/src/app/modules/language/language.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api-languages/src/app/modules/language/language.service.ts b/apps/api-languages/src/app/modules/language/language.service.ts index 58748b1b647..d73718dca42 100644 --- a/apps/api-languages/src/app/modules/language/language.service.ts +++ b/apps/api-languages/src/app/modules/language/language.service.ts @@ -7,7 +7,8 @@ import { DocumentCollection } from 'arangojs/collection' export class LanguageService extends BaseService { collection: DocumentCollection = this.db.collection('languages') - async getAll(offset = 0, limit = 1000): Promise { + async getAll(page = 1, limit = 1000): Promise { + const offset = limit * (page - 1) const res = await this.db.query(aql` FOR item IN ${this.collection} LIMIT ${offset}, ${limit} From 8a662aeaa1a405c7efe8f89cbf0d48a6fb510924 Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Thu, 10 Mar 2022 00:30:49 +0000 Subject: [PATCH 07/13] chore: add arclight api key env --- apps/api-languages/.env | 2 +- apps/api-languages/.env.example | 2 +- apps/api-languages/db/seed.ts | 69 ++++++++++++++++++++------------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/apps/api-languages/.env b/apps/api-languages/.env index c7ab440c167..e0df6388eda 100644 --- a/apps/api-languages/.env +++ b/apps/api-languages/.env @@ -1,2 +1,2 @@ DATABASE_URL="arangodb://arangodb:8529" -GOOGLE_APPLICATION_JSON= \ No newline at end of file +ARCLIGHT_API_KEY= \ No newline at end of file diff --git a/apps/api-languages/.env.example b/apps/api-languages/.env.example index c7ab440c167..e0df6388eda 100644 --- a/apps/api-languages/.env.example +++ b/apps/api-languages/.env.example @@ -1,2 +1,2 @@ DATABASE_URL="arangodb://arangodb:8529" -GOOGLE_APPLICATION_JSON= \ No newline at end of file +ARCLIGHT_API_KEY= \ No newline at end of file diff --git a/apps/api-languages/db/seed.ts b/apps/api-languages/db/seed.ts index 1613edbb150..2b8a90da7e4 100644 --- a/apps/api-languages/db/seed.ts +++ b/apps/api-languages/db/seed.ts @@ -5,10 +5,33 @@ import { ArangoDB } from './db' interface Language { _key: string + name: Array<{ value: string; languageId: string; primary: boolean }> + bcp47?: string + iso3?: string } const db = ArangoDB() +async function getLanguage(languageId: number): Promise { + const rst = await db.query(aql` + FOR item IN ${db.collection('languages')} + FILTER item._key == ${languageId.toString()} + LIMIT 1 + RETURN item`) + return await rst.next() +} + +async function getLanguageByBcp47( + bcp47: string +): Promise { + const rst = await db.query(aql` + FOR item IN ${db.collection('languages')} + FILTER item.bcp47 == ${bcp47} + LIMIT 1 + RETURN item`) + return await rst.next() +} + async function main(): Promise { try { await db.createCollection('languages', { keyOptions: { type: 'uuid' } }) @@ -16,40 +39,27 @@ async function main(): Promise { const collection = db.collection('languages') const data = await ( await fetch( - 'https://api.arclight.org/v2/media-languages?page=1&limit=5000&filter=default&apiKey=50105582a2e5a6.72068725' + `https://api.arclight.org/v2/media-languages?limit=5000&apiKey=${ + process.env.ARCLIGHT_API_KEY ?? '' + }` ) ).json() - async function getLanguage( - languageId: number - ): Promise { - const rst = await db.query(aql` - FOR item IN ${collection} - FILTER item._key == ${languageId.toString()} - LIMIT 1 - RETURN item`) - return await rst.next() - } - - async function getLanguageByBcp47( - bcp47: string - ): Promise { - const rst = await db.query(aql` - FOR item IN ${collection} - FILTER item.bcp47 == ${bcp47} - LIMIT 1 - RETURN item`) - return await rst.next() - } - for (const mediaLanguage of data._embedded.mediaLanguages) { const { languageId, bcp47, iso3, nameNative } = mediaLanguage - const body = { + const body: Omit = { bcp47: isEmpty(bcp47) ? undefined : bcp47, iso3: isEmpty(iso3) ? undefined : iso3, - nameNative + name: [ + { + value: nameNative, + languageId: languageId.toString(), + primary: true + } + ] } const language = await getLanguage(languageId) + if (language != null) { await collection.update(languageId.toString(), body) } else { @@ -59,14 +69,19 @@ async function main(): Promise { for (const mediaLanguage of data._embedded.mediaLanguages) { const { languageId, metadataLanguageTag, name } = mediaLanguage + const language = await getLanguage(languageId) + if (language == null) continue + const metadataLanguage = await getLanguageByBcp47(metadataLanguageTag) if (metadataLanguage == null) continue - const body = { + + const body: Omit = { name: [ + ...language.name, { value: name, languageId: metadataLanguage._key, - primary: true + primary: false } ] } From 8347572b347b2f6fd9acad029a3e0565e87347be Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Thu, 10 Mar 2022 00:48:07 +0000 Subject: [PATCH 08/13] chore: refactor seed --- apps/api-languages/db/seed.ts | 131 ++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 47 deletions(-) diff --git a/apps/api-languages/db/seed.ts b/apps/api-languages/db/seed.ts index 2b8a90da7e4..b5df04a57cc 100644 --- a/apps/api-languages/db/seed.ts +++ b/apps/api-languages/db/seed.ts @@ -10,12 +10,21 @@ interface Language { iso3?: string } +interface MediaLanguage { + languageId: number + bcp47?: string + iso3?: string + nameNative: string + metadataLanguageTag: string + name: string +} + const db = ArangoDB() -async function getLanguage(languageId: number): Promise { +async function getLanguage(languageId: string): Promise { const rst = await db.query(aql` FOR item IN ${db.collection('languages')} - FILTER item._key == ${languageId.toString()} + FILTER item._key == ${languageId} LIMIT 1 RETURN item`) return await rst.next() @@ -32,60 +41,88 @@ async function getLanguageByBcp47( return await rst.next() } -async function main(): Promise { - try { - await db.createCollection('languages', { keyOptions: { type: 'uuid' } }) - } catch {} - const collection = db.collection('languages') - const data = await ( +async function getMediaLanguages(): Promise { + const response: { + _embedded: { mediaLanguages: MediaLanguage[] } + } = await ( await fetch( `https://api.arclight.org/v2/media-languages?limit=5000&apiKey=${ process.env.ARCLIGHT_API_KEY ?? '' }` ) ).json() + return response._embedded.mediaLanguages +} + +async function digestMediaLanguage( + mediaLanguage: MediaLanguage +): Promise { + const { languageId, bcp47, iso3, nameNative } = mediaLanguage + const body: Omit = { + bcp47: isEmpty(bcp47) ? undefined : bcp47, + iso3: isEmpty(iso3) ? undefined : iso3, + name: [ + { + value: nameNative, + languageId: languageId.toString(), + primary: true + } + ] + } + const language = await getLanguage(languageId.toString()) + + if (language != null) { + await db.collection('languages').update(languageId.toString(), body) + } else { + await db + .collection('languages') + .save({ _key: languageId.toString(), ...body }) + } +} + +async function digestMediaLanguageMetadata( + mediaLanguage: MediaLanguage +): Promise { + const { languageId, metadataLanguageTag, name } = mediaLanguage + const language = await getLanguage(languageId.toString()) + if (language == null) return + + const metadataLanguage = await getLanguageByBcp47(metadataLanguageTag) + if (metadataLanguage == null) return + + if ( + language.name.find( + ({ languageId }) => languageId === metadataLanguage._key + ) != null + ) + return + + const body: Omit = { + name: [ + ...language.name, + { + value: name, + languageId: metadataLanguage._key, + primary: false + } + ] + } + await db.collection('languages').update(languageId.toString(), body) +} + +async function main(): Promise { + try { + await db.createCollection('languages', { keyOptions: { type: 'uuid' } }) + } catch {} + const mediaLanguages = await getMediaLanguages() - for (const mediaLanguage of data._embedded.mediaLanguages) { - const { languageId, bcp47, iso3, nameNative } = mediaLanguage - const body: Omit = { - bcp47: isEmpty(bcp47) ? undefined : bcp47, - iso3: isEmpty(iso3) ? undefined : iso3, - name: [ - { - value: nameNative, - languageId: languageId.toString(), - primary: true - } - ] - } - const language = await getLanguage(languageId) - - if (language != null) { - await collection.update(languageId.toString(), body) - } else { - await collection.save({ _key: languageId.toString(), ...body }) - } + for (const mediaLanguage of mediaLanguages) { + console.log('language:', mediaLanguage.languageId) + await digestMediaLanguage(mediaLanguage) } - for (const mediaLanguage of data._embedded.mediaLanguages) { - const { languageId, metadataLanguageTag, name } = mediaLanguage - const language = await getLanguage(languageId) - if (language == null) continue - - const metadataLanguage = await getLanguageByBcp47(metadataLanguageTag) - if (metadataLanguage == null) continue - - const body: Omit = { - name: [ - ...language.name, - { - value: name, - languageId: metadataLanguage._key, - primary: false - } - ] - } - await collection.update(languageId.toString(), body) + for (const mediaLanguage of mediaLanguages) { + await digestMediaLanguageMetadata(mediaLanguage) } } From e73dd6be88efd40f6a4100111ef0b0dc1b80b88c Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Thu, 10 Mar 2022 21:56:04 +0000 Subject: [PATCH 09/13] chore: add translation field decorator --- .eslintrc.json | 3 +- apps/api-gateway/schema.graphql | 3 +- apps/api-languages/schema.graphql | 3 +- .../src/app/__generated__/graphql.ts | 1 - .../src/app/modules/language/language.graphql | 3 +- .../language/language.resolver.spec.ts | 31 ++++-- .../app/modules/language/language.resolver.ts | 14 +-- .../TranslationField/TranslationField.spec.ts | 100 ++++++++++++++++++ .../lib/TranslationField/TranslationField.ts | 46 ++++++++ .../src/lib/TranslationField/index.ts | 1 + libs/nest/decorators/src/lib/decorators.ts | 1 + 11 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts create mode 100644 libs/nest/decorators/src/lib/TranslationField/TranslationField.ts create mode 100644 libs/nest/decorators/src/lib/TranslationField/index.ts diff --git a/.eslintrc.json b/.eslintrc.json index 2ec8b141755..b61589246f6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,7 +26,8 @@ "import/newline-after-import": "error", "import/no-named-default": "error", "import/no-anonymous-default-export": "error", - "import/dynamic-import-chunkname": "error" + "import/dynamic-import-chunkname": "error", + "@typescript-eslint/no-empty-function": { "allow": ["decoratedFunctions"] } }, "overrides": [ { diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 07d928c570d..1e43b8858b3 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -357,8 +357,7 @@ type Language bcp47: String @join__field(graph: LANGUAGES) id: ID! @join__field(graph: LANGUAGES) iso3: String @join__field(graph: LANGUAGES) - name(languageId: ID): [Translation]! @join__field(graph: LANGUAGES) - nameNative: String @join__field(graph: LANGUAGES) + name(languageId: String, primary: Boolean): [Translation]! @join__field(graph: LANGUAGES) } type LinkAction implements Action { diff --git a/apps/api-languages/schema.graphql b/apps/api-languages/schema.graphql index 43e9308ca61..300529ef3e8 100644 --- a/apps/api-languages/schema.graphql +++ b/apps/api-languages/schema.graphql @@ -2,8 +2,7 @@ type Language @key(fields: "id") { id: ID! bcp47: String iso3: String - nameNative: String - name(languageId: ID): [Translation]! + name(languageId: String, primary: Boolean): [Translation]! } type Translation { diff --git a/apps/api-languages/src/app/__generated__/graphql.ts b/apps/api-languages/src/app/__generated__/graphql.ts index f7b1c2ae5fd..f7392a1fc28 100644 --- a/apps/api-languages/src/app/__generated__/graphql.ts +++ b/apps/api-languages/src/app/__generated__/graphql.ts @@ -12,7 +12,6 @@ export class Language { id: string; bcp47?: Nullable; iso3?: Nullable; - nameNative?: Nullable; name: Nullable[]; } diff --git a/apps/api-languages/src/app/modules/language/language.graphql b/apps/api-languages/src/app/modules/language/language.graphql index ffd67af0909..18defdc404e 100644 --- a/apps/api-languages/src/app/modules/language/language.graphql +++ b/apps/api-languages/src/app/modules/language/language.graphql @@ -2,8 +2,7 @@ type Language @key(fields: "id") { id: ID! bcp47: String iso3: String - nameNative: String - name(languageId: ID): [Translation]! + name(languageId: String, primary: Boolean): [Translation]! } extend type Query { diff --git a/apps/api-languages/src/app/modules/language/language.resolver.spec.ts b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts index df001bcef7b..11fde839f03 100644 --- a/apps/api-languages/src/app/modules/language/language.resolver.spec.ts +++ b/apps/api-languages/src/app/modules/language/language.resolver.spec.ts @@ -6,15 +6,18 @@ describe('LangaugeResolver', () => { let resolver: LanguageResolver, service: LanguageService const language = { - id: '1', - bscp47: 'TEC', - iso3: 'TEC', - nameNative: 'Teke, Central', + id: '20615', + bcp47: 'zh', name: [ { - value: 'Teke, Central', - languageId: '529', - primary: true + value: '普通話', + primary: true, + languageId: '20615' + }, + { + value: 'Chinese, Mandarin', + primary: false, + languageId: '529' } ] } @@ -48,8 +51,18 @@ describe('LangaugeResolver', () => { }) describe('name', () => { - it('should return translation', () => { - expect(resolver.name(language, '529')).toEqual([...language.name]) + it('should return translations', () => { + expect(resolver.name(language)).toEqual(language.name) + }) + + it('should return translations filtered by languageId', () => { + expect(resolver.name(language, '529')).toEqual([language.name[1]]) + }) + + it('should return translations filtered by primary', () => { + expect(resolver.name(language, undefined, true)).toEqual([ + language.name[0] + ]) }) }) }) diff --git a/apps/api-languages/src/app/modules/language/language.resolver.ts b/apps/api-languages/src/app/modules/language/language.resolver.ts index aca090fa0e4..9ac759123e9 100644 --- a/apps/api-languages/src/app/modules/language/language.resolver.ts +++ b/apps/api-languages/src/app/modules/language/language.resolver.ts @@ -1,5 +1,5 @@ import { Resolver, Query, Args, ResolveField, Parent } from '@nestjs/graphql' -import { KeyAsId } from '@core/nest/decorators' +import { KeyAsId, TranslationField } from '@core/nest/decorators' import { Language } from '../../__generated__/graphql' import { LanguageService } from './language.service' @@ -23,14 +23,10 @@ export class LanguageResolver { } @ResolveField() + @TranslationField('name') name( @Parent() language, - @Args('languageId') languageId: string - ): Array<{ value: string; languageId: string; primary: boolean }> { - if (languageId == null) return language.name - - return language.name.filter( - (translation) => translation.languageId === languageId - ) - } + @Args('languageId') languageId?: string, + @Args('primary') primary?: boolean + ): void {} } diff --git a/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts b/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts new file mode 100644 index 00000000000..4e29262ac83 --- /dev/null +++ b/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts @@ -0,0 +1,100 @@ +import { TranslationField } from '.' +import type { Translation } from './TranslationField' + +describe('TranslationField', () => { + describe('when array of translations', () => { + const parent = { + name: [ + { + value: '普通話', + primary: true, + languageId: '20615' + }, + { + value: 'Chinese, Mandarin', + primary: false, + languageId: '529' + } + ] + } + + class Translatable { + @TranslationField('name') + name( + _parent: { name: Translation[] }, + _languageId?: string, + _primary?: boolean + ): void {} + } + + it('should return translations', () => { + expect(new Translatable().name(parent)).toEqual(parent.name) + }) + + it('should return translations filtered by languageId', () => { + expect(new Translatable().name(parent, '529')).toEqual([parent.name[1]]) + }) + + it('should return translations filtered by primary', () => { + expect(new Translatable().name(parent, undefined, true)).toEqual([ + parent.name[0] + ]) + }) + + it('should return tranlsations filtered by languageId or primary', () => { + expect(new Translatable().name(parent, '529', true)).toEqual(parent.name) + }) + }) + + describe('when array of array of translations', () => { + const parent = { + studyQuestions: [ + [ + { + value: '普通話', + primary: true, + languageId: '20615' + }, + { + value: 'Chinese, Mandarin', + primary: false, + languageId: '529' + } + ] + ] + } + + class Translatable { + @TranslationField('studyQuestions') + studyQuestions( + _parent: { studyQuestions: Translation[][] }, + _languageId?: string, + _primary?: boolean + ): void {} + } + + it('should return array of translations', () => { + expect(new Translatable().studyQuestions(parent)).toEqual( + parent.studyQuestions + ) + }) + + it('should return array of translations filtered by languageId', () => { + expect(new Translatable().studyQuestions(parent, '529')).toEqual([ + [parent.studyQuestions[0][1]] + ]) + }) + + it('should return array of translations filtered by primary', () => { + expect( + new Translatable().studyQuestions(parent, undefined, true) + ).toEqual([[parent.studyQuestions[0][0]]]) + }) + + it('should return array of tranlsations filtered by languageId or primary', () => { + expect(new Translatable().studyQuestions(parent, '529', true)).toEqual( + parent.studyQuestions + ) + }) + }) +}) diff --git a/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts b/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts new file mode 100644 index 00000000000..63f922f2864 --- /dev/null +++ b/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts @@ -0,0 +1,46 @@ +export interface Translation { + languageId: string + primary: boolean + value: string +} + +export function TranslationField(name: string) { + return ( + _target: unknown, + _propertyKey: string, + descriptor: PropertyDescriptor + ) => { + descriptor.value = function ( + parent: { [key: string]: Translation[][] | Translation[] }, + languageId?: string, + primary?: boolean + ) { + const translations = parent[name] + if (Array.isArray(translations[0])) { + return (translations as Translation[][]).map((translations) => + filterTranslations(translations, languageId, primary) + ) + } else { + return filterTranslations( + translations as Translation[], + languageId, + primary + ) + } + } + } +} + +function filterTranslations( + translations: Translation[], + languageId?: string, + primary?: boolean +) { + if (translations == null || (languageId == null && primary == null)) + return translations + + return translations.filter( + ({ languageId: _languageId, primary: _primary }) => + _languageId === languageId || _primary === primary + ) +} diff --git a/libs/nest/decorators/src/lib/TranslationField/index.ts b/libs/nest/decorators/src/lib/TranslationField/index.ts new file mode 100644 index 00000000000..0a56f749eeb --- /dev/null +++ b/libs/nest/decorators/src/lib/TranslationField/index.ts @@ -0,0 +1 @@ +export { TranslationField } from './TranslationField' diff --git a/libs/nest/decorators/src/lib/decorators.ts b/libs/nest/decorators/src/lib/decorators.ts index 30e64f8ae8d..0dcbd8eb381 100644 --- a/libs/nest/decorators/src/lib/decorators.ts +++ b/libs/nest/decorators/src/lib/decorators.ts @@ -2,6 +2,7 @@ import { createParamDecorator } from '@nestjs/common' import { get, has, omit } from 'lodash' import { GqlExecutionContext } from '@nestjs/graphql' import { AuthenticationError } from 'apollo-server-errors' +export { TranslationField } from './TranslationField' interface TransformObject { _key?: string From e172ad754485c66b87bac4d202fa34221f1b2b20 Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Fri, 11 Mar 2022 00:30:15 +0000 Subject: [PATCH 10/13] chore: change lint syntax --- .eslintrc.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index b61589246f6..ee5ab3d6764 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,7 +27,10 @@ "import/no-named-default": "error", "import/no-anonymous-default-export": "error", "import/dynamic-import-chunkname": "error", - "@typescript-eslint/no-empty-function": { "allow": ["decoratedFunctions"] } + "@typescript-eslint/no-empty-function": [ + "error", + { "allow": ["decoratedFunctions"] } + ] }, "overrides": [ { From f531d6fb1becfcd12a2b051372505cee15f47d35 Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Fri, 11 Mar 2022 00:45:48 +0000 Subject: [PATCH 11/13] fix: nest decorator lint --- .../src/lib/TranslationField/TranslationField.spec.ts | 2 +- .../src/lib/TranslationField/TranslationField.ts | 10 ++++++++-- libs/nest/decorators/src/lib/decorators.ts | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts b/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts index 4e29262ac83..1e0ca767d91 100644 --- a/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts +++ b/libs/nest/decorators/src/lib/TranslationField/TranslationField.spec.ts @@ -1,5 +1,5 @@ -import { TranslationField } from '.' import type { Translation } from './TranslationField' +import { TranslationField } from '.' describe('TranslationField', () => { describe('when array of translations', () => { diff --git a/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts b/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts index 63f922f2864..1d4e8410610 100644 --- a/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts +++ b/libs/nest/decorators/src/lib/TranslationField/TranslationField.ts @@ -4,7 +4,13 @@ export interface Translation { value: string } -export function TranslationField(name: string) { +export function TranslationField( + name: string +): ( + _target: unknown, + _propertyKey: string, + descriptor: PropertyDescriptor +) => void { return ( _target: unknown, _propertyKey: string, @@ -35,7 +41,7 @@ function filterTranslations( translations: Translation[], languageId?: string, primary?: boolean -) { +): Translation[] { if (translations == null || (languageId == null && primary == null)) return translations diff --git a/libs/nest/decorators/src/lib/decorators.ts b/libs/nest/decorators/src/lib/decorators.ts index 0dcbd8eb381..033eb08cf77 100644 --- a/libs/nest/decorators/src/lib/decorators.ts +++ b/libs/nest/decorators/src/lib/decorators.ts @@ -2,6 +2,7 @@ import { createParamDecorator } from '@nestjs/common' import { get, has, omit } from 'lodash' import { GqlExecutionContext } from '@nestjs/graphql' import { AuthenticationError } from 'apollo-server-errors' + export { TranslationField } from './TranslationField' interface TransformObject { From 921e94cb03d7d7e05faf267a2639375e9b59e4bd Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Fri, 11 Mar 2022 03:29:43 +0000 Subject: [PATCH 12/13] fix: languageId should be id --- apps/api-gateway/schema.graphql | 2 +- apps/api-languages/schema.graphql | 2 +- apps/api-languages/src/app/modules/language/language.graphql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api-gateway/schema.graphql b/apps/api-gateway/schema.graphql index 0cfebd76d07..c56111d07b7 100644 --- a/apps/api-gateway/schema.graphql +++ b/apps/api-gateway/schema.graphql @@ -357,7 +357,7 @@ type Language bcp47: String @join__field(graph: LANGUAGES) id: ID! @join__field(graph: LANGUAGES) iso3: String @join__field(graph: LANGUAGES) - name(languageId: String, primary: Boolean): [Translation]! @join__field(graph: LANGUAGES) + name(languageId: ID, primary: Boolean): [Translation]! @join__field(graph: LANGUAGES) } type LinkAction implements Action { diff --git a/apps/api-languages/schema.graphql b/apps/api-languages/schema.graphql index 300529ef3e8..f61eaa8cb5d 100644 --- a/apps/api-languages/schema.graphql +++ b/apps/api-languages/schema.graphql @@ -2,7 +2,7 @@ type Language @key(fields: "id") { id: ID! bcp47: String iso3: String - name(languageId: String, primary: Boolean): [Translation]! + name(languageId: ID, primary: Boolean): [Translation]! } type Translation { diff --git a/apps/api-languages/src/app/modules/language/language.graphql b/apps/api-languages/src/app/modules/language/language.graphql index 18defdc404e..17e32e68ca9 100644 --- a/apps/api-languages/src/app/modules/language/language.graphql +++ b/apps/api-languages/src/app/modules/language/language.graphql @@ -2,7 +2,7 @@ type Language @key(fields: "id") { id: ID! bcp47: String iso3: String - name(languageId: String, primary: Boolean): [Translation]! + name(languageId: ID, primary: Boolean): [Translation]! } extend type Query { From 15740b2cb13f32c5abd52345e5e6cecad776672b Mon Sep 17 00:00:00 2001 From: Tataihono Nikora Date: Fri, 11 Mar 2022 03:37:17 +0000 Subject: [PATCH 13/13] fix: pr feedback --- .gitignore | 1 + apps/api-languages/.env | 2 -- apps/api-languages/Dockerfile | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 apps/api-languages/.env diff --git a/.gitignore b/.gitignore index 49b72d104e9..58ddb994d38 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ testem.log Thumbs.db #local .env files +.env .env*local # kube secrets diff --git a/apps/api-languages/.env b/apps/api-languages/.env deleted file mode 100644 index e0df6388eda..00000000000 --- a/apps/api-languages/.env +++ /dev/null @@ -1,2 +0,0 @@ -DATABASE_URL="arangodb://arangodb:8529" -ARCLIGHT_API_KEY= \ No newline at end of file diff --git a/apps/api-languages/Dockerfile b/apps/api-languages/Dockerfile index 12496372dc0..ed4bbb63c49 100644 --- a/apps/api-languages/Dockerfile +++ b/apps/api-languages/Dockerfile @@ -1,7 +1,7 @@ FROM node:14-alpine WORKDIR /app COPY ./dist/apps/api-languages . -EXPOSE 4002 +EXPOSE 4003 # dependencies that nestjs needs RUN npm install --production --silent RUN npm install tslib apollo-server-express @nestjs/mapped-types