What is a lot of fucking work!

This commit is contained in:
Andrew 2023-04-23 16:35:02 +07:00
parent 85c07ed4f3
commit 3414b5c334
8 changed files with 586 additions and 216 deletions

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}

4
____test.py Normal file
View file

@ -0,0 +1,4 @@
from psycopg.sql import SQL, Identifier, Literal
print(SQL("{} SERIAL NOT NULL").format(Identifier("asset_ref")))
print(Identifier("asset_ref").as_string(None))

553
app.py
View file

@ -1,7 +1,7 @@
import io import io
from typing import Any from typing import Any
from fastapi import FastAPI, status, Header, UploadFile, Response from fastapi import FastAPI, status, Header, UploadFile, Response
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from based import db from based import db
import psycopg import psycopg
@ -14,13 +14,16 @@ import uvicorn
from dba import * from dba import *
from models import ( from models import (
AuthModel, AuthModel,
ColumnsDefinitionList, ColumnConditionCompat,
ErrorResponseDefinition, CreateUserDefinition,
ItemDeletionDefinitionList,
ItemsFieldSelectorList, ItemsFieldSelectorList,
TableDefinition, TableDefinition,
TableListDefinition, UserUpdateDefinition,
UserDefinition, OkResponse,
ErrorResponse,
AccessTokenResponse,
TableItemsResponse,
CreateAssetResponse,
) )
from utils import ( from utils import (
check_if_admin_access_token, check_if_admin_access_token,
@ -45,7 +48,11 @@ if found:
else: else:
minioClient.make_bucket(BUCKET_NAME) minioClient.make_bucket(BUCKET_NAME)
app = FastAPI() app = FastAPI(
title="Tuuli API",
description="Tuuli API for Tuuli frontend\n\nUse `c2316f9686e7a764688b8c1b4c60c5a088b07a3c23a2f6b7c25915a5118d7acc` as access token to test the API",
version="0.1.0",
)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
@ -55,107 +62,275 @@ app.add_middleware(
) )
@app.post("/api/getAccessToken") @app.post(
"/api/getAccessToken",
name="Get access token",
responses={
200: {"model": AccessTokenResponse, "description": "Successful response"},
401: {"model": ErrorResponse, "description": "User not found"},
},
)
async def getAccessToken(userData: AuthModel): async def getAccessToken(userData: AuthModel):
user = check_user(connector, userData.username, userData.password) user = check_user(connector, userData.username, userData.password)
if not user: if not user:
return {"error": "Wrong username or password"} return JSONResponse(
ErrorResponse(error="Wrong username or password"),
return {"access_token": user.access_token} status_code=status.HTTP_401_UNAUTHORIZED,
@app.get("/api/listTables")
async def listTables(
response: Response,
access_token: str | None = Header(default=None),
) -> TableListDefinition | ErrorResponseDefinition:
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return ErrorResponseDefinition(error="Not allowed")
return TableListDefinition(
tables=[TableDefinition.parse_obj(table) for table in connector.tables()]
) )
return AccessTokenResponse(access_token=user.access_token)
@app.post("/api/createTable/{tableName}")
async def createTable( @app.get(
tableName: str, "/api/listTables",
columns: ColumnsDefinitionList, name="List tables",
responses={
200: {"model": list[TableDefinition], "description": "List of tables"},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def listTables(
response: Response,
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin: if not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
return [TableDefinition.parse_obj(table) for table in connector.tables()]
@app.post(
"/api/createTable/{tableName}",
name="Create table",
responses={
200: {"model": OkResponse, "description": "Table created successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during table creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
409: {
"model": ErrorResponse,
"description": "Table with this name already exists",
},
},
)
async def createTable(
tableName: str,
columns: list[str],
access_token: str | None = Header(default=None),
):
"""
Parameter `columns` should be a list of strings
Each string should be in a following format:
`column_name:column_type[:column_options]`
Where *column_type* should be one of the following:
- serial
- str
- bool
- datetime
- float
- int
Also *column_options* can be one of the following:
- unique
- default
Example:
```json
[
"id:serial:primary",
"name:str:unique",
"description:str",
"is_active:bool",
"price:float",
"quantity:int"
]
```
Notes:
- you cannot use *unique* and *default* at the same time
- in current implementation you cannot use *default*, because there is no way to
specify default value
"""
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try: try:
columnsDefinition = parse_columns_from_definition(",".join(columns.columns)) columnsDefinition = parse_columns_from_definition(",".join(columns))
create_table(connector, tableName, columnsDefinition) create_table(connector, tableName, columnsDefinition)
except psycopg.errors.UniqueViolation: except psycopg.errors.UniqueViolation:
return {"error": "Username already exists"} return JSONResponse(
ErrorResponse(error="Table already exists"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/api/dropTable/{tableName}") @app.post(
"/api/dropTable/{tableName}",
name="Drop table",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during table creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def dropTable( async def dropTable(
tableName: str, tableName: str,
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin: if not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try: try:
drop_table(connector, tableName) ok, e = drop_table(connector, tableName)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/api/createUser") @app.post(
"/api/createUser",
name="Create user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during user creation",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
409: {
"model": ErrorResponse,
"description": "User with this username already exists",
},
},
)
async def createUser( async def createUser(
user: UserDefinition, user: CreateUserDefinition,
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin: if not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
try: try:
create_user(connector, user.username, user.password) ok, e = create_user(connector, user.username, user.password)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except psycopg.errors.UniqueViolation: except psycopg.errors.UniqueViolation:
return {"error": "Username already exists"} return JSONResponse(
ErrorResponse(error="Username already exists"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/api/updateUser") @app.post(
"/api/updateUser",
name="Update user",
responses={
200: {"model": OkResponse, "description": "Table dropped successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during updating user",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires admin-level user access token",
},
},
)
async def updateUser( async def updateUser(
user: UserDefinition, user: UserUpdateDefinition,
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin: if not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
if not user.user_id or not user.password or not user.access_token: )
return {"error": "Malformed request"}
try: try:
update_user(connector, user.user_id, user.password, user.access_token) ok, e = update_user(connector, user.user_id, user.password, user.access_token)
if not ok:
if e:
raise e
raise Exception("Unknown error")
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/items/{tableName}") @app.post(
"/items/{tableName}",
name="Get items from table",
responses={
200: {"model": TableItemsResponse, "description": "Table items"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during getting table items",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table not found",
},
},
)
async def items( async def items(
tableName: str, tableName: str,
selector: ItemsFieldSelectorList, selector: ItemsFieldSelectorList,
@ -163,11 +338,16 @@ async def items(
): ):
table_info = connector.getTable(tableName) table_info = connector.getTable(tableName)
if not table_info: if not table_info:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Table not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin: if table_info["system"] and not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
columns = parse_columns_from_definition(table_info["columns"]) columns = parse_columns_from_definition(table_info["columns"])
columnsNames = set(column.name for column in columns) columnsNames = set(column.name for column in columns)
@ -175,47 +355,87 @@ async def items(
if userSelectedColumns != ["*"]: if userSelectedColumns != ["*"]:
for column in userSelectedColumns: for column in userSelectedColumns:
if column not in columnsNames: if column not in columnsNames:
return {"error": f"Column {column} not found on table {tableName}"} return JSONResponse(
ErrorResponse(
error=f"Column {column} not found on table {tableName}"
),
status_code=status.HTTP_404_NOT_FOUND,
)
else: else:
userSelectedColumns = columnsNames userSelectedColumns = list(columnsNames)
user, group = get_user_by_access_token(connector, access_token) user, group = get_user_by_access_token(connector, access_token)
if not user: if not user:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
if not is_admin: if not is_admin:
allowedColumns = get_allowed_columns_for_group( allowedColumns = get_allowed_columns_for_group(
connector, tableName, group.id if group else -1 connector, tableName, group.id if group else -1
) )
if not allowedColumns: if not allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*": elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass pass
else: else:
for column in userSelectedColumns: for column in userSelectedColumns:
if column not in allowedColumns: if column not in allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
table_items = connector.selectFromTable( table_items = connector.selectFromTable(
tableName, selector.fields if selector.fields else ["*"] tableName, selector.fields if selector.fields else ["*"]
) )
return {"items": table_items} return TableItemsResponse(items=table_items)
@app.post("/items/{tableName}/+") @app.post(
"/items/{tableName}/+",
name="Create item",
responses={
200: {"model": OkResponse, "description": "Item created successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during creating item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
409: {
"model": ErrorResponse,
"description": "Unique constraint violation",
},
},
)
async def itemsCreate( async def itemsCreate(
tableName: str, tableName: str,
item: dict[str, str], item: dict[str, Any],
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
table_info = connector.getTable(tableName) table_info = connector.getTable(tableName)
if not table_info: if not table_info:
return {"error": "Not found"} return JSONResponse(
ErrorResponse(error="Table not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin: if table_info["system"] and not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
user, group = get_user_by_access_token(connector, access_token) user, group = get_user_by_access_token(connector, access_token)
if not is_admin: if not is_admin:
@ -223,27 +443,63 @@ async def itemsCreate(
connector, tableName, group.id if group else -1 connector, tableName, group.id if group else -1
) )
if not allowedColumns: if not allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*": elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass pass
else: else:
for column in item: for column in item:
if column not in allowedColumns: if column not in allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try: try:
connector.insertIntoTable(tableName, item) connector.insertIntoTable(tableName, item)
except psycopg.errors.UndefinedColumn: except psycopg.errors.UndefinedColumn:
return {"error": "Column not found"} return JSONResponse(
ErrorResponse(error="Column not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
except psycopg.errors.UniqueViolation: except psycopg.errors.UniqueViolation:
return {"error": "Unique constraint violation"} return JSONResponse(
ErrorResponse(error="Unique violation"),
status_code=status.HTTP_409_CONFLICT,
)
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/items/{tableName}/*") @app.post(
"/items/{tableName}/*",
name="Update item in table",
responses={
200: {"model": OkResponse, "description": "Item updated successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during updating item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
409: {
"model": ErrorResponse,
"description": "Unique constraint violation",
},
},
)
async def itemsUpdate( async def itemsUpdate(
tableName: str, tableName: str,
item: dict[str, str], item: dict[str, str],
@ -252,11 +508,17 @@ async def itemsUpdate(
): ):
table_info = connector.getTable(tableName) table_info = connector.getTable(tableName)
if not table_info: if not table_info:
return {"error": "Not found"} return JSONResponse(
ErrorResponse(error="Table not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin: if table_info["system"] and not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
user, group = get_user_by_access_token(connector, access_token) user, group = get_user_by_access_token(connector, access_token)
if not is_admin: if not is_admin:
@ -264,41 +526,84 @@ async def itemsUpdate(
connector, tableName, group.id if group else -1 connector, tableName, group.id if group else -1
) )
if not allowedColumns: if not allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*": elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass pass
else: else:
for column in item: for column in item:
if column not in allowedColumns: if column not in allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try: try:
connector.updateDataInTable( connector.updateDataInTable(
tableName, tableName,
[ColumnUpdate(column=c, value=item[c]) for c in item], [ColumnUpdate(column=c, value=item[c]) for c in item],
[ColumnCondition(column=c, value=oldItem[c]) for c in oldItem], [
ColumnCondition(column=c, operator="eq", value=oldItem[c])
for c in oldItem
],
)
except psycopg.errors.UniqueViolation:
return JSONResponse(
ErrorResponse(error="Unique violation"),
status_code=status.HTTP_409_CONFLICT,
) )
except psycopg.errors.UndefinedColumn: except psycopg.errors.UndefinedColumn:
return {"error": "Column not found"} return JSONResponse(
ErrorResponse(error="Column not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.post("/items/{tableName}/-") @app.post(
"/items/{tableName}/-",
name="Delete item from table",
responses={
200: {"model": OkResponse, "description": "Item deleted successfully"},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during deleting item",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
404: {
"model": ErrorResponse,
"description": "Table or column not found",
},
},
)
async def itemsDelete( async def itemsDelete(
tableName: str, tableName: str,
deleteWhere: ItemDeletionDefinitionList, deleteWhere: list[ColumnConditionCompat],
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
table_info = connector.getTable(tableName) table_info = connector.getTable(tableName)
if not table_info: if not table_info:
return {"error": "Not found"} return JSONResponse(
ErrorResponse(error="Table not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
is_admin = check_if_admin_access_token(connector, access_token) is_admin = check_if_admin_access_token(connector, access_token)
if table_info["system"] and not is_admin: if table_info["system"] and not is_admin:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
user, group = get_user_by_access_token(connector, access_token) user, group = get_user_by_access_token(connector, access_token)
if not is_admin: if not is_admin:
@ -306,27 +611,44 @@ async def itemsDelete(
connector, tableName, group.id if group else -1 connector, tableName, group.id if group else -1
) )
if not allowedColumns: if not allowedColumns:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
elif len(allowedColumns) == 1 and allowedColumns[0] == "*": elif len(allowedColumns) == 1 and allowedColumns[0] == "*":
pass pass
else: else:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try: try:
connector.deleteFromTable( connector.deleteFromTable(
tableName, tableName,
[ [ColumnCondition(dw.column, dw.operator, dw.value) for dw in deleteWhere],
ColumnCondition(where.name, where.value, where.isString, where.isLike)
for where in deleteWhere.defs
],
) )
except Exception as e: except Exception as e:
return {"error": str(e)} return JSONResponse(
ErrorResponse(error=str(e)), status_code=status.HTTP_400_BAD_REQUEST
)
return {"ok": True} return OkResponse()
@app.get("/assets/{fid}") @app.get(
"/assets/{fid}",
name="Get asset",
responses={
200: {
"description": "Asset found",
"content": {"application/octet-stream": {}},
},
404: {
"description": "Asset not found",
},
},
)
async def getAsset(fid: str, access_token: str | None = Header(default=None)): async def getAsset(fid: str, access_token: str | None = Header(default=None)):
asset = get_asset(connector, access_token, fid) asset = get_asset(connector, access_token, fid)
if not asset: if not asset:
@ -349,14 +671,38 @@ async def getAsset(fid: str, access_token: str | None = Header(default=None)):
response.release_conn() response.release_conn()
@app.post("/assets/+") @app.post(
"/assets/+",
name="Put asset",
responses={
200: {
"model": CreateAssetResponse,
"description": "Asset created successfully",
},
400: {
"model": ErrorResponse,
"description": "Some generic error happened during creating asset",
},
403: {
"model": ErrorResponse,
"description": "Requesting this endpoint requires user access token",
},
500: {
"model": ErrorResponse,
"description": "Failed put asset into storage",
},
},
)
async def createAsset( async def createAsset(
asset: UploadFile, asset: UploadFile,
access_token: str | None = Header(default=None), access_token: str | None = Header(default=None),
): ):
user, _ = get_user_by_access_token(connector, access_token) user, _ = get_user_by_access_token(connector, access_token)
if not user: if not user:
return {"error": "Not allowed"} return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
filename = asset.filename filename = asset.filename
if not filename: if not filename:
@ -372,10 +718,19 @@ async def createAsset(
), ),
length=asset.size, length=asset.size,
) )
if not result:
return JSONResponse(
ErrorResponse(error="Failed put asset into storage"),
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if not create_asset(connector, filename, "", str(result.version_id)): if not create_asset(connector, filename, "", str(result.version_id)):
return {"error": "Failed to create asset"} return JSONResponse(
return {"ok": True, "fid": result.version_id} ErrorResponse(error="Failed to create asset"),
status_code=status.HTTP_400_BAD_REQUEST,
)
return CreateAssetResponse(fid=result.version_id)
if __name__ == "__main__": if __name__ == "__main__":

26
db_addendum.py Normal file
View file

@ -0,0 +1,26 @@
from psycopg.sql import SQL, Identifier, Literal, Composed
from based.columns import (
IntegerColumnDefinition,
)
class UserRefColumnDefinition(IntegerColumnDefinition):
def __init__(self, name: str):
super().__init__(name)
def sql(self):
return SQL("{} INTEGER NOT NULL").format(Identifier(self.name))
def serialize(self):
return f"{self.name}:asset"
class AssetRefColumnDefinition(IntegerColumnDefinition):
def __init__(self, name: str):
super().__init__(name)
def sql(self):
return SQL("{} INTEGER NOT NULL").format(Identifier(self.name))
def serialize(self):
return f"{self.name}:asset"

View file

@ -3,7 +3,6 @@ from based.columns import (
PrimarySerialColumnDefinition, PrimarySerialColumnDefinition,
TextColumnDefinition, TextColumnDefinition,
IntegerColumnDefinition, IntegerColumnDefinition,
make_column_unique,
) )
from pydantic import BaseModel from pydantic import BaseModel
@ -18,7 +17,7 @@ class AccessType(enum.Enum):
META_INFO_TABLE_NAME = "meta_info" META_INFO_TABLE_NAME = "meta_info"
META_INFO_TABLE_SCHEMA = [ META_INFO_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"), PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("name")), TextColumnDefinition("name", unique=True),
TextColumnDefinition("value"), TextColumnDefinition("value"),
TextColumnDefinition("allowed_columns", default="*"), TextColumnDefinition("allowed_columns", default="*"),
] ]
@ -34,7 +33,7 @@ class MetaInfo(BaseModel):
USER_GROUP_TABLE_NAME = "user_group" USER_GROUP_TABLE_NAME = "user_group"
USER_GROUP_TABLE_SCHEMA = [ USER_GROUP_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"), PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("name")), TextColumnDefinition("name", unique=True),
TextColumnDefinition("description", default=""), TextColumnDefinition("description", default=""),
] ]
@ -48,7 +47,7 @@ class UserGroup(BaseModel):
USERS_TABLE_NAME = "users" USERS_TABLE_NAME = "users"
USERS_TABLE_SCHEMA = [ USERS_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"), PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("username")), TextColumnDefinition("username", unique=True),
TextColumnDefinition("password"), TextColumnDefinition("password"),
TextColumnDefinition("access_token"), TextColumnDefinition("access_token"),
] ]

56
dba.py
View file

@ -9,17 +9,21 @@ logger = logging.getLogger(__name__)
def bootstrapDB(conn: DBConnector): def bootstrapDB(conn: DBConnector):
if not conn.tableExists(META_INFO_TABLE_NAME): if not conn.tableExists(META_INFO_TABLE_NAME):
logger.info("Creating meta info table")
conn.createTable( conn.createTable(
META_INFO_TABLE_NAME, META_INFO_TABLE_SCHEMA, system=True, hidden=True META_INFO_TABLE_NAME, META_INFO_TABLE_SCHEMA, system=True, hidden=True
) )
if not conn.tableExists(USER_GROUP_TABLE_NAME): if not conn.tableExists(USER_GROUP_TABLE_NAME):
logger.info("Creating user group table")
conn.createTable(USER_GROUP_TABLE_NAME, USER_GROUP_TABLE_SCHEMA, system=True) conn.createTable(USER_GROUP_TABLE_NAME, USER_GROUP_TABLE_SCHEMA, system=True)
if not conn.tableExists(USERS_TABLE_NAME): if not conn.tableExists(USERS_TABLE_NAME):
logger.info("Creating users table")
conn.createTable(USERS_TABLE_NAME, USERS_TABLE_SCHEMA, system=True) conn.createTable(USERS_TABLE_NAME, USERS_TABLE_SCHEMA, system=True)
if not conn.tableExists(USER_IN_USER_GROUP_JOIN_TABLE_NAME): if not conn.tableExists(USER_IN_USER_GROUP_JOIN_TABLE_NAME):
logger.info("Creating user in user group join table")
conn.createTable( conn.createTable(
USER_IN_USER_GROUP_JOIN_TABLE_NAME, USER_IN_USER_GROUP_JOIN_TABLE_NAME,
USER_IN_USER_GROUP_JOIN_TABLE_SCHEMA, USER_IN_USER_GROUP_JOIN_TABLE_SCHEMA,
@ -27,6 +31,7 @@ def bootstrapDB(conn: DBConnector):
) )
if not conn.tableExists(TABLE_ACCESS_TABLE_NAME): if not conn.tableExists(TABLE_ACCESS_TABLE_NAME):
logger.info("Creating table access table")
conn.createTable( conn.createTable(
TABLE_ACCESS_TABLE_NAME, TABLE_ACCESS_TABLE_NAME,
TABLE_ACCESS_TABLE_SCHEMA, TABLE_ACCESS_TABLE_SCHEMA,
@ -34,6 +39,7 @@ def bootstrapDB(conn: DBConnector):
) )
if not conn.tableExists(ASSETS_TABLE_NAME): if not conn.tableExists(ASSETS_TABLE_NAME):
logger.info("Creating assets table")
conn.createTable( conn.createTable(
ASSETS_TABLE_NAME, ASSETS_TABLE_NAME,
ASSETS_TABLE_SCHEMA, ASSETS_TABLE_SCHEMA,
@ -41,6 +47,7 @@ def bootstrapDB(conn: DBConnector):
) )
if not conn.tableExists(ASSET_ACCESS_TABLE_NAME): if not conn.tableExists(ASSET_ACCESS_TABLE_NAME):
logger.info("Creating asset access table")
conn.createTable( conn.createTable(
ASSET_ACCESS_TABLE_NAME, ASSET_ACCESS_TABLE_NAME,
ASSET_ACCESS_TABLE_SCHEMA, ASSET_ACCESS_TABLE_SCHEMA,
@ -50,6 +57,7 @@ def bootstrapDB(conn: DBConnector):
meta = get_metadata(conn, "admin_created") meta = get_metadata(conn, "admin_created")
testAdminCreated = meta and meta.value == "yes" testAdminCreated = meta and meta.value == "yes"
if not testAdminCreated: if not testAdminCreated:
logger.info("Creating admin user and group")
create_user(conn, "admin", "admin") create_user(conn, "admin", "admin")
create_group(conn, "admin") create_group(conn, "admin")
@ -72,7 +80,7 @@ def add_metadata(conn: DBConnector, name: str, value: str):
def get_metadata(conn: DBConnector, name: str): def get_metadata(conn: DBConnector, name: str):
try: try:
metadata = conn.filterFromTable( metadata = conn.filterFromTable(
META_INFO_TABLE_NAME, ["*"], [ColumnCondition("name", name)] META_INFO_TABLE_NAME, ["*"], [ColumnCondition("name", "eq", name)]
) )
if len(metadata) == 0: if len(metadata) == 0:
logger.warning(f"Metadata {name} not found") logger.warning(f"Metadata {name} not found")
@ -106,7 +114,7 @@ def update_user(conn: DBConnector, id: int, password: str, access_token: str):
ColumnUpdate("access_token", access_token), ColumnUpdate("access_token", access_token),
], ],
[ [
ColumnCondition("id", id), ColumnCondition("id", "eq", id),
], ],
) )
return True, None return True, None
@ -118,7 +126,7 @@ def update_user(conn: DBConnector, id: int, password: str, access_token: str):
def get_user_by_username(conn: DBConnector, username: str): def get_user_by_username(conn: DBConnector, username: str):
try: try:
users = conn.filterFromTable( users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("username", username)] USERS_TABLE_NAME, ["*"], [ColumnCondition("username", "eq", username)]
) )
if len(users) == 0: if len(users) == 0:
logger.warning(f"User {username} not found") logger.warning(f"User {username} not found")
@ -132,7 +140,7 @@ def get_user_by_username(conn: DBConnector, username: str):
def get_user_by_id(conn: DBConnector, user_id: int): def get_user_by_id(conn: DBConnector, user_id: int):
try: try:
users = conn.filterFromTable( users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("id", user_id)] USERS_TABLE_NAME, ["*"], [ColumnCondition("id", "eq", user_id)]
) )
if len(users) == 0: if len(users) == 0:
logger.warning(f"User with id {user_id} not found") logger.warning(f"User with id {user_id} not found")
@ -146,7 +154,9 @@ def get_user_by_id(conn: DBConnector, user_id: int):
def get_user_by_access_token(conn: DBConnector, access_token: str | None): def get_user_by_access_token(conn: DBConnector, access_token: str | None):
try: try:
users = conn.filterFromTable( users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("access_token", access_token)] USERS_TABLE_NAME,
["*"],
[ColumnCondition("access_token", "eq", access_token)],
) )
if len(users) == 0: if len(users) == 0:
logger.warning("Invalid access token") logger.warning("Invalid access token")
@ -168,8 +178,8 @@ def check_user(conn: DBConnector, username: str, password: str):
USERS_TABLE_NAME, USERS_TABLE_NAME,
["*"], ["*"],
[ [
ColumnCondition("username", username), ColumnCondition("username", "eq", username),
ColumnCondition("password", hashedPwd), ColumnCondition("password", "eq", hashedPwd),
], ],
) )
if len(user) == 0: if len(user) == 0:
@ -195,7 +205,7 @@ def create_group(conn: DBConnector, name: str, description: str = ""):
def get_group_by_name(conn: DBConnector, name: str): def get_group_by_name(conn: DBConnector, name: str):
try: try:
groups = conn.filterFromTable( groups = conn.filterFromTable(
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("name", name)] USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("name", "eq", name)]
) )
if len(groups) == 0: if len(groups) == 0:
logger.warning(f"Group {name} not found") logger.warning(f"Group {name} not found")
@ -209,7 +219,7 @@ def get_group_by_name(conn: DBConnector, name: str):
def get_group_by_id(conn: DBConnector, group_id: int): def get_group_by_id(conn: DBConnector, group_id: int):
try: try:
groups = conn.filterFromTable( groups = conn.filterFromTable(
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("id", group_id)] USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("id", "eq", group_id)]
) )
if len(groups) == 0: if len(groups) == 0:
logger.warning(f"Group with id {group_id} not found") logger.warning(f"Group with id {group_id} not found")
@ -226,7 +236,7 @@ def set_user_group(conn: DBConnector, user_id: int, group_id: int):
USER_IN_USER_GROUP_JOIN_TABLE_NAME, USER_IN_USER_GROUP_JOIN_TABLE_NAME,
["*"], ["*"],
[ [
ColumnCondition("user_id", user_id), ColumnCondition("user_id", "eq", user_id),
], ],
): ):
conn.insertIntoTable( conn.insertIntoTable(
@ -240,7 +250,7 @@ def set_user_group(conn: DBConnector, user_id: int, group_id: int):
ColumnUpdate("user_group_id", group_id), ColumnUpdate("user_group_id", group_id),
], ],
[ [
ColumnCondition("user_id", user_id), ColumnCondition("user_id", "eq", user_id),
], ],
) )
return True, None return True, None
@ -254,7 +264,7 @@ def get_user_group(conn: DBConnector, user_id: int):
grp_usr_joint = conn.filterFromTable( grp_usr_joint = conn.filterFromTable(
USER_IN_USER_GROUP_JOIN_TABLE_NAME, USER_IN_USER_GROUP_JOIN_TABLE_NAME,
["*"], ["*"],
[ColumnCondition("user_id", user_id)], [ColumnCondition("user_id", "eq", user_id)],
) )
if len(grp_usr_joint) == 0: if len(grp_usr_joint) == 0:
logger.warning(f"User with id {user_id} not found, so no group") logger.warning(f"User with id {user_id} not found, so no group")
@ -272,7 +282,7 @@ def get_group_users(conn: DBConnector, group_id: int) -> list[User]:
users = conn.filterFromTable( users = conn.filterFromTable(
USER_IN_USER_GROUP_JOIN_TABLE_NAME, USER_IN_USER_GROUP_JOIN_TABLE_NAME,
["*"], ["*"],
[ColumnCondition("user_group_id", group_id)], [ColumnCondition("user_group_id", "eq", group_id)],
) )
return [*map(User.parse_obj, users)] return [*map(User.parse_obj, users)]
except Exception as e: except Exception as e:
@ -321,8 +331,8 @@ def get_table_access_level(
TABLE_ACCESS_TABLE_NAME, TABLE_ACCESS_TABLE_NAME,
["*"], ["*"],
[ [
ColumnCondition("table_name", table_name), ColumnCondition("table_name", "eq", table_name),
ColumnCondition("user_group_id", user_group.id), ColumnCondition("user_group_id", "eq", user_group.id),
], ],
) )
if not access: if not access:
@ -349,8 +359,8 @@ def get_allowed_columns_for_group(
TABLE_ACCESS_TABLE_NAME, TABLE_ACCESS_TABLE_NAME,
["*"], ["*"],
[ [
ColumnCondition("table_name", table_name), ColumnCondition("table_name", "eq", table_name),
ColumnCondition("user_group_id", group_id), ColumnCondition("user_group_id", "eq", group_id),
], ],
) )
if not allowed_columns: if not allowed_columns:
@ -400,7 +410,7 @@ def create_asset(conn: DBConnector, name: str, description: str, fid: str):
}, },
) )
# TODO: add asset access # TODO: add asset access
# TODO: add asset to seaweedfs # TODO: add asset to minio
return True return True
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
@ -409,9 +419,9 @@ def create_asset(conn: DBConnector, name: str, description: str, fid: str):
def remove_asset(conn: DBConnector, token: str | None, asset_id: int): def remove_asset(conn: DBConnector, token: str | None, asset_id: int):
try: try:
conn.deleteFromTable(ASSETS_TABLE_NAME, [ColumnCondition("id", asset_id)]) conn.deleteFromTable(ASSETS_TABLE_NAME, [ColumnCondition("id", "eq", asset_id)])
# TODO: remove asset access # TODO: remove asset access
# TODO: remove asset from seaweedfs # TODO: remove asset from minio
return True return True
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)
@ -422,7 +432,7 @@ def get_asset(conn: DBConnector, token: str | None, fid: str):
try: try:
user, group = get_user_by_access_token(conn, token) user, group = get_user_by_access_token(conn, token)
assets = conn.filterFromTable( assets = conn.filterFromTable(
ASSETS_TABLE_NAME, ["*"], [ColumnCondition("fid", fid)] ASSETS_TABLE_NAME, ["*"], [ColumnCondition("fid", "eq", fid)]
) )
print(assets) print(assets)
if len(assets) == 0: if len(assets) == 0:
@ -453,7 +463,9 @@ def create_asset_access(conn: DBConnector, asset_id: int, user_group_id: int):
def get_asset_access(conn: DBConnector, asset_id: int): def get_asset_access(conn: DBConnector, asset_id: int):
try: try:
access = conn.filterFromTable( access = conn.filterFromTable(
ASSET_ACCESS_TABLE_NAME, ["*"], [ColumnCondition("asset_id", asset_id)] ASSET_ACCESS_TABLE_NAME,
["*"],
[ColumnCondition("asset_id", "eq", asset_id)],
) )
if not access: if not access:
return AccessType.NONE return AccessType.NONE

View file

@ -1,6 +1,6 @@
from typing import Any from typing import Any
from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from based.db import CONDITION_OPERATORS
class AuthModel(BaseModel): class AuthModel(BaseModel):
@ -9,11 +9,12 @@ class AuthModel(BaseModel):
class ItemsFieldSelectorList(BaseModel): class ItemsFieldSelectorList(BaseModel):
fields: list[str] = [] fields: list[str] | None = []
class ColumnsDefinitionList(BaseModel): class CreateUserDefinition(BaseModel):
columns: list[str] username: str
password: str
class UserDefinition(BaseModel): class UserDefinition(BaseModel):
@ -23,28 +24,42 @@ class UserDefinition(BaseModel):
access_token: str | None = None access_token: str | None = None
class ColumnDefinition(BaseModel): class UserUpdateDefinition(BaseModel):
name: str user_id: int
password: str
access_token: str
class ColumnConditionCompat(BaseModel):
column: str
operator: CONDITION_OPERATORS
value: Any value: Any
isString: bool = False
isLike: bool = True
class ItemDeletionDefinitionList(BaseModel):
defs: list[ColumnDefinition]
class TableDefinition(BaseModel): class TableDefinition(BaseModel):
table_id: UUID table_id: str
table_name: str table_name: str
columns: str columns: str
system: bool system: bool
hidden: bool hidden: bool
class TableListDefinition(BaseModel): class OkResponse(BaseModel):
tables: list[TableDefinition] ok: bool = True
class ErrorResponseDefinition(BaseModel): class ErrorResponse(BaseModel):
error: str error: str
class AccessTokenResponse(BaseModel):
access_token: str
class TableItemsResponse(BaseModel):
items: list[dict[str, Any]]
class CreateAssetResponse(BaseModel):
ok: bool = True
fid: str

107
utils.py
View file

@ -1,17 +1,12 @@
from based.db import DBConnector from based.db import DBConnector
from based.columns import ( from based.columns import (
ColumnDefinition, ColumnDefinition,
make_column_unique,
PrimarySerialColumnDefinition, PrimarySerialColumnDefinition,
PrimaryUUIDColumnDefinition,
TextColumnDefinition, TextColumnDefinition,
BigintColumnDefinition,
BooleanColumnDefinition, BooleanColumnDefinition,
DateColumnDefinition,
TimestampColumnDefinition, TimestampColumnDefinition,
DoubleColumnDefinition, DoubleColumnDefinition,
IntegerColumnDefinition, IntegerColumnDefinition,
UUIDColumnDefinition,
) )
from dba import get_user_by_access_token from dba import get_user_by_access_token
@ -34,82 +29,40 @@ def get_column_from_definition(definition: str) -> ColumnDefinition | None:
case [name, "serial", "primary"]: case [name, "serial", "primary"]:
return PrimarySerialColumnDefinition(name) return PrimarySerialColumnDefinition(name)
case [name, "uuid", "primary"]: case [name, "str", *rest]:
return PrimaryUUIDColumnDefinition(name) is_unique = "unique" in rest
has_default = "default" in rest
td = TextColumnDefinition(name, unique=is_unique)
td.has_default = has_default
return td
case [name, "str"]: case [name, "bool", *rest]:
return TextColumnDefinition(name) is_unique = "unique" in rest
case [name, "str", "unique"]: has_default = "default" in rest
return make_column_unique(TextColumnDefinition(name)) td = BooleanColumnDefinition(name, unique=is_unique)
case [name, "str", "default", default]: td.has_default = has_default
return TextColumnDefinition(name, default=default) return td
case [name, "str", "default", default, "unique"]:
return make_column_unique(TextColumnDefinition(name, default=default))
case [name, "bigint"]: case [name, "datetime", *rest]:
return BigintColumnDefinition(name) is_unique = "unique" in rest
case [name, "bigint", "unique"]: has_default = "default" in rest
return make_column_unique(BigintColumnDefinition(name)) td = TimestampColumnDefinition(name, unique=is_unique)
case [name, "bigint", "default", default]: td.has_default = has_default
return BigintColumnDefinition(name, default=int(default)) return td
case [name, "bigint", "default", default, "unique"]:
return make_column_unique(
BigintColumnDefinition(name, default=int(default))
)
case [name, "bool"]: case [name, "float", *rest]:
return BooleanColumnDefinition(name) is_unique = "unique" in rest
case [name, "bool", "unique"]: has_default = "default" in rest
return make_column_unique(BooleanColumnDefinition(name)) td = DoubleColumnDefinition(name, unique=is_unique)
case [name, "bool", "default", default]: td.has_default = has_default
return BooleanColumnDefinition(name, default=bool(default)) return td
case [name, "bool", "default", default, "unique"]:
return make_column_unique(
BooleanColumnDefinition(name, default=bool(default))
)
case [name, "date"]: case [name, "int", *rest]:
return DateColumnDefinition(name) is_unique = "unique" in rest
case [name, "date", "unique"]: has_default = "default" in rest
return make_column_unique(DateColumnDefinition(name)) td = IntegerColumnDefinition(name, unique=is_unique)
# TODO: Add default value for date td.has_default = has_default
return td
case [name, "datetime"]:
return TimestampColumnDefinition(name)
case [name, "datetime", "unique"]:
return make_column_unique(TimestampColumnDefinition(name))
# TODO: Add default value for timestamp
case [name, "float"]:
return DoubleColumnDefinition(name)
case [name, "float", "unique"]:
return make_column_unique(DoubleColumnDefinition(name))
case [name, "float", "default", default]:
return DoubleColumnDefinition(name, default=float(default))
case [name, "float", "default", default, "unique"]:
return make_column_unique(
DoubleColumnDefinition(name, default=float(default))
)
case [name, "int"]:
return IntegerColumnDefinition(name)
case [name, "int", "unique"]:
return make_column_unique(IntegerColumnDefinition(name))
case [name, "int", "default", default]:
return IntegerColumnDefinition(name, default=int(default))
case [name, "int", "default", default, "unique"]:
return make_column_unique(
IntegerColumnDefinition(name, default=int(default))
)
case [name, "uuid"]:
return UUIDColumnDefinition(name)
case [name, "uuid", "unique"]:
return make_column_unique(UUIDColumnDefinition(name))
case [name, "uuid", "default", default]:
return UUIDColumnDefinition(name, default=default)
case [name, "uuid", "default", default, "unique"]:
return make_column_unique(UUIDColumnDefinition(name, default=default))
return None return None