diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..fd027960 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: local + hooks: + - id: ruff + name: ruff + language: system + entry: bash -c 'cd backend && uvx ruff check --fix; uvx ruff format' diff --git a/backend/alembic/versions/021f1bdc162b_add_categories_to_users.py b/backend/alembic/versions/021f1bdc162b_add_categories_to_users.py new file mode 100644 index 00000000..b7b6983d --- /dev/null +++ b/backend/alembic/versions/021f1bdc162b_add_categories_to_users.py @@ -0,0 +1,43 @@ +"""Add categories to users + +Revision ID: 021f1bdc162b +Revises: f3e847c3ee9d +Create Date: 2024-09-22 11:48:13.802703 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "021f1bdc162b" +down_revision: Union[str, None] = "f3e847c3ee9d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user_category", + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("category_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["category.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + ), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("user_category") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/a73902039c96_make_email_unique.py b/backend/alembic/versions/a73902039c96_make_email_unique.py new file mode 100644 index 00000000..dde3def2 --- /dev/null +++ b/backend/alembic/versions/a73902039c96_make_email_unique.py @@ -0,0 +1,30 @@ +"""Make email unique + +Revision ID: a73902039c96 +Revises: 021f1bdc162b +Create Date: 2024-09-22 11:56:00.470507 + +""" + +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "a73902039c96" +down_revision: Union[str, None] = "021f1bdc162b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, "user", ["email"]) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "user", type_="unique") + # ### end Alembic commands ### diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 07c6fc64..e6b88f12 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -28,4 +28,5 @@ build-backend = "hatchling.build" dev-dependencies = [ "alembic>=1.13.2", "alembic-postgresql-enum>=1.3.0", + "pre-commit>=3.8.0", ] diff --git a/backend/src/app.py b/backend/src/app.py index 50b348e9..ae76036f 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,6 +1,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from src.auth.router import router as auth_router +from src.categories.router import router as category_router +from src.profile.router import router as profile_router +from src.events.router import router as events_router from contextlib import asynccontextmanager import logging @@ -30,3 +33,6 @@ async def lifespan(app: FastAPI): ) server.include_router(auth_router) +server.include_router(category_router) +server.include_router(profile_router) +server.include_router(events_router) diff --git a/backend/src/auth/dependencies.py b/backend/src/auth/dependencies.py index 8eeaabf8..201483b5 100644 --- a/backend/src/auth/dependencies.py +++ b/backend/src/auth/dependencies.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone from fastapi import Cookie, Depends, HTTPException, Request, status from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, selectinload from src.common.database import engine from .models import User import jwt @@ -46,7 +46,11 @@ def get_password_hash(password: str): def authenticate_user(email: str, password: str): with Session(engine) as session: - user = session.scalars(select(User).where(User.email == email)).first() + user = session.scalars( + select(User) + .where(User.email == email) + .options(selectinload(User.categories)) + ).first() if not user: return False if not verify_password(password, user.hashed_password): @@ -90,11 +94,13 @@ async def get_current_user( payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) id = payload.get("sub") with Session(engine) as session: - staff = session.get(User, id) - if not staff: + user = session.scalar( + select(User).where(User.id == id).options(selectinload(User.categories)) + ) + if not user: raise InvalidTokenError() - return staff + return user except InvalidTokenError: raise credentials_exception diff --git a/backend/src/auth/models.py b/backend/src/auth/models.py index aae63bb8..c0385446 100644 --- a/backend/src/auth/models.py +++ b/backend/src/auth/models.py @@ -1,6 +1,8 @@ from enum import Enum -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import Column, ForeignKey, Table +from sqlalchemy.orm import Mapped, mapped_column, relationship from src.common.base import Base +from src.events.models import Category class AccountType(str, Enum): @@ -8,10 +10,20 @@ class AccountType(str, Enum): GOOGLE = "google" +user_category_table = Table( + "user_category", + Base.metadata, + Column("user_id", ForeignKey("user.id")), + Column("category_id", ForeignKey("category.id")), +) + + class User(Base): __tablename__ = "user" id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] + email: Mapped[str] = mapped_column(unique=True) hashed_password: Mapped[str] account_type: Mapped[AccountType] + + categories: Mapped[list[Category]] = relationship(secondary=user_category_table) diff --git a/backend/src/auth/router.py b/backend/src/auth/router.py index b3f06401..510a6110 100644 --- a/backend/src/auth/router.py +++ b/backend/src/auth/router.py @@ -6,6 +6,7 @@ from fastapi.security import OAuth2PasswordRequestForm import httpx from sqlalchemy import select +from sqlalchemy.orm import selectinload from src.auth.utils import create_token from src.common.constants import ( GOOGLE_CLIENT_ID, @@ -23,7 +24,7 @@ ) from .models import AccountType, User -router = APIRouter(prefix="/auth") +router = APIRouter(prefix="/auth", tags=["auth"]) ####################### # username & password # @@ -97,7 +98,9 @@ def auth_google( ).json() # 2. Check for existing user. email = user_info["email"] - user = session.scalars(select(User).where(User.email == email)).first() + user = session.scalars( + select(User).where(User.email == email).options(selectinload(User.categories)) + ).first() if user: if user.account_type == AccountType.NORMAL: raise HTTPException( diff --git a/backend/src/auth/schemas.py b/backend/src/auth/schemas.py index 9eefd4f2..90d0c726 100644 --- a/backend/src/auth/schemas.py +++ b/backend/src/auth/schemas.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, ConfigDict, EmailStr, Field +from src.categories.schemas import CategoryDTO class UserPublic(BaseModel): @@ -7,6 +8,8 @@ class UserPublic(BaseModel): id: int email: EmailStr + categories: list[CategoryDTO] + class Token(BaseModel): access_token: str diff --git a/backend/src/categories/router.py b/backend/src/categories/router.py new file mode 100644 index 00000000..1b203f75 --- /dev/null +++ b/backend/src/categories/router.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy import select +from src.events.models import Category +from src.categories.schemas import CategoryDTO +from src.common.dependencies import get_session + + +router = APIRouter(prefix="/categories", tags=["categories"]) + + +@router.get("/") +def get_categories(session=Depends(get_session)) -> list[CategoryDTO]: + categories = session.scalars(select(Category)) + category_dtos = [CategoryDTO.model_validate(category) for category in categories] + return category_dtos diff --git a/backend/src/categories/schemas.py b/backend/src/categories/schemas.py new file mode 100644 index 00000000..b69a41e1 --- /dev/null +++ b/backend/src/categories/schemas.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, ConfigDict + + +class CategoryDTO(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + name: str diff --git a/backend/src/common/database.py b/backend/src/common/database.py index bb069480..e91e6ae8 100644 --- a/backend/src/common/database.py +++ b/backend/src/common/database.py @@ -2,4 +2,4 @@ from src.common.constants import DATABASE_URL -engine = create_engine(DATABASE_URL) +engine = create_engine(DATABASE_URL, echo=True) diff --git a/backend/src/events/router.py b/backend/src/events/router.py new file mode 100644 index 00000000..823773e3 --- /dev/null +++ b/backend/src/events/router.py @@ -0,0 +1,53 @@ +from typing import Annotated +from fastapi import APIRouter, Depends, Query +from sqlalchemy import select +from sqlalchemy.orm import selectinload +from src.auth.dependencies import get_current_user +from src.auth.models import User +from src.events.models import Category, Event +from src.common.dependencies import get_session +from src.events.schemas import EventDTO, EventIndexResponse + + +router = APIRouter(prefix="/events", tags=["events"]) + + +@router.get("/") +def get_events( + _: Annotated[User, Depends(get_current_user)], + session=Depends(get_session), + category_ids: Annotated[list[int] | None, Query()] = None, + limit: int | None = None, + offset: int | None = None, +) -> EventIndexResponse: + query = select(Event.id).distinct() + if category_ids: + query = query.join(Event.categories.and_(Category.id.in_(category_ids))) + relevant_ids = [id for id in session.scalars(query)] + + total_count = len(relevant_ids) + event_query = ( + select(Event) + .options(selectinload(Event.categories)) + .where(Event.id.in_(relevant_ids)) + ) + if limit is not None: + event_query = event_query.limit(limit) + if offset is not None: + event_query = event_query.offset(offset) + + events = list(session.scalars(event_query)) + return EventIndexResponse(total_count=total_count, count=len(events), data=events) + + +@router.get("/:id") +def get_event( + id: int, + _: Annotated[User, Depends(get_current_user)], + session=Depends(get_session), +) -> EventDTO: + event = session.scalar( + select(Event).where(Event.id == id).options(selectinload(Event.categories)) + ) + # TODO: link to more models, give more data + return event diff --git a/backend/src/events/schemas.py b/backend/src/events/schemas.py new file mode 100644 index 00000000..ff3f93f6 --- /dev/null +++ b/backend/src/events/schemas.py @@ -0,0 +1,21 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict +from src.categories.schemas import CategoryDTO + + +class EventDTO(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: int + title: str + description: str + analysis: str + is_singapore: bool + date: datetime + + categories: list[CategoryDTO] + + +class EventIndexResponse(BaseModel): + total_count: int + count: int + data: list[EventDTO] diff --git a/backend/src/profile/router.py b/backend/src/profile/router.py new file mode 100644 index 00000000..fb72c4a5 --- /dev/null +++ b/backend/src/profile/router.py @@ -0,0 +1,32 @@ +from typing import Annotated +from fastapi import APIRouter, Depends +from sqlalchemy import select +from src.auth.dependencies import get_current_user +from src.auth.models import User +from src.auth.schemas import UserPublic +from src.events.models import Category +from src.common.dependencies import get_session +from src.profile.schemas import ProfileUpdate + + +router = APIRouter(prefix="/profile", tags=["profile"]) + + +@router.put("/") +def update_profile( + data: ProfileUpdate, + user: Annotated[User, Depends(get_current_user)], + session=Depends(get_session), +) -> UserPublic: + categories = session.scalars( + select(Category).where(Category.id.in_(data.category_ids)) + ).all() + + user = session.get(User, user.id) + user.categories = categories + + session.add(user) + session.commit() + session.refresh(user) + + return user diff --git a/backend/src/profile/schemas.py b/backend/src/profile/schemas.py new file mode 100644 index 00000000..12a71008 --- /dev/null +++ b/backend/src/profile/schemas.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ProfileUpdate(BaseModel): + category_ids: list[int] diff --git a/backend/uv.lock b/backend/uv.lock index 19f52892..f9fdba32 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -138,6 +138,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + [[package]] name = "click" version = "8.1.7" @@ -188,6 +197,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/c6/c09cee6968add5ff868525c3815e5dccc0e3c6e89eec58dc9135d3c40e88/cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", size = 3070445 }, ] +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + [[package]] name = "dnspython" version = "2.6.1" @@ -252,6 +270,15 @@ standard = [ { name = "uvicorn", extra = ["standard"] }, ] +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + [[package]] name = "greenlet" version = "3.1.0" @@ -331,6 +358,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, ] +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 }, +] + [[package]] name = "idna" version = "3.10" @@ -414,6 +450,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + [[package]] name = "passlib" version = "1.7.4" @@ -423,6 +468,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, ] +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pre-commit" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 }, +] + [[package]] name = "psycopg2" version = "2.9.9" @@ -647,6 +717,7 @@ dependencies = [ dev = [ { name = "alembic" }, { name = "alembic-postgresql-enum" }, + { name = "pre-commit" }, ] [package.metadata] @@ -670,6 +741,7 @@ requires-dist = [ dev = [ { name = "alembic", specifier = ">=1.13.2" }, { name = "alembic-postgresql-enum", specifier = ">=1.3.0" }, + { name = "pre-commit", specifier = ">=3.8.0" }, ] [[package]] @@ -746,6 +818,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/52/deb4be09060637ef4752adaa0b75bf770c20c823e8108705792f99cd4a6f/uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00", size = 4115980 }, ] +[[package]] +name = "virtualenv" +version = "20.26.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/4c/66ce54c8736ff164e85117ca36b02a1e14c042a6963f85eeda82664fda4e/virtualenv-20.26.5.tar.gz", hash = "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4", size = 9371932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/1d/e1a44fdd6d30829ba21fc58b5d98a67e7aae8f4165f11d091e53aec12560/virtualenv-20.26.5-py3-none-any.whl", hash = "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6", size = 5999288 }, +] + [[package]] name = "watchfiles" version = "0.24.0" diff --git a/frontend/client/core/index.ts b/frontend/client/core/index.ts index 76dca954..afe16e6a 100644 --- a/frontend/client/core/index.ts +++ b/frontend/client/core/index.ts @@ -1,9 +1,8 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { AxiosError } from "axios"; -import axios from "axios"; +import type { AxiosError } from 'axios'; +import axios from 'axios'; -import type { Client, Config, RequestOptions } from "./types"; -import { createConfig, getUrl, mergeConfigs, mergeHeaders } from "./utils"; +import type { Client, Config, RequestOptions } from './types'; +import { createConfig, getUrl, mergeConfigs, mergeHeaders } from './utils'; export const createClient = (config: Config): Client => { let _config = mergeConfigs(createConfig(), config); @@ -24,7 +23,7 @@ export const createClient = (config: Config): Client => { }; // @ts-expect-error - const request: Client["request"] = async (options) => { + const request: Client['request'] = async (options) => { const opts: RequestOptions = { ..._config, ...options, @@ -52,7 +51,7 @@ export const createClient = (config: Config): Client => { let { data } = response; - if (opts.responseType === "json" && opts.responseTransformer) { + if (opts.responseType === 'json' && opts.responseTransformer) { data = await opts.responseTransformer(data); } @@ -72,15 +71,15 @@ export const createClient = (config: Config): Client => { }; return { - delete: (options) => request({ ...options, method: "delete" }), - get: (options) => request({ ...options, method: "get" }), + delete: (options) => request({ ...options, method: 'delete' }), + get: (options) => request({ ...options, method: 'get' }), getConfig, - head: (options) => request({ ...options, method: "head" }), + head: (options) => request({ ...options, method: 'head' }), instance, - options: (options) => request({ ...options, method: "options" }), - patch: (options) => request({ ...options, method: "patch" }), - post: (options) => request({ ...options, method: "post" }), - put: (options) => request({ ...options, method: "put" }), + options: (options) => request({ ...options, method: 'options' }), + patch: (options) => request({ ...options, method: 'patch' }), + post: (options) => request({ ...options, method: 'post' }), + put: (options) => request({ ...options, method: 'put' }), request, setConfig, } as Client; diff --git a/frontend/client/core/types.ts b/frontend/client/core/types.ts index 18a974f1..98db41f5 100644 --- a/frontend/client/core/types.ts +++ b/frontend/client/core/types.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { AxiosError, AxiosInstance, @@ -6,14 +5,14 @@ import type { AxiosResponse, AxiosStatic, CreateAxiosDefaults, -} from "axios"; +} from 'axios'; -import type { BodySerializer } from "./utils"; +import type { BodySerializer } from './utils'; type OmitKeys = Pick>; export interface Config - extends Omit { + extends Omit { /** * Axios implementation. You can use this option to provide a custom * Axios instance. @@ -38,7 +37,7 @@ export interface Config * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} */ headers?: - | CreateAxiosDefaults["headers"] + | CreateAxiosDefaults['headers'] | Record< string, | string @@ -55,15 +54,15 @@ export interface Config * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ method?: - | "connect" - | "delete" - | "get" - | "head" - | "options" - | "patch" - | "post" - | "put" - | "trace"; + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; /** * A function for transforming response data before it's returned to the * caller function. This is an ideal place to post-process server data, @@ -100,7 +99,7 @@ type MethodFn = < TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, "method">, + options: Omit, 'method'>, ) => RequestResult; type RequestFn = < @@ -108,8 +107,8 @@ type RequestFn = < TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, "method"> & - Pick>, "method">, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; export interface Client { @@ -128,12 +127,12 @@ export interface Client { export type RequestOptions = RequestOptionsBase & Config & { - headers: AxiosRequestConfig["headers"]; + headers: AxiosRequestConfig['headers']; }; type OptionsBase = Omit< RequestOptionsBase, - "url" + 'url' > & { /** * You can provide a client instance returned by `createClient()` instead of @@ -148,12 +147,12 @@ export type Options< ThrowOnError extends boolean = boolean, > = T extends { body?: any } ? T extends { headers?: any } - ? OmitKeys, "body" | "headers"> & T - : OmitKeys, "body"> & + ? OmitKeys, 'body' | 'headers'> & T + : OmitKeys, 'body'> & T & - Pick, "headers"> + Pick, 'headers'> : T extends { headers?: any } - ? OmitKeys, "headers"> & + ? OmitKeys, 'headers'> & T & - Pick, "body"> + Pick, 'body'> : OptionsBase & T; diff --git a/frontend/client/core/utils.ts b/frontend/client/core/utils.ts index 309135e0..2d3e8a8e 100644 --- a/frontend/client/core/utils.ts +++ b/frontend/client/core/utils.ts @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import type { Config } from "./types"; +import type { Config } from './types'; interface PathSerializer { path: Record; @@ -9,10 +7,10 @@ interface PathSerializer { const PATH_PARAM_RE = /\{[^{}]+\}/g; -type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; -type MatrixStyle = "label" | "matrix" | "simple"; +type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +type MatrixStyle = 'label' | 'matrix' | 'simple'; type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type ObjectStyle = "form" | "deepObject"; +type ObjectStyle = 'form' | 'deepObject'; type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; export type BodySerializer = (body: any) => any; @@ -42,12 +40,12 @@ const serializePrimitiveParam = ({ value, }: SerializePrimitiveParam) => { if (value === undefined || value === null) { - return ""; + return ''; } - if (typeof value === "object") { + if (typeof value === 'object') { throw new Error( - "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', ); } @@ -56,40 +54,40 @@ const serializePrimitiveParam = ({ const separatorArrayExplode = (style: ArraySeparatorStyle) => { switch (style) { - case "label": - return "."; - case "matrix": - return ";"; - case "simple": - return ","; + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; default: - return "&"; + return '&'; } }; const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { switch (style) { - case "form": - return ","; - case "pipeDelimited": - return "|"; - case "spaceDelimited": - return "%20"; + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; default: - return ","; + return ','; } }; const separatorObjectExplode = (style: ObjectSeparatorStyle) => { switch (style) { - case "label": - return "."; - case "matrix": - return ";"; - case "simple": - return ","; + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; default: - return "&"; + return '&'; } }; @@ -107,11 +105,11 @@ const serializeArrayParam = ({ allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) ).join(separatorArrayNoExplode(style)); switch (style) { - case "label": + case 'label': return `.${joinedValues}`; - case "matrix": + case 'matrix': return `;${name}=${joinedValues}`; - case "simple": + case 'simple': return joinedValues; default: return `${name}=${joinedValues}`; @@ -121,7 +119,7 @@ const serializeArrayParam = ({ const separator = separatorArrayExplode(style); const joinedValues = value .map((v) => { - if (style === "label" || style === "simple") { + if (style === 'label' || style === 'simple') { return allowReserved ? v : encodeURIComponent(v as string); } @@ -132,7 +130,7 @@ const serializeArrayParam = ({ }); }) .join(separator); - return style === "label" || style === "matrix" + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; @@ -146,7 +144,7 @@ const serializeObjectParam = ({ }: SerializeOptions & { value: Record; }) => { - if (style !== "deepObject" && !explode) { + if (style !== 'deepObject' && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { values = [ @@ -155,13 +153,13 @@ const serializeObjectParam = ({ allowReserved ? (v as string) : encodeURIComponent(v as string), ]; }); - const joinedValues = values.join(","); + const joinedValues = values.join(','); switch (style) { - case "form": + case 'form': return `${name}=${joinedValues}`; - case "label": + case 'label': return `.${joinedValues}`; - case "matrix": + case 'matrix': return `;${name}=${joinedValues}`; default: return joinedValues; @@ -173,12 +171,12 @@ const serializeObjectParam = ({ .map(([key, v]) => serializePrimitiveParam({ allowReserved, - name: style === "deepObject" ? `${name}[${key}]` : key, + name: style === 'deepObject' ? `${name}[${key}]` : key, value: v as string, }), ) .join(separator); - return style === "label" || style === "matrix" + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; @@ -190,19 +188,19 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { for (const match of matches) { let explode = false; let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = "simple"; + let style: ArraySeparatorStyle = 'simple'; - if (name.endsWith("*")) { + if (name.endsWith('*')) { explode = true; name = name.substring(0, name.length - 1); } - if (name.startsWith(".")) { + if (name.startsWith('.')) { name = name.substring(1); - style = "label"; - } else if (name.startsWith(";")) { + style = 'label'; + } else if (name.startsWith(';')) { name = name.substring(1); - style = "matrix"; + style = 'matrix'; } const value = path[name]; @@ -219,7 +217,7 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { continue; } - if (typeof value === "object") { + if (typeof value === 'object') { url = url.replace( match, serializeObjectParam({ @@ -232,7 +230,7 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { continue; } - if (style === "matrix") { + if (style === 'matrix') { url = url.replace( match, `;${serializePrimitiveParam({ @@ -244,7 +242,7 @@ const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { } const replaceValue = encodeURIComponent( - style === "label" ? `.${value as string}` : (value as string), + style === 'label' ? `.${value as string}` : (value as string), ); url = url.replace(match, replaceValue); } @@ -265,7 +263,7 @@ const serializeFormDataPair = ( key: string, value: unknown, ) => { - if (typeof value === "string" || value instanceof Blob) { + if (typeof value === 'string' || value instanceof Blob) { formData.append(key, value); } else { formData.append(key, JSON.stringify(value)); @@ -279,11 +277,11 @@ export const mergeConfigs = (a: Config, b: Config): Config => { }; export const mergeHeaders = ( - ...headers: Array["headers"] | undefined> -): Required["headers"] => { - const mergedHeaders: Required["headers"] = {}; + ...headers: Array['headers'] | undefined> +): Required['headers'] => { + const mergedHeaders: Required['headers'] = {}; for (const header of headers) { - if (!header || typeof header !== "object") { + if (!header || typeof header !== 'object') { continue; } @@ -303,7 +301,7 @@ export const mergeHeaders = ( // content value in OpenAPI specification is 'application/json' // @ts-expect-error mergedHeaders[key] = - typeof value === "object" ? JSON.stringify(value) : (value as string); + typeof value === 'object' ? JSON.stringify(value) : (value as string); } } } @@ -340,7 +338,7 @@ const serializeUrlSearchParamsPair = ( key: string, value: unknown, ) => { - if (typeof value === "string") { + if (typeof value === 'string') { data.append(key, value); } else { data.append(key, JSON.stringify(value)); @@ -369,6 +367,6 @@ export const urlSearchParamsBodySerializer = { }; export const createConfig = (override: Config = {}): Config => ({ - baseURL: "", + baseURL: '', ...override, }); diff --git a/frontend/client/index.ts b/frontend/client/index.ts index 1cb041de..0a2b84ba 100644 --- a/frontend/client/index.ts +++ b/frontend/client/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export * from "./schemas.gen"; -export * from "./services.gen"; -export * from "./types.gen"; +export * from './schemas.gen'; +export * from './services.gen'; +export * from './types.gen'; \ No newline at end of file