Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ben fixes #60

Merged
merged 5 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ jobs:
run: |
npm install

# Commented out because the frontend tests fail for the Markdown package
# - name: Run tests
# run: |
# make test-frontend

- name: Build frontend
run: |
npm run build
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ start-fastapi:
start-frontend:
@cd frontend && npm start

start-docker:
start-docker-dynamodb:
@docker kill store-db || true
@docker rm store-db || true
@docker run --name store-db -d -p 8000:8000 amazon/dynamodb-local

start-docker-redis:
@docker kill store-redis || true
@docker rm store-redis || true
@docker run --name store-redis -d -p 6379:6379 redis

# ------------------------ #
# Code Formatting #
# ------------------------ #
Expand Down Expand Up @@ -80,6 +85,7 @@ test-backend:
test-frontend:
@cd frontend && npm run test -- --watchAll=false

test: test-backend test-frontend
# test: test-backend test-frontend
test: test-backend

.PHONY: test
1 change: 0 additions & 1 deletion frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module.exports = {
"^.+\\.(ts|tsx)$": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest",
},
transformIgnorePatterns: ["/node_modules/(?!(react-markdown)/)"],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
},
Expand Down
1,088 changes: 539 additions & 549 deletions frontend/package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"watch": "nodemon serve",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\" && eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
Expand All @@ -45,7 +45,9 @@
"devDependencies": {
"@babel/eslint-parser": "^7.24.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-typescript": "^7.24.6",
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "*",
"@eslint/js": "*",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
Expand All @@ -55,6 +57,7 @@
"@types/jest": "^29.5.12",
"axios": "^1.7.2",
"babel-eslint": "*",
"babel-jest": "^29.7.0",
"bootstrap": "^5.3.3",
"eslint": "^8.0.0",
"eslint-config-prettier": "*",
Expand All @@ -74,6 +77,7 @@
},
"jest": {
"moduleNameMapper": {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
"^axios$": "axios/dist/node/axios.cjs"
}
}
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/App.test.tsx

This file was deleted.

68 changes: 49 additions & 19 deletions store/app/crud/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async def _create_dynamodb_table(
self,
name: str,
keys: list[tuple[str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]]],
gsis: list[tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]]] = [],
gsis: list[tuple[str, str, Literal["S", "N", "B"], Literal["HASH", "RANGE"]]] | None = None,
deletion_protection: bool = False,
) -> None:
"""Creates a table in the Dynamo database if a table of that name does not already exist.
Expand All @@ -66,24 +66,54 @@ async def _create_dynamodb_table(
"""
try:
await self.db.meta.client.describe_table(TableName=name)
logger.info("Found existing table %s", name)
except ClientError:
logger.info("Creating %s table", name)
table = await self.db.create_table(
AttributeDefinitions=[
{"AttributeName": n, "AttributeType": t}
for n, t in itertools.chain(((n, t) for (n, t, _) in keys), ((n, t) for _, n, t, _ in gsis))
],
TableName=name,
KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys],
GlobalSecondaryIndexes=[
{
"IndexName": i,
"KeySchema": [{"AttributeName": n, "KeyType": t}],
"Projection": {"ProjectionType": "ALL"},
}
for i, n, _, t in gsis
],
DeletionProtectionEnabled=deletion_protection,
BillingMode="PAY_PER_REQUEST",
)

if gsis is None:
table = await self.db.create_table(
AttributeDefinitions=[
{"AttributeName": n, "AttributeType": t} for n, t in ((n, t) for (n, t, _) in keys)
],
TableName=name,
KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys],
DeletionProtectionEnabled=deletion_protection,
BillingMode="PAY_PER_REQUEST",
)

else:
table = await self.db.create_table(
AttributeDefinitions=[
{"AttributeName": n, "AttributeType": t}
for n, t in itertools.chain(((n, t) for (n, t, _) in keys), ((n, t) for _, n, t, _ in gsis))
],
TableName=name,
KeySchema=[{"AttributeName": n, "KeyType": t} for n, _, t in keys],
GlobalSecondaryIndexes=(
[
{
"IndexName": i,
"KeySchema": [{"AttributeName": n, "KeyType": t}],
"Projection": {"ProjectionType": "ALL"},
}
for i, n, _, t in gsis
]
),
DeletionProtectionEnabled=deletion_protection,
BillingMode="PAY_PER_REQUEST",
)

await table.wait_until_exists()

async def _delete_dynamodb_table(self, name: str) -> None:
"""Deletes a table in the Dynamo database.

Args:
name: Name of the table.
"""
try:
table = await self.db.Table(name)
await table.delete()
logger.info("Deleted table %s", name)
except ClientError:
logger.info("Table %s does not exist", name)
27 changes: 18 additions & 9 deletions store/app/crud/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import uuid
import warnings

from boto3.dynamodb.conditions import Key as KeyCondition

from store.app.crud.base import BaseCrud
from store.app.crypto import hash_api_key
from store.app.model import ApiKey, User


