Skip to content
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

New graphql schema #785

Merged
merged 23 commits into from
May 3, 2021
Merged

New graphql schema #785

merged 23 commits into from
May 3, 2021

Conversation

adriangmweb
Copy link
Contributor

Description

Note: This branch has been extended from #616 as the metadata reflection introduced by the TypeScript transformers is necessary. Unfortunately, this has its limitations as stated in #661.

This branch introduces the GraphQL new schema in the core, allowing filtering by more than one property at the same time and by complex properties as well in queries and subscriptions.

Changes

The GraphQL schema now is way more secure and complete, allows complex and multiple filters instead of just one basic one. Also, the filter API is way more user-friendly.

New: now it's possible to use non-compatible types like interfaces which will be transcribed as JSON types. In the future, they will be compatible with the typing

Checks

  • Project Builds
  • Project passes tests and checks
  • Updated documentation accordingly

Additional information

This PR solves #600 #602 and also #603. It also covers subscriptions adapters to the new filters #751

Due to a DynamoDB limitation, the schema has to make use of generic GraphQLJSONType types, which doesn't give much info to the user and doesn't give us full security when updating types. This also doesn't give us the ability yet to query by properties of a complex property that is inside a List. You can see more information about this limitation here #652

Next steps

@adriangmweb adriangmweb added feature-request New feature or request size: L Tasks that imply many files. package:core Affects the core package refactor Refactor or rearchitecture labels Apr 13, 2021
@boostercloud boostercloud locked and limited conversation to collaborators Apr 13, 2021
@boostercloud boostercloud unlocked this conversation Apr 13, 2021
@boostercloud boostercloud locked and limited conversation to collaborators Apr 14, 2021
@boostercloud boostercloud unlocked this conversation Apr 14, 2021
Copy link
Contributor

@juanjoman juanjoman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just have a question and a request (docs) but it's looking great ✌️

docs/README.md Outdated Show resolved Hide resolved
@todo
Copy link

todo bot commented Apr 15, 2021

Make retrieval of auth token cloud agnostic

// TODO: Make retrieval of auth token cloud agnostic
await createUser(userEmail, userPassword, 'UserWithEmail')
const userAuthInformation = await getUserAuthInformation(userEmail, userPassword)
loggedClient = await graphQLClient(userAuthInformation.idToken)
})


This comment was generated by todo based on a TODO comment in 70da128 in #785. cc @boostercloud.

@todo
Copy link

todo bot commented Apr 15, 2021

make tests cloud agnostic

