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

feat(backend): add webhook event query #1454

Merged
merged 23 commits into from
Jun 6, 2023
Merged

feat(backend): add webhook event query #1454

merged 23 commits into from
Jun 6, 2023

Conversation

BlairCurrey
Copy link
Contributor

@BlairCurrey BlairCurrey commented May 17, 2023

Changes proposed in this pull request

Also, I mocked up some changes for mapping gql filters to knex where clauses on a seperate branch (not sure we want to go that far here but wanted to get the idea down on "paper"). https://github.com/interledger/rafiki/compare/bc-events-query...bc-filter-to-knex-where-mapper?expand=1

The gist of it is we parse the info/filter arg in the resolver to create a config for mapFilterToKnexWhere. We make a mapper for each filter type (string, float, date, etc.) and use the config to call these accordingly in mapFilterToKnexWhere. Then we use mapFilterToKnexWhere to build the query in the service.

The idea behind this is that it makes a standardized way to filter certain field types (string, float, date, etc.). This would make adding new filters easy, ensure consistency, and lets us test the mapping logic once instead of separately in each service/resolver. Kinda overkill without much filtering but my hope is we can at least stick to a consistent structure in our gql filter types so that we could easily move to something like this in the future if we want.

Context

fixes #234

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Documentation added
  • Make sure that all checks pass

@netlify
Copy link

netlify bot commented May 17, 2023

Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit f868359
🔍 Latest deploy log https://app.netlify.com/sites/brilliant-pasca-3e80ec/deploys/647a03120b7be8000897ac38

@github-actions github-actions bot added pkg: backend Changes in the backend package. type: source Changes business logic labels May 17, 2023
data: String!
}