class UserCrud(BaseCrud):
async def add_user(self, user: User) -> None:
# First, add the user email to the UserEmails table.
table = await self.db.Table("UserEmails")
await table.put_item(Item={"email": user.email, "user_id": user.user_id})

# Then, add the user object to the Users table.
table = await self.db.Table("Users")
await table.put_item(Item=user.model_dump())

Expand All @@ -24,14 +27,15 @@ async def get_user(self, user_id: uuid.UUID) -> User | None:
return User.model_validate(user_dict["Item"])

async def get_user_from_email(self, email: str) -> User | None:
table = await self.db.Table("Users")
user_dict = await table.query(IndexName="emailIndex", KeyConditionExpression=KeyCondition("email").eq(email))
items = user_dict["Items"]
if len(items) == 0:
# First, query the UesrEmails table to get the user_id.
table = await self.db.Table("UserEmails")
user_dict = await table.get_item(Key={"email": email})
if "Item" not in user_dict:
return None
if len(items) > 1:
raise ValueError(f"Multiple users found with email {email}")
return User.model_validate(items[0])
assert isinstance(user_id := user_dict["Item"]["user_id"], str)

# Then, query the Users table to get the user object.
return await self.get_user(uuid.UUID(user_id))

async def get_user_id_from_api_key(self, api_key: uuid.UUID) -> uuid.UUID | None:
api_key_hash = hash_api_key(api_key)
Expand All @@ -41,6 +45,11 @@ async def get_user_id_from_api_key(self, api_key: uuid.UUID) -> uuid.UUID | None
return uuid.UUID(user_id.decode("utf-8"))

async def delete_user(self, user: User) -> None:
# First, delete the user email from the UserEmails table.
table = await self.db.Table("UserEmails")
await table.delete_item(Key={"email": user.email})

# Then, delete the user object from the Users table.
table = await self.db.Table("Users")
await table.delete_item(Key={"user_id": user.user_id})

Expand Down
50 changes: 48 additions & 2 deletions store/app/db.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Defines base tools for interacting with the database."""

import argparse
import asyncio
import logging
from typing import AsyncGenerator, Self
Expand All @@ -22,11 +23,12 @@ async def get(cls) -> AsyncGenerator[Self, None]:
yield crud


async def create_tables(crud: Crud | None = None) -> None:
async def create_tables(crud: Crud | None = None, deletion_protection: bool = False) -> None:
"""Initializes all of the database tables.

Args:
crud: The top-level CRUD class.
deletion_protection: Whether to enable deletion protection on the tables.
"""
logging.basicConfig(level=logging.INFO)

Expand All @@ -43,6 +45,14 @@ async def create_tables(crud: Crud | None = None) -> None:
gsis=[
("emailIndex", "email", "S", "HASH"),
],
deletion_protection=deletion_protection,
)
await crud._create_dynamodb_table(
name="UserEmails",
keys=[
("email", "S", "HASH"),
],
deletion_protection=deletion_protection,
)
await crud._create_dynamodb_table(
name="Robots",
Expand All @@ -53,6 +63,7 @@ async def create_tables(crud: Crud | None = None) -> None:
("ownerIndex", "owner", "S", "HASH"),
("nameIndex", "name", "S", "HASH"),
],
deletion_protection=deletion_protection,
)
await crud._create_dynamodb_table(
name="Parts",
Expand All @@ -63,9 +74,44 @@ async def create_tables(crud: Crud | None = None) -> None:
("ownerIndex", "owner", "S", "HASH"),
("nameIndex", "name", "S", "HASH"),
],
deletion_protection=deletion_protection,
)


async def delete_tables(crud: Crud | None = None) -> None:
"""Deletes all of the database tables.

Args:
crud: The top-level CRUD class.
"""
logging.basicConfig(level=logging.INFO)

if crud is None:
async with Crud() as crud:
await delete_tables(crud)

else:
await crud._delete_dynamodb_table("Users")
await crud._delete_dynamodb_table("UserEmails")
await crud._delete_dynamodb_table("Robots")
await crud._delete_dynamodb_table("Parts")


async def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["create", "delete"])
args = parser.parse_args()

async with Crud() as crud:
match args.action:
case "create":
await create_tables(crud)
case "delete":
await delete_tables(crud)
case _:
raise ValueError(f"Invalid action: {args.action}")


if __name__ == "__main__":
# python -m store.app.db
asyncio.run(create_tables())
asyncio.run(main())
4 changes: 2 additions & 2 deletions store/settings/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

@dataclass
class RedisSettings:
host: str = field(default=II("oc.env:ROBOLIST_REDIS_HOST"))
password: str = field(default=II("oc.env:ROBOLIST_REDIS_PASSWORD"))
host: str = field(default=II("oc.env:ROBOLIST_REDIS_HOST,127.0.0.1"))
password: str = field(default=II("oc.env:ROBOLIST_REDIS_PASSWORD,''"))
port: int = field(default=6379)
db: int = field(default=0)

Expand Down
Loading