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))

549
app.py
View file

@ -1,7 +1,7 @@
import io
from typing import Any
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 based import db
import psycopg
@ -14,13 +14,16 @@ import uvicorn
from dba import *
from models import (
AuthModel,
ColumnsDefinitionList,
ErrorResponseDefinition,
ItemDeletionDefinitionList,
ColumnConditionCompat,
CreateUserDefinition,
ItemsFieldSelectorList,
TableDefinition,
TableListDefinition,
UserDefinition,
UserUpdateDefinition,
OkResponse,
ErrorResponse,
AccessTokenResponse,
TableItemsResponse,
CreateAssetResponse,
)
from utils import (
check_if_admin_access_token,
@ -45,7 +48,11 @@ if found:
else:
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(
CORSMiddleware,
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):
user = check_user(connector, userData.username, userData.password)
if not user:
return {"error": "Wrong username or password"}
return JSONResponse(
ErrorResponse(error="Wrong username or password"),
status_code=status.HTTP_401_UNAUTHORIZED,
)
return {"access_token": user.access_token}
return AccessTokenResponse(access_token=user.access_token)
@app.get("/api/listTables")
@app.get(
"/api/listTables",
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),
) -> 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()]
)
@app.post("/api/createTable/{tableName}")
async def createTable(
tableName: str,
columns: ColumnsDefinitionList,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
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:
columnsDefinition = parse_columns_from_definition(",".join(columns.columns))
columnsDefinition = parse_columns_from_definition(",".join(columns))
create_table(connector, tableName, columnsDefinition)
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:
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(
tableName: str,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
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:
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(
user: UserDefinition,
user: CreateUserDefinition,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
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:
return {"error": "Username already exists"}
return JSONResponse(
ErrorResponse(error="Username already exists"),
status_code=status.HTTP_409_CONFLICT,
)
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(
user: UserDefinition,
user: UserUpdateDefinition,
access_token: str | None = Header(default=None),
):
is_admin = check_if_admin_access_token(connector, access_token)
if not is_admin:
return {"error": "Not allowed"}
if not user.user_id or not user.password or not user.access_token:
return {"error": "Malformed request"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
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:
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(
tableName: str,
selector: ItemsFieldSelectorList,
@ -163,11 +338,16 @@ async def items(
):
table_info = connector.getTable(tableName)
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)
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"])
columnsNames = set(column.name for column in columns)
@ -175,47 +355,87 @@ async def items(
if userSelectedColumns != ["*"]:
for column in userSelectedColumns:
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:
userSelectedColumns = columnsNames
userSelectedColumns = list(columnsNames)
user, group = get_user_by_access_token(connector, access_token)
if not user:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"), status_code=status.HTTP_403_FORBIDDEN
)
if not is_admin:
allowedColumns = get_allowed_columns_for_group(
connector, tableName, group.id if group else -1
)
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] == "*":
pass
else:
for column in userSelectedColumns:
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(
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(
tableName: str,
item: dict[str, str],
item: dict[str, Any],
access_token: str | None = Header(default=None),
):
table_info = connector.getTable(tableName)
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)
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)
if not is_admin:
@ -223,27 +443,63 @@ async def itemsCreate(
connector, tableName, group.id if group else -1
)
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] == "*":
pass
else:
for column in item:
if column not in allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.insertIntoTable(tableName, item)
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:
return {"error": "Unique constraint violation"}
return JSONResponse(
ErrorResponse(error="Unique violation"),
status_code=status.HTTP_409_CONFLICT,
)
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(
tableName: str,
item: dict[str, str],
@ -252,11 +508,17 @@ async def itemsUpdate(
):
table_info = connector.getTable(tableName)
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)
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)
if not is_admin:
@ -264,41 +526,84 @@ async def itemsUpdate(
connector, tableName, group.id if group else -1
)
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] == "*":
pass
else:
for column in item:
if column not in allowedColumns:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.updateDataInTable(
tableName,
[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:
return {"error": "Column not found"}
return JSONResponse(
ErrorResponse(error="Column not found"),
status_code=status.HTTP_404_NOT_FOUND,
)
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(
tableName: str,
deleteWhere: ItemDeletionDefinitionList,
deleteWhere: list[ColumnConditionCompat],
access_token: str | None = Header(default=None),
):
table_info = connector.getTable(tableName)
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)
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)
if not is_admin:
@ -306,27 +611,44 @@ async def itemsDelete(
connector, tableName, group.id if group else -1
)
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] == "*":
pass
else:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
try:
connector.deleteFromTable(
tableName,
[
ColumnCondition(where.name, where.value, where.isString, where.isLike)
for where in deleteWhere.defs
],
[ColumnCondition(dw.column, dw.operator, dw.value) for dw in deleteWhere],
)
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)):
asset = get_asset(connector, access_token, fid)
if not asset:
@ -349,14 +671,38 @@ async def getAsset(fid: str, access_token: str | None = Header(default=None)):
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(
asset: UploadFile,
access_token: str | None = Header(default=None),
):
user, _ = get_user_by_access_token(connector, access_token)
if not user:
return {"error": "Not allowed"}
return JSONResponse(
ErrorResponse(error="Not allowed"),
status_code=status.HTTP_403_FORBIDDEN,
)
filename = asset.filename
if not filename:
@ -372,10 +718,19 @@ async def createAsset(
),
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)):
return {"error": "Failed to create asset"}
return {"ok": True, "fid": result.version_id}
return JSONResponse(
ErrorResponse(error="Failed to create asset"),
status_code=status.HTTP_400_BAD_REQUEST,
)
return CreateAssetResponse(fid=result.version_id)
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,
TextColumnDefinition,
IntegerColumnDefinition,
make_column_unique,
)
from pydantic import BaseModel
@ -18,7 +17,7 @@ class AccessType(enum.Enum):
META_INFO_TABLE_NAME = "meta_info"
META_INFO_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("name")),
TextColumnDefinition("name", unique=True),
TextColumnDefinition("value"),
TextColumnDefinition("allowed_columns", default="*"),
]
@ -34,7 +33,7 @@ class MetaInfo(BaseModel):
USER_GROUP_TABLE_NAME = "user_group"
USER_GROUP_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("name")),
TextColumnDefinition("name", unique=True),
TextColumnDefinition("description", default=""),
]
@ -48,7 +47,7 @@ class UserGroup(BaseModel):
USERS_TABLE_NAME = "users"
USERS_TABLE_SCHEMA = [
PrimarySerialColumnDefinition("id"),
make_column_unique(TextColumnDefinition("username")),
TextColumnDefinition("username", unique=True),
TextColumnDefinition("password"),
TextColumnDefinition("access_token"),
]

