├── .env.template ├── .gitignore ├── Makefile ├── backend ├── .gitignore ├── Dockerfile └── app │ ├── alembic.ini │ ├── app │ ├── __init__.py │ ├── crud.py │ ├── database.py │ ├── main.py │ ├── models.py │ └── schemas.py │ ├── database.py │ ├── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 84ab2fcf267f_add_users_and_items_tables.py │ └── prestart.sh ├── docker-compose.devel.yml ├── docker-compose.openapi-generator.yml ├── docker-compose.yml └── frontend ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── api-client │ ├── .gitignore │ ├── .openapi-generator-ignore │ ├── .openapi-generator │ │ └── VERSION │ ├── api.ts │ ├── base.ts │ ├── configuration.ts │ ├── git_push.sh │ └── index.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.env.template: -------------------------------------------------------------------------------- 1 | MYSQL_USER= 2 | MYSQL_PASSWORD= 3 | MYSQL_DATABASE= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | db 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BACKEND_CONTEXT ?= docker-compose -f docker-compose.yml -f docker-compose.devel.yml exec backend 2 | migrate: 3 | $(BACKEND_CONTEXT) alembic revision --autogenerate -m "${MESSAGE}" 4 | 5 | upgrade: 6 | $(BACKEND_CONTEXT) alembic upgrade head 7 | 8 | downgrade: 9 | $(BACKEND_CONTEXT) alembic downgrade -1 10 | 11 | oapi/gen: 12 | docker-compose -f docker-compose.yml -f docker-compose.openapi-generator.yml up openapi-generator \ 13 | && docker-compose -f docker-compose.yml -f docker-compose.openapi-generator.yml rm -f openapi-generator 14 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | default-mysql-client \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | RUN pip install sqlalchemy PyMySQL alembic 9 | 10 | COPY ./app /app 11 | -------------------------------------------------------------------------------- /backend/app/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to migrations/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | 39 | [post_write_hooks] 40 | # post_write_hooks defines scripts or Python functions that are run 41 | # on newly generated revision scripts. See the documentation for further 42 | # detail and examples 43 | 44 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 45 | # hooks=black 46 | # black.type=console_scripts 47 | # black.entrypoint=black 48 | # black.options=-l 79 49 | 50 | # Logging configuration 51 | [loggers] 52 | keys = root,sqlalchemy,alembic 53 | 54 | [handlers] 55 | keys = console 56 | 57 | [formatters] 58 | keys = generic 59 | 60 | [logger_root] 61 | level = WARN 62 | handlers = console 63 | qualname = 64 | 65 | [logger_sqlalchemy] 66 | level = WARN 67 | handlers = 68 | qualname = sqlalchemy.engine 69 | 70 | [logger_alembic] 71 | level = INFO 72 | handlers = 73 | qualname = alembic 74 | 75 | [handler_console] 76 | class = StreamHandler 77 | args = (sys.stderr,) 78 | level = NOTSET 79 | formatter = generic 80 | 81 | [formatter_generic] 82 | format = %(levelname)-5.5s [%(name)s] %(message)s 83 | datefmt = %H:%M:%S 84 | -------------------------------------------------------------------------------- /backend/app/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitphx/fastapi-typescript-openapi-example/1d38242a63c9a5477fa1cde506f36f68a19060a7/backend/app/app/__init__.py -------------------------------------------------------------------------------- /backend/app/app/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models, schemas 4 | 5 | 6 | def get_user(db: Session, user_id: int): 7 | return db.query(models.User).filter(models.User.id == user_id).first() 8 | 9 | 10 | def get_user_by_email(db: Session, email: str): 11 | return db.query(models.User).filter(models.User.email == email).first() 12 | 13 | 14 | def get_users(db: Session, skip: int = 0, limit: int = 100): 15 | return db.query(models.User).offset(skip).limit(limit).all() 16 | 17 | 18 | def create_user(db: Session, user: schemas.UserCreate): 19 | fake_hashed_password = user.password + "notreallyhashed" 20 | db_user = models.User(email=user.email, hashed_password=fake_hashed_password) 21 | db.add(db_user) 22 | db.commit() 23 | db.refresh(db_user) 24 | return db_user 25 | 26 | 27 | def get_items(db: Session, skip: int = 0, limit: int = 100): 28 | return db.query(models.Item).offset(skip).limit(limit).all() 29 | 30 | 31 | def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): 32 | db_item = models.Item(**item.dict(), owner_id=user_id) 33 | db.add(db_item) 34 | db.commit() 35 | db.refresh(db_item) 36 | return db_item 37 | -------------------------------------------------------------------------------- /backend/app/app/database.py: -------------------------------------------------------------------------------- 1 | from starlette.config import Config 2 | from sqlalchemy import create_engine, MetaData 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | config = Config() 7 | MYSQL_USER = config("MYSQL_USER") 8 | MYSQL_PASSWORD = config("MYSQL_PASSWORD") 9 | MYSQL_HOST = "mysql" 10 | MYSQL_DATABASE = config("MYSQL_DATABASE") 11 | SQLALCHEMY_DATABASE_URL = ( 12 | f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}/{MYSQL_DATABASE}" 13 | ) 14 | 15 | engine = create_engine( 16 | SQLALCHEMY_DATABASE_URL 17 | ) 18 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 19 | 20 | convention = { 21 | "ix": "ix_%(column_0_label)s", 22 | "uq": "uq_%(table_name)s_%(column_0_name)s", 23 | "ck": "ck_%(table_name)s_%(constraint_name)s", 24 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 25 | "pk": "pk_%(table_name)s", 26 | } 27 | 28 | metadata = MetaData(naming_convention=convention) 29 | 30 | Base = declarative_base() 31 | -------------------------------------------------------------------------------- /backend/app/app/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import Depends, FastAPI, HTTPException 4 | from fastapi.routing import APIRoute 5 | from starlette.middleware.cors import CORSMiddleware 6 | from sqlalchemy.orm import Session 7 | 8 | from . import crud, models, schemas 9 | from .database import SessionLocal, engine 10 | 11 | # models.Base.metadata.create_all(bind=engine) 12 | 13 | app = FastAPI() 14 | 15 | # TODO: This is for development. Remove it for production. 16 | origins = [ 17 | "http://localhost:3000", 18 | ] 19 | 20 | app.add_middleware( 21 | CORSMiddleware, 22 | allow_origins=origins, 23 | allow_credentials=True, 24 | allow_methods=["*"], 25 | allow_headers=["*"], 26 | ) 27 | 28 | 29 | # Dependency 30 | def get_db(): 31 | try: 32 | db = SessionLocal() 33 | yield db 34 | finally: 35 | db.close() 36 | 37 | 38 | @app.post("/users/", response_model=schemas.User) 39 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 40 | db_user = crud.get_user_by_email(db, email=user.email) 41 | if db_user: 42 | raise HTTPException(status_code=400, detail="Email already registered") 43 | return crud.create_user(db=db, user=user) 44 | 45 | 46 | @app.get("/users/", response_model=List[schemas.User]) 47 | def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 48 | users = crud.get_users(db, skip=skip, limit=limit) 49 | return users 50 | 51 | 52 | @app.get("/users/{user_id}", response_model=schemas.User) 53 | def read_user(user_id: int, db: Session = Depends(get_db)): 54 | db_user = crud.get_user(db, user_id=user_id) 55 | if db_user is None: 56 | raise HTTPException(status_code=404, detail="User not found") 57 | return db_user 58 | 59 | 60 | @app.post("/users/{user_id}/items/", response_model=schemas.Item) 61 | def create_item_for_user( 62 | user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) 63 | ): 64 | return crud.create_user_item(db=db, item=item, user_id=user_id) 65 | 66 | 67 | @app.get("/items/", response_model=List[schemas.Item]) 68 | def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 69 | items = crud.get_items(db, skip=skip, limit=limit) 70 | return items 71 | 72 | 73 | def use_route_names_as_operation_ids(app: FastAPI) -> None: 74 | """ 75 | Simplify operation IDs so that generated API clients have simpler function 76 | names. 77 | 78 | Should be called only after all routes have been added. 79 | """ 80 | for route in app.routes: 81 | if isinstance(route, APIRoute): 82 | route.operation_id = route.name 83 | 84 | 85 | use_route_names_as_operation_ids(app) 86 | -------------------------------------------------------------------------------- /backend/app/app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from .database import Base 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, index=True) 11 | email = Column(String(255), unique=True, index=True) 12 | hashed_password = Column(String(255)) 13 | is_active = Column(Boolean, default=True) 14 | 15 | items = relationship("Item", back_populates="owner") 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | 21 | id = Column(Integer, primary_key=True, index=True) 22 | title = Column(String(255), index=True) 23 | description = Column(String(255), index=True) 24 | owner_id = Column(Integer, ForeignKey("users.id")) 25 | 26 | owner = relationship("User", back_populates="items") 27 | -------------------------------------------------------------------------------- /backend/app/app/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ItemBase(BaseModel): 7 | title: str 8 | description: str = None 9 | 10 | 11 | class ItemCreate(ItemBase): 12 | pass 13 | 14 | 15 | class Item(ItemBase): 16 | id: int 17 | owner_id: int 18 | 19 | class Config: 20 | orm_mode = True 21 | 22 | 23 | class UserBase(BaseModel): 24 | email: str 25 | 26 | 27 | class UserCreate(UserBase): 28 | password: str 29 | 30 | 31 | class User(UserBase): 32 | id: int 33 | is_active: bool 34 | items: List[Item] = [] 35 | 36 | class Config: 37 | orm_mode = True 38 | -------------------------------------------------------------------------------- /backend/app/database.py: -------------------------------------------------------------------------------- 1 | from starlette.config import Config 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import sessionmaker 5 | 6 | config = Config() 7 | MYSQL_USER = config("MYSQL_USER") 8 | MYSQL_PASSWORD = config("MYSQL_PASSWORD") 9 | MYSQL_HOST = "mysql" 10 | MYSQL_DATABASE = config("MYSQL_DATABASE") 11 | SQLALCHEMY_DATABASE_URL = ( 12 | f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}/{MYSQL_DATABASE}" 13 | ) 14 | 15 | engine = create_engine( 16 | SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} 17 | ) 18 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 19 | 20 | Base = declarative_base() 21 | -------------------------------------------------------------------------------- /backend/app/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/app/migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | target_metadata = None 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | from app.models import Base 27 | from app.database import SQLALCHEMY_DATABASE_URL 28 | 29 | config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL) 30 | target_metadata = Base.metadata 31 | 32 | 33 | def run_migrations_offline(): 34 | """Run migrations in 'offline' mode. 35 | 36 | This configures the context with just a URL 37 | and not an Engine, though an Engine is acceptable 38 | here as well. By skipping the Engine creation 39 | we don't even need a DBAPI to be available. 40 | 41 | Calls to context.execute() here emit the given string to the 42 | script output. 43 | 44 | """ 45 | url = config.get_main_option("sqlalchemy.url") 46 | context.configure( 47 | url=url, 48 | target_metadata=target_metadata, 49 | literal_binds=True, 50 | dialect_opts={"paramstyle": "named"}, 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | connectable = engine_from_config( 65 | config.get_section(config.config_ini_section), 66 | prefix="sqlalchemy.", 67 | poolclass=pool.NullPool, 68 | ) 69 | 70 | with connectable.connect() as connection: 71 | context.configure( 72 | connection=connection, target_metadata=target_metadata 73 | ) 74 | 75 | with context.begin_transaction(): 76 | context.run_migrations() 77 | 78 | 79 | if context.is_offline_mode(): 80 | run_migrations_offline() 81 | else: 82 | run_migrations_online() 83 | -------------------------------------------------------------------------------- /backend/app/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /backend/app/migrations/versions/84ab2fcf267f_add_users_and_items_tables.py: -------------------------------------------------------------------------------- 1 | """Add users and items tables 2 | 3 | Revision ID: 84ab2fcf267f 4 | Revises: 5 | Create Date: 2020-01-09 08:37:19.998883 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '84ab2fcf267f' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('email', sa.String(length=255), nullable=True), 24 | sa.Column('hashed_password', sa.String(length=255), nullable=True), 25 | sa.Column('is_active', sa.Boolean(), nullable=True), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) 29 | op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) 30 | op.create_table('items', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('title', sa.String(length=255), nullable=True), 33 | sa.Column('description', sa.String(length=255), nullable=True), 34 | sa.Column('owner_id', sa.Integer(), nullable=True), 35 | sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ), 36 | sa.PrimaryKeyConstraint('id') 37 | ) 38 | op.create_index(op.f('ix_items_description'), 'items', ['description'], unique=False) 39 | op.create_index(op.f('ix_items_id'), 'items', ['id'], unique=False) 40 | op.create_index(op.f('ix_items_title'), 'items', ['title'], unique=False) 41 | # ### end Alembic commands ### 42 | 43 | 44 | def downgrade(): 45 | # ### commands auto generated by Alembic - please adjust! ### 46 | op.drop_index(op.f('ix_items_title'), table_name='items') 47 | op.drop_index(op.f('ix_items_id'), table_name='items') 48 | op.drop_index(op.f('ix_items_description'), table_name='items') 49 | op.drop_table('items') 50 | op.drop_index(op.f('ix_users_id'), table_name='users') 51 | op.drop_index(op.f('ix_users_email'), table_name='users') 52 | op.drop_table('users') 53 | # ### end Alembic commands ### 54 | -------------------------------------------------------------------------------- /backend/app/prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | until mysqladmin ping -h"mysql" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD"; do 4 | echo 'waiting for mysqld to be connectable...' 5 | sleep 3 6 | done 7 | 8 | alembic upgrade head 9 | -------------------------------------------------------------------------------- /docker-compose.devel.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | backend: 4 | volumes: 5 | - ./backend/app:/app 6 | command: /start-reload.sh 7 | -------------------------------------------------------------------------------- /docker-compose.openapi-generator.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | openapi-generator: 4 | image: openapitools/openapi-generator-cli 5 | volumes: 6 | - ./frontend:/frontend 7 | working_dir: /frontend 8 | command: 9 | - generate 10 | - -g 11 | - typescript-axios 12 | - -i 13 | - http://backend/openapi.json 14 | - -o 15 | - /frontend/src/api-client 16 | - --additional-properties=supportsES6=true,modelPropertyNaming=original 17 | # modelPropertyNaming=original is necessary though camelCase is preferred. 18 | # See https://github.com/OpenAPITools/openapi-generator/issues/2976 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | backend: 4 | build: 5 | context: backend 6 | image: tuttieee/fastapi-typescript-sample-backend 7 | ports: 8 | - 8000:80 9 | environment: 10 | - MYSQL_USER=${MYSQL_USER} 11 | - MYSQL_PASSWORD=${MYSQL_PASSWORD} 12 | - MYSQL_DATABASE=${MYSQL_DATABASE} 13 | depends_on: 14 | - mysql 15 | 16 | mysql: 17 | image: mysql:8.0.18 18 | command: --default-authentication-plugin=mysql_native_password 19 | restart: always 20 | environment: 21 | - MYSQL_USER=${MYSQL_USER} 22 | - MYSQL_PASSWORD=${MYSQL_PASSWORD} 23 | - MYSQL_DATABASE=${MYSQL_DATABASE} 24 | - MYSQL_RANDOM_ROOT_PASSWORD=yes 25 | - MYSQL_ROOT_HOST=localhost 26 | volumes: 27 | - ./db:/var/lib/mysql 28 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@types/jest": "^24.0.0", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.0", 12 | "@types/react-dom": "^16.9.0", 13 | "axios": "^0.19.1", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0", 16 | "react-scripts": "3.3.0", 17 | "typescript": "~3.7.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitphx/fastapi-typescript-openapi-example/1d38242a63c9a5477fa1cde506f36f68a19060a7/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitphx/fastapi-typescript-openapi-example/1d38242a63c9a5477fa1cde506f36f68a19060a7/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitphx/fastapi-typescript-openapi-example/1d38242a63c9a5477fa1cde506f36f68a19060a7/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitphx/fastapi-typescript-openapi-example/1d38242a63c9a5477fa1cde506f36f68a19060a7/frontend/src/App.css -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './App.css'; 3 | import { DefaultApi, Configuration, User } from './api-client'; 4 | 5 | const config = new Configuration({ basePath: 'http://localhost:8000' }); // TODO: This is for dev 6 | export const apiClient = new DefaultApi(config); 7 | 8 | const App: React.FC = () => { 9 | const [users, setUsers] = useState([]); 10 | 11 | useEffect(() => { 12 | apiClient.readUsers().then((response) => { 13 | setUsers(response.data); 14 | }) 15 | }) 16 | 17 | return ( 18 |
19 |
    20 | {users.map(user => 21 |
  • {user.email}
  • 22 | )} 23 |