type WebhookEventsConnection {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure why Assets and Peers pagination results were called AssetsConnection and PeersConnection originally... something like WebhookEventsPaginationResults make more sense to me IMO

Copy link
Contributor Author

@BlairCurrey BlairCurrey May 18, 2023

Choose a reason for hiding this comment

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

My understanding is that it's tied to the way we do pagination in our baseModel.ts: https://relay.dev/graphql/connections.htm

I'm not that familiar with cursor based pagination and the relay spec, so Connection was not immediately obvious to me. But it looks like the correct name for the specification we implemented.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, thanks for the article, now I remember!
There are a few related posts we can consider when adding additional 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.

great resources on this topic, thank you for sharing. I will read these and revisit the GetWebhookEventsInput - I suspect there will be some improvements I can make in light of these.

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me think about keeping the Relay connections convention in using multiple parameters instead of a unifying Input Object (GetWebhookEventsInput), as much as I prefer using object as inputs. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Generally speaking I'd like to follow the convention if there is one. I only did a cursory scan of those links but the "Expanding Relay Cursor Connections" article shows this for querying which I think makes sense:

type Query {
    users(
        first: Int,
        after: String,
        filterBy: UserFilterBy
        orderBy: UserOrderBy
    ): UserConnection
}

So the pagination args are all seperate top-level args and then filterting and ordering is scoped to different input objects. This also remains consistent with our other paginated endpoints. If we ever added filtering we could just add a filterBy input and not have to refactor the pagination stuff.

Copy link
Contributor Author

@BlairCurrey BlairCurrey May 23, 2023

Choose a reason for hiding this comment

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

As of now, query would look like this:

query WebhookEvents($first: Int, $after: String, $filter: WebhookEventFilter) {
  webhookEvents(first: $first, after: $after, filter: $filter) {
    edges {
      cursor
      node {
        createdAt
        data
        id
        type
      }
    }
    pageInfo { ... }
  }
}

with inputs like this:

{
  "first": 10,
  "after": "some_id",
  "filter": {
    "type": { "in": ["some_type"]  }
  }
}

packages/backend/src/graphql/schema.graphql Outdated Show resolved Hide resolved
packages/backend/src/graphql/schema.graphql Outdated Show resolved Hide resolved
@github-actions github-actions bot added pkg: frontend Changes in the frontend package. pkg: mock-ase type: tests Testing related labels May 22, 2023
Comment on lines 184 to 186
if (type) {
query.where({ type })
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Still thinking through where and how to handle applying the filters to the query.

Either we manually apply each one in the service like I'm doing here, which seems reasonable for very simple filters like this, or we create some utility function that applies them. I think this utility will be necessary if we do something like:

input WebhookEventFilter {
  type: {eq: String, contains: String, startsWith: String etc.}
  withdrawalAmount: {gt: UInt64, lt: UInt64, eq: Uint64 etc.}
}

Seconds, is where to apply them. If doing the utility that can handle any given filter, I wonder about passing the filter into baseModel.getPage and call the utility internally. This would save calling the function in every service and ensure it's always applied correctly (before pagination). Additionally, if we need to encode the filter in the cursor (TBD) then we'll need to pass the filter in anyways.

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 place for them for now 👍. If we start adding more filters to different models, we could see a pattern, and how we could abstract it away

return {
pageInfo,
edges: webhookEvents.map((webhookEvent: WebhookEvent) => ({
cursor: webhookEvent.id,
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'm trying to determine if I need to encode the filter in the cursor, as a few sources suggest doing so [0] [1]. I believe this would require changes in getPage and getPageInfo as well. However, I'm not sure I understand how it would be a problem to keep the cursor as the ID. If we make subsequent requests with the same filters I think it will work as expected. It seems like encoding the filter in the cursor is more about getting the next page from the cursor alone. Is that necessary?

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 talking w/ Max it seems clear that this is not required for functionality.

I think there a few things that encoding is for:

  • allows grabbing more pages from the cursor alone
  • can be used to validate cursor matches filters
  • may help handle some scenarios related to duplicate records

There is already a TODO in the baseModel.getPage method for base64 encoding. I'll just leave it at that.

Comment on lines 184 to 186
if (type) {
query.where({ type })
}
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 place for them for now 👍. If we start adding more filters to different models, we could see a pattern, and how we could abstract it away

packages/backend/src/graphql/resolvers/webhooks.ts Outdated Show resolved Hide resolved
packages/backend/src/tests/webhook.ts Outdated Show resolved Hide resolved
@@ -333,6 +341,65 @@ describe('Pagination', (): void => {
}
)
})
describe('webhookEvents', (): void => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if someone else would agree with me, but I don't see the need to test getPageInfo for all of the resources. It's outside the scope of this test to test everything that getPage could be, all this function knows/should know about is the type definition. As long as we give it a proper function to test, I think we should reduce this test suite.

As long as we test getPage functions in each of the service, (as you do here: https://github.com/interledger/rafiki/pull/1454/files#diff-f6ae8e955305efe9c101fc4a66e9c257713a099b0018dcc5061a08983b854134R99), we are good to remove this IMO

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 happened to find these tests by accident - very easily could have missed them. My initial reaction was kinda similar in that I didn't think these tests should concern themselves with every resource that uses them. It seems analogous to getPageTests so I thought (ideally) there should be something like getPageInfoTests that are exported and run in whichever resource uses them. But I suppose I can also see an argument for testing getPageInfo once and then just ensuring each resource calls it correctly (side note, but wouldn't this also apply to getPageTests? does being a method on the inherited baseModel instead of a utility function make some difference there?).

In fact that is really what I am interested in with getPageInfo is ensuring the right function is passed in (it needs the pagination from the getPageInfo scope but still needs the filter received by the resolver, which should also be the filter passed into the service's getPage). But this doesn't test that.

As long as we test getPage functions in each of the service

I think it's actually the resolver tests that need it (that's where it's called - service doesn't know about it) but I think I agree in principle.

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 it's actually the resolver tests that need it (that's where it's called - service doesn't know about it) but I think I agree in principle.

Yes, you are right, one for the webhook resolver (for making sure the return type confirms to the GraphQL schema), and the service like you have

packages/backend/src/webhook/service.test.ts Outdated Show resolved Hide resolved
packages/backend/src/webhook/service.test.ts Outdated Show resolved Hide resolved
@BlairCurrey BlairCurrey marked this pull request as ready for review May 28, 2023 18:24
@mkurapov
Copy link
Contributor

mkurapov commented Jun 1, 2023

Some of the packages/backend/src/graphql/resolvers/webhooks.test.ts are failing for me locally, is this happening to you?

@BlairCurrey
Copy link
Contributor Author

Some of the packages/backend/src/graphql/resolvers/webhooks.test.ts are failing for me locally, is this happening to you?

They are passing for me locally... can you share the output?

@mkurapov
Copy link
Contributor

mkurapov commented Jun 5, 2023

So, the tests were failing sporadically for me, and I'm guessing would be hard to replicate on another machine. The TL;DR of it was that models were being created with the same createdAt timestamp, which ended up resulting in a different order of models when they were stored in memory: let modelsCreated: Type[] in baseModel.test.ts vs when they were fetched from the database. The tests would fail when the cursor fell on one of these models with the same createdAt.

To make sure this wouldn't happen, we could either not use modelsCreated, but always fetch all of the results from the DB directly to compare with pagination results or add some sort of timeout to ensure all of the models would have a different createdAt. In a real world scenario, this shouldn't happen since the cursor that is used for pagination across multiple queries would always come from the DB anyway, meaning, the order would be correct, regardless of whether createdAt is the same. (In addition, it would be very unlikely for createdAt to be the same in the DB, anyway)

Copy link
Contributor

@mkurapov mkurapov left a comment

Choose a reason for hiding this comment

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

Looks good, approving postman PR as well

@BlairCurrey
Copy link
Contributor Author

So, the tests were failing sporadically for me, and I'm guessing would be hard to replicate on another machine. The TL;DR of it was that models were being created with the same createdAt timestamp, which ended up resulting in a different order of models when they were stored in memory: let modelsCreated: Type[] in baseModel.test.ts vs when they were fetched from the database. The tests would fail when the cursor fell on one of these models with the same createdAt.

To make sure this wouldn't happen, we could either not use modelsCreated, but always fetch all of the results from the DB directly to compare with pagination results or add some sort of timeout to ensure all of the models would have a different createdAt. In a real world scenario, this shouldn't happen since the cursor that is used for pagination across multiple queries would always come from the DB anyway, meaning, the order would be correct, regardless of whether createdAt is the same. (In addition, it would be very unlikely for createdAt to be the same in the DB, anyway)

@mkurapov Thanks for digging into that and sharing. I guess this means all pagination tests are liable to cause these sporadic errors then. Although I'm surprised it's event possible for them to have the same createdAt since they are done in a loop. Guess you just have really fast hardware 😄

Comparing those two options I think adding a the smallest possible delay makes sense. If we didn't check modelsCreated and refetched form the database then I guess we'd need to pass in the tablename everywhere (otherwise we wouldn't know which records to get). Changing to fetch all from the db would also behave different than referencing modelsCreated with regards to records created outside the getPageTests (setup in beforeEach for example). Although I'm not sure if that is necessarily bad. I often try to avoid adding delays because they can be flakey but in this case any amount should work.

@BlairCurrey BlairCurrey merged commit b96ddf1 into main Jun 6, 2023
@BlairCurrey BlairCurrey deleted the bc-events-query branch June 6, 2023 11:45
sabineschaller pushed a commit that referenced this pull request Jun 6, 2023
* feat: add webhook event query to schema

* fix: webhook event gql schema

* feat: start webhookevents query and service method

* feat: add filtering by type to webhook events

* fix: make get webhookevent input optional

* test: WIP get webhookevents filter & pagination

* fix: format

* refactor: webhookevent query and getpage interface

* test(backend): getpageinfo tests for webhookevents

adds getPageInfo tests for webhookEvents, including filtering

* chore: remove unused table

* Update packages/backend/src/tests/webhook.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* Update packages/backend/src/graphql/resolvers/webhooks.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* refactor: webhookevent filter

* chore: format

* chore: webhook test cleanup

* test(backend): webhookevents query

* chore: format

* fix(backend): linter warning

* refactor: webhook event service tests to use create webhook event

* fix(backend): rm console logs

---------

Co-authored-by: Max Kurapov <max@interledger.org>
sabineschaller pushed a commit that referenced this pull request Jun 8, 2023
* feat: add webhook event query to schema

* fix: webhook event gql schema

* feat: start webhookevents query and service method

* feat: add filtering by type to webhook events

* fix: make get webhookevent input optional

* test: WIP get webhookevents filter & pagination

* fix: format

* refactor: webhookevent query and getpage interface

* test(backend): getpageinfo tests for webhookevents

adds getPageInfo tests for webhookEvents, including filtering

* chore: remove unused table

* Update packages/backend/src/tests/webhook.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* Update packages/backend/src/graphql/resolvers/webhooks.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* refactor: webhookevent filter

* chore: format

* chore: webhook test cleanup

* test(backend): webhookevents query

* chore: format

* fix(backend): linter warning

* refactor: webhook event service tests to use create webhook event

* fix(backend): rm console logs

---------

Co-authored-by: Max Kurapov <max@interledger.org>
sabineschaller added a commit that referenced this pull request Jun 8, 2023
* chore(deps): update dependency openapi-types to ^12.1.3

* feat(backend): add webhook event query (#1454)

* feat: add webhook event query to schema

* fix: webhook event gql schema

* feat: start webhookevents query and service method

* feat: add filtering by type to webhook events

* fix: make get webhookevent input optional

* test: WIP get webhookevents filter & pagination

* fix: format

* refactor: webhookevent query and getpage interface

* test(backend): getpageinfo tests for webhookevents

adds getPageInfo tests for webhookEvents, including filtering

* chore: remove unused table

* Update packages/backend/src/tests/webhook.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* Update packages/backend/src/graphql/resolvers/webhooks.ts

Co-authored-by: Max Kurapov <max@interledger.org>

* refactor: webhookevent filter

* chore: format

* chore: webhook test cleanup

* test(backend): webhookevents query

* chore: format

* fix(backend): linter warning

* refactor: webhook event service tests to use create webhook event

* fix(backend): rm console logs

---------

Co-authored-by: Max Kurapov <max@interledger.org>

* chore: update incoming payment creation payload (#1498)

* fix: lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Blair Currey <12960453+BlairCurrey@users.noreply.github.com>
Co-authored-by: Max Kurapov <max@interledger.org>
Co-authored-by: Sabine Schaller <sabine@interledger.org>
@mkurapov mkurapov mentioned this pull request Jun 29, 2023
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: backend Changes in the backend package. pkg: frontend Changes in the frontend package. pkg: mock-ase type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add webhookEvents query resolver
2 participants