Skip to content
This repository has been archived by the owner on Jun 12, 2024. It is now read-only.

Commit

Permalink
feat(api): 🚀 Add anime data handling and environment setup
Browse files Browse the repository at this point in the history
- Introduced `.env.example` for environment variable management.
- Moved and updated `package.json` within the `apps/api` directory, adding new dependencies and scripts for development and build processes.
- Created a new Prisma migration for the `Anime` table, outlining the structure for anime data storage.
- Developed new services and routes for fetching, caching, and serving anime data.
- Configured Docker Compose with PostgreSQL and Redis services to support local development.
- Enhanced the codebase with TypeScript configurations and utility functions for environment variable and fetch operations.
  • Loading branch information
l31-dev committed Apr 3, 2024
1 parent 155eceb commit b436bfd
Show file tree
Hide file tree
Showing 23 changed files with 242 additions and 32 deletions.
7 changes: 7 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Rename this file to ".env" and put your datas
PROXY_URL=
NEKO_JSON_URL=
NEKO_URL=
HOST=
PORT=
JWT_SECRET=
6 changes: 4 additions & 2 deletions packages/api/package.json → apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"name": "@gazes/api",
"scripts": {
"dev": "bun run src/index.ts",
"dev": "bun run --hot src/index.ts",
"build": "bun build src/index.ts --outdir dist/ --target bun"
},
"dependencies": {
"@fastify/type-provider-typebox": "^4.0.0",
"@gazes/types": "workspace:*",
"fastify": "^4.26.2",
"@gazes/types": "workspace:*"
"redis": "^4.6.13"
}
}
25 changes: 25 additions & 0 deletions apps/api/prisma/migrations/20240403165832_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "Anime" (
"id" INTEGER NOT NULL,
"titleOriginal" TEXT NOT NULL,
"titleEnglish" TEXT,
"titleRomanized" TEXT,
"titleFrench" TEXT,
"alternativeTitles" TEXT,
"mediaType" TEXT NOT NULL,
"airingStatus" TEXT NOT NULL,
"externalUrl" TEXT NOT NULL,
"genres" TEXT[],
"coverImageUrl" TEXT NOT NULL,
"popularityScore" DOUBLE PRECISION NOT NULL,
"averageScore" DOUBLE PRECISION NOT NULL,
"startYear" INTEGER NOT NULL,
"episodesCount" INTEGER,
"synopsis" TEXT,
"bannerImageUrl" TEXT,

CONSTRAINT "Anime_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Anime_id_key" ON "Anime"("id");
3 changes: 3 additions & 0 deletions apps/api/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ model Anime {
alternativeTitles String?
mediaType String
airingStatus String
popularityScore Float
externalUrl String
genres String[]
coverImageUrl String
popularityScore Float
averageScore Float
startYear Int
episodesCount Int?
synopsis String?
bannerImageUrl String?
}
17 changes: 15 additions & 2 deletions packages/api/src/app.ts → apps/api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import type { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { Glob } from "bun";
import fastify from "fastify";
import { join } from "node:path";
import { createClient, type RedisClientType } from "redis";
import { AnimeService } from "./services/animeService";
import { PrismaClient } from "prisma/prisma-client";

// Initialize a Fastify application instance with specific configurations.
export const app = fastify({
logger: true, // Enable loggin for the application
disableRequestLogging: true, // Disable request logging to avoid clutter in the log output
});
}).withTypeProvider<TypeBoxTypeProvider>();

// Synamically import and register router modules found by globbing thep roject directory for files
// with a ".router." in their names.
for await (const file of new Glob("**/*Router.*").scan(import.meta.dir)) {
const prismaClient = new PrismaClient();
const redisClient = (await createClient().connect()) as RedisClientType;

const animeService = new AnimeService(redisClient, prismaClient)
await animeService.updateDatabase()

// Register each found router module with the Fastify application.
// The router modules are expected to export a default function that defines routes.
app.register((await import(join(import.meta.dir, file))).default);
app.register((await import(join(import.meta.dir, file))).default, {
prismaClient,
redisClient,
});
}
10 changes: 10 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { app } from "@/app";

const { ADDRESS = "0.0.0.0", PORT = "3000" } = Bun.env;

app.listen({ port: Number.parseInt(PORT, 10), host: ADDRESS }, (error) => {
if (error) {
console.error(error);
process.exit(1);
}
});
21 changes: 21 additions & 0 deletions apps/api/src/routers/animeRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AnimeService } from "@/services/animeService";
import { AnimeListQuerystring, type RouteOptions } from "@/types/routeTypes";
import type { FastifyInstance } from "fastify";

export default async function (
app: FastifyInstance,
{ prismaClient, redisClient }: RouteOptions,
) {
const animeService = new AnimeService(redisClient, prismaClient);

app.get<{ Querystring: AnimeListQuerystring }>(
"/",
{
schema: { querystring: AnimeListQuerystring },
},
(request, response) => {
const { page = 1, title, genres, status, releaseDate } = request.query;

},
);
}
50 changes: 50 additions & 0 deletions apps/api/src/services/animeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { RedisClientType } from "redis";
import { CacheService } from "./cacheService";
import { fetchType } from "@/utils/fetchUtils";
import { getEnv } from "@/utils/envUtils";
import type { Anime, PrismaClient } from "@prisma/client";

