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

662/mk/use open payments types #1024

Merged
merged 33 commits into from
Feb 2, 2023
Merged

Conversation

mkurapov
Copy link
Contributor

@mkurapov mkurapov commented Jan 24, 2023

Changes proposed in this pull request

  • Exporting additional IncomingPayment types from open-payments package
  • Adding toOpenPaymentsType() method for subresources to convert from model to the corresponding open-payments type, removing toJSON types and method where necessary
  • use toOpenPaymentsType() methods in the route handlers when creating response

Context

Checklist

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

@github-actions github-actions bot added pkg: backend Changes in the backend package. pkg: open-payments type: source Changes business logic type: tests Testing related labels Jan 24, 2023
@mkurapov
Copy link
Contributor Author

mkurapov commented Jan 26, 2023

Currently running into this challenge: when calling .toOpenPaymentsType on a model, I need the corresponding paymentPointer.url. However, other than the IncomingPayment model, Model.paymentPointer is optional, since it needs to be fetched from the DB. The IncomingPayment model gets around this via .withGraphFetched(..., 'paymentPointer') existing on every IncomingPayment query, but I feel like it could be easy to miss when making those calls in the service, and as a result, erroring when expecting the non-nullable paymentPointer field. A few options exist here:

  • Go with the IncomingPayment method, add a non-null assertion (paymentPointer!) to each of the PaymentPointerSubresources, and add .withGraphFetched(..., 'paymentPointer') everytime when requesting the model from the DB.
  • Make toOpenPaymentsType async, and have a helper method getPaymentPointer/getWithPaymentPointer that fetches the paymentPointer if doesn't already exist on the model instance. This is pretty straightforward but will require some finessing converting all methods that call toOpenPaymentsType to be async
  • Make paymentPointer non-nullable on PaymentPointerSubresources by always eager fetching it. I'm exploring a few ways of doing this, one of them (possibly?) being as such:
class SubresourceQueryBuilder {
  ...
  execute() {
    this.withGraphFetched('paymentPointer')
    return super.execute()
  }
}

related issue

Am I overthinking this, and should just go with the standard way we have it now via .withGraphFetched method, like option 1, since we are already doing this with asking for asset on almost every PaymentSubresource query?

@wilsonianb @sabineschaller

@wilsonianb
Copy link
Contributor

Reading option 1 made think of doing something like option 3.

Adrian's idea 👇 might eventually help with this.

@mkurapov
Copy link
Contributor Author

I was trying it out with the example above of overriding execute(), it seemed to work. I was testing out with the debugger, seeing all of the different SQL calls that were made, and even though its straightforward request, I'm a bit concerned that every single request, insert, get, update, it would do this DB call.

Instead of solving it programmatically, what if we just stored a paymentPointerUrl foreign key on the PaymentPointerSubresource models? It is already a non-nullable unique index.

Otherwise, I'm leaning toward making toOpenPaymentsType async, since we fully control when we need that data, and there will never be a possibility of us not adding a field to withGraphFetched

@wilsonianb
Copy link
Contributor

I vote async toOpenPaymentsType over storing paymentPointerUrl on each subresource
but only if we rename every synchronous function *Sync to make it clear that toOpenPaymentsType is async 😜

@@ -84,7 +84,6 @@ export class IncomingPayment
}
}

public paymentPointer!: PaymentPointer
Copy link
Contributor Author

@mkurapov mkurapov Jan 28, 2023

Choose a reason for hiding this comment

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

no non-null assertion since we can start removing withGraphFetched('paymentPointer') from the DB calls

@@ -131,8 +130,15 @@ export class IncomingPayment
this.receivedAmountValue = amount.value
}

