Skip to content

Commit d8734fb

Browse files
committed
Add helper to map API field name to DB column
1 parent a792beb commit d8734fb

4 files changed

Lines changed: 46 additions & 9 deletions

File tree

server/mergin/sync/public_api_v2_controller.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from .storages.disk import move_to_tmp, save_to_file
5050
from .utils import get_device_id, get_ip, get_user_agent, get_chunk_location
5151
from .workspace import WorkspaceRole
52-
from ..utils import parse_order_params
52+
from ..utils import parse_order_params, get_schema_fields_map
5353

5454

5555
@auth_required
@@ -437,7 +437,9 @@ def list_workspace_projects(workspace_id, page, per_page, order_params=None, q=N
437437
projects = projects.filter(Project.name.ilike(f"%{q}%"))
438438

439439
if order_params:
440-
order_by_params = parse_order_params(Project, order_params)
440+
order_by_params = parse_order_params(
441+
Project, order_params, field_map=ProjectSchemaV2.field_map
442+
)
441443
projects = projects.order_by(*order_by_params)
442444

443445
result = projects.paginate(page, per_page).items

server/mergin/sync/schemas_v2.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Project,
1212
ProjectVersion,
1313
)
14+
from ..utils import get_schema_fields_map
1415

1516

1617
class ProjectSchema(ma.SQLAlchemyAutoSchema):
@@ -46,3 +47,6 @@ class Meta:
4647
"workspace",
4748
"role",
4849
)
50+
51+
52+
ProjectSchema.field_map = get_schema_fields_map(ProjectSchema)

server/mergin/tests/test_public_api_v2.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,13 @@ def test_list_workspace_projects(client):
643643
url + f"?page={page}&per_page={per_page}&q=1&order_params=created DESC"
644644
)
645645
assert response.json["projects"][0]["name"] == "project_10"
646+
# using field name instead column names for sorting
647+
p4 = Project.query.filter(Project.name == project_name).first()
648+
p4.disk_usage = 1234567
649+
db.session.commit()
650+
response = client.get(url + f"?page=1&per_page=10&order_params=size DESC")
651+
resp_data = json.loads(response.data)
652+
assert resp_data["projects"][0]["name"] == project_name
646653

647654
# no permissions to workspace
648655
user2 = add_user("user", "password")

server/mergin/utils.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from datetime import datetime, timedelta, timezone
77
from enum import Enum
88
import os
9-
from flask import current_app
9+
from flask import current_app, abort
1010
from flask_sqlalchemy import Model
11+
from marshmallow import Schema
1112
from pathvalidate import sanitize_filename
1213
from sqlalchemy import Column, JSON
1314
from sqlalchemy.sql.elements import UnaryExpression
@@ -33,7 +34,7 @@ def split_order_param(order_param: str) -> Optional[OrderParam]:
3334

3435

3536
def get_order_param(
36-
cls: Model, order_param: OrderParam, json_sort: dict = None
37+
cls: Model, order_param: OrderParam, json_sort: dict = None, field_map: dict = None
3738
) -> Optional[UnaryExpression]:
3839
"""Return order by clause parameter for SQL query
3940
@@ -43,12 +44,18 @@ def get_order_param(
4344
:type order_param: OrderParam
4445
:param json_sort: type mapping for sort by json field, e.g. '{"storage": "int"}', defaults to None
4546
:type json_sort: dict
47+
:param field_map: mapping for translating public field names to internal DB columns, e.g. '{"size": "disk_usage"}'
48+
:type field_map: dict
4649
"""
50+
# translate field name to column name
51+
db_column_name = order_param.name
52+
if field_map and order_param.name in field_map:
53+
db_column_name = field_map[order_param.name]
4754
# find candidate for nested json sort
48-
if "." in order_param.name:
49-
col, attr = order_param.name.split(".")
55+
if "." in db_column_name:
56+
col, attr = db_column_name.split(".")
5057
else:
51-
col = order_param.name
58+
col = db_column_name
5259
attr = None
5360
order_attr = cls.__table__.c.get(col, None)
5461
if not isinstance(order_attr, Column):
@@ -80,7 +87,9 @@ def get_order_param(
8087
return order_attr.desc()
8188

8289

83-
def parse_order_params(cls: Model, order_params: str, json_sort: dict = None):
90+
def parse_order_params(
91+
cls: Model, order_params: str, json_sort: dict = None, field_map: dict = None
92+
) -> list[UnaryExpression]:
8493
"""Convert order parameters in query string to list of order by clauses.
8594
8695
:param cls: Db model class
@@ -89,6 +98,8 @@ def parse_order_params(cls: Model, order_params: str, json_sort: dict = None):
8998
:type order_params: str
9099
:param json_sort: type mapping for sort by json field, e.g. '{"storage": "int"}', defaults to None
91100
:type json_sort: dict
101+
:param field_map: mapping response fields to database column names, e.g. '{"size": "disk_usage"}'
102+
:type field_map: dict
92103
93104
:rtype: List[Column]
94105
"""
@@ -97,7 +108,7 @@ def parse_order_params(cls: Model, order_params: str, json_sort: dict = None):
97108
order_param = split_order_param(p)
98109
if not order_param:
99110
continue
100-
order_attr = get_order_param(cls, order_param, json_sort)
111+
order_attr = get_order_param(cls, order_param, json_sort, field_map)
101112
if order_attr is not None:
102113
order_by_params.append(order_attr)
103114
return order_by_params
@@ -135,3 +146,16 @@ def save_diagnostic_log_file(app: str, username: str, body: bytes) -> str:
135146
f.write(content)
136147

137148
return file_name
149+
150+
151+
def get_schema_fields_map(schema: Schema) -> dict:
152+
"""
153+
Creates a mapping of schema field names to corresponding DB columns.
154+
This allows sorting by the API field name (e.g. 'size') while
155+
actually sorting by the database column (e.g. 'disk_usage').
156+
"""
157+
mapping = {}
158+
for name, field in schema._declared_fields.items():
159+
if field and field.attribute:
160+
mapping[name] = field.attribute
161+
return mapping

0 commit comments

Comments
 (0)