Unifying GQL interface for referenced entities
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).
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 { ... }
}
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.
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
Mediaentity (all entity references toMediaentity 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
Mediaentity with specific set of reference attributes (all entity references toMediaentity 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
ParameterValueentity withParameterentity group (all entity references toParameterValueentity withParametergroup will implement this)
{
listProduct(
filterBy: {
referenceParameterValuesHaving: {}
},
limit: 1
) {
primaryKey
parameterValues {
... Abc
}
}
}
fragment Abc on ParameterValueWithParameterGroupReference {
groupEntity {
primaryKey
}
referencedEntity {
attributes {
code
}
}
}
- reference to
Mediaentity with specific set of attributes and possibly a specific group entity (all entity references toMediaentity 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.
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.
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).