feat: initial subscriptions implementation#32
Conversation
| @@ -0,0 +1,91 @@ | |||
| const _ = require('lodash') | |||
|
|
|||
| module.exports = function ExpressionParser () { | |||
There was a problem hiding this comment.
This is actually really generic and could be published as its own npm module with docs that demonstrate usage.
There was a problem hiding this comment.
I think it might be a better idea to extract this.
BTW, have you searched for a lib that does the same thing?
There was a problem hiding this comment.
I tried to search for something like this but I couldn't find anything. What would you even call this? I don't even know how to describe it in 2-3 words so I found it very hard to search for.
I think it would be great to extract. We should be constantly looking for opportunities to extract and open source internal libs like this. Where should the repo go? under github.com/aerogear?
|
|
||
| type Subscription { | ||
| _: Boolean | ||
| memeAdded(photoUrl: String):Meme! |
There was a problem hiding this comment.
[Outside scope of this Pr] I think it will be more benefitial to add more fields to the meme like votes as this will help us to test various use cases.
|
|
||
| // Set up the WebSocket for handling GraphQL subscriptions | ||
| /* eslint-disable no-new */ | ||
| new SubscriptionServer({ |
There was a problem hiding this comment.
this won't pick up new schema with hot reload
There was a problem hiding this comment.
👍 This completely slipped my mind
| let dataSourcesJson = dataSources.map((dataSource) => { | ||
| return dataSource.toJSON() | ||
| }) | ||
| let dataSourcesJson = await models.DataSource.findAll({raw: true}) |
| const PubSub = require('./pubsubNotifiers').InMemory | ||
|
|
||
| module.exports = function (schemaString, dataSourcesJson, resolverMappingsJson, subscriptionMappingsJson) { | ||
| const pubsub = PubSub() |
There was a problem hiding this comment.
what's the plan to abstract this away?
| if (typeof str !== 'string' || str.length <= 1) { | ||
| return str | ||
| } | ||
| var c1 = str[0] |
| return new Promise(async (resolve, reject) => { | ||
| try { | ||
| const result = await resolverFn(obj, args, context, info) | ||
| resolve(result) |
There was a problem hiding this comment.
+1 on resolving and then publishing
| let payload = compiledPayload(compileOpts) | ||
|
|
||
| // The InMemory pubsub implementation wants an object | ||
| // Whereas the postgres one would expect a string |
There was a problem hiding this comment.
as you noted, we need abstraction for this. let's not forget about that.
| @@ -0,0 +1,91 @@ | |||
| const _ = require('lodash') | |||
|
|
|||
| module.exports = function ExpressionParser () { | |||
There was a problem hiding this comment.
I think it might be a better idea to extract this.
BTW, have you searched for a lib that does the same thing?
| payload: `{ | ||
| "memeAdded": {{ toJSON context.result }}, | ||
| }` | ||
| }), |
There was a problem hiding this comment.
can you also add sample into memeolist.query.graphql file so that we don't have to type anything when we open Graphiql?
|
@darahayes really great work! and, thanks for the long PR description which explains everything! I haven't done any verification yet. I will do it after the requested changes are addressed. I think we would need some integration tests where we create 2 clients and mutate on one and listen on other one. Feel free to implement it as part of this pr OR we can implement it in a separate ticket/pr. Another thing: we need to discuss with David and the team about syncing the sync server instances. I know we talked about it and it seems necessary, but maybe it is not in the scope right now. |
| const { evalWithContext } = require('../util/filterEvaluator')() | ||
| const { log } = require('../util/logger') | ||
|
|
||
| module.exports = function mapSubscriptions (subsciptionMappings, pubsub) { |
There was a problem hiding this comment.
I would add some comments on what this module is doing.
| const result = await resolverFn(obj, args, context, info) | ||
| resolve(result) | ||
|
|
||
| const { pubsub, topic, compiledPayload } = publishOpts |
There was a problem hiding this comment.
I would wrap the code from here into separate function like notifySubscriptions() that could be used in different context.
|
@darahayes Performing verification and adjusting memelist app |
|
@darahayes Adjusting now the memolist app for testing. Initial subscription looked like this. Do we support action filter from mobile? Something like here: https://github.com/aerogear/aerogear-android-example-apps/blob/07fcffdb17b4a7bd2543097b1b5e8a6aef159dc5/memeolist/app/src/main/graphql/org/aerogear/android/app/memeolist/graphql/memes.graphql#L17-L19 |
|
@wtrocki I have never seen this kind of syntax before. We definitely don't support it right now. Can you link to some Apollo/Gprahql documentation that shows this off in more detail? I'd like to learn more |
|
@darahayes @wtrocki that I think is from the schema generated by Graphcool. When you define a type it generates queries/mutations/subscriptions. Portion of generated stuff: That's still valid Graphql. |
|
Verified with graphql. Verified on the Android application. |
david-martin
left a comment
There was a problem hiding this comment.
Verified from a functionality point of view.
Structure of code looks very reasonable too. Good to have the wrapper around postgres pubsub
Approved.
|
@aliok Thanks for the great feedback. I believe I have incorporated all of it except the thing about extracting the filterEvaluator library. That will require a lot of extra time. Let me know what you think! |
| log.info('Received schema change notification. Rebuilding it') | ||
| let newSchema | ||
| try { | ||
| newSubScriptionServer(server, schema) |
There was a problem hiding this comment.
Should this be the newSchema that is build bellow?
| throw new Error(`PubSub implementation for ${pubsubConfig.type} is missing a constructor`) | ||
| } | ||
|
|
||
| const pubsub = new PubSubClass(pubsubConfig.config) |
There was a problem hiding this comment.
Is not better add all const on top of it? All together.
There was a problem hiding this comment.
@camilamacedo86 not sure what you mean, it wouldn't be possible to declare this at an earlier point in the code.
There was a problem hiding this comment.
I think it should be inside of the func getPublish (PubSubClass); as described in the next comment.
| module.exports = function NewPubSub (pubsubConfig) { | ||
| const PubSubClass = notifiers[pubsubConfig.type] | ||
|
|
||
| if (!PubSubClass) { |
There was a problem hiding this comment.
Is not better encapsulate these if in functions the call them, something like as:
module.exports = function NewPubSub (pubsubConfig) {
const PubSubClass = notifiers[pubsubConfig.type]
try {
checkObject(PubSubClass);
return getPublish(PubSubClass);
} catch (error) {
throws error;
}
}There was a problem hiding this comment.
Hi @camilamacedo86 thanks for reviewing! Your suggestion is very good and fair, but I do not see the need to move any of this stuff into separate functions right now because because this is the only place we will ever run these checks.
There was a problem hiding this comment.
@darahayes the problem is if we don't start the code as it should be then it will not grow as we wish. This code has many ifs, and if someone will improve it will add more ifs then if we encapsulate the logic is not required comments because what it is doing will be clear as we are doing things that allow improvements in a healthy way. Is it makes sense for you?
| } | ||
| } | ||
|
|
||
| const port = process.env.HTTP_PORT || '8000' |
There was a problem hiding this comment.
Should not better add all const/var declarations in the top without space?
There was a problem hiding this comment.
Variable declarations at the top are typically used for declaring imports.
There was a problem hiding this comment.
@darahayes at on top typically is declared all that is global to the program in all languages. For default, it follows the following standard/convention.
imports ...
global vars ...
functions/methods ...It is something important to make easier to keep the project maintained, global vars in many places make harder to read and understand the code as could cause mistakes, for example, declare the same value again in another global var.
Note that this standard is valid for any language. global variables are often available by declaring a variable at the top level of the program. See here
Also, it is a good practice to avoid global variables in JS and try to use always local variable instead of it. Following some references.
| subscriptionsEndpoint: `ws://${hostname()}:${port}/subscriptions` | ||
| }, | ||
| postgresConfig: { | ||
| database: process.env.POSTGRES_DATABASE || 'aerogear_data_sync_db', |
There was a problem hiding this comment.
IHMO the fixed values as 'aerogear_data_sync_db' could be declared as const on to of it.
There was a problem hiding this comment.
+1 on that. @camilamacedo86 great suggestion. I will create a GH issue for that.
I don't want this PR to get any bigger. I am too lazy to create a JIRA for something minor though.
|
@darahayes verified functionality locally but only the case in memeo list schema. unit tests look good as well. Approving this PR. |
What do you think about this? cc @darahayes This is about syncing the sync server states using the same mechanisms we have. @darahayes showed some great thinking and brought this, but we're not sure if this is something in scope. Imagine the scenario:
|
|
@aliok This will work when using the postgres pubsub implementation.
It's basically implemented although the postgres Pubsub implementation in this PR is really a skeleton and I haven't tried it at all. |
|
^^ I would suggest adding the following steps to the integration testing ticket:
I feel that we should focus the integration tests much more on the Postgres implementation because the In Memory one is really not for production use. |
You're right. I think I got confused with the in memory pubsub.
added to the ticket.
We can target having Postgres pubsub for integration tests initially and can see how easy it is to create separate tests for pubsub later. |
…into subscriptions
|
@aliok Great! I have rebased now so ready to merge as long as you're happy! |
To do
This is runnable right now though. You can try it out as follows:
npm run db:init:memeonpm run devhttp://localhost:8000/graphiqlin two different tabs"Your subscription data will appear here after server publication!"How does the subscription piece work?
On the server side we define a new
memeAddedsubscription.Clients will be able to use this query to listen for updates. The photoUrl variable is optional (more on that later).
The next thing we need to be able to do is define a way to publish an event during some query or mutation. GraphQL uses a Pub/Sub model to do this. So when a query/mutation occurs, we can publish some events+data to a Pub/Sub mechanism and we can react to these events elsewhere. Below shows how a user could configure a
createMemeresolver to also publish an event.The last piece of the puzzle is to be able to listen for those events and push them back down to a client. We can do this with the new
Subscriptionconfig object. Take a look at the example one for the memeolist (included in this PR)This config tells our server to listen for messages published to the 'memeCreated' topic on the internal pubsub mechanism and to fire the
memeAddedresolver as a result.Trying out Filtering
Filtering is an important aspect of Grapqhl subscriptions. In most real world apps, it doesn't make sense for a client to be listening for notifications about every single record that was added to a collection. It would be like getting a notification in slack every time anybody in the world sent a message.
Taking the Slack example further, on the client side we might subscribe to a messageAdded query as follows:
The idea is we only want to listen for notifications in a particular slack channel -
#random.How would this look on the server side?
How can we enable an end user of our service to define some kind of filtering that's relatively good?
I have come up with small DSL that allows the user to define some conditions in a declarative way using JSON. We add this filter to the subscription config object.
Here's when it might look like in the Slack example
This filter would check that the channelId in the newly created message is equal to the channelId that the client passed as a variable when they subscribed. i.e. the user can say give me real time updates about messages for this particular channel.
The filter expression has access to a context object that looks like this:
{ payload: {...}, variables: {}}wherepayloadis the message received from the pubsub layer and variables contains whatever variables were passed by the client to initialize the subscription. Using the dollar syntax like$payload.messageAdded.idwe can reference the context object.I figured that 99% of the time, the basic use case will like the one above where we check that two things are equal. I made a more concise syntax for this particular case. The same example above can be expressed as follows:
This is the most basic usage of the filter language. You can do many more things. Try some of these out with the memeolist schema:
(you can modify the subscription object in the file located under
seeders, then run the db init and start commands again)Why Did I do it this way?
trueorfalse. It would also mean users would have to write a horrible syntax. In my opinion, this syntax is pretty intuitive.