evitaDB icon indicating copy to clipboard operation
evitaDB copied to clipboard

Unifying GQL interface for referenced entities

Open novoj opened this issue 6 months ago • 1 comments

When reference targets same entity, it would be beneficial for generated TypeScript classes to share the same common interface - example:

fragment imageFields on ProductMediaReference {
  referencedEntity {
    primaryKey
    attributes {
      ...imageMediaFields
    }
  }
}
fragment imageFieldsCategory on CategoryMediaReference {
  referencedEntity {
    primaryKey
    attributes {
      ...imageMediaFields
    }
  }
}

So that the shared part:

referencedEntity {
    primaryKey
    attributes {
      ...imageMediaFields
    }
}

Can be treated as the same TypeScript type. The type could be used only for the entites of same "referenced entity type" and also having the same contents (i.e. same fetched fields).

novoj avatar Jun 17 '25 08:06 novoj

We should be able to move both referencedPrimaryKey and referencedEntity to the common GQL interface per referenced entity type.

The question is, if we are able use the interface in a fragment. Usually the approach is to return list of interfaces which can be casted to specific types. Here we will return specific type from the reference field but we want to insert following fragment into it:

fragment abc on MediaReference {
  referencedEntity { ... }
}

lukashornych avatar Jun 17 '25 08:06 lukashornych

After some discussion with Next.js team, although there will not be breaking change in the final data types, the actual GraphQL schema will be extended with the interfaces, thus making the GraphQL schema not 1:1 with the previous. The Next.js team uses some shema merging between core application and project applications where the interface differences cause build errors.

lukashornych avatar Dec 01 '25 08:12 lukashornych

I have working GraphQL prototype that I need to verify with Next.js team. The original entity - reference types are intact, so original queries will work as expected. On top of that, there are additional interfaces that the specific entity reference object implements (e.g., ProductMediaReference). Here is an example diagram of the interface hierarchy for parameterValues reference on Product entity:

classDiagram
    class Reference {
        int referencePrimaryKey
    }
    class ParameterValueReference {
        ParameterValue referencedEntity
    }
    class ParameterValueWithParameterGroupReference {
        Parameter groupEntity
    }
    class ParameterValueWithABC123AttributesReference {
        ReferenceABC123Attributes attributes
    }
    class ParameterValueXYZ456Reference {

    }
    class ProductParameterValueReference {

    }
    class Product {
        ProductParameterValueReference[] parameterValues
    }
    Reference <|-- ParameterValueReference
    ParameterValueReference <|-- ParameterValueWithParameterGroupReference : if there is a reference with this entity type and group type
    ParameterValueReference <|-- ParameterValueWithABC123AttributesReference : if there is a reference with this entity type and attributes

    Reference <|-- ParameterValueXYZ456Reference
    ParameterValueReference <|-- ParameterValueXYZ456Reference
    ParameterValueWithParameterGroupReference <|-- ParameterValueXYZ456Reference : if exists
    ParameterValueWithABC123AttributesReference <|-- ParameterValueXYZ456Reference : if exists

    Reference <|-- ProductParameterValueReference
    ParameterValueXYZ456Reference <|-- ProductParameterValueReference    

    note for ProductParameterValueReference "The original entity - reference object type"

    Product *-- ProductParameterValueReference

Which means that following fragments can be used to generalize queries (output is the same as if specific reference object types would be used in original GraphQL API):

  • base reference
{
  listProduct(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    media {
      ... Abc
    }
  }

  listCategory(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    media {
      ... Abc
    }
  }
}

fragment Abc on Reference {
  referencedPrimaryKey
}
  • reference to Media entity (all entity references to Media entity will implement this)
{
  listProduct(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    media {
      ... Abc
    }
  }

  listCategory(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    media {
      ... Abc
    }
  }
}

fragment Abc on MediaReference {
  referencedEntity {
    attributes {
      fileName
    }
  }
}
  • reference to Media entity with specific set of reference attributes (all entity references to Media entity with same attributes will implement this)
{
  listProduct(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    media {
      ... Abc
    }
  }

  listCategory(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    media {
      ... Abc
    }
  }
}

