-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add entities
GraphQL query
#392
Conversation
Signed-off-by: Dmitriy Lazarev <[email protected]>
🦋 Changeset detectedLatest commit: c0e10f9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Signed-off-by: Dmitriy Lazarev <[email protected]>
Signed-off-by: Dmitriy Lazarev <[email protected]>
The following preview packages were published:
Generated by @thefrontside/actions |
Signed-off-by: Dmitriy Lazarev <[email protected]>
Signed-off-by: Dmitriy Lazarev <[email protected]>
…ssed Signed-off-by: Dmitriy Lazarev <[email protected]>
Signed-off-by: Dmitriy Lazarev <[email protected]>
Signed-off-by: Dmitriy Lazarev <[email protected]>
36b5eda
to
82f7c78
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Great work, this looks really good.
Ohhhh BUDDY!! right in time for us at the start of Q2!! 😍🤩🤠🫵👏 |
Motivation
At this point the Backstage GraphQL plugin doesn't allow to query entities with various filters, which was significant stumbling block for usage, because it requires to do additional work like queries entities with REST API, getting entities refs, encode them to Node ids and then query GraphQL API.
Approach
As we have nicely typed GraphQL schema it would be great to have similar type support for filter argument of entities query. To achieve that we need to understand how Catalog Entity fields are mapped to GraphQL types. So the first step is to walkthrough any
Entity
interface implementations' fields and storefieldName => @field(at: "path")
information. Each field might be a primitive type such asString
orInt
or it might be an object type/interface. If thefieldName
's type is composite we have to go deeper and store info about all nested fields.With that information we can start generating filter input type. Actually three filter types. Because
filter
argument ofentities
query has 3 parts:order
- to specify in which way sort entitiessearch
- a full text search across whole entity record or just a few specific fieldsmatch
- strict equality for sets of fields to specific valuesAs a result we have to generate a few input types that allow us to query entities with a filter like this:
Let's walkthrough each type.
order
it's an array with
EntityOrderField
items. Each item represents a compilation of variousEntity
and its implementations fields withOrderDirection
enum type.To sort properly you must describe a sorting priority of fields, with
EntityOrderField
items. Each item must include only one field withOrderDirection
value.You may notice that some fields have different types instead of
OrderDirection
, likeprofile: [EntityOrderField_Profile!]
orsteps: [EntityOrderField_Steps!]
. This is because most top level fields are primitives, unlikeprofile
andsteps
and some others, they are objects which have their own nested fields.There is one special case that I'd like to describe. In the nature how fields are combined it is possible that an order field might refer to a primitive and object for different type, like this:
So as the result
EntityOrderField
will look like:The
location
field instead of an array isEntityOrderField_Location
type, which has two fieldsorder
andfields
. Theorder
is used for sortingComponent.location
andfields
field for sortingUser.location.*
fieldsYou may notice
annotations/labels/parameters
have strange type[EntityRawOrderField!]
this is because those fields are loose typed and usually value is just simple dictionary, but sometimes you might want to filter entities by such fields if you know by which key you would like to do it.search
The
search
field has some similar structure asorder
, except that entity's fields priority doesn't matter, so it's possible to use plain object.EntityTextFilter
input type has two fieldsterm
- a substring value which is used for searching across all entity fields or specific onesfields
- isEntityTextFilterFieldsset
type of fields to limit search scopeEach field of
EntityTextFilterFields
isBoolean
type except some of them which refers to fields with non-primitive valuesAs with
order
thesearch
filter query pretty straight forward and has less chance to make a mistake and will look likeNote: Keep in mind using
false
as search field value doesn't have any effect and similar to just don't mention that fieldYou may notice that
fields.profile
andfields.location
have similar structure properties and might ask yourself how is it possible to distinguish ifinclude
field is a real entity field or it refers tolocation
primitive field for some entity types. Pretty simple. At first we walkthrough each@field
directive we look at field's type and store that information, and when we parseentities
query we are looking at stored type informationmatch
The
match
filter field is a little bit complicated. It is an array ofEntityFilterExpression
items. Between eachEntityFilterExpression
item effectively anOR
is applied. And between fields ofEntityFilterExpression
item effectively an AND is appliedmatch
filter has some similarities to/entities/by-query
REST Catalog API. So it shouldn't be a problem for migrationIs the same to Catalog API
If we look to the same example as we mentioned in
order
andsearch
section with different field types it will look like:Bear in mind that combining
values
andfields
matching filters in one filter expression leads to empty resultSimply because it's impossible that any entity field might be primitive and object types at the same
Using
by-query
Catalog API through GraphQLIt's possible to migrate from Catalog API smoothly without rewriting all client's requests to the new format. You can use
rawFilter
argument ofentities
query. AlthoughrawFilter
slightly different tocatalog.queryEntities()
method it's pretty similar.And here is how this query will look with
rawFilter
:The only difference is
filter
field to handle similar strictness toEntityFilterQuery
TypeScript typeTransforming GraphQL query to Catalog query
With
/entities/by-query
isn't possible easily start paginating from the last page, there is nooffset
either. Catalog REST API only allows to specify filter andlimit
returning items and only after that you are able to go back and forth with cursors. So to implement usual for GraphQLfirst/after
andlast/before
arguments I had to construct and encode Catalog REST API cursor manually. Catalog API cursor is represented as Base64 encoded JSON objectfirstSortFieldValues
- contains two values, first is a value of the field which is described inorderFields
or entity's uid if there is no order fields, second is always entity's uid. The entity's values for this field is the first entity that found with particular sets of filters at specific orderorderFieldValues
- has similar two values, except that is used the last entity of a page.totalItems
- pretty straight forward, number of entities that are matched with particular filterisPrevious
- a flag is used to going backwards and set totrue
iflast/before
arguments are usedorderFields/fullTextSearch/filter
- set of filters and sorting parametersWe expect that if one of
after/before
arguments is passed it must be a valid Catalog API cursor, then we decode it and changeisPrevious
flag depends on which argumentafter
orbefore
is used.If there is no
after/before
arguments we map fields in each filter partorder/search/match
to Catalog fields structure according to field mappings described in@field
directives. And create a new cursor object, luckily to usfirstSortFieldValues
andorderFieldValues
are not mandatory and we will receive the first page of entities for our filterThen we encode our new cursor and call
catalog.queryEntities()
methodPossible Drawbacks or Risks
As you can see here we have two types with the same
foo
field, which we use to generate input filter with only onefoo
field. But because bothfoo
fields are produced from different location, atentities
query resolver we don't know which type user is asking for, so we have to include bothspec.fooA
andspec.fooB
fields in Catalog Entities query.Simple GraphQL query
Becomes
I presume it might be a way to simplify such queries, because Backstage passes them directly to knex API.
@relation
directive. So queries like{ kind: ["Component"], owner: { kind: ["Group"], members: { name: ["John Dow"] } }
are impossible. But I think it would be awesome to have them.TODOs and Open Questions
Entity.labels
andEntity.annotations
have custom resolvers, so we should transform filter values for these fields back to their source, or as more possible solution, just useJSONObject
for filterJSONObject
type to correct filter