Skip to content

Commit 19940f0

Browse files
committed
feat(bff): api adjustments
1 parent 24d7c7d commit 19940f0

File tree

30 files changed

+430
-12
lines changed

30 files changed

+430
-12
lines changed

apps/blog-bff/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { toArticle, toArticlePreviewList } from './mappers';
1515
import { articles } from '@angular-love/api';
1616
import { HTTPException } from 'hono/http-exception';
17+
import { authors } from '@angular-love/blog/bff/authors';
1718

1819
type Bindings = {
1920
GRAPHQL_URI: string;
@@ -86,6 +87,7 @@ app.get('/articles/:slug', async (c) => {
8687
});
8788

8889
app.route('/v2/articles', articles);
90+
app.route('/v2/authors', authors);
8991

9092
app.onError((err, c) => {
9193
if (err instanceof HTTPException) {

apps/blog-bff/src/mappers.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
GetPostBySlugQuery,
66
GetPostsQuery,
77
} from '@angular-love/wp/graphql/data-access';
8-
import {
9-
Article,
10-
ArticlePreview,
11-
} from '@angular-love/blog/articles/data-access';
8+
import { Article, ArticlePreview } from '@angular-love/contracts/articles';
129

1310
const DEFAULT_LANGUAGE_SUBSET = ['typescript', 'html', 'css', 'scss', 'json'];
1411

@@ -25,6 +22,7 @@ export const toArticlePreviewList = (query: {
2522
featuredImageUrl: node.featuredImage?.node.sourceUrl || '',
2623
publishDate: new Date(node.date || '').toISOString(),
2724
author: {
25+
slug: '',
2826
name: node.author?.node?.name || '',
2927
avatarUrl: node.author?.node?.avatar?.url || '',
3028
},
@@ -72,6 +70,7 @@ export const toArticle = (query: { data: GetPostBySlugQuery }): Article => {
7270
title: query.data.postBy?.title || '',
7371
publishDate: query.data.postBy?.date || '',
7472
author: {
73+
slug: '',
7574
name: query.data.postBy?.author?.node.name || '',
7675
description: query.data.postBy?.author?.node.description || '',
7776
avatarUrl: query.data.postBy?.author?.node?.avatar?.url || '',

libs/blog-bff/articles/src/lib/api.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,54 @@
11
import { Hono } from 'hono';
22
import { wpClientMw } from '@angular-love/util-wp';
3-
import { toArticlePreviewList, toArticle } from './mappers';
3+
import { toArticle, toArticlePreviewList } from './mappers';
44
import { WPPostDetailsDto, WPPostDto } from './dtos';
5+
import {
6+
ArrayResponse,
7+
ArticlePreview,
8+
} from '@angular-love/contracts/articles';
59

610
const app = new Hono();
711

12+
const defaultQuery = {
13+
skip: '0',
14+
take: '10',
15+
lang: 'en',
16+
};
17+
818
app.get('/', wpClientMw, async (c) => {
19+
const queryParams = c.req.query();
20+
21+
const take = queryParams.take || defaultQuery.take;
22+
const skip = queryParams.skip || defaultQuery.skip;
23+
const page = Math.floor(Number(skip) / Number(take)) + 1;
24+
25+
const query: Record<string, string | number> = {
26+
lang: queryParams.lang || defaultQuery.lang,
27+
per_page: take,
28+
page: page,
29+
};
30+
31+
if (queryParams.authorSlug) {
32+
const authorResult = await c.var.wpClient.get<{ id }[]>('users', {
33+
slug: queryParams.authorSlug,
34+
_fields: 'id',
35+
});
36+
const id = authorResult.data[0]?.id;
37+
if (id) {
38+
query.author = id;
39+
}
40+
}
41+
942
const result = await c.var.wpClient.get<WPPostDto[]>('posts', {
10-
lang: 'en',
11-
per_page: '10',
43+
...query,
1244
_fields:
1345
'slug,title.rendered,author,excerpt.rendered,date,featured_image_url,author_details.name,author_details.avatar_url,author_details.slug,acf',
1446
});
1547

16-
return c.json(toArticlePreviewList(result));
48+
return c.json(<ArrayResponse<ArticlePreview>>{
49+
data: toArticlePreviewList(result.data),
50+
total: Number(result.headers.get('x-wp-total')),
51+
});
1752
});
1853

1954
app.get('/:slug', wpClientMw, async (c) => {
@@ -25,7 +60,7 @@ app.get('/:slug', wpClientMw, async (c) => {
2560
'slug,title.rendered,author,content.rendered,date,featured_image_url,author_details,acf',
2661
});
2762

28-
return c.json(toArticle(result[0]));
63+
return c.json(toArticle(result.data[0]));
2964
});
3065

3166
export default app;

libs/blog-bff/articles/src/lib/mappers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const toArticlePreviewList = (data: WPPostDto[]): ArticlePreview[] => {
1717
featuredImageUrl: node.featured_image_url || '',
1818
publishDate: new Date(node.date || '').toISOString(),
1919
author: {
20+
slug: node.author_details.slug || '',
2021
name: node.author_details.name || '',
2122
avatarUrl: node.author_details.avatar_url || '',
2223
},
@@ -63,6 +64,7 @@ export const toArticle = (dto?: WPPostDetailsDto): Article => {
6364
title: dto?.title.rendered || '',
6465
publishDate: dto?.date || '',
6566
author: {
67+
slug: dto?.author_details.slug || '',
6668
name: dto?.author_details.name || '',
6769
description: dto?.author_details.description || '',
6870
avatarUrl: dto?.author_details?.avatar_url || '',

libs/blog-bff/authors/.eslintrc.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts"],
7+
"extends": [
8+
"plugin:@nx/angular",
9+
"plugin:@angular-eslint/template/process-inline-templates"
10+
],
11+
"rules": {
12+
"@angular-eslint/directive-selector": [
13+
"error",
14+
{
15+
"type": "attribute",
16+
"prefix": "al",
17+
"style": "camelCase"
18+
}
19+
],
20+
"@angular-eslint/component-selector": [
21+
"error",
22+
{
23+
"type": "element",
24+
"prefix": "al",
25+
"style": "kebab-case"
26+
}
27+
]
28+
}
29+
},
30+
{
31+
"files": ["*.html"],
32+
"extends": ["plugin:@nx/angular-template"],
33+
"rules": {}
34+
}
35+
]
36+
}

libs/blog-bff/authors/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# blog-bff-authors
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Running unit tests
6+
7+
Run `nx test blog-bff-authors` to execute the unit tests.

libs/blog-bff/authors/jest.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'blog-bff-authors',
4+
preset: '../../../jest.preset.js',
5+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
6+
coverageDirectory: '../../../coverage/libs/blog-bff/authors',
7+
transform: {
8+
'^.+\\.(ts|mjs|js|html)$': [
9+
'jest-preset-angular',
10+
{
11+
tsconfig: '<rootDir>/tsconfig.spec.json',
12+
stringifyContentPathRegex: '\\.(html|svg)$',
13+
},
14+
],
15+
},
16+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
17+
snapshotSerializers: [
18+
'jest-preset-angular/build/serializers/no-ng-attributes',
19+
'jest-preset-angular/build/serializers/ng-snapshot',
20+
'jest-preset-angular/build/serializers/html-comment',
21+
],
22+
};

libs/blog-bff/authors/project.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "blog-bff-authors",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/blog-bff/authors/src",
5+
"prefix": "al",
6+
"projectType": "library",
7+
"tags": [],
8+
"targets": {
9+
"test": {
10+
"executor": "@nx/jest:jest",
11+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
12+
"options": {
13+
"jestConfig": "libs/blog-bff/authors/jest.config.ts"
14+
}
15+
},
16+
"lint": {
17+
"executor": "@nx/eslint:lint"
18+
}
19+
}
20+
}

libs/blog-bff/authors/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as authors } from './lib/api';

libs/blog-bff/authors/src/lib/api.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Hono } from 'hono';
2+
import { wpClientMw } from '@angular-love/util-wp';
3+
import { toAuthor } from './mappers';
4+
import { WPAuthorDto } from './dtos';
5+
6+
const app = new Hono();
7+
8+
app.get('/:slug', wpClientMw, async (c) => {
9+
const slug = c.req.param('slug');
10+
11+
const result = await c.var.wpClient.get<WPAuthorDto[]>('users', {
12+
slug: slug,
13+
_fields: 'slug,name,description,avatar_urls',
14+
});
15+
16+
return c.json(toAuthor(result.data[0]));
17+
});
18+
19+
export default app;

libs/blog-bff/authors/src/lib/dtos.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface WPAuthorDto {
2+
name: string;
3+
slug: string;
4+
avatar_urls: Record<string, string>;
5+
description: string;
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { WPAuthorDto } from './dtos';
2+
import { Author } from '@angular-love/blog/contracts/authors';
3+
4+
export const toAuthor = (dto: WPAuthorDto): Author => {
5+
return {
6+
slug: dto.slug,
7+
name: dto.name,
8+
description: dto.description,
9+
avatarUrl:
10+
Object.entries(dto.avatar_urls).find(([, url]) =>
11+
url.includes('96'),
12+
)?.[1] || Object.values(dto.avatar_urls)[0],
13+
position: 'to do add position',
14+
};
15+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
2+
globalThis.ngJest = {
3+
testEnvironmentOptions: {
4+
errorOnUnknownElements: true,
5+
errorOnUnknownProperties: true,
6+
},
7+
};
8+
import 'jest-preset-angular/setup-jest';

libs/blog-bff/authors/tsconfig.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"useDefineForClassFields": false,
5+
"forceConsistentCasingInFileNames": true,
6+
"strict": true,
7+
"noImplicitOverride": true,
8+
"noPropertyAccessFromIndexSignature": true,
9+
"noImplicitReturns": true,
10+
"noFallthroughCasesInSwitch": true
11+
},
12+
"files": [],
13+
"include": [],
14+
"references": [
15+
{
16+
"path": "./tsconfig.lib.json"
17+
},
18+
{
19+
"path": "./tsconfig.spec.json"
20+
}
21+
],
22+
"extends": "../../../tsconfig.base.json",
23+
"angularCompilerOptions": {
24+
"enableI18nLegacyMessageIdFormat": false,
25+
"strictInjectionParameters": true,
26+
"strictInputAccessModifiers": true,
27+
"strictTemplates": true
28+
}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../../dist/out-tsc",
5+
"declaration": true,
6+
"declarationMap": true,
7+
"inlineSources": true,
8+
"types": []
9+
},
10+
"exclude": [
11+
"src/**/*.spec.ts",
12+
"src/test-setup.ts",
13+
"jest.config.ts",
14+
"src/**/*.test.ts"
15+
],
16+
"include": ["src/**/*.ts"]
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "../../../dist/out-tsc",
5+
"module": "commonjs",
6+
"target": "es2016",
7+
"types": ["jest", "node"]
8+
},
9+
"files": ["src/test-setup.ts"],
10+
"include": [
11+
"jest.config.ts",
12+
"src/**/*.test.ts",
13+
"src/**/*.spec.ts",
14+
"src/**/*.d.ts"
15+
]
16+
}

libs/blog-bff/shared/util-wp/src/lib/wp-client.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@ import type { StatusCode } from 'hono/utils/http-status';
33

44
type FetchConfig = Partial<Pick<RequestInit, 'method' | 'headers' | 'body'>>;
55

6+
export type WPRespone<T> = {
7+
data: T;
8+
headers: Headers;
9+
};
10+
611
export class WPRestClient {
712
constructor(
813
protected readonly baseUrl: string,
914
protected readonly fetchConfig: FetchConfig,
1015
) {}
1116

12-
get<T>(url: string, params: Record<string, string>): Promise<T> {
17+
get<T>(url: string, params: Record<string, string>): Promise<WPRespone<T>> {
1318
const searchParams = new URLSearchParams(params);
1419
return this.request(`${url}?${searchParams.toString()}`, { method: 'GET' });
1520
}
1621

1722
private async request<T>(
1823
url: string,
1924
{ body, headers, method }: FetchConfig = {},
20-
): Promise<T> {
25+
): Promise<WPRespone<T>> {
2126
const request = await fetch(`${this.baseUrl}/wp-json/wp/v2/${url}`, {
2227
...this.fetchConfig,
2328
method: method ?? 'GET',
@@ -35,6 +40,9 @@ export class WPRestClient {
3540
);
3641
}
3742

38-
return request.json<T>();
43+
return {
44+
data: await request.json(),
45+
headers: request.headers,
46+
};
3947
}
4048
}

libs/blog-contracts/articles/src/lib/articles.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface ArticlePreview {
55
featuredImageUrl: string;
66
publishDate: string;
77
author: {
8+
slug: string;
89
name: string;
910
avatarUrl: string;
1011
};
@@ -15,8 +16,15 @@ export interface Article {
1516
content: string;
1617
publishDate: string;
1718
author: {
19+
slug: string;
1820
name: string;
1921
avatarUrl: string;
2022
description: string;
2123
};
2224
}
25+
26+
//TODO: move shared contract lib
27+
export interface ArrayResponse<T> {
28+
data: T[];
29+
total: number;
30+
}

0 commit comments

Comments
 (0)