Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move articles endpoint from ./data to ./api #564

Merged
merged 5 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ If you use VSCode, please make sure to have a `.vscode/settings.json` file with
```json
{
"prettier.configPath": "packages/tooling/.prettierrc",
"eslint.options": { "overrideConfigFile": "packages/tooling/.eslintrc.json" }
"eslint.options": { "overrideConfigFile": "packages/tooling/.eslintrc.json" },
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
```

Expand Down
16 changes: 6 additions & 10 deletions api/src/app/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { LanguageEntity } from "@dzcode.io/models/dist/language";
import { GetArticleResponseDto, GetArticlesResponseDto } from "src/article/types";
import { GetContributionsResponseDto } from "src/contribution/types";
import { GetContributorsResponseDto } from "src/contributor/types";
import { GetUserResponseDto } from "src/github-user/types";
import { GetMilestonesResponseDto } from "src/milestone/types";
import { GetProjectsResponseDto } from "src/project/types";
import { GetTeamResponseDto } from "src/team/types";

import { Article, Document } from "./types/legacy";
import { Document } from "./types/legacy";

// @TODO-ZM: remove old endpoints
export interface Endpoints {
"data:articles/list.c.json": {
response: Pick<Article, "title" | "slug">[];
query: [["language", LanguageEntity["code"]]];
"api:Articles": {
response: GetArticlesResponseDto;
};
"data:articles/:slug.json": {
response: Article;
"api:Articles/:slug": {
response: GetArticleResponseDto;
params: { slug: string };
query: [["language", LanguageEntity["code"]]];
};
"data:articles/top-articles.c.json": {
response: Article[]; // TODO-ZM: should be: Pick<Article, "title" | "slug">[] instead
};
"data:documentation/list.c.json": {
response: Pick<Document, "title" | "slug">[];
Expand Down
2 changes: 2 additions & 0 deletions api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fsConfig } from "@dzcode.io/utils/dist/config";
import * as Sentry from "@sentry/node";
import { Application } from "express";
import { createExpressServer, RoutingControllersOptions, useContainer } from "routing-controllers";
import { ArticleController } from "src/article/controller";
import { ConfigService } from "src/config/service";
import { ContributionController } from "src/contribution/controller";
import { ContributorController } from "src/contributor/controller";
Expand Down Expand Up @@ -48,6 +49,7 @@ export const routingControllersOptions: RoutingControllersOptions = {
GithubController,
MilestoneController,
ProjectController,
ArticleController,
],
middlewares: [
SentryRequestHandlerMiddleware,
Expand Down
101 changes: 101 additions & 0 deletions api/src/article/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Controller, Get, Param } from "routing-controllers";
import { OpenAPI, ResponseSchema } from "routing-controllers-openapi";
import { GithubUser } from "src/app/types/legacy";
import { DataService } from "src/data/service";
import { GithubService } from "src/github/service";
import { Service } from "typedi";

import { GetArticleResponseDto, GetArticlesResponseDto } from "./types";

@Service()
@Controller("/Articles")
export class ArticleController {
constructor(
private readonly githubService: GithubService,
private readonly dataService: DataService,
) {}

@Get("/")
@OpenAPI({
summary: "Return list of all articles",
})
@ResponseSchema(GetArticlesResponseDto)
public async getArticles(): Promise<GetArticlesResponseDto> {
// get articles from /data folder:
const articles = await this.dataService.listArticles();

return {
articles,
};
}

@Get("/:slug")
@OpenAPI({
summary: "Return info about a single article",
})
@ResponseSchema(GetArticleResponseDto)
public async getArticle(@Param("slug") slug: string): Promise<GetArticleResponseDto> {
// get articles from /data folder:
const { ...article } = await this.dataService.getArticle(slug);

// get authors and contributors info from github:
const authors = await Promise.all(
article.authors.map(async (author) => {
const githubUser = await this.githubService.getUser({ username: author });
return {
id: `github/${githubUser.id}`,
name: githubUser.login,
link: githubUser.html_url,
image: githubUser.avatar_url,
};
}),
);

const contributorsBatches = await Promise.all([
// current place for data:
this.githubService.listContributors({
owner: "dzcode-io",
repository: "dzcode.io",
path: `data/models/articles/${slug}`,
}),
// also check old place for data, to not lose contribution effort:
this.githubService.listContributors({
owner: "dzcode-io",
repository: "dzcode.io",
path: `data/articles/${slug}`,
}),
]);

// filter and sort contributors:
const uniqUsernames: Record<string, number> = {};
const contributors: GetArticleResponseDto["article"]["contributors"] = [
...contributorsBatches[0],
...contributorsBatches[1],
]
.reduce<GithubUser[]>((pV, cV) => {
if (uniqUsernames[cV.login]) {
uniqUsernames[cV.login]++;
return pV;
} else {
uniqUsernames[cV.login] = 1;
return [...pV, cV];
}
}, [])
.sort((a, b) => uniqUsernames[b.login] - uniqUsernames[a.login])
.map((contributor) => ({
id: `github/${contributor.id}`,
name: contributor.login,
link: contributor.html_url,
image: contributor.avatar_url,
}))
.filter(({ id }) => !authors.find((author) => author.id === id));

return {
article: {
...article,
authors,
contributors,
},
};
}
}
16 changes: 16 additions & 0 deletions api/src/article/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Model } from "@dzcode.io/models/dist/_base";
import { ArticleEntity, ArticleInfoEntity } from "@dzcode.io/models/dist/article";
import { Type } from "class-transformer";
import { ValidateNested } from "class-validator";
import { GeneralResponseDto } from "src/app/types";

export class GetArticlesResponseDto extends GeneralResponseDto {
@ValidateNested({ each: true })
@Type(() => ArticleInfoEntity)
articles!: Model<ArticleInfoEntity>[];
}

export class GetArticleResponseDto extends GeneralResponseDto {
@ValidateNested()
article!: Model<ArticleEntity, "authors" | "contributors">;
}
28 changes: 22 additions & 6 deletions api/src/data/service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { getCollection } from "@dzcode.io/data/dist/get/collection";
import { getEntry } from "@dzcode.io/data/dist/get/entry";
import { join } from "path";
import { ConfigService } from "src/config/service";
import { Service } from "typedi";

import { ProjectReferenceEntity } from "./types";
import { DataArticleEntity, DataProjectEntity } from "./types";

@Service()
export class DataService {
constructor(private readonly configService: ConfigService) {}

public listProjects = async (): Promise<ProjectReferenceEntity[]> => {
const projects = getCollection<ProjectReferenceEntity>(
public listProjects = async (): Promise<DataProjectEntity[]> => {
const projects = getCollection<DataProjectEntity>(
this.dataModelsPath,
"projects-v2",
"list.json",
Expand All @@ -21,5 +19,23 @@ export class DataService {
return projects;
};

public listArticles = async (): Promise<Pick<DataArticleEntity, "slug" | "title">[]> => {
const articles = getCollection<DataArticleEntity>(this.dataModelsPath, "articles", "list.json");

if (articles === 404) throw new Error("Articles list not found");

const mappedArticles = articles.map(({ slug, title }) => ({ slug, title }));

return mappedArticles;
};

public getArticle = async (slug: string): Promise<DataArticleEntity> => {
const article = getEntry<DataArticleEntity>(this.dataModelsPath, `articles/${slug}`);

if (article === 404) throw new Error("Article not found");

return article;
};

private dataModelsPath = join(__dirname, "../../../data");
}
13 changes: 9 additions & 4 deletions api/src/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Model } from "@dzcode.io/models/dist/_base";
import { ArticleInfoEntity } from "@dzcode.io/models/dist/article";
import { ProjectEntity } from "@dzcode.io/models/dist/project";
import { RepositoryEntity } from "@dzcode.io/models/dist/repository";

export interface ProjectReferenceEntity extends Model<ProjectEntity> {
repositories: Model<RepositoryEntity>[];
}
export type DataProjectEntity = Model<ProjectEntity, "repositories">;

export type DataArticleEntity = Model<ArticleInfoEntity> & {
authors: string[];
description: string;
content: string;
image: string;
};
5 changes: 2 additions & 3 deletions mobile/src/redux/actions/articles-screen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetchV2 } from "src/utils/fetch";

export const fetchArticles = createAsyncThunk("articlesScreen/fetchArticles", async () => {
try {
const articles = await fetchV2("data:articles/list.c.json", {
const { articles } = await fetchV2("api:Articles", {
query: [["language", "en"]],
});
return articles;
Expand All @@ -16,9 +16,8 @@ export const fetchArticle = createAsyncThunk(
"articlesScreen/fetchArticle",
async (slug: string) => {
try {
const article = await fetchV2(`data:articles/:slug.json`, {
const { article } = await fetchV2(`api:Articles/:slug`, {
params: { slug },
query: [["language", "en"]],
});
return article;
} catch (error: any) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Endpoints } from "@dzcode.io/api/dist/app/endpoints";
import { createEntityAdapter } from "@reduxjs/toolkit";

type Article =
| Endpoints["data:articles/list.c.json"]["response"][number]
| Endpoints["data:articles/:slug.json"]["response"];
| Endpoints["api:Articles"]["response"]["articles"][number]
| Endpoints["api:Articles/:slug"]["response"]["article"];

export const articlesAdapter = createEntityAdapter<Article>({
selectId: (article) => article.slug,
Expand Down
4 changes: 3 additions & 1 deletion mobile/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
"compilerOptions": {
"strict": true,
"baseUrl": "."
}
},
"include": ["src"],
"exclude": ["src/_e2e-test"]
}
24 changes: 12 additions & 12 deletions packages/models/src/article/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ ArticleEntity {
"content": "test-content",
"contributors": [],
"description": "test-description",
"id": "learn/Getting_Started",
"image": "https://images.unsplash.com/photo-1520338661084-680395057c93?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=formatfit=crop&w=800&q=100",
"slug": "learn/Getting_Started",
"title": "Getting Started",
}
`;
Expand All @@ -22,8 +22,8 @@ ArticleEntity {
"content": "test-content",
"contributors": [],
"description": "test-description",
"id": "learn/Getting_Started",
"image": "https://images.unsplash.com/photo-1520338661084-680395057c93?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=formatfit=crop&w=800&q=100",
"slug": "learn/Getting_Started",
"title": "Getting Started",
}
`;
Expand All @@ -33,45 +33,45 @@ exports[`should show an error that matches snapshot when passing empty object: e
ValidationError {
"children": [],
"constraints": {
"isString": "id must be a string",
"isString": "image must be a string",
},
"property": "id",
"property": "image",
"target": ArticleEntity {},
"value": undefined,
},
ValidationError {
"children": [],
"constraints": {
"isString": "image must be a string",
"isString": "description must be a string",
},
"property": "image",
"property": "description",
"target": ArticleEntity {},
"value": undefined,
},
ValidationError {
"children": [],
"constraints": {
"isString": "title must be a string",
"isString": "content must be a string",
},
"property": "title",
"property": "content",
"target": ArticleEntity {},
"value": undefined,
},
ValidationError {
"children": [],
"constraints": {
"isString": "description must be a string",
"isString": "slug must be a string",
},
"property": "description",
"property": "slug",
"target": ArticleEntity {},
"value": undefined,
},
ValidationError {
"children": [],
"constraints": {
"isString": "content must be a string",
"isString": "title must be a string",
},
"property": "content",
"property": "title",
"target": ArticleEntity {},
"value": undefined,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/models/src/article/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ runDTOTestCases(
{
image:
"https://images.unsplash.com/photo-1520338661084-680395057c93?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=formatfit=crop&w=800&q=100",
id: "learn/Getting_Started",
slug: "learn/Getting_Started",
title: "Getting Started",
description: "test-description",
content: "test-content",
Expand Down
10 changes: 6 additions & 4 deletions packages/models/src/article/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import { IsString, ValidateNested } from "class-validator";
import { BaseEntity, Model } from "src/_base";
import { AccountEntity } from "src/account";

export class ArticleEntity extends BaseEntity {
export class ArticleInfoEntity extends BaseEntity {
@IsString()
id!: string;
slug!: string;

@IsString()
image!: string;
title!: string;
}

export class ArticleEntity extends ArticleInfoEntity {
@IsString()
title!: string;
image!: string;

@IsString()
description!: string;
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/contributor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IsString, IsUrl, ValidateNested } from "class-validator";
import { BaseEntity, Model } from "src/_base";
import { RepositoryReferenceEntity } from "src/repository-reference";

// @TODO-ZM: remove this in favour of AccountEntity
export class ContributorEntity extends BaseEntity {
@IsString()
id!: string;
Expand Down
Loading