24 |
25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /frontend/src/api-client/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /frontend/src/api-client/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /frontend/src/api-client/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 4.2.3-SNAPSHOT -------------------------------------------------------------------------------- /frontend/src/api-client/api.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Fast API 4 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 5 | * 6 | * The version of the OpenAPI document: 0.1.0 7 | * 8 | * 9 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 10 | * https://openapi-generator.tech 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | import * as globalImportUrl from 'url'; 16 | import { Configuration } from './configuration'; 17 | import globalAxios, { AxiosPromise, AxiosInstance } from 'axios'; 18 | // Some imports not used depending on template conditions 19 | // @ts-ignore 20 | import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base'; 21 | 22 | /** 23 | * 24 | * @export 25 | * @interface HTTPValidationError 26 | */ 27 | export interface HTTPValidationError { 28 | /** 29 | * 30 | * @type {Array} 31 | * @memberof HTTPValidationError 32 | */ 33 | detail?: Array; 34 | } 35 | /** 36 | * 37 | * @export 38 | * @interface Item 39 | */ 40 | export interface Item { 41 | /** 42 | * 43 | * @type {string} 44 | * @memberof Item 45 | */ 46 | title: string; 47 | /** 48 | * 49 | * @type {string} 50 | * @memberof Item 51 | */ 52 | description?: string; 53 | /** 54 | * 55 | * @type {number} 56 | * @memberof Item 57 | */ 58 | id: number; 59 | /** 60 | * 61 | * @type {number} 62 | * @memberof Item 63 | */ 64 | owner_id: number; 65 | } 66 | /** 67 | * 68 | * @export 69 | * @interface ItemCreate 70 | */ 71 | export interface ItemCreate { 72 | /** 73 | * 74 | * @type {string} 75 | * @memberof ItemCreate 76 | */ 77 | title: string; 78 | /** 79 | * 80 | * @type {string} 81 | * @memberof ItemCreate 82 | */ 83 | description?: string; 84 | } 85 | /** 86 | * 87 | * @export 88 | * @interface User 89 | */ 90 | export interface User { 91 | /** 92 | * 93 | * @type {string} 94 | * @memberof User 95 | */ 96 | email: string; 97 | /** 98 | * 99 | * @type {number} 100 | * @memberof User 101 | */ 102 | id: number; 103 | /** 104 | * 105 | * @type {boolean} 106 | * @memberof User 107 | */ 108 | is_active: boolean; 109 | /** 110 | * 111 | * @type {Array} 112 | * @memberof User 113 | */ 114 | items?: Array; 115 | } 116 | /** 117 | * 118 | * @export 119 | * @interface UserCreate 120 | */ 121 | export interface UserCreate { 122 | /** 123 | * 124 | * @type {string} 125 | * @memberof UserCreate 126 | */ 127 | email: string; 128 | /** 129 | * 130 | * @type {string} 131 | * @memberof UserCreate 132 | */ 133 | password: string; 134 | } 135 | /** 136 | * 137 | * @export 138 | * @interface ValidationError 139 | */ 140 | export interface ValidationError { 141 | /** 142 | * 143 | * @type {Array} 144 | * @memberof ValidationError 145 | */ 146 | loc: Array; 147 | /** 148 | * 149 | * @type {string} 150 | * @memberof ValidationError 151 | */ 152 | msg: string; 153 | /** 154 | * 155 | * @type {string} 156 | * @memberof ValidationError 157 | */ 158 | type: string; 159 | } 160 | 161 | /** 162 | * DefaultApi - axios parameter creator 163 | * @export 164 | */ 165 | export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { 166 | return { 167 | /** 168 | * 169 | * @summary Create Item For User 170 | * @param {number} user_id 171 | * @param {ItemCreate} ItemCreate 172 | * @param {*} [options] Override http request option. 173 | * @throws {RequiredError} 174 | */ 175 | createItemForUser(user_id: number, ItemCreate: ItemCreate, options: any = {}): RequestArgs { 176 | // verify required parameter 'user_id' is not null or undefined 177 | if (user_id === null || user_id === undefined) { 178 | throw new RequiredError('user_id','Required parameter user_id was null or undefined when calling createItemForUser.'); 179 | } 180 | // verify required parameter 'ItemCreate' is not null or undefined 181 | if (ItemCreate === null || ItemCreate === undefined) { 182 | throw new RequiredError('ItemCreate','Required parameter ItemCreate was null or undefined when calling createItemForUser.'); 183 | } 184 | const localVarPath = `/users/{user_id}/items/` 185 | .replace(`{${"user_id"}}`, encodeURIComponent(String(user_id))); 186 | const localVarUrlObj = globalImportUrl.parse(localVarPath, true); 187 | let baseOptions; 188 | if (configuration) { 189 | baseOptions = configuration.baseOptions; 190 | } 191 | const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; 192 | const localVarHeaderParameter = {} as any; 193 | const localVarQueryParameter = {} as any; 194 | 195 | 196 | 197 | localVarHeaderParameter['Content-Type'] = 'application/json'; 198 | 199 | localVarUrlObj.query = {...localVarUrlObj.query, ...localVarQueryParameter, ...options.query}; 200 | // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 201 | delete localVarUrlObj.search; 202 | localVarRequestOptions.headers = {...localVarHeaderParameter, ...options.headers}; 203 | const needsSerialization = (typeof ItemCreate !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; 204 | localVarRequestOptions.data = needsSerialization ? JSON.stringify(ItemCreate !== undefined ? ItemCreate : {}) : (ItemCreate || ""); 205 | 206 | return { 207 | url: globalImportUrl.format(localVarUrlObj), 208 | options: localVarRequestOptions, 209 | }; 210 | }, 211 | /** 212 | * 213 | * @summary Create User 214 | * @param {UserCreate} UserCreate 215 | * @param {*} [options] Override http request option. 216 | * @throws {RequiredError} 217 | */ 218 | createUser(UserCreate: UserCreate, options: any = {}): RequestArgs { 219 | // verify required parameter 'UserCreate' is not null or undefined 220 | if (UserCreate === null || UserCreate === undefined) { 221 | throw new RequiredError('UserCreate','Required parameter UserCreate was null or undefined when calling createUser.'); 222 | } 223 | const localVarPath = `/users/`; 224 | const localVarUrlObj = globalImportUrl.parse(localVarPath, true); 225 | let baseOptions; 226 | if (configuration) { 227 | baseOptions = configuration.baseOptions; 228 | } 229 | const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; 230 | const localVarHeaderParameter = {} as any; 231 | const localVarQueryParameter = {} as any; 232 | 233 | 234 | 235 | localVarHeaderParameter['Content-Type'] = 'application/json'; 236 | 237 | localVarUrlObj.query = {...localVarUrlObj.query, ...localVarQueryParameter, ...options.query}; 238 | // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 239 | delete localVarUrlObj.search; 240 | localVarRequestOptions.headers = {...localVarHeaderParameter, ...options.headers}; 241 | const needsSerialization = (typeof UserCreate !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; 242 | localVarRequestOptions.data = needsSerialization ? JSON.stringify(UserCreate !== undefined ? UserCreate : {}) : (UserCreate || ""); 243 | 244 | return { 245 | url: globalImportUrl.format(localVarUrlObj), 246 | options: localVarRequestOptions, 247 | }; 248 | }, 249 | /** 250 | * 251 | * @summary Read Items 252 | * @param {number} [skip] 253 | * @param {number} [limit] 254 | * @param {*} [options] Override http request option. 255 | * @throws {RequiredError} 256 | */ 257 | readItems(skip?: number, limit?: number, options: any = {}): RequestArgs { 258 | const localVarPath = `/items/`; 259 | const localVarUrlObj = globalImportUrl.parse(localVarPath, true); 260 | let baseOptions; 261 | if (configuration) { 262 | baseOptions = configuration.baseOptions; 263 | } 264 | const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; 265 | const localVarHeaderParameter = {} as any; 266 | const localVarQueryParameter = {} as any; 267 | 268 | if (skip !== undefined) { 269 | localVarQueryParameter['skip'] = skip; 270 | } 271 | 272 | if (limit !== undefined) { 273 | localVarQueryParameter['limit'] = limit; 274 | } 275 | 276 | 277 | 278 | localVarUrlObj.query = {...localVarUrlObj.query, ...localVarQueryParameter, ...options.query}; 279 | // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 280 | delete localVarUrlObj.search; 281 | localVarRequestOptions.headers = {...localVarHeaderParameter, ...options.headers}; 282 | 283 | return { 284 | url: globalImportUrl.format(localVarUrlObj), 285 | options: localVarRequestOptions, 286 | }; 287 | }, 288 | /** 289 | * 290 | * @summary Read User 291 | * @param {number} user_id 292 | * @param {*} [options] Override http request option. 293 | * @throws {RequiredError} 294 | */ 295 | readUser(user_id: number, options: any = {}): RequestArgs { 296 | // verify required parameter 'user_id' is not null or undefined 297 | if (user_id === null || user_id === undefined) { 298 | throw new RequiredError('user_id','Required parameter user_id was null or undefined when calling readUser.'); 299 | } 300 | const localVarPath = `/users/{user_id}` 301 | .replace(`{${"user_id"}}`, encodeURIComponent(String(user_id))); 302 | const localVarUrlObj = globalImportUrl.parse(localVarPath, true); 303 | let baseOptions; 304 | if (configuration) { 305 | baseOptions = configuration.baseOptions; 306 | } 307 | const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; 308 | const localVarHeaderParameter = {} as any; 309 | const localVarQueryParameter = {} as any; 310 | 311 | 312 | 313 | localVarUrlObj.query = {...localVarUrlObj.query, ...localVarQueryParameter, ...options.query}; 314 | // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 315 | delete localVarUrlObj.search; 316 | localVarRequestOptions.headers = {...localVarHeaderParameter, ...options.headers}; 317 | 318 | return { 319 | url: globalImportUrl.format(localVarUrlObj), 320 | options: localVarRequestOptions, 321 | }; 322 | }, 323 | /** 324 | * 325 | * @summary Read Users 326 | * @param {number} [skip] 327 | * @param {number} [limit] 328 | * @param {*} [options] Override http request option. 329 | * @throws {RequiredError} 330 | */ 331 | readUsers(skip?: number, limit?: number, options: any = {}): RequestArgs { 332 | const localVarPath = `/users/`; 333 | const localVarUrlObj = globalImportUrl.parse(localVarPath, true); 334 | let baseOptions; 335 | if (configuration) { 336 | baseOptions = configuration.baseOptions; 337 | } 338 | const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; 339 | const localVarHeaderParameter = {} as any; 340 | const localVarQueryParameter = {} as any; 341 | 342 | if (skip !== undefined) { 343 | localVarQueryParameter['skip'] = skip; 344 | } 345 | 346 | if (limit !== undefined) { 347 | localVarQueryParameter['limit'] = limit; 348 | } 349 | 350 | 351 | 352 | localVarUrlObj.query = {...localVarUrlObj.query, ...localVarQueryParameter, ...options.query}; 353 | // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 354 | delete localVarUrlObj.search; 355 | localVarRequestOptions.headers = {...localVarHeaderParameter, ...options.headers}; 356 | 357 | return { 358 | url: globalImportUrl.format(localVarUrlObj), 359 | options: localVarRequestOptions, 360 | }; 361 | }, 362 | } 363 | }; 364 | 365 | /** 366 | * DefaultApi - functional programming interface 367 | * @export 368 | */ 369 | export const DefaultApiFp = function(configuration?: Configuration) { 370 | return { 371 | /** 372 | * 373 | * @summary Create Item For User 374 | * @param {number} user_id 375 | * @param {ItemCreate} ItemCreate 376 | * @param {*} [options] Override http request option. 377 | * @throws {RequiredError} 378 | */ 379 | createItemForUser(user_id: number, ItemCreate: ItemCreate, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 380 | const localVarAxiosArgs = DefaultApiAxiosParamCreator(configuration).createItemForUser(user_id, ItemCreate, options); 381 | return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 382 | const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; 383 | return axios.request(axiosRequestArgs); 384 | }; 385 | }, 386 | /** 387 | * 388 | * @summary Create User 389 | * @param {UserCreate} UserCreate 390 | * @param {*} [options] Override http request option. 391 | * @throws {RequiredError} 392 | */ 393 | createUser(UserCreate: UserCreate, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 394 | const localVarAxiosArgs = DefaultApiAxiosParamCreator(configuration).createUser(UserCreate, options); 395 | return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 396 | const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; 397 | return axios.request(axiosRequestArgs); 398 | }; 399 | }, 400 | /** 401 | * 402 | * @summary Read Items 403 | * @param {number} [skip] 404 | * @param {number} [limit] 405 | * @param {*} [options] Override http request option. 406 | * @throws {RequiredError} 407 | */ 408 | readItems(skip?: number, limit?: number, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 409 | const localVarAxiosArgs = DefaultApiAxiosParamCreator(configuration).readItems(skip, limit, options); 410 | return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 411 | const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; 412 | return axios.request(axiosRequestArgs); 413 | }; 414 | }, 415 | /** 416 | * 417 | * @summary Read User 418 | * @param {number} user_id 419 | * @param {*} [options] Override http request option. 420 | * @throws {RequiredError} 421 | */ 422 | readUser(user_id: number, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise { 423 | const localVarAxiosArgs = DefaultApiAxiosParamCreator(configuration).readUser(user_id, options); 424 | return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 425 | const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; 426 | return axios.request(axiosRequestArgs); 427 | }; 428 | }, 429 | /** 430 | * 431 | * @summary Read Users 432 | * @param {number} [skip] 433 | * @param {number} [limit] 434 | * @param {*} [options] Override http request option. 435 | * @throws {RequiredError} 436 | */ 437 | readUsers(skip?: number, limit?: number, options?: any): (axios?: AxiosInstance, basePath?: string) => AxiosPromise> { 438 | const localVarAxiosArgs = DefaultApiAxiosParamCreator(configuration).readUsers(skip, limit, options); 439 | return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { 440 | const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; 441 | return axios.request(axiosRequestArgs); 442 | }; 443 | }, 444 | } 445 | }; 446 | 447 | /** 448 | * DefaultApi - factory interface 449 | * @export 450 | */ 451 | export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { 452 | return { 453 | /** 454 | * 455 | * @summary Create Item For User 456 | * @param {number} user_id 457 | * @param {ItemCreate} ItemCreate 458 | * @param {*} [options] Override http request option. 459 | * @throws {RequiredError} 460 | */ 461 | createItemForUser(user_id: number, ItemCreate: ItemCreate, options?: any) { 462 | return DefaultApiFp(configuration).createItemForUser(user_id, ItemCreate, options)(axios, basePath); 463 | }, 464 | /** 465 | * 466 | * @summary Create User 467 | * @param {UserCreate} UserCreate 468 | * @param {*} [options] Override http request option. 469 | * @throws {RequiredError} 470 | */ 471 | createUser(UserCreate: UserCreate, options?: any) { 472 | return DefaultApiFp(configuration).createUser(UserCreate, options)(axios, basePath); 473 | }, 474 | /** 475 | * 476 | * @summary Read Items 477 | * @param {number} [skip] 478 | * @param {number} [limit] 479 | * @param {*} [options] Override http request option. 480 | * @throws {RequiredError} 481 | */ 482 | readItems(skip?: number, limit?: number, options?: any) { 483 | return DefaultApiFp(configuration).readItems(skip, limit, options)(axios, basePath); 484 | }, 485 | /** 486 | * 487 | * @summary Read User 488 | * @param {number} user_id 489 | * @param {*} [options] Override http request option. 490 | * @throws {RequiredError} 491 | */ 492 | readUser(user_id: number, options?: any) { 493 | return DefaultApiFp(configuration).readUser(user_id, options)(axios, basePath); 494 | }, 495 | /** 496 | * 497 | * @summary Read Users 498 | * @param {number} [skip] 499 | * @param {number} [limit] 500 | * @param {*} [options] Override http request option. 501 | * @throws {RequiredError} 502 | */ 503 | readUsers(skip?: number, limit?: number, options?: any) { 504 | return DefaultApiFp(configuration).readUsers(skip, limit, options)(axios, basePath); 505 | }, 506 | }; 507 | }; 508 | 509 | /** 510 | * DefaultApi - object-oriented interface 511 | * @export 512 | * @class DefaultApi 513 | * @extends {BaseAPI} 514 | */ 515 | export class DefaultApi extends BaseAPI { 516 | /** 517 | * 518 | * @summary Create Item For User 519 | * @param {number} user_id 520 | * @param {ItemCreate} ItemCreate 521 | * @param {*} [options] Override http request option. 522 | * @throws {RequiredError} 523 | * @memberof DefaultApi 524 | */ 525 | public createItemForUser(user_id: number, ItemCreate: ItemCreate, options?: any) { 526 | return DefaultApiFp(this.configuration).createItemForUser(user_id, ItemCreate, options)(this.axios, this.basePath); 527 | } 528 | 529 | /** 530 | * 531 | * @summary Create User 532 | * @param {UserCreate} UserCreate 533 | * @param {*} [options] Override http request option. 534 | * @throws {RequiredError} 535 | * @memberof DefaultApi 536 | */ 537 | public createUser(UserCreate: UserCreate, options?: any) { 538 | return DefaultApiFp(this.configuration).createUser(UserCreate, options)(this.axios, this.basePath); 539 | } 540 | 541 | /** 542 | * 543 | * @summary Read Items 544 | * @param {number} [skip] 545 | * @param {number} [limit] 546 | * @param {*} [options] Override http request option. 547 | * @throws {RequiredError} 548 | * @memberof DefaultApi 549 | */ 550 | public readItems(skip?: number, limit?: number, options?: any) { 551 | return DefaultApiFp(this.configuration).readItems(skip, limit, options)(this.axios, this.basePath); 552 | } 553 | 554 | /** 555 | * 556 | * @summary Read User 557 | * @param {number} user_id 558 | * @param {*} [options] Override http request option. 559 | * @throws {RequiredError} 560 | * @memberof DefaultApi 561 | */ 562 | public readUser(user_id: number, options?: any) { 563 | return DefaultApiFp(this.configuration).readUser(user_id, options)(this.axios, this.basePath); 564 | } 565 | 566 | /** 567 | * 568 | * @summary Read Users 569 | * @param {number} [skip] 570 | * @param {number} [limit] 571 | * @param {*} [options] Override http request option. 572 | * @throws {RequiredError} 573 | * @memberof DefaultApi 574 | */ 575 | public readUsers(skip?: number, limit?: number, options?: any) { 576 | return DefaultApiFp(this.configuration).readUsers(skip, limit, options)(this.axios, this.basePath); 577 | } 578 | 579 | } 580 | 581 | 582 | -------------------------------------------------------------------------------- /frontend/src/api-client/base.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Fast API 4 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 5 | * 6 | * The version of the OpenAPI document: 0.1.0 7 | * 8 | * 9 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 10 | * https://openapi-generator.tech 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | import { Configuration } from "./configuration"; 16 | // Some imports not used depending on template conditions 17 | // @ts-ignore 18 | import globalAxios, { AxiosPromise, AxiosInstance } from 'axios'; 19 | 20 | export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); 21 | 22 | /** 23 | * 24 | * @export 25 | */ 26 | export const COLLECTION_FORMATS = { 27 | csv: ",", 28 | ssv: " ", 29 | tsv: "\t", 30 | pipes: "|", 31 | }; 32 | 33 | /** 34 | * 35 | * @export 36 | * @interface RequestArgs 37 | */ 38 | export interface RequestArgs { 39 | url: string; 40 | options: any; 41 | } 42 | 43 | /** 44 | * 45 | * @export 46 | * @class BaseAPI 47 | */ 48 | export class BaseAPI { 49 | protected configuration: Configuration | undefined; 50 | 51 | constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { 52 | if (configuration) { 53 | this.configuration = configuration; 54 | this.basePath = configuration.basePath || this.basePath; 55 | } 56 | } 57 | }; 58 | 59 | /** 60 | * 61 | * @export 62 | * @class RequiredError 63 | * @extends {Error} 64 | */ 65 | export class RequiredError extends Error { 66 | name: "RequiredError" = "RequiredError"; 67 | constructor(public field: string, msg?: string) { 68 | super(msg); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/api-client/configuration.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Fast API 4 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 5 | * 6 | * The version of the OpenAPI document: 0.1.0 7 | * 8 | * 9 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 10 | * https://openapi-generator.tech 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | export interface ConfigurationParameters { 16 | apiKey?: string | ((name: string) => string); 17 | username?: string; 18 | password?: string; 19 | accessToken?: string | ((name?: string, scopes?: string[]) => string); 20 | basePath?: string; 21 | baseOptions?: any; 22 | } 23 | 24 | export class Configuration { 25 | /** 26 | * parameter for apiKey security 27 | * @param name security name 28 | * @memberof Configuration 29 | */ 30 | apiKey?: string | ((name: string) => string); 31 | /** 32 | * parameter for basic security 33 | * 34 | * @type {string} 35 | * @memberof Configuration 36 | */ 37 | username?: string; 38 | /** 39 | * parameter for basic security 40 | * 41 | * @type {string} 42 | * @memberof Configuration 43 | */ 44 | password?: string; 45 | /** 46 | * parameter for oauth2 security 47 | * @param name security name 48 | * @param scopes oauth2 scope 49 | * @memberof Configuration 50 | */ 51 | accessToken?: string | ((name?: string, scopes?: string[]) => string); 52 | /** 53 | * override base path 54 | * 55 | * @type {string} 56 | * @memberof Configuration 57 | */ 58 | basePath?: string; 59 | /** 60 | * base options for axios calls 61 | * 62 | * @type {any} 63 | * @memberof Configuration 64 | */ 65 | baseOptions?: any; 66 | 67 | constructor(param: ConfigurationParameters = {}) { 68 | this.apiKey = param.apiKey; 69 | this.username = param.username; 70 | this.password = param.password; 71 | this.accessToken = param.accessToken; 72 | this.basePath = param.basePath; 73 | this.baseOptions = param.baseOptions; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /frontend/src/api-client/git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=`git remote` 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | 59 | -------------------------------------------------------------------------------- /frontend/src/api-client/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | /** 3 | * Fast API 4 | * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) 5 | * 6 | * The version of the OpenAPI document: 0.1.0 7 | * 8 | * 9 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 10 | * https://openapi-generator.tech 11 | * Do not edit the class manually. 12 | */ 13 | 14 | 15 | export * from "./api"; 16 | export * from "./configuration"; 17 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready.then(registration => { 142 | registration.unregister(); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------