diff --git a/mwdb/model/object.py b/mwdb/model/object.py index f75b64f5f..38f0a21fd 100644 --- a/mwdb/model/object.py +++ b/mwdb/model/object.py @@ -7,7 +7,7 @@ from sqlalchemy import and_, cast, distinct, exists, func, or_, select from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import aliased, column_property, contains_eager +from sqlalchemy.orm import column_property from sqlalchemy.sql.expression import true from mwdb.core.capabilities import Capabilities @@ -668,6 +668,21 @@ def _get_or_create( return created_object, is_new + def query_visible_parents(self, requestor=None): + """ + Queries for parents visible by specified requestor. + """ + if requestor is None: + requestor = g.auth_user + + return ( + db.session.query(Object) + .join(relation, relation.c.parent_id == Object.id) + .filter(relation.c.child_id == self.id) + .order_by(relation.c.creation_time.desc()) + .filter(requestor.has_access_to_object(Object.id)) + ) + @classmethod def access(cls, identifier, requestor=None): """ @@ -686,35 +701,12 @@ def access(cls, identifier, requestor=None): if requestor is None: requestor = g.auth_user - obj = cls.get(identifier) + obj_query = cls.get(identifier) + obj = obj_query.first() # If object doesn't exist - it doesn't exist - if obj.first() is None: + if obj is None: return None - # In that case we want only those parents to which requestor has access. - stmtp = ( - db.session.query(Object) - .join(relation, relation.c.parent_id == Object.id) - .filter( - Object.id.in_( - db.session.query(relation.c.parent_id).filter( - relation.c.child_id == obj.first().id - ) - ) - ) - .order_by(relation.c.creation_time.desc()) - .filter(requestor.has_access_to_object(Object.id)) - ) - stmtp = stmtp.subquery() - - parent = aliased(Object, stmtp) - - obj = ( - obj.outerjoin(parent, Object.parents) - .options(contains_eager(Object.parents, alias=parent)) - .all()[0] - ) - # Ok, now let's check whether requestor has explicit access if obj.has_explicit_access(requestor): return obj diff --git a/mwdb/resources/blob.py b/mwdb/resources/blob.py index fde206a6f..566235edb 100644 --- a/mwdb/resources/blob.py +++ b/mwdb/resources/blob.py @@ -9,6 +9,7 @@ from mwdb.schema.blob import ( BlobCreateRequestSchema, BlobItemResponseSchema, + BlobItemAndRelationsResponseSchema, BlobLegacyCreateRequestSchema, BlobListResponseSchema, ) @@ -186,6 +187,7 @@ def post(self): class TextBlobItemResource(ObjectItemResource, TextBlobUploader): ObjectType = TextBlob ItemResponseSchema = BlobItemResponseSchema + ItemAndRelationsResponseSchema = BlobItemAndRelationsResponseSchema CreateRequestSchema = BlobLegacyCreateRequestSchema def call_specialised_remove_hook(self, text_blob): diff --git a/mwdb/resources/config.py b/mwdb/resources/config.py index 8e0460a90..8086bf5bb 100644 --- a/mwdb/resources/config.py +++ b/mwdb/resources/config.py @@ -14,6 +14,7 @@ from mwdb.schema.config import ( ConfigCreateRequestSchema, ConfigItemResponseSchema, + ConfigItemAndRelationsResponseSchema, ConfigLegacyCreateRequestSchema, ConfigListResponseSchema, ConfigStatsRequestSchema, @@ -166,6 +167,7 @@ class ConfigResource(ObjectResource, ConfigUploader): ObjectType = Config ListResponseSchema = ConfigListResponseSchema ItemResponseSchema = ConfigItemResponseSchema + ItemAndRelationsResponseSchema = ConfigItemAndRelationsResponseSchema @requires_authorization def get(self): @@ -309,6 +311,7 @@ class ConfigItemResource(ObjectItemResource, ConfigUploader): ObjectType = Config ItemResponseSchema = ConfigItemResponseSchema + ItemAndRelationsResponseSchema = ConfigItemAndRelationsResponseSchema CreateRequestSchema = ConfigLegacyCreateRequestSchema def call_specialised_remove_hook(self, config): diff --git a/mwdb/resources/file.py b/mwdb/resources/file.py index a765012ee..b00870786 100644 --- a/mwdb/resources/file.py +++ b/mwdb/resources/file.py @@ -12,6 +12,7 @@ FileCreateRequestSchema, FileDownloadTokenResponseSchema, FileItemResponseSchema, + FileItemAndRelationsResponseSchema, FileLegacyCreateRequestSchema, FileListResponseSchema, ) @@ -203,6 +204,7 @@ def post(self): class FileItemResource(ObjectItemResource, FileUploader): ObjectType = File ItemResponseSchema = FileItemResponseSchema + ItemAndRelationsResponseSchema = FileItemAndRelationsResponseSchema CreateRequestSchema = FileLegacyCreateRequestSchema def call_specialised_remove_hook(self, file): @@ -225,6 +227,15 @@ def get(self, identifier): schema: type: string description: File identifier (SHA256/SHA512/SHA1/MD5) + - in: query + name: exclude_relations + schema: + type: integer + description: | + If set, results doesn't include relations + which will be default behavior on next major release of MWDB + required: false + default: 0 responses: 200: description: Information about file diff --git a/mwdb/resources/object.py b/mwdb/resources/object.py index b03395ea9..5a25609f2 100644 --- a/mwdb/resources/object.py +++ b/mwdb/resources/object.py @@ -16,6 +16,8 @@ from mwdb.schema.object import ( ObjectCountRequestSchema, ObjectCountResponseSchema, + ObjectItemAndRelationsResponseSchema, + ObjectItemRequestSchema, ObjectItemResponseSchema, ObjectListRequestSchema, ObjectListResponseSchema, @@ -262,6 +264,7 @@ def get(self): class ObjectItemResource(Resource, ObjectUploader): ObjectType = Object ItemResponseSchema = ObjectItemResponseSchema + ItemAndRelationsResponseSchema = ObjectItemAndRelationsResponseSchema CreateRequestSchema = None def call_specialised_remove_hook(self, obj): @@ -284,6 +287,15 @@ def get(self, identifier): schema: type: string description: Object identifier + - in: query + name: exclude_relations + schema: + type: integer + description: | + If set, results doesn't include relations + which will be default behavior on next major release of MWDB + required: false + default: 0 responses: 200: description: Information about object @@ -298,10 +310,15 @@ def get(self, identifier): description: | Request canceled due to database statement timeout. """ + args = load_schema(request.args, ObjectItemRequestSchema()) obj = self.ObjectType.access(identifier) if obj is None: raise NotFound("Object not found") - schema = self.ItemResponseSchema() + schema = ( + self.ItemResponseSchema() + if args["exclude_relations"] + else self.ItemAndRelationsResponseSchema() + ) return schema.dump(obj) def _get_upload_args(self, parent_identifier): diff --git a/mwdb/resources/relations.py b/mwdb/resources/relations.py index cf9738b3c..f8ed1c223 100644 --- a/mwdb/resources/relations.py +++ b/mwdb/resources/relations.py @@ -5,7 +5,7 @@ from mwdb.core.plugins import hooks from mwdb.core.rate_limit import rate_limited_resource from mwdb.model import Object, db -from mwdb.schema.relations import RelationsResponseSchema +from mwdb.schema.object import RelationsResponseSchema from . import access_object, logger, requires_authorization, requires_capabilities diff --git a/mwdb/schema/blob.py b/mwdb/schema/blob.py index 578847641..20c726c3f 100644 --- a/mwdb/schema/blob.py +++ b/mwdb/schema/blob.py @@ -7,6 +7,7 @@ ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, + RelationsResponseSchema, ) from .utils import UTCDateTime @@ -47,3 +48,12 @@ class BlobItemResponseSchema(ObjectItemResponseSchema): latest_config = fields.Nested( ConfigItemResponseSchema, required=True, allow_none=True ) + + +class BlobItemAndRelationsResponseSchema( + BlobItemResponseSchema, RelationsResponseSchema +): + """ + This is legacy schema that returns object item along with relations + It is awfully slow when object is bound with lots of relatives + """ diff --git a/mwdb/schema/config.py b/mwdb/schema/config.py index 1fddbb037..849ae8826 100644 --- a/mwdb/schema/config.py +++ b/mwdb/schema/config.py @@ -6,6 +6,7 @@ ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, + RelationsResponseSchema, ) @@ -47,6 +48,15 @@ class ConfigItemResponseSchema(ObjectItemResponseSchema): cfg = fields.Dict(required=True, allow_none=False) +class ConfigItemAndRelationsResponseSchema( + ConfigItemResponseSchema, RelationsResponseSchema +): + """ + This is legacy schema that returns object item along with relations + It is awfully slow when object is bound with lots of relatives + """ + + class ConfigStatsItemResponseSchema(Schema): family = fields.Str(required=True, allow_none=False) last_upload = fields.Date(required=True, allow_none=False) diff --git a/mwdb/schema/file.py b/mwdb/schema/file.py index 8825135ce..4ead04096 100644 --- a/mwdb/schema/file.py +++ b/mwdb/schema/file.py @@ -9,6 +9,7 @@ ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, + RelationsResponseSchema, ) @@ -71,5 +72,14 @@ class FileItemResponseSchema(ObjectItemResponseSchema): ) +class FileItemAndRelationsResponseSchema( + FileItemResponseSchema, RelationsResponseSchema +): + """ + This is legacy schema that returns object item along with relations + It is awfully slow when object is bound with lots of relatives + """ + + class FileDownloadTokenResponseSchema(Schema): token = fields.Str(required=True, allow_none=False) diff --git a/mwdb/schema/object.py b/mwdb/schema/object.py index f4db16d72..17e2d807f 100644 --- a/mwdb/schema/object.py +++ b/mwdb/schema/object.py @@ -30,6 +30,17 @@ def validate_key(self, data, **kwargs): ) +class ObjectItemRequestSchema(Schema): + exclude_relations = fields.Boolean( + truthy={ + "1", "", + }, + falsy={"0"}, + missing=False, + allow_none=False, + ) + + class ObjectCountRequestSchema(Schema): query = fields.Str(missing=None) @@ -103,12 +114,6 @@ class ObjectItemResponseSchema(Schema): upload_time = UTCDateTime(required=True, allow_none=False) favorite = fields.Boolean(required=True, allow_none=False) - parents = fields.Nested( - ObjectListItemResponseSchema, many=True, required=True, allow_none=False - ) - children = fields.Nested( - ObjectListItemResponseSchema, many=True, required=True, allow_none=False - ) attributes = fields.Nested( AttributeItemResponseSchema, many=True, required=True, allow_none=False ) @@ -125,5 +130,26 @@ def get_accessible_attributes(self, data, object, **kwargs): return {**data, "attributes": attributes_serialized} +class RelationsResponseSchema(Schema): + parents = fields.Method("get_parents") + children = fields.Nested( + ObjectListItemResponseSchema, many=True, required=True, allow_none=False + ) + + def get_parents(self, obj): + parents = obj.query_visible_parents().all() + schema = ObjectListItemResponseSchema() + return schema.dump(parents, many=True) + + +class ObjectItemAndRelationsResponseSchema( + ObjectItemResponseSchema, RelationsResponseSchema +): + """ + This is legacy schema that returns object item along with relations + It is awfully slow when object is bound with lots of relatives + """ + + class ObjectCountResponseSchema(Schema): count = fields.Int() diff --git a/mwdb/schema/relations.py b/mwdb/schema/relations.py deleted file mode 100644 index 731b26365..000000000 --- a/mwdb/schema/relations.py +++ /dev/null @@ -1,12 +0,0 @@ -from marshmallow import Schema, fields - -from .object import ObjectListItemResponseSchema - - -class RelationsResponseSchema(Schema): - parents = fields.Nested( - ObjectListItemResponseSchema, many=True, required=True, allow_none=False - ) - children = fields.Nested( - ObjectListItemResponseSchema, many=True, required=True, allow_none=False - )