public get url(): string {
return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}`
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
Copy link
Contributor Author

@mkurapov mkurapov Jan 28, 2023

Choose a reason for hiding this comment

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

Adding fetchedPaymentPointer in-case you already have the paymentPointer and don't want to grab it again, like in toOpenPaymentsType

Copy link
Contributor

Choose a reason for hiding this comment

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

What if callers just assigned incomingPayment.paymentPointer = fetchedPaymentPointer, so that getUrl+getPaymentPointer only dealt with whether this.paymentPointer is defined or not?

Copy link
Contributor

Choose a reason for hiding this comment

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

Taking a step back, if/when we change subresource urls to not include the payment pointer, we'll no longer need an async getUrl.

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 hesitant to do that, since
a) we either have to change the type definitions of getUrl/toOpenPaymentsType to possibly return undefined, or throw inside the method if paymentPointer isn't defined
b) it's not entirely clear that the caller has to do that in order to make these methods work.

Maybe we can

  1. have toOpenPaymentsType take in a paymentPointer (this would also make it sync), it would require passing in ctx.paymentPointer in most places but I think that's ok
  2. A bit more complicated, but we make a separate <subresource>WithPaymentPointer class that constructs a new model with the paymentPointer defined, although I'm not sure the additional boilerplate is worth it

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify, I'm only suggesting changing getUrl to

Suggested change
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
public async getUrl(): Promise<string> {

and callers can optionally define incomingPayment.paymentPointer to avoid $relatedQuery in getPaymentPointer.

return {
id: await this.getUrl(paymentPointer),
paymentPointer: paymentPointer.url,
quoteId: (await this.quote?.getUrl()) ?? 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.

this is optional in the Open Payments API -> should we keep it?

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 I wonder if we should make it required in the spec

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you can only create an Outgoing Payment with a required quoteId, it would make sense that this would be required here

@@ -66,10 +68,7 @@ export const listSubresource = async <M extends PaymentPointerSubresource>({
)
const result = {
pagination: pageInfo,
result: page.map((item: M) => {
item.paymentPointer = ctx.paymentPointer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are fetching paymentPointer on the fly (whenever it's missing), this line isn't necessary

@mkurapov mkurapov marked this pull request as ready for review January 30, 2023 07:19
id: string
paymentPointer: string
incomingAmount?: AmountJSON
receivedAmount: AmountJSON
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wilsonianb Keeping AmountJSON over OpenPaymentsAmount makes sense right? The Amount interface is a more generic thing we use across the code, not necessarily being an "open payments" concept

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 good with keeping amount values as strings in open-payments and leaving it to callers to (de)serialize.

@mkurapov
Copy link
Contributor Author

Going to update the auth package with a few of it's types in a follow-up

):
| OpenPaymentsIncomingPayment
| OpenPaymentsIncomingPaymentWithConnection
| OpenPaymentsIncomingPaymentWithConnectionUrl {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to suggest adding an abstract toOpenPaymentsType to PaymentPointerSubresource but this one for incoming payments would complicate it

export abstract class PaymentPointerSubresource extends BaseModel {

toBody: async (payment) =>
await incomingPaymentToBody(
payment,
deps.connectionService.getUrl(payment)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we either make this getUrl async for consistency or rename 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.

I think it should be ok as it is, since it's a service method, not a model?

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 also ok keeping it as is.
It just stood out against the new async getUrls

return {
id: await this.getUrl(paymentPointer),
paymentPointer: paymentPointer.url,
quoteId: (await this.quote?.getUrl()) ?? undefined,
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 I wonder if we should make it required in the spec

@@ -80,7 +80,7 @@ async function createQuote(
}

ctx.status = 201
ctx.body = quoteToBody(quoteOrErr)
ctx.body = await quoteToBody(quoteOrErr)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
ctx.body = await quoteToBody(quoteOrErr)
ctx.body = await quoteOrErr.toOpenPaymentsType()

Remove quoteToBody

@@ -131,8 +130,15 @@ export class IncomingPayment
this.receivedAmountValue = amount.value
}

public get url(): string {
return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}`
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if callers just assigned incomingPayment.paymentPointer = fetchedPaymentPointer, so that getUrl+getPaymentPointer only dealt with whether this.paymentPointer is defined or not?

* chore(backend): make toOpenPaymentType sync

* chore(backend): make quote.toOpenPaymentsType sync