// TODO: make tests cloud agnostic
client = await graphQLClientWithSubscriptions()
})
after(() => {


This comment was generated by todo based on a TODO comment in 70da128 in #785. cc @boostercloud.

@boostercloud boostercloud locked and limited conversation to collaborators Apr 15, 2021
@boostercloud boostercloud unlocked this conversation Apr 15, 2021
@adriangmweb adriangmweb requested a review from a team April 15, 2021 15:17
Copy link
Contributor

@juanjoman juanjoman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✌️

Copy link
Collaborator

@davidverdu davidverdu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Member

@NickSeagull NickSeagull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely! Left some suggestions

docs/chapters/03_booster-architecture.md Outdated Show resolved Hide resolved
const meta: ClassMetadata = Reflect.getMetadata('booster:typeinfo', classType)
if (!meta) {
console.log(`Could not get proper metadata information of ${classType.name}`)
return []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning an empty array here? Shouldn't this fail in the case it doesn't find it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was failing before, and that was causing an error when creating the schema. The idea is that instead of falling, it returns an empty value, so the property type will be unknown and set as JSONObject, allowing us to have a schema when working with interfaces or custom types.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be related to the inconsistencies we saw yesterday in Mayday schema?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image (1)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 @otoumas I guess the types (the Typescript classes) you are using in the fields messages and participants are exactly the same in both read models, right?
 
 (BTW, Why do you have two identical read models with different names? Or was it a test?)


const primitiveType = this.typeInformer.getPrimitiveExtendedType(prop.typeInfo.type)
if (primitiveType === Array) return this.generateArrayFilterFor(prop)
if (!this.generatedFiltersByTypeName[filterName]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps reverse this if condition and make it an early return?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

Comment on lines +46 to 56
case 'lt':
if (readModelPropValue >= value) return false
break
case '>':
case 'gt':
if (readModelPropValue <= value) return false
break
case '>=':
case 'gte':
if (readModelPropValue < value) return false
break
case '<=':
case 'lte':
if (readModelPropValue > value) return false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the conditions switched here? This looks so weird...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the way the subscriptions work, they definitely will need some refactoring in the future

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (@adriangmweb correct me if I'm wrong) that right now this code is broken, as it relies on the old filters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a conversation with Adrián, this code is not broken

packages/framework-integration-tests/tsconfig.json Outdated Show resolved Hide resolved
"emitDecoratorMetadata": true
"plugins": [
{ "transform": "metadata-booster" }
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain me what does this plugin do?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Moneyba Here is a lot of information: #616

It is a plugin we created to add more metadata to the types of the project when they are compiled from Typescript to Javascript. This way, we can generate the GraphQL schema using correctly

@adriangmweb adriangmweb linked an issue Apr 28, 2021 that may be closed by this pull request
Copy link
Contributor

@alvaroloes alvaroloes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adriangmweb I see that the base branch is main, Should it be use-metadata-booster?

@@ -37,7 +39,7 @@ export const template = `{
"scripts": {
"lint:check": "eslint --ext '.js,.ts' **/*.ts",
"lint:fix": "eslint --quiet --fix --ext '.js,.ts' **/*.ts",
"compile": "npx tsc -b tsconfig.json",
"compile": "npx ttypescript -b tsconfig.json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be using the ttsc command (Ttypescript compiler) instead of ttypescript?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but when using npx with ttsc it fails on retrieving the package. That's why I'm requesting the whole package instead of using just the binary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Oh, that's when ttypescript is not installed (or it is not listed in the dependencies), so npx tries to find that package and install it. This is not ideal because it installs a "random version" (normally the latest).
I guess we should see why it is failing (it is not failing in the integration-tests package and we are using ttsc there (if I recall correctly)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to sort this, to have it ready for merge 💪🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a conversation with @alvaroloes, we found out that it's necessary to use npm >= 7.0.0 in order to have npx behaving properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the docs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, turns out that npxit's not necessary when using npm scripts on the package.json file as npm is able to retrieve the binary from the root dependencies. You can see a deeper explanation here: https://www.freecodecamp.org/news/npm-vs-npx-whats-the-difference/#npm-the-package-manager

"emitDecoratorMetadata": true
"plugins": [
{ "transform": "metadata-booster" }
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Moneyba Here is a lot of information: #616

It is a plugin we created to add more metadata to the types of the project when they are compiled from Typescript to Javascript. This way, we can generate the GraphQL schema using correctly

const meta: ClassMetadata = Reflect.getMetadata('booster:typeinfo', classType)
if (!meta) {
console.log(`Could not get proper metadata information of ${classType.name}`)
return []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 @otoumas I guess the types (the Typescript classes) you are using in the fields messages and participants are exactly the same in both read models, right?
 
 (BTW, Why do you have two identical read models with different names? Or was it a test?)

@@ -151,7 +151,7 @@ function toReadModelRequestEnvelope(
requestID: context.requestID,
currentUser: context.user,
typeName: readModelName,
filters: args,
filters: args.filter ? (args.filter as Record<string, ReadModelPropertyFilter>) : args,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you explained this to me long ago, but now I don't remember (Sorry! 😰 )

The truth is that it is strange. If args contains filter, then use that, if not we use args. Should we always use either one or the other?
If you could elaborate a bit on the reasons why this is this way I will really appreciate it.

Copy link
Contributor Author

@adriangmweb adriangmweb Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! The thing is that for being able to use filter combinators on the schema, we need to create a new GrahpQL input type where filter is the key and the properties are in the value. But, when using the filters on code-level like in commands, that is not necessary so we don't have to use the filter key.

I've tried to do it more elegant, but I didn't succeed back then.

Copy link
Contributor

@alvaroloes alvaroloes Apr 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks for the explanation, but still don't quite get it. This code is only executed when the search is done through GraphQL. The search executed in code with Booster.readModels uses the searcher directly.

If I'm correct, this code then should be just like this:

Suggested change
filters: args.filter ? (args.filter as Record<string, ReadModelPropertyFilter>) : args,
filters: args.filter,

Let me know if I'm not seeing something 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted 🚀


const primitiveType = this.typeInformer.getPrimitiveExtendedType(prop.typeInfo.type)
if (primitiveType === Array) return this.generateArrayFilterFor(prop)
if (!this.generatedFiltersByTypeName[filterName]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

let nestedProperties: GraphQLInputFieldConfigMap = {}
const properties = getPropertiesMetadata(prop.typeInfo.type)
if (properties.length > 0) {
this.typeInformer.generateGraphQLTypeFromMetadata({ class: prop.typeInfo.type, properties })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate on why is this line needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By calling this function the custom types like Money are created and included in the schema. Without this line, those properties would be JSONObject

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha! Thanks. Yep, that's because Money is not included among the types of the typeInformer. I think the TypeInformer requires a little refactor to not require to pass all the types it can handle in the constructor, but to generate them as requested. But for another PR.
👏

...nestedProperties,
and: { type: new GraphQLList(this.generatedFiltersByTypeName[filterName]) },
or: { type: new GraphQLList(this.generatedFiltersByTypeName[filterName]) },
not: { type: this.generatedFiltersByTypeName[filterName] },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woah! here my head exploded and I couldn't follow anymore. Isn't this.generatedFiltersByTypeName[filterName] undefined? We are exactly inside the if branch, which checked that that expression was undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha I don't blame you! We are setting it here before the value is set. Why is that possible? It's because using fields as a function instead of as an object, the graphql library understands that it's a recursive call and the value will be set later.

There is not much info about it, but you can find an example when setting the PersonType on this example: https://graphql.org/graphql-js/type/#examples

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha! Thanks for explaining

in: { type: GraphQLList(GraphQLString) },
beginsWith: { type: GraphQLString },
contains: { type: GraphQLString },
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Copy link
Contributor

@alvaroloes alvaroloes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brutal work @adriangmweb !! Merging this would be a massive improvement for the framework 👏

Comment on lines +46 to 56
case 'lt':
if (readModelPropValue >= value) return false
break
case '>':
case 'gt':
if (readModelPropValue <= value) return false
break
case '>=':
case 'gte':
if (readModelPropValue < value) return false
break
case '<=':
case 'lte':
if (readModelPropValue > value) return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (@adriangmweb correct me if I'm wrong) that right now this code is broken, as it relies on the old filters.


const originalSubscriptionsCount = await countSubscriptionsItems()
// Let's create two subscriptions to the same read model
const observable = cartFilteredSubscription(client, { id: { eq: cartID } })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this is working then? Even with complex filters?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it works even better than the queries because you can filter subscriptions by ID of property inside an array! 🎉

const originalSubscriptionsCount = await countSubscriptionsItems()
// Let's create two subscriptions to the same read model
const observable = cartFilteredSubscription(client, {
cartItemsIds: { includes: productId },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whaat? then subscriptions with the new schema are working???

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! 🕺🏻

@@ -48,7 +49,7 @@
"watch:local": "nodemon --watch ../framework-provider-local/dist --watch ../framework-provider-local-infrastructure --watch dist --exec \"../cli/bin/run start -e local\"",
"lint:check": "eslint --ext '.js,.ts' **/*.ts",
"lint:fix": "eslint --quiet --fix --ext '.js,.ts' **/*.ts",
"compile": "tsc -b tsconfig.json",
"compile": "npx ttsc -b tsconfig.json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right command. Please, check the other pacakge.json because I think you used ttypescript instead of ttsc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk why but it was failing in the other one when using ttsc, that's why it's changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check the other comment, I think it could be related to installing dependencies when generating the user project.

packages/framework-types/src/searcher.ts Show resolved Hide resolved
Copy link
Contributor

@alvaroloes alvaroloes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more missing comments

docs/chapters/03_booster-architecture.md Outdated Show resolved Hide resolved
docs/chapters/03_booster-architecture.md Show resolved Hide resolved
@boostercloud boostercloud locked and limited conversation to collaborators May 3, 2021
@boostercloud boostercloud unlocked this conversation May 3, 2021
@adriangmweb adriangmweb merged commit 67f2ebf into main May 3, 2021
@adriangmweb adriangmweb deleted the new-graphql-schema branch May 3, 2021 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request New feature or request package:core Affects the core package refactor Refactor or rearchitecture size: L Tasks that imply many files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unable to query a read model with a filter
7 participants