const HOUR = 3.6e+6;

export class AnimeService {
private cacheService: CacheService;

constructor(
private redisClient: RedisClientType,
private prismaClient: PrismaClient,
) {
this.cacheService = new CacheService(redisClient);
}

public async updateDatabase() {
await this.prismaClient.anime.deleteMany({})
const animesList: any[] = await fetchType(getEnv("NEKO_JSON_URL"), "json")

const animesData: Anime[] = animesList.map(anime => ({
id: anime.id,
titleOriginal: anime.title,
titleEnglish: anime.title_english,
titleRomanized: anime.title_romanji,
titleFrench: anime.title_french,
alternativeTitles: anime.others,
mediaType: anime.type,
airingStatus: anime.type === "1" ? "en cours" : "finis",
popularityScore: anime.popularity,
externalUrl: anime.url,
genres: anime.genres,
coverImageUrl: anime.url_image,
averageScore: Number.parseFloat(anime.score),
startYear: Number.parseInt(anime.start_date_year),
episodesCount: Number.parseInt(anime.nb_eps),
bannerImageUrl: null,
synopsis: null
}))

await this.prismaClient.anime.createMany({
data: animesData,
skipDuplicates: true
})

setInterval(this.updateDatabase, HOUR)
}
}
19 changes: 19 additions & 0 deletions apps/api/src/services/cacheService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { RedisClientType } from "redis";

export class CacheService {
constructor(
private readonly redisClient: RedisClientType
) {}

async setCache(key: string, value: unknown, ttl = 3600): Promise<void> {
const stringValue = JSON.stringify(value);
await this.redisClient.setEx(key, ttl, stringValue);
}

async getCache<T>(key: string): Promise<T | undefined> {
const cachedValue = await this.redisClient.get(key);
if (!cachedValue) return undefined;

return JSON.parse(cachedValue) as T;
}
}
Empty file.
35 changes: 35 additions & 0 deletions apps/api/src/types/routeTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { PrismaClient } from "@prisma/client";
import { Type, type Static } from "@sinclair/typebox";
import type { RedisClientType } from "redis";

// Route-related types and interfaces below

export type RouteOptions = {
prismaClient: PrismaClient;
redisClient: RedisClientType;
};

export type SuccessResponse = {
data: unknown;
error: never;
};

export type ErrorResponse = {
data: never;
error: {
title: string;
detail: string;
};
};

export type Response = SuccessResponse|ErrorResponse

export const AnimeListQuerystring = Type.Object({
page: Type.Optional(Type.Number()),
title: Type.Optional(Type.String()),
genres: Type.Optional(Type.String()),
status: Type.Optional(Type.Number()),
releaseDate: Type.Optional(Type.Number()),
});

export type AnimeListQuerystring = Static<typeof AnimeListQuerystring>
5 changes: 5 additions & 0 deletions apps/api/src/utils/envUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function getEnv(key: string): string {
const value = Bun.env[key]
if (!value) throw `Missing ${key} env variable`
return value
}
8 changes: 8 additions & 0 deletions apps/api/src/utils/fetchUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export async function fetchType<T>(
url: string,
type: "json" | "text",
): Promise<T> {
const response = await fetch(url);
if (!response.ok) throw Error("Error fetching the URL");
return await response[type]();
}
File renamed without changes.
15 changes: 15 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.3/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny":"off"
}
}
}
}
Binary file modified bun.lockb
Binary file not shown.
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:

db:
image: postgres:alpine
restart: always
shm_size: 128mb
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: pg-dev
ports:
- "5432:5432"

redis:
image: 'redislabs/redismod'
ports:
- '6379:6379'
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "gazes",
"private": true,
"workspaces": ["packages/*"],
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"format": "biome format . --write",
"clean": "rm -rf node_modules packages/a/node_modules packages/b/node_modules bun.lockb packages/a/bun.lockb packages/b/bun.lockb"
Expand All @@ -14,6 +17,7 @@
"typescript": "^5.0.0"
},
"dependencies": {
"@sinclair/typebox": "^0.32.20",
"prisma": "^5.12.0"
}
}
5 changes: 0 additions & 5 deletions packages/api/src/index.ts

This file was deleted.

11 changes: 0 additions & 11 deletions packages/api/src/routers/animeRouter.ts

This file was deleted.

3 changes: 0 additions & 3 deletions packages/api/src/services/animeService.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/api/src/types/routeTypes.ts

This file was deleted.

2 comments on commit b436bfd

@maxchrr
Copy link
Contributor

@maxchrr maxchrr commented on b436bfd Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GNU-ify your project dude

@ncs-pl
Copy link

@ncs-pl ncs-pl commented on b436bfd Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.