56
dba.py
View file

@ -9,17 +9,21 @@ logger = logging.getLogger(__name__)
def bootstrapDB(conn: DBConnector):
if not conn.tableExists(META_INFO_TABLE_NAME):
logger.info("Creating meta info table")
conn.createTable(
META_INFO_TABLE_NAME, META_INFO_TABLE_SCHEMA, system=True, hidden=True
)
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)
if not conn.tableExists(USERS_TABLE_NAME):
logger.info("Creating users table")
conn.createTable(USERS_TABLE_NAME, USERS_TABLE_SCHEMA, system=True)
if not conn.tableExists(USER_IN_USER_GROUP_JOIN_TABLE_NAME):
logger.info("Creating user in user group join table")
conn.createTable(
USER_IN_USER_GROUP_JOIN_TABLE_NAME,
USER_IN_USER_GROUP_JOIN_TABLE_SCHEMA,
@ -27,6 +31,7 @@ def bootstrapDB(conn: DBConnector):
)
if not conn.tableExists(TABLE_ACCESS_TABLE_NAME):
logger.info("Creating table access table")
conn.createTable(
TABLE_ACCESS_TABLE_NAME,
TABLE_ACCESS_TABLE_SCHEMA,
@ -34,6 +39,7 @@ def bootstrapDB(conn: DBConnector):
)
if not conn.tableExists(ASSETS_TABLE_NAME):
logger.info("Creating assets table")
conn.createTable(
ASSETS_TABLE_NAME,
ASSETS_TABLE_SCHEMA,
@ -41,6 +47,7 @@ def bootstrapDB(conn: DBConnector):
)
if not conn.tableExists(ASSET_ACCESS_TABLE_NAME):
logger.info("Creating asset access table")
conn.createTable(
ASSET_ACCESS_TABLE_NAME,
ASSET_ACCESS_TABLE_SCHEMA,
@ -50,6 +57,7 @@ def bootstrapDB(conn: DBConnector):
meta = get_metadata(conn, "admin_created")
testAdminCreated = meta and meta.value == "yes"
if not testAdminCreated:
logger.info("Creating admin user and group")
create_user(conn, "admin", "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):
try:
metadata = conn.filterFromTable(
META_INFO_TABLE_NAME, ["*"], [ColumnCondition("name", name)]
META_INFO_TABLE_NAME, ["*"], [ColumnCondition("name", "eq", name)]
)
if len(metadata) == 0:
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),
],
[
ColumnCondition("id", id),
ColumnCondition("id", "eq", id),
],
)
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):
try:
users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("username", username)]
USERS_TABLE_NAME, ["*"], [ColumnCondition("username", "eq", username)]
)
if len(users) == 0:
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):
try:
users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("id", user_id)]
USERS_TABLE_NAME, ["*"], [ColumnCondition("id", "eq", user_id)]
)
if len(users) == 0:
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):
try:
users = conn.filterFromTable(
USERS_TABLE_NAME, ["*"], [ColumnCondition("access_token", access_token)]
USERS_TABLE_NAME,
["*"],
[ColumnCondition("access_token", "eq", access_token)],
)
if len(users) == 0:
logger.warning("Invalid access token")
@ -168,8 +178,8 @@ def check_user(conn: DBConnector, username: str, password: str):
USERS_TABLE_NAME,
["*"],
[
ColumnCondition("username", username),
ColumnCondition("password", hashedPwd),
ColumnCondition("username", "eq", username),
ColumnCondition("password", "eq", hashedPwd),
],
)
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):
try:
groups = conn.filterFromTable(
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("name", name)]
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("name", "eq", name)]
)
if len(groups) == 0:
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):
try:
groups = conn.filterFromTable(
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("id", group_id)]
USER_GROUP_TABLE_NAME, ["*"], [ColumnCondition("id", "eq", group_id)]
)
if len(groups) == 0:
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,
["*"],
[
ColumnCondition("user_id", user_id),
ColumnCondition("user_id", "eq", user_id),
],
):
conn.insertIntoTable(
@ -240,7 +250,7 @@ def set_user_group(conn: DBConnector, user_id: int, group_id: int):
ColumnUpdate("user_group_id", group_id),
],
[
ColumnCondition("user_id", user_id),
ColumnCondition("user_id", "eq", user_id),
],
)
return True, None
@ -254,7 +264,7 @@ def get_user_group(conn: DBConnector, user_id: int):
grp_usr_joint = conn.filterFromTable(
USER_IN_USER_GROUP_JOIN_TABLE_NAME,
["*"],
[ColumnCondition("user_id", user_id)],
[ColumnCondition("user_id", "eq", user_id)],
)
if len(grp_usr_joint) == 0:
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(
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)]
except Exception as e:
@ -321,8 +331,8 @@ def get_table_access_level(
TABLE_ACCESS_TABLE_NAME,
["*"],
[
ColumnCondition("table_name", table_name),
ColumnCondition("user_group_id", user_group.id),
ColumnCondition("table_name", "eq", table_name),
ColumnCondition("user_group_id", "eq", user_group.id),
],
)
if not access:
@ -349,8 +359,8 @@ def get_allowed_columns_for_group(
TABLE_ACCESS_TABLE_NAME,
["*"],
[
ColumnCondition("table_name", table_name),
ColumnCondition("user_group_id", group_id),
ColumnCondition("table_name", "eq", table_name),
ColumnCondition("user_group_id", "eq", group_id),
],
)
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 to seaweedfs
# TODO: add asset to minio
return True
except Exception as 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):
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 from seaweedfs
# TODO: remove asset from minio
return True
except Exception as e:
logger.exception(e)
@ -422,7 +432,7 @@ def get_asset(conn: DBConnector, token: str | None, fid: str):
try:
user, group = get_user_by_access_token(conn, token)
assets = conn.filterFromTable(
ASSETS_TABLE_NAME, ["*"], [ColumnCondition("fid", fid)]
ASSETS_TABLE_NAME, ["*"], [ColumnCondition("fid", "eq", fid)]
)
print(assets)
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):
try:
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:
return AccessType.NONE