* chore(backend): convert tests to sync

* chore(backend): update tests
@mkurapov mkurapov requested a review from wilsonianb February 2, 2023 13:20
Copy link
Contributor

@wilsonianb wilsonianb left a comment

Choose a reason for hiding this comment

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

Oops, here's some several day old comments that I didn't realize were pending in a review

id: string
paymentPointer: string
incomingAmount?: AmountJSON
receivedAmount: AmountJSON
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 good with keeping amount values as strings in open-payments and leaving it to callers to (de)serialize.

@@ -131,8 +130,15 @@ export class IncomingPayment
this.receivedAmountValue = amount.value
}

public get url(): string {
return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}`
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Taking a step back, if/when we change subresource urls to not include the payment pointer, we'll no longer need an async getUrl.

toBody: async (payment) =>
await incomingPaymentToBody(
payment,
deps.connectionService.getUrl(payment)
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 also ok keeping it as is.
It just stood out against the new async getUrls

@@ -131,8 +130,15 @@ export class IncomingPayment
this.receivedAmountValue = amount.value
}

public get url(): string {
return `${this.paymentPointer.url}${IncomingPayment.urlPath}/${this.id}`
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify, I'm only suggesting changing getUrl to

Suggested change
public async getUrl(fetchedPaymentPointer?: PaymentPointer): Promise<string> {
public async getUrl(): Promise<string> {

and callers can optionally define incomingPayment.paymentPointer to avoid $relatedQuery in getPaymentPointer.

| OpenPaymentsIncomingPaymentWithConnection
| OpenPaymentsIncomingPaymentWithConnectionUrl

public toOpenPaymentsType(
Copy link
Contributor

Choose a reason for hiding this comment

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

What if we added some model.test.tss at least to test toOpenPaymentsType?
Or are existing response body checks in routes tests good enough?

assetScale: incomingPayment.receivedAmount.assetScale
}
}),
getBody: (incomingPayment, list) => {
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 instead be

Suggested change
getBody: (incomingPayment, list) => {
getBody: (incomingPayment, list) => incomingPayment.toOpenPaymentsType(paymentPointer)

with some possible special assignment for ilpStreamConnection?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In terms of the connection, we would be able to do something like:

getBody: (incomingPayment, list) => ({
        ...incomingPayment.toOpenPaymentsType(paymentPointer),
        ilpStreamConnection: list
          ? `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`
          : {
              id: `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`,
              ilpAddress: expect.stringMatching(
                /^test\.rafiki\.[a-zA-Z0-9_-]{95}$/
              ),
              sharedSecret: expect.stringMatching(/^[a-zA-Z0-9-_]{43}$/),
              assetCode: incomingPayment.receivedAmount.assetCode,
              assetScale: incomingPayment.receivedAmount.assetScale
            }
      }),

Since incomingPayment.toOpenPaymentsType expect a Connection, we wouldn't be able to use expect.stringMatching(...), but would have to create a Connection instance with proper credentials etc

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.e. I'm not sure if that's a much better solution

})

describe('toOpenPaymentsType', () => {
test('returns incoming payment without connection provided', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if there was a single test.each to make clear that output is the same other than ilpStreamConnection?

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 was thinking this, but having a test case where the connection var in the test.each case was either

const connection = `${config.openPaymentsUrl}/connections/${incomingPayment.connectionId}` 

or

const connection = Connection.fromPayment({
        payment: incomingPayment,
        openPaymentsUrl: config.openPaymentsUrl,
        credentials: {
          ilpAddress: 'test.ilp' as IlpAddress,
          sharedSecret: Buffer.from('')
        }
      })

didn't feel nice

packages/open-payments/src/test/helpers.ts Outdated Show resolved Hide resolved
@mkurapov mkurapov merged commit 8a2dff6 into main Feb 2, 2023
@mkurapov mkurapov deleted the 662/mk/use-open-payments-types branch February 2, 2023 21:46
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. type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use generated Open Payments OpenAPI types
2 participants