-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add account/egress/get capability
#77
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| type GetCaveats struct { | ||
| spaces optional [DID] | ||
| period optional Period | ||
| } | ||
|
|
||
| type Period struct { | ||
| from ISO8601Date | ||
| to ISO8601Date | ||
| } | ||
|
|
||
| type GetOk struct { | ||
| total Int | ||
| spaces {DID:SpaceEgress} | ||
| } | ||
|
|
||
| type SpaceEgress struct { | ||
| total Int | ||
| dailyStats [DailyStat] | ||
| } | ||
|
|
||
| type DailyStat struct { | ||
| date ISO8601Date | ||
| egress Int | ||
| } | ||
|
|
||
| type GetError struct { | ||
| errorName String (rename "name") | ||
| message String | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| package egress | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "slices" | ||
| "time" | ||
|
|
||
| "github.com/ipld/go-ipld-prime/datamodel" | ||
| "github.com/storacha/go-ucanto/core/ipld" | ||
| "github.com/storacha/go-ucanto/core/receipt" | ||
| "github.com/storacha/go-ucanto/core/result/failure" | ||
| "github.com/storacha/go-ucanto/core/schema" | ||
| "github.com/storacha/go-ucanto/did" | ||
| "github.com/storacha/go-ucanto/ucan" | ||
| "github.com/storacha/go-ucanto/validator" | ||
|
|
||
| "github.com/storacha/go-libstoracha/capabilities/types" | ||
| ) | ||
|
|
||
| const GetAbility = "account/egress/get" | ||
|
|
||
| type Period struct { | ||
| From time.Time | ||
| To time.Time | ||
| } | ||
|
|
||
| type GetCaveats struct { | ||
| Spaces []did.DID | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume you can specify no spaces here to get egress for them all? Maybe add a comment.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes yes |
||
| Period *Period | ||
| } | ||
|
|
||
| func (gc GetCaveats) ToIPLD() (datamodel.Node, error) { | ||
| return ipld.WrapWithRecovery(&gc, GetCaveatsType(), types.Converters...) | ||
| } | ||
|
|
||
| var GetCaveatsReader = schema.Struct[GetCaveats](GetCaveatsType(), nil, types.Converters...) | ||
|
|
||
| type DailyStats struct { | ||
| Date time.Time | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again, I'd prefer keeping it as is for now and see if the extra flexibility is actually needed in the future |
||
| Egress uint64 | ||
| } | ||
|
|
||
| type SpaceEgress struct { | ||
| Total uint64 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could use a comment - is this just the sum of all the egress values in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
| DailyStats []DailyStats | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these ordered by date ascending? Would be good to know.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the date is the sort key in the table where the etracker stores these stats, so yes
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might it be worth calling this something more generic to allow breaking down in smaller increments in the future. Like right now you want daily, but it might be desirable to break down into 10 minute intervals or something for a "zoomed in" view.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's an interesting idea. These could be just I think it's preferable to keep it simple for now and we can improve it if/when the need arises. I'm not sure right now whether resolutions smaller than a day would be useful for customers. |
||
| } | ||
|
|
||
| type SpacesModel struct { | ||
| Keys []did.DID | ||
| Values map[did.DID]SpaceEgress | ||
| } | ||
|
|
||
| type GetOk struct { | ||
| Total uint64 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The total egress across all spaces for the period?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exactly |
||
| Spaces SpacesModel | ||
| } | ||
|
|
||
| func (gok GetOk) ToIPLD() (datamodel.Node, error) { | ||
| return ipld.WrapWithRecovery(&gok, GetOkType(), types.Converters...) | ||
| } | ||
|
|
||
| type GetError struct { | ||
| ErrorName string | ||
| Message string | ||
| } | ||
|
|
||
| const AccountNotFoundErrorName = "AccountNotFoundError" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need an error for if you ask for egress for space(s) you do not have access to? Also a range error for when you ask for a period that is bigger than what we want to allow?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
if there are no stats for the space, you'll get the space in the map with 0 egress. If you don't have access to the space, you won't have the space in the map at all. That would be enough for the client to know what happened. But I like being explicit. What should happen if you request several spaces and have access to some of them but not all? Should we fail the whole request? I think ignoring non-authorized spaces could be a better approach. At the same time, if none of the requested spaces is accessible, returning an ok result with an empty spaces map sounds a bit weird to me. I think ignoring non-authorized spaces works here because the resource of the capability is the account, so we will expose spaces that were created by that account, and not those the account has access to. WDYT?
👍🏻
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. about the spaces thing, I'll keep it consistent with the behavior of |
||
|
|
||
| func NewAccountNotFoundError(msg string) GetError { | ||
| return GetError{ | ||
| ErrorName: AccountNotFoundErrorName, | ||
| Message: msg, | ||
| } | ||
| } | ||
|
|
||
| func (ge GetError) Name() string { | ||
| return ge.ErrorName | ||
| } | ||
|
|
||
| func (ge GetError) Error() string { | ||
| return ge.Message | ||
| } | ||
|
|
||
| func (ge GetError) ToIPLD() (datamodel.Node, error) { | ||
| return ipld.WrapWithRecovery(&ge, GetErrorType(), types.Converters...) | ||
| } | ||
|
|
||
| var GetErrorReader = schema.Mapped( | ||
| schema.Struct[GetError](GetErrorType(), nil, types.Converters...), | ||
| func(ge GetError) (GetError, failure.Failure) { | ||
| if ge.Name() != AccountNotFoundErrorName { | ||
| return GetError{}, failure.FromError(fmt.Errorf("incorrect name: %s, expected: %s", ge.Name(), AccountNotFoundErrorName)) | ||
| } | ||
| return ge, nil | ||
| }, | ||
| ) | ||
|
|
||
| type GetReceipt receipt.Receipt[GetOk, GetError] | ||
| type GetReceiptReader receipt.ReceiptReader[GetOk, GetError] | ||
|
|
||
| func NewGetReceiptReader() (GetReceiptReader, error) { | ||
| return receipt.NewReceiptReaderFromTypes[GetOk, GetError](GetOkType(), GetErrorType(), types.Converters...) | ||
| } | ||
|
|
||
| var GetOkReader = schema.Struct[GetOk](GetOkType(), nil, types.Converters...) | ||
|
|
||
| var Get = validator.NewCapability( | ||
| GetAbility, | ||
| schema.DIDString(), | ||
| GetCaveatsReader, | ||
| getDerives, | ||
| ) | ||
|
|
||
| func getDerives(claimed, delegated ucan.Capability[GetCaveats]) failure.Failure { | ||
| if claimed.With() != delegated.With() { | ||
| return failure.FromError(fmt.Errorf("can not derive %s with %s from %s", claimed.Can(), claimed.With(), delegated.With())) | ||
| } | ||
|
|
||
| if delegated.Nb().Spaces != nil { | ||
| if claimed.Nb().Spaces == nil { | ||
| return failure.FromError(fmt.Errorf("constraint violation: violates imposed spaces constraint %v because it asks for all spaces", delegated.Nb().Spaces)) | ||
| } | ||
|
|
||
| for _, s := range claimed.Nb().Spaces { | ||
| if !slices.Contains(delegated.Nb().Spaces, s) { | ||
| return failure.FromError(fmt.Errorf("constraint violation: violates imposed spaces constraint %v because it asks for space %s", delegated.Nb().Spaces, s)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if delegated.Nb().Period != nil { | ||
| if claimed.Nb().Period == nil { | ||
| return failure.FromError(fmt.Errorf("constraint violation: violates imposed period constraint %v because it doesn't have a period constraint", delegated.Nb().Period)) | ||
| } | ||
| if claimed.Nb().Period.From.Before(delegated.Nb().Period.From) { | ||
| return failure.FromError(fmt.Errorf("constraint violation: violates imposed period constraint because it requests dates before %s", delegated.Nb().Period.From)) | ||
| } | ||
| if claimed.Nb().Period.To.After(delegated.Nb().Period.To) { | ||
| return failure.FromError(fmt.Errorf("constraint violation: violates imposed period constraint because it requests dates after %s", delegated.Nb().Period.To)) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package egress_test | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/storacha/go-libstoracha/capabilities/account/egress" | ||
| "github.com/storacha/go-libstoracha/testutil" | ||
| "github.com/storacha/go-ucanto/did" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestRoundTripGetCaveats(t *testing.T) { | ||
| t.Run("marshals and unmarshals correctly", func(t *testing.T) { | ||
| space1 := testutil.RandomDID(t) | ||
| space2 := testutil.RandomDID(t) | ||
|
|
||
| nb := egress.GetCaveats{ | ||
| Spaces: []did.DID{space1, space2}, | ||
| Period: &egress.Period{ | ||
| From: time.UnixMilli(123000).UTC(), | ||
| To: time.UnixMilli(456000).UTC(), | ||
| }, | ||
| } | ||
|
|
||
| node, err := nb.ToIPLD() | ||
| require.NoError(t, err) | ||
|
|
||
| rnb, err := egress.GetCaveatsReader.Read(node) | ||
| require.NoError(t, err) | ||
| require.Equal(t, nb.Spaces[0].String(), rnb.Spaces[0].String()) | ||
| require.Equal(t, nb.Spaces[1].String(), rnb.Spaces[1].String()) | ||
| require.True(t, nb.Period.From.Equal(rnb.Period.From)) | ||
| require.True(t, nb.Period.To.Equal(rnb.Period.To)) | ||
| }) | ||
|
|
||
| t.Run("properties are optional", func(t *testing.T) { | ||
| nb := egress.GetCaveats{} | ||
|
|
||
| node, err := nb.ToIPLD() | ||
| require.NoError(t, err) | ||
|
|
||
| rnb, err := egress.GetCaveatsReader.Read(node) | ||
| require.NoError(t, err) | ||
| require.Nil(t, rnb.Spaces) | ||
| require.Nil(t, rnb.Period) | ||
| }) | ||
| } | ||
|
|
||
| func TestNewGetReceiptReader(t *testing.T) { | ||
| _, err := egress.NewGetReceiptReader() | ||
| require.NoError(t, err) | ||
| } | ||
|
|
||
| func TestRoundTripGetOk(t *testing.T) { | ||
| space1 := testutil.RandomDID(t) | ||
|
|
||
| ok := egress.GetOk{ | ||
| Total: 1000, | ||
| Spaces: egress.SpacesModel{ | ||
| Keys: []did.DID{space1}, | ||
| Values: map[did.DID]egress.SpaceEgress{ | ||
| space1: { | ||
| Total: 500, | ||
| DailyStats: []egress.DailyStats{ | ||
| { | ||
| Date: time.Now().Truncate(time.Second).UTC(), | ||
| Egress: 250, | ||
| }, | ||
| { | ||
| Date: time.Now().Add(-24 * time.Hour).Truncate(time.Second).UTC(), | ||
| Egress: 250, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| node, err := ok.ToIPLD() | ||
| require.NoError(t, err) | ||
|
|
||
| rok, err := egress.GetOkReader.Read(node) | ||
| require.NoError(t, err) | ||
| require.Equal(t, ok.Total, rok.Total) | ||
| require.Equal(t, len(ok.Spaces.Keys), len(rok.Spaces.Keys)) | ||
| require.Equal(t, ok.Spaces.Values[space1].Total, rok.Spaces.Values[space1].Total) | ||
| require.Equal(t, len(ok.Spaces.Values[space1].DailyStats), len(rok.Spaces.Values[space1].DailyStats)) | ||
| require.True(t, ok.Spaces.Values[space1].DailyStats[0].Date.Equal(rok.Spaces.Values[space1].DailyStats[0].Date)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package egress | ||
|
|
||
| import ( | ||
| _ "embed" | ||
| "fmt" | ||
|
|
||
| "github.com/ipld/go-ipld-prime/schema" | ||
| captypes "github.com/storacha/go-libstoracha/capabilities/types" | ||
| ) | ||
|
|
||
| //go:embed egress.ipldsch | ||
| var egressSchema []byte | ||
|
|
||
| var egressTS = mustLoadTS() | ||
|
|
||
| func mustLoadTS() *schema.TypeSystem { | ||
| ts, err := captypes.LoadSchemaBytes(egressSchema) | ||
| if err != nil { | ||
| panic(fmt.Errorf("loading egress schema: %w", err)) | ||
| } | ||
| return ts | ||
| } | ||
|
|
||
| func GetCaveatsType() schema.Type { | ||
| return egressTS.TypeByName("GetCaveats") | ||
| } | ||
|
|
||
| func GetOkType() schema.Type { | ||
| return egressTS.TypeByName("GetOk") | ||
| } | ||
|
|
||
| func GetErrorType() schema.Type { | ||
| return egressTS.TypeByName("GetError") | ||
| } |
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.
Could use a comment. What kind of resolution are we allowed here, if I specify "to" as midday, will I only get egress registered before midday on that day?
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.
From inclusive and to exclusive right?
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.
you'd get egress for the whole day. The focus is on daily stats, so only the date part of the
time.Timeis used.the etracker will do a
BETWEENquery to the dynamoDB table, so yes, both inclusive.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.
sorry, apparently I cannot read. My initial idea was for both of them to be inclusive, but I see
fromis inclusive andtois exclusive inaccount/usage/get, so I guess it's better to keep it consistent here.