View file

@ -1,6 +1,6 @@
from typing import Any
from uuid import UUID
from pydantic import BaseModel
from based.db import CONDITION_OPERATORS
class AuthModel(BaseModel):
@ -9,11 +9,12 @@ class AuthModel(BaseModel):
class ItemsFieldSelectorList(BaseModel):
fields: list[str] = []
fields: list[str] | None = []
class ColumnsDefinitionList(BaseModel):
columns: list[str]
class CreateUserDefinition(BaseModel):
username: str
password: str
class UserDefinition(BaseModel):
@ -23,28 +24,42 @@ class UserDefinition(BaseModel):
access_token: str | None = None
class ColumnDefinition(BaseModel):
name: str
class UserUpdateDefinition(BaseModel):
user_id: int
password: str
access_token: str
class ColumnConditionCompat(BaseModel):
column: str
operator: CONDITION_OPERATORS
value: Any
isString: bool = False
isLike: bool = True
class ItemDeletionDefinitionList(BaseModel):
defs: list[ColumnDefinition]
class TableDefinition(BaseModel):
table_id: UUID
table_id: str
table_name: str
columns: str
system: bool
hidden: bool
class TableListDefinition(BaseModel):
tables: list[TableDefinition]
class OkResponse(BaseModel):
ok: bool = True
class ErrorResponseDefinition(BaseModel):
class ErrorResponse(BaseModel):
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.columns import (
ColumnDefinition,
make_column_unique,
PrimarySerialColumnDefinition,
PrimaryUUIDColumnDefinition,
TextColumnDefinition,
BigintColumnDefinition,
BooleanColumnDefinition,
DateColumnDefinition,
TimestampColumnDefinition,
DoubleColumnDefinition,
IntegerColumnDefinition,
UUIDColumnDefinition,
)
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"]:
return PrimarySerialColumnDefinition(name)
case [name, "uuid", "primary"]:
return PrimaryUUIDColumnDefinition(name)
case [name, "str", *rest]:
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"]:
return TextColumnDefinition(name)
case [name, "str", "unique"]:
return make_column_unique(TextColumnDefinition(name))
case [name, "str", "default", default]:
return TextColumnDefinition(name, default=default)
case [name, "str", "default", default, "unique"]:
return make_column_unique(TextColumnDefinition(name, default=default))
case [name, "bool", *rest]:
is_unique = "unique" in rest
has_default = "default" in rest
td = BooleanColumnDefinition(name, unique=is_unique)
td.has_default = has_default
return td
case [name, "bigint"]:
return BigintColumnDefinition(name)
case [name, "bigint", "unique"]:
return make_column_unique(BigintColumnDefinition(name))
case [name, "bigint", "default", default]:
return BigintColumnDefinition(name, default=int(default))
case [name, "bigint", "default", default, "unique"]:
return make_column_unique(
BigintColumnDefinition(name, default=int(default))
)
case [name, "datetime", *rest]:
is_unique = "unique" in rest
has_default = "default" in rest
td = TimestampColumnDefinition(name, unique=is_unique)
td.has_default = has_default
return td
case [name, "bool"]:
return BooleanColumnDefinition(name)
case [name, "bool", "unique"]:
return make_column_unique(BooleanColumnDefinition(name))
case [name, "bool", "default", default]:
return BooleanColumnDefinition(name, default=bool(default))
case [name, "bool", "default", default, "unique"]:
return make_column_unique(
BooleanColumnDefinition(name, default=bool(default))
)
case [name, "float", *rest]:
is_unique = "unique" in rest
has_default = "default" in rest
td = DoubleColumnDefinition(name, unique=is_unique)
td.has_default = has_default
return td
case [name, "date"]:
return DateColumnDefinition(name)
case [name, "date", "unique"]:
return make_column_unique(DateColumnDefinition(name))
# TODO: Add default value for date
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))
case [name, "int", *rest]:
is_unique = "unique" in rest
has_default = "default" in rest
td = IntegerColumnDefinition(name, unique=is_unique)
td.has_default = has_default
return td
return None