What is a lot of fucking work!
This commit is contained in:
parent
85c07ed4f3
commit
3414b5c334
8 changed files with 586 additions and 216 deletions
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"python.formatting.provider": "none"
|
||||||
|
}
|
||||||
4
____test.py
Normal file
4
____test.py
Normal 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
553
app.py
|
|
@ -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
26
db_addendum.py
Normal 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"
|
||||||
|
|
@ -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
56
dba.py
|
|
@ -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
|
||||||
|
|
|
||||||
47
models.py
47
models.py
|
|
@ -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
107
utils.py
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue