11# Copyright (C) Lutra Consulting Limited
22#
33# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial
4+ import logging
5+
46import math
57from collections import namedtuple
68from datetime import datetime , timedelta , timezone
79from enum import Enum
810import os
911from flask import current_app
1012from flask_sqlalchemy .model import Model
13+ from marshmallow import Schema , fields
1114from pathvalidate import sanitize_filename
1215from sqlalchemy import Column , JSON
1316from sqlalchemy .sql .elements import UnaryExpression
14- from typing import Optional
15-
17+ from typing import Optional , Type
1618
1719OrderParam = namedtuple ("OrderParam" , "name direction" )
1820
@@ -33,7 +35,7 @@ def split_order_param(order_param: str) -> Optional[OrderParam]:
3335
3436
3537def get_order_param (
36- cls : Model , order_param : OrderParam , json_sort : dict = None
38+ cls : Model , order_param : OrderParam , json_sort : dict = None , field_map : dict = None
3739) -> Optional [UnaryExpression ]:
3840 """Return order by clause parameter for SQL query
3941
@@ -43,15 +45,22 @@ def get_order_param(
4345 :type order_param: OrderParam
4446 :param json_sort: type mapping for sort by json field, e.g. '{"storage": "int"}', defaults to None
4547 :type json_sort: dict
48+ :param field_map: mapping for translating public field names to internal DB columns, e.g. '{"size": "disk_usage"}'
49+ :type field_map: dict
4650 """
51+ # translate field name to column name
52+ db_column_name = order_param .name
53+ if field_map and order_param .name in field_map :
54+ db_column_name = field_map [order_param .name ]
4755 # find candidate for nested json sort
48- if "." in order_param . name :
49- col , attr = order_param . name .split ("." )
56+ if "." in db_column_name :
57+ col , attr = db_column_name .split ("." )
5058 else :
51- col = order_param . name
59+ col = db_column_name
5260 attr = None
5361 order_attr = cls .__table__ .c .get (col , None )
5462 if not isinstance (order_attr , Column ):
63+ logging .warning ("Ignoring invalid order parameter." )
5564 return
5665 # sort by key in JSON field
5766 if attr :
@@ -80,7 +89,9 @@ def get_order_param(
8089 return order_attr .desc ()
8190
8291
83- def parse_order_params (cls : Model , order_params : str , json_sort : dict = None ):
92+ def parse_order_params (
93+ cls : Model , order_params : str , json_sort : dict = None , field_map : dict = None
94+ ) -> list [UnaryExpression ]:
8495 """Convert order parameters in query string to list of order by clauses.
8596
8697 :param cls: Db model class
@@ -89,6 +100,8 @@ def parse_order_params(cls: Model, order_params: str, json_sort: dict = None):
89100 :type order_params: str
90101 :param json_sort: type mapping for sort by json field, e.g. '{"storage": "int"}', defaults to None
91102 :type json_sort: dict
103+ :param field_map: mapping response fields to database column names, e.g. '{"size": "disk_usage"}'
104+ :type field_map: dict
92105
93106 :rtype: List[Column]
94107 """
@@ -97,7 +110,7 @@ def parse_order_params(cls: Model, order_params: str, json_sort: dict = None):
97110 order_param = split_order_param (p )
98111 if not order_param :
99112 continue
100- order_attr = get_order_param (cls , order_param , json_sort )
113+ order_attr = get_order_param (cls , order_param , json_sort , field_map )
101114 if order_attr is not None :
102115 order_by_params .append (order_attr )
103116 return order_by_params
@@ -135,3 +148,27 @@ def save_diagnostic_log_file(app: str, username: str, body: bytes) -> str:
135148 f .write (content )
136149
137150 return file_name
151+
152+
153+ def get_schema_fields_map (schema : Type [Schema ]) -> dict :
154+ """
155+ Creates a mapping of schema field names to corresponding DB columns.
156+ This allows sorting by the API field name (e.g. 'size') while
157+ actually sorting by the database column (e.g. 'disk_usage').
158+ """
159+ mapping = {}
160+ for name , field in schema ._declared_fields .items ():
161+ # some fields could have been overridden with None to be excluded
162+ if not field :
163+ continue
164+ # skip virtual fields as DB cannot sort by them
165+ if isinstance (
166+ field , (fields .Function , fields .Method , fields .Nested , fields .List )
167+ ):
168+ continue
169+ if field .attribute :
170+ mapping [name ] = field .attribute
171+ # keep the map complete
172+ else :
173+ mapping [name ] = name
174+ return mapping
0 commit comments