diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..d8c9619 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,7 @@ +# Rename this file to ".env" and put your datas +PROXY_URL= +NEKO_JSON_URL= +NEKO_URL= +HOST= +PORT= +JWT_SECRET= diff --git a/packages/api/package.json b/apps/api/package.json similarity index 52% rename from packages/api/package.json rename to apps/api/package.json index f0ddb34..7d238f7 100644 --- a/packages/api/package.json +++ b/apps/api/package.json @@ -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" } } diff --git a/apps/api/prisma/migrations/20240403165832_init/migration.sql b/apps/api/prisma/migrations/20240403165832_init/migration.sql new file mode 100644 index 0000000..5a6d7e7 --- /dev/null +++ b/apps/api/prisma/migrations/20240403165832_init/migration.sql @@ -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"); diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/apps/api/prisma/migrations/migration_lock.toml @@ -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" \ No newline at end of file diff --git a/packages/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma similarity index 90% rename from packages/api/prisma/schema.prisma rename to apps/api/prisma/schema.prisma index 29a3e9a..07e4bce 100644 --- a/packages/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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? } diff --git a/packages/api/src/app.ts b/apps/api/src/app.ts similarity index 58% rename from packages/api/src/app.ts rename to apps/api/src/app.ts index e1cb4c9..ebceab8 100644 --- a/packages/api/src/app.ts +++ b/apps/api/src/app.ts @@ -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(); // 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, + }); } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..ba8aeee --- /dev/null +++ b/apps/api/src/index.ts @@ -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); + } +}); diff --git a/apps/api/src/routers/animeRouter.ts b/apps/api/src/routers/animeRouter.ts new file mode 100644 index 0000000..bc9e6e8 --- /dev/null +++ b/apps/api/src/routers/animeRouter.ts @@ -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; + + }, + ); +} diff --git a/apps/api/src/services/animeService.ts b/apps/api/src/services/animeService.ts new file mode 100644 index 0000000..ff76ddb --- /dev/null +++ b/apps/api/src/services/animeService.ts @@ -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) + } +} diff --git a/apps/api/src/services/cacheService.ts b/apps/api/src/services/cacheService.ts new file mode 100644 index 0000000..422ee17 --- /dev/null +++ b/apps/api/src/services/cacheService.ts @@ -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 { + const stringValue = JSON.stringify(value); + await this.redisClient.setEx(key, ttl, stringValue); + } + + async getCache(key: string): Promise { + const cachedValue = await this.redisClient.get(key); + if (!cachedValue) return undefined; + + return JSON.parse(cachedValue) as T; + } +} \ No newline at end of file diff --git a/apps/api/src/types/animeTypes.ts b/apps/api/src/types/animeTypes.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/src/types/routeTypes.ts b/apps/api/src/types/routeTypes.ts new file mode 100644 index 0000000..a199668 --- /dev/null +++ b/apps/api/src/types/routeTypes.ts @@ -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 \ No newline at end of file diff --git a/apps/api/src/utils/envUtils.ts b/apps/api/src/utils/envUtils.ts new file mode 100644 index 0000000..557569f --- /dev/null +++ b/apps/api/src/utils/envUtils.ts @@ -0,0 +1,5 @@ +export function getEnv(key: string): string { + const value = Bun.env[key] + if (!value) throw `Missing ${key} env variable` + return value +} diff --git a/apps/api/src/utils/fetchUtils.ts b/apps/api/src/utils/fetchUtils.ts new file mode 100644 index 0000000..e331578 --- /dev/null +++ b/apps/api/src/utils/fetchUtils.ts @@ -0,0 +1,8 @@ +export async function fetchType( + url: string, + type: "json" | "text", +): Promise { + const response = await fetch(url); + if (!response.ok) throw Error("Error fetching the URL"); + return await response[type](); +} diff --git a/packages/api/tsconfig.json b/apps/api/tsconfig.json similarity index 100% rename from packages/api/tsconfig.json rename to apps/api/tsconfig.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..3c75881 --- /dev/null +++ b/biome.json @@ -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" + } + } + } +} diff --git a/bun.lockb b/bun.lockb index b7772c2..bb219de 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5d13ac9 --- /dev/null +++ b/docker-compose.yml @@ -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' diff --git a/package.json b/package.json index faa7ad9..c7b0b57 100644 --- a/package.json +++ b/package.json @@ -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" @@ -14,6 +17,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "@sinclair/typebox": "^0.32.20", "prisma": "^5.12.0" } } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index bd69129..0000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {app} from "@/app" - -const FASTIFY_PORT = Number(Bun.env.PORT) || 3000; - -app.listen({ port: FASTIFY_PORT }); \ No newline at end of file diff --git a/packages/api/src/routers/animeRouter.ts b/packages/api/src/routers/animeRouter.ts deleted file mode 100644 index 07eff0d..0000000 --- a/packages/api/src/routers/animeRouter.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { FastifyInstance } from "fastify"; -import type { IReply } from "@gazes/types"; -import type { RouteOptions } from "@/types/routeTypes"; - -export default async function (app: FastifyInstance, opts: RouteOptions) { - - app.get<{ Reply: IReply }>("/animes", (request, reply) => { - - }); - -} diff --git a/packages/api/src/services/animeService.ts b/packages/api/src/services/animeService.ts deleted file mode 100644 index 5606f94..0000000 --- a/packages/api/src/services/animeService.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class AnimeService { - -} \ No newline at end of file diff --git a/packages/api/src/types/routeTypes.ts b/packages/api/src/types/routeTypes.ts deleted file mode 100644 index fc664ec..0000000 --- a/packages/api/src/types/routeTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Route-related types and interfaces below - -// Define a type for route options, specifying the structure of objects passed to routes. -export type RouteOptions = { - prismaClient: unknown; - redisClient: unknown; -} \ No newline at end of file