Skip to content

Commit

Permalink
ben fixes (#60)
Browse files Browse the repository at this point in the history
* ben fixes

* more fixes

* user email stuff in sync

* remove markdown

* add back markdown
  • Loading branch information
codekansas authored Jun 10, 2024
1 parent c2b5f74 commit 4cb6019
Show file tree
Hide file tree
Showing 10 changed files with 670 additions and 599 deletions.
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

0 comments on commit 4cb6019

Please sign in to comment.