fragment Abc on MediaWithC81766fb11990a1fAttributesReference {
  attributes {
    gallery
  }
  referencedEntity {
    attributes {
      fileName
    }
  }
}
  • reference to ParameterValue entity with Parameter entity group (all entity references to ParameterValue entity with Parameter group will implement this)
{
  listProduct(
    filterBy: {
      referenceParameterValuesHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    parameterValues {
      ... Abc
    }
  }
}

fragment Abc on ParameterValueWithParameterGroupReference {
  groupEntity {
    primaryKey
  }
  referencedEntity {
    attributes {
      code
    }
  }
}
  • reference to Media entity with specific set of attributes and possibly a specific group entity (all entity references to Media entity with the same attributes and group entity will implement this)
{
  listProduct(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    media {
      ... Abc
    }
  }

  listCategory(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    media {
      ... Abc
    }
  }
}

fragment Abc on Media61be313e7173962cReference {
  attributes {
    gallery
  }
  referencedEntity {
    attributes {
      fileName
    }
  }
}

None of the mentioned reference interfaces is tied to the Product - Media/ParameterValue association. The mentioned reference interfaces are strictly generated based on the actual reference fields extracted from the final entity - reference associations and grouped together. That's why you can see use of hash codes in the interfaces names.

lukashornych avatar Dec 05 '25 09:12 lukashornych

Our Next.js team approved the suggested design.

They also suggested another form, that is that the source entity would have implement an interface for a reference which would be dynamically constructed from reference entity type, group type, and attributes. Similarly to the existing Media61be313e7173962cReference interface that groups together entity type, group type and attributes under hash code without ties to the source entity, new interface e.g. WithMedia61be313e7173962cReference would be implemented by the source entities directly (e.g., Product, Category,...) and would contain the entire reference field with arguments (filterBy, orderBy, ...). This would allow following fragments:

{
  listProduct(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    primaryKey
    ... MediaRefFragment
  }

  listCategory(
    filterBy: {
      referenceMediaHaving: {}
    },
    limit: 1
  ) {
    ... MediaRefFragment
  }
}

fragment MediaRefFragment on WithMedia61be313e7173962cReference {
   media(filterBy: { attributeGalleryEquals: "hlavni-motiv" }) {
    ... Abc
  }
}

fragment Abc on Media61be313e7173962cReference {
  attributes {
    gallery
  }
  referencedEntity {
    attributes {
      fileName
    }
  }
}

and would be helpful if all the references have same name, filter and order constraints (which make sense because, the reference attributes and reference entities are the same).

However, this would also mean breaking change in the fields themselfs because they would no longer be of type e.g., ProductMediaReference, but directly Media61be313e7173962cReference. Also, we need to investigate if it is even possible to construct the filterBy and orderBy containers on this generic level (combination of reference attributes and referenced entity), because right now, the containers are tied to a specific ReferenceSchema.

lukashornych avatar Dec 05 '25 13:12 lukashornych

I have managed to create a working prototype of the WithXYZReference interfaces on entities. In our demo dataset, the Product entity implements following interfaces effectively adding all reference fields as before:

type Product implements Entity & WithBonusVisibilities1e805007fa3e72c3Reference & WithBrandB2644ca64b65864Reference & WithBundlesBeab91e7a2541ae9Reference & WithCategoriesBa0874fe540652feReference & WithGroups7cd9582b19a90806Reference & WithMaster9f00fe0fd32a4ccReference & WithMedia20568925f95d394aReference & WithParameterValues74d8050e1e2b4ddbReference & WithProductSetItems2623886901cad240Reference & WithRelatedProductsF05f5e6e65c984faReference & WithStockVisibilitiesBf623195a615259bReference & WithStocks8cc2e182f0c3d6c0Reference & WithTags4bb74fc39c29cc2eReference & WithVariantParametersEe2a52f7bb263920Reference & WithVariantsE55b2c0c3052c364Reference

which e.g., in the case of the media reference, multiple entities share the WithMedia20568925f95d394aReference interface, and thus, they can reuse the same fragment referencing the WithMedia20568925f95d394aReference interface.

The Product entity can be used as before without a query change (note that output object types will be different if used directly in client code), but there is now ability to operate with the interfaces.

Note: there may be WithXYZReference interfaces (especially in systems with no similar references across entities) that are used only in a single entity object. This would be nice to eliminate and inline the contents of the interface into the entity object itself. However, that would be difficult to do in our GraphQL schema builder, but more importantly, if a user of the evitaDB decides later to add a similar reference to another entity, which would lead to generating the unifying interface, this way there is no breaking change in the GraphQL schema, because the schema already contains that interface. Although, the GraphQL schema will be slightly larger in that case with no added benefit for the user (@novoj if this will be a problem in the future, we could add flag to evitaDB settings to optionally generate some sort of compact GraphQL schema).

To complete the package, I will also add generic ReferencePage and ReferenceStrip interfaces (note: this will also implement the first two point in https://github.com/FgForrest/evitaDB/issues/676).

lukashornych avatar Dec 12 '25 13:12 lukashornych