Skip to content

Commit f4260ac

Browse files
authored
Limit exposed relationships type-wise to 100 elements (#1023)
1 parent ee0187f commit f4260ac

File tree

7 files changed

+160
-40
lines changed

7 files changed

+160
-40
lines changed

mwdb/model/object.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
from uuid import UUID
55

66
from flask import g
7-
from sqlalchemy import and_, cast, distinct, exists, func, select
7+
from sqlalchemy import and_, cast, distinct, exists, func
88
from sqlalchemy.dialects.postgresql import JSONB
99
from sqlalchemy.exc import IntegrityError
1010
from sqlalchemy.orm import column_property
11-
from sqlalchemy.sql.expression import true
11+
from sqlalchemy.sql.expression import column, select, true, values
12+
from sqlalchemy.sql.sqltypes import String
1213

1314
from mwdb.core.capabilities import Capabilities
1415

@@ -18,6 +19,8 @@
1819
from .object_permission import AccessType, ObjectPermission
1920
from .tag import Tag
2021

22+
RELATIONS_VIEW_LIMIT_PER_TYPE = 100
23+
2124
relation = db.Table(
2225
"relation",
2326
db.Column(
@@ -152,8 +155,31 @@ def latest_config(self):
152155
def favorite(self):
153156
return g.auth_user in self.followers
154157

155-
@property
156-
def accessible_parents(self):
158+
@classmethod
159+
def _get_object_types(cls):
160+
mapper = cls.__mapper__
161+
return mapper.polymorphic_map.keys() - {
162+
Object.__mapper_args__["polymorphic_identity"]
163+
}
164+
165+
def _get_relations_limited_per_type(self, relation_query, limit_each):
166+
object_type_names = [(object_type,) for object_type in self._get_object_types()]
167+
object_types = values(column("object_type", String), name="object_types").data(
168+
object_type_names
169+
)
170+
relations = db.aliased(
171+
Object,
172+
(
173+
relation_query.filter(Object.type == object_types.c.object_type)
174+
.limit(limit_each)
175+
.subquery()
176+
.lateral()
177+
),
178+
)
179+
entries = db.session.query(object_types, relations).join(relations, true).all()
180+
return [related_object for _, related_object in entries]
181+
182+
def get_parents_subquery(self):
157183
"""
158184
Parent objects that are accessible for current user
159185
"""
@@ -165,6 +191,35 @@ def accessible_parents(self):
165191
.filter(g.auth_user.has_access_to_object(Object.id))
166192
)
167193

194+
def get_children_subquery(self):
195+
"""
196+
Child objects that are accessible for current user
197+
"""
198+
return (
199+
db.session.query(Object)
200+
.join(relation, relation.c.child_id == Object.id)
201+
.filter(relation.c.parent_id == self.id)
202+
.order_by(relation.c.creation_time.desc())
203+
.filter(g.auth_user.has_access_to_object(Object.id))
204+
)
205+
206+
def get_limited_parents_per_type(self, limit_each=RELATIONS_VIEW_LIMIT_PER_TYPE):
207+
"""
208+
Parent objects that are directly loaded for API.
209+
Query loads only *limit_each* number of relations for each object type.
210+
"""
211+
return self._get_relations_limited_per_type(
212+
self.get_parents_subquery(), limit_each
213+
)
214+
215+
def get_limited_children_per_type(self, limit_each=RELATIONS_VIEW_LIMIT_PER_TYPE):
216+
"""
217+
Parent objects that are accessible for current user
218+
"""
219+
return self._get_relations_limited_per_type(
220+
self.get_children_subquery(), limit_each
221+
)
222+
168223
def add_parent(self, parent, commit=True):
169224
"""
170225
Adding parent with permission inheritance

mwdb/resources/relations.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ def get(self, type, identifier):
5454
raise NotFound("Object not found")
5555

5656
relations = RelationsResponseSchema()
57-
return relations.dump(db_object)
57+
parents = db_object.get_limited_parents_per_type()
58+
children = db_object.get_limited_children_per_type()
59+
return relations.dump(
60+
{
61+
"parents": parents,
62+
"children": children,
63+
}
64+
)
5865

5966

6067
class ObjectChildResource(Resource):

mwdb/schema/object.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,30 +103,25 @@ class ObjectItemResponseSchema(Schema):
103103
upload_time = UTCDateTime(required=True, allow_none=False)
104104
favorite = fields.Boolean(required=True, allow_none=False)
105105

106-
parents = fields.Nested(
107-
ObjectListItemResponseSchema,
108-
many=True,
109-
required=True,
110-
allow_none=False,
111-
attribute="accessible_parents",
112-
)
113-
children = fields.Nested(
114-
ObjectListItemResponseSchema, many=True, required=True, allow_none=False
115-
)
116-
attributes = fields.Nested(
117-
AttributeItemResponseSchema, many=True, required=True, allow_none=False
118-
)
106+
parents = fields.Method(serialize="_get_parents")
107+
children = fields.Method(serialize="_get_children")
108+
attributes = fields.Method(serialize="_get_attributes")
119109
share_3rd_party = fields.Boolean(required=True, allow_none=False)
120110

121-
@post_dump(pass_original=True)
122-
def get_accessible_attributes(self, data, object, **kwargs):
123-
"""
124-
Replace all object attributes with attributes accessible for current user
125-
"""
111+
def _get_parents(self, object):
112+
object_parents = object.get_limited_parents_per_type()
113+
schema = ObjectListItemResponseSchema()
114+
return schema.dump(object_parents, many=True)
115+
116+
def _get_children(self, object):
117+
object_children = object.get_limited_children_per_type()
118+
schema = ObjectListItemResponseSchema()
119+
return schema.dump(object_children, many=True)
120+
121+
def _get_attributes(self, object):
126122
object_attributes = object.get_attributes()
127123
schema = AttributeItemResponseSchema()
128-
attributes_serialized = schema.dump(object_attributes, many=True)
129-
return {**data, "attributes": attributes_serialized}
124+
return schema.dump(object_attributes, many=True)
130125

131126

132127
class ObjectCountResponseSchema(Schema):

mwdb/schema/relations.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,5 @@
44

55

66
class RelationsResponseSchema(Schema):
7-
parents = fields.Nested(
8-
ObjectListItemResponseSchema,
9-
many=True,
10-
required=True,
11-
allow_none=False,
12-
attribute="accessible_parents",
13-
)
14-
children = fields.Nested(
15-
ObjectListItemResponseSchema, many=True, required=True, allow_none=False
16-
)
7+
parents = fields.Nested(ObjectListItemResponseSchema, many=True)
8+
children = fields.Nested(ObjectListItemResponseSchema, many=True)

mwdb/web/src/commons/helpers/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GenericOrJSX } from "@mwdb-web/types/types";
1+
import { GenericOrJSX, ObjectLegacyType } from "@mwdb-web/types/types";
22

33
export { downloadData } from "./download";
44
export * from "./search";
@@ -48,6 +48,18 @@ export function mapObjectType(objectType: string): string {
4848
);
4949
}
5050

51+
export function mapObjectTypeToSearchPath(
52+
objectType: ObjectLegacyType
53+
): string {
54+
return (
55+
{
56+
file: "/",
57+
static_config: "/configs",
58+
text_blob: "/blobs",
59+
}[objectType] || objectType
60+
);
61+
}
62+
5163
// negate the buffer contents (xor with key equal 0xff)
5264
export function negateBuffer(buffer: ArrayBuffer) {
5365
const uint8View = new Uint8Array(buffer);

mwdb/web/src/components/ShowObject/common/RelationBox.tsx

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,18 @@ import {
1717
ConfirmationModal,
1818
TagList,
1919
} from "@mwdb-web/commons/ui";
20-
import { getErrorMessage } from "@mwdb-web/commons/helpers";
20+
import {
21+
getErrorMessage,
22+
makeSearchLink,
23+
mapObjectTypeToSearchPath,
24+
} from "@mwdb-web/commons/helpers";
2125
import { useRemotePath } from "@mwdb-web/commons/remotes";
2226
import { RelationsAddModal } from "../Actions/RelationsAddModal";
23-
import { Capability, RelationItem } from "@mwdb-web/types/types";
27+
import {
28+
Capability,
29+
ObjectLegacyType,
30+
RelationItem,
31+
} from "@mwdb-web/types/types";
2432

2533
type RelationToRemove = {
2634
relation: "parent" | "child";
@@ -32,6 +40,9 @@ type Props = {
3240
parents?: RelationItem[];
3341
header?: string;
3442
icon?: IconDefinition;
43+
parentsCount?: number;
44+
childrenCount?: number;
45+
elementType?: ObjectLegacyType;
3546
updateRelationsActivePage?: () => void;
3647
};
3748

@@ -191,11 +202,56 @@ export function RelationsBox(props: Props) {
191202
</td>
192203
</tr>
193204
));
205+
206+
let header;
207+
let elementCount = (props.parentsCount || 0) + (props.childrenCount || 0);
208+
if (!props.header) {
209+
header = "Relations";
210+
} else {
211+
if (props.parentsCount && props.parentsCount >= 100) {
212+
let queryLink = makeSearchLink({
213+
field: "child",
214+
value: `(dhash:${context.object?.id})`,
215+
noEscape: true,
216+
pathname: mapObjectTypeToSearchPath(
217+
props.elementType || "file"
218+
),
219+
});
220+
header = (
221+
<>
222+
{props.header}: {elementCount}+ (
223+
<Link to={queryLink}>query all parents</Link>)
224+
</>
225+
);
226+
} else if (props.childrenCount && props.childrenCount >= 100) {
227+
let queryLink = makeSearchLink({
228+
field: "parent",
229+
value: `(dhash:${context.object?.id})`,
230+
noEscape: true,
231+
pathname: mapObjectTypeToSearchPath(
232+
props.elementType || "file"
233+
),
234+
});
235+
header = (
236+
<>
237+
{props.header}: {elementCount}+ (
238+
<Link to={queryLink}>query all children</Link>)
239+
</>
240+
);
241+
} else {
242+
header = (
243+
<>
244+
{props.header}: {elementCount}
245+
</>
246+
);
247+
}
248+
}
249+
194250
return (
195251
<div className="card card-default">
196252
<div className="card-header">
197253
{props.icon && <FontAwesomeIcon icon={props.icon} size="1x" />}
198-
{props.header || "Relations"}
254+
{header}
199255
{!api.remote ? (
200256
<Link
201257
to="#"

mwdb/web/src/components/ShowObject/common/TypedRelationsBox.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ export function TypedRelationsBox(props: Props) {
6666
return (
6767
<div>
6868
<RelationsBox
69-
header={`${props.header}: ${typedRelationsCount}`}
69+
header={props.header}
70+
parentsCount={parentsFiltered.length}
71+
childrenCount={childrenFiltered.length}
72+
elementType={props.type}
7073
icon={props.icon}
7174
updateRelationsActivePage={() =>
7275
updateActivePage(

0 commit comments

Comments
 (0)