-
Notifications
You must be signed in to change notification settings - Fork 89
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): tenants service (#3123) #3140
Conversation
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.
Generally looks ok. Me personally, I would like to see method getPage to be implemented, that would be used for getting list of tenants
@@ -8,6 +8,9 @@ exports.up = function (knex) { | |||
table.string('email').notNullable() | |||
table.string('publicName') | |||
table.string('apiSecret') | |||
|
|||
table.timestamp('createdAt').defaultTo(knex.fn.now()) | |||
table.timestamp('updatedAt').defaultTo(knex.fn.now()) |
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.
Maybe also deletedAt ? So we would leverage soft delete. What do you think?
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.
Makes sense to me
const apolloSpy = jest.spyOn(apolloClient, 'query') | ||
const tenant = await tenantService.get(createdTenant.id) | ||
assert.ok(tenant) | ||
expect(tenant.id).toEqual(createdTenant.id) |
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.
Maybe just
expect(tenant).toEqual(createdTenant)
? Don't know if this works :D
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.
It breaks because the updatedAt
for one will always be different from the other, sadly.
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.
Why would updatedAt field be different ? You are creating a tenant and getting back data, and then you are querying the exact same entity ... so it should be the same one? Or am I missing something here?
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.
Nevermind that, I conflated that result with a different test. This will work, but createdTenant
just needs to be augmented with the idpSecret
and idpConsentUrl
.
} | ||
|
||
// TODO: add type to this in https://github.com/interledger/rafiki/issues/3125 | ||
await deps.apolloClient.mutate({ mutation, variables }) |
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.
Does mutate method throw error in case of 4xx / 5xx errors ?
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.
It does - a manifestation of this is having to catch 400
responses inside a try/catch block in the tests.
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.
My understanding is this will be a new gql server for service-to-service communication so I guess maybe we can simply return a 400 from that server like this expects, but I just wanted to note that our current resolvers dont behave this way.
For errors we do something like throw new GraphQLError('tenant not found', { extensions: { code: 'NOT_FOUND' } })
. I tested an existing one of these on Bruno and see the gql server replies with a 200 and error details in the body like:
{
"errors": [
{
"message": "No grant",
"locations": [
{
"line": 2,
"column": 5
}
],
"path": [
"grant"
],
"extensions": {
"code": "NOT_FOUND"
}
}
],
"data": null
}
Im not necessarily against doing it a different way, if that seems better. I just wanted to note this.
My assumption is the apolloClient
doesnt parse this and throw. Even if it did the behavior should be
correct but the mock would need to be updated still. If the client doesnt do this I think the test would incorrectly pass if we updated the mock to something like the above response because the expects in the catch would never be called. We safeguard against this sort of incorrect pass elsewhere by doing expect.assertions(n)
. I also wondered if apolloClient
parses that sort of 200 response and throws but in any case the test mock would need to be updated.
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.
Thanks for pointing that out. I updated the nocks to return proper error structure. Mercifully, the client still parses that as an error and throws, but it's a better test to have the correct response from the API.
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.
Mercifully, the client still parses that as an error and throws
Good to know, was curious about that
export type TenantWithIdpConfig = Pick< | ||
Tenant, | ||
'id' | 'email' | 'apiSecret' | 'publicName' | 'createdAt' | 'updatedAt' | ||
> & { | ||
idpConsentUrl: string | ||
idpSecret: string |
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.
Do we need idpConsentUrl
and idpSecret
in backend?
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.
We need this in backend, so that with one call, we add tenant data on both backend, and auth service.
I'm guessing you are referring to this comment @golobitch?
Also, I see, we are not storing the idpConsentUrl
and idpSecret
in the tenants table, (TenantWithIdpConfig
is just a type). At first, not storing the data makes sense to me (we shouldn't care about IDP stuff in backend
). However, how would that work for doing pagination over tenants? If we don't store these, then we would need to call getTenant
in the auth service for every tenant result
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.
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.
Perhaps we could treat the backend's idpConsentUrl
and idpSecret
as some sort of cache-like thing. As in for pagination, we return the backend's entries for each tenant row, but in the query for an individual tenant, make the call to the authorization server. Whenever a tenant is mutated such that the IDP fields are changed, it would update the columns in both backend and auth.
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.
but in the query for an individual tenant, make the call to the authorization server
The fields should basically always be the same, so I think calling to the AS isn't needed. The updates shouldn't happen frequently, and if they do, they'll be stored in the backend tenants table anyway as you describe
@@ -31,6 +31,8 @@ services: | |||
PRIVATE_KEY_FILE: /workspace/private-key.pem | |||
AUTH_SERVER_INTROSPECTION_URL: http://cloud-nine-wallet-test-auth:3107 | |||
AUTH_SERVER_GRANT_URL: http://cloud-nine-wallet-test-auth:3106 | |||
AUTH_ADMIN_API_URL: 'http://cloud-nine-wallet-test-auth:3003/graphql' | |||
AUTH_ADMIN_API_SECRET: 'test-secret' |
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.
Does this need to correspond to auth's ADMIN_API_SECRET
? Or will this be a separate API altogether?
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.
@mkurapov I would say that on both of your comments the answer is affirmitive. We need this in backend, so that with one call, we add tenant data on both backend, and auth service.
68f523a
to
3945c35
Compare
3945c35
to
fb3d702
Compare
await Tenant.query(trx).patchAndFetchById(id, { | ||
deletedAt: new Date(Date.now()) | ||
}) |
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.
maybe we can pass through the deletedAt
to the mutation, so both tables in backend and auth have the same value
const mutation = gql` | ||
mutation DeleteAuthTenantMutation($input: DeleteTenantInput!) { | ||
deleteTenant(input: $input) { | ||
sucess |
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.
I assume this must be success
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.
It's a basic response, but this could be changed in the PR for the auth graphql API if it is necessary.
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.
LGTM. 🟢
@@ -6,8 +6,14 @@ exports.up = function (knex) { | |||
return knex.schema.createTable('tenants', function (table) { | |||
table.uuid('id').notNullable().primary() | |||
table.string('email').notNullable() |
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.
lets have email
nullable, based on our slack convo?
table.string('idpConsentUrl').notNullable() | ||
table.string('idpSecret').notNullable() |
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.
I think idpConsentUrl
and idpSecret
can be null, integrators may not actually be integrated with an IDP anyway. (echoing @tadejgolobic on our Tues call)
Changes proposed in this pull request
TenantService
to the backend IoC containerupdatedAt
andcreatedAt
in the backend tenants table.Context
Closes #3123.
Checklist
fixes #number
user-docs
label (if necessary)