diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7202afa..37222e7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: gotest on: pull_request: - branches: [ '**' ] + branches: ["**"] jobs: tests: @@ -12,7 +12,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.20.5 - name: Checkout code uses: actions/checkout@v3 diff --git a/cmd/identity-api/main.go b/cmd/identity-api/main.go index c9691c30..e53624c3 100644 --- a/cmd/identity-api/main.go +++ b/cmd/identity-api/main.go @@ -12,6 +12,7 @@ import ( "github.com/DIMO-Network/identity-api/graph" "github.com/DIMO-Network/identity-api/internal/config" + "github.com/DIMO-Network/identity-api/internal/loader" "github.com/DIMO-Network/identity-api/internal/repositories" "github.com/DIMO-Network/identity-api/internal/services" "github.com/DIMO-Network/shared" @@ -48,16 +49,21 @@ func main() { repo := repositories.NewVehiclesRepo(dbs) - srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ + s := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ Repo: repo, }})) + srv := loader.Middleware(dbs, s) + http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) logger.Info().Msgf("Server started on port: %d", settings.Port) - http.ListenAndServe(fmt.Sprintf(":%d", settings.Port), nil) + if err := http.ListenAndServe(fmt.Sprintf(":%d", settings.Port), nil); err != nil { + logger.Fatal().Msgf("failed to start identity api: %v\n", err) + } + } func startContractEventsConsumer(ctx context.Context, logger *zerolog.Logger, settings *config.Settings, dbs db.Store) { diff --git a/go.mod b/go.mod index 9d6374bc..25038536 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/friendsofgo/errors v0.9.2 github.com/gofiber/fiber/v2 v2.47.0 github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/graph-gophers/dataloader/v7 v7.1.0 github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.11.2 github.com/rs/zerolog v1.29.1 @@ -22,6 +23,12 @@ require ( github.com/volatiletech/strmangle v0.0.4 ) +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + golang.org/x/time v0.3.0 // indirect +) + require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect @@ -85,6 +92,7 @@ require ( github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/cast v1.5.0 // indirect + github.com/tidwall/gjson v1.14.4 github.com/tinylib/msgp v1.1.8 // indirect github.com/urfave/cli/v2 v2.25.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -99,7 +107,6 @@ require ( golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.9.0 // indirect - golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index cb681bdf..efcbabc2 100644 --- a/go.sum +++ b/go.sum @@ -318,6 +318,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= +github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -581,6 +583,12 @@ github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/testcontainers/testcontainers-go v0.21.0 h1:syePAxdeTzfkap+RrJaQZpJQ/s/fsUgn11xIvHrOE9U= github.com/testcontainers/testcontainers-go v0.21.0/go.mod h1:c1ez3WVRHq7T/Aj+X3TIipFBwkBaNT5iNCY8+1b83Ng= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= diff --git a/gqlgen.yml b/gqlgen.yml index 2de1dfa9..9af5fa3a 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -87,3 +87,11 @@ models: - github.com/99designs/gqlgen/graphql.Int32 Address: model: github.com/DIMO-Network/identity-api/graph/types.Address + AftermarketDevice: + fields: + vehicle: + resolver: true + Vehicle: + fields: + aftermarketDevice: + resolver: true diff --git a/graph/generated.go b/graph/generated.go index 0d10ddd9..dcb56711 100644 --- a/graph/generated.go +++ b/graph/generated.go @@ -40,7 +40,9 @@ type Config struct { } type ResolverRoot interface { + AftermarketDevice() AftermarketDeviceResolver Query() QueryResolver + Vehicle() VehicleResolver } type DirectiveRoot struct { @@ -48,12 +50,14 @@ type DirectiveRoot struct { type ComplexityRoot struct { AftermarketDevice struct { - Address func(childComplexity int) int - ID func(childComplexity int) int - Imei func(childComplexity int) int - MintedAt func(childComplexity int) int - Owner func(childComplexity int) int - Serial func(childComplexity int) int + Address func(childComplexity int) int + Beneficiary func(childComplexity int) int + ID func(childComplexity int) int + Imei func(childComplexity int) int + MintedAt func(childComplexity int) int + Owner func(childComplexity int) int + Serial func(childComplexity int) int + Vehicle func(childComplexity int) int } AftermarketDeviceConnection struct { @@ -78,12 +82,13 @@ type ComplexityRoot struct { } Vehicle struct { - ID func(childComplexity int) int - Make func(childComplexity int) int - MintedAt func(childComplexity int) int - Model func(childComplexity int) int - Owner func(childComplexity int) int - Year func(childComplexity int) int + AftermarketDevice func(childComplexity int) int + ID func(childComplexity int) int + Make func(childComplexity int) int + MintedAt func(childComplexity int) int + Model func(childComplexity int) int + Owner func(childComplexity int) int + Year func(childComplexity int) int } VehicleConnection struct { @@ -98,10 +103,16 @@ type ComplexityRoot struct { } } +type AftermarketDeviceResolver interface { + Vehicle(ctx context.Context, obj *model.AftermarketDevice) (*model.Vehicle, error) +} type QueryResolver interface { OwnedVehicles(ctx context.Context, address common.Address, first *int, after *string) (*model.VehicleConnection, error) OwnedAftermarketDevices(ctx context.Context, address common.Address, first *int, after *string) (*model.AftermarketDeviceConnection, error) } +type VehicleResolver interface { + AftermarketDevice(ctx context.Context, obj *model.Vehicle) (*model.AftermarketDevice, error) +} type executableSchema struct { resolvers ResolverRoot @@ -125,6 +136,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AftermarketDevice.Address(childComplexity), true + case "AftermarketDevice.beneficiary": + if e.complexity.AftermarketDevice.Beneficiary == nil { + break + } + + return e.complexity.AftermarketDevice.Beneficiary(childComplexity), true + case "AftermarketDevice.id": if e.complexity.AftermarketDevice.ID == nil { break @@ -160,6 +178,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.AftermarketDevice.Serial(childComplexity), true + case "AftermarketDevice.vehicle": + if e.complexity.AftermarketDevice.Vehicle == nil { + break + } + + return e.complexity.AftermarketDevice.Vehicle(childComplexity), true + case "AftermarketDeviceConnection.edges": if e.complexity.AftermarketDeviceConnection.Edges == nil { break @@ -233,6 +258,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.OwnedVehicles(childComplexity, args["address"].(common.Address), args["first"].(*int), args["after"].(*string)), true + case "Vehicle.aftermarketDevice": + if e.complexity.Vehicle.AftermarketDevice == nil { + break + } + + return e.complexity.Vehicle.AftermarketDevice(childComplexity), true + case "Vehicle.id": if e.complexity.Vehicle.ID == nil { break @@ -792,6 +824,104 @@ func (ec *executionContext) fieldContext_AftermarketDevice_mintedAt(ctx context. return fc, nil } +func (ec *executionContext) _AftermarketDevice_vehicle(ctx context.Context, field graphql.CollectedField, obj *model.AftermarketDevice) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AftermarketDevice_vehicle(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.AftermarketDevice().Vehicle(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Vehicle) + fc.Result = res + return ec.marshalOVehicle2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋidentityᚑapiᚋgraphᚋmodelᚐVehicle(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AftermarketDevice_vehicle(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AftermarketDevice", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Vehicle_id(ctx, field) + case "owner": + return ec.fieldContext_Vehicle_owner(ctx, field) + case "make": + return ec.fieldContext_Vehicle_make(ctx, field) + case "model": + return ec.fieldContext_Vehicle_model(ctx, field) + case "year": + return ec.fieldContext_Vehicle_year(ctx, field) + case "mintedAt": + return ec.fieldContext_Vehicle_mintedAt(ctx, field) + case "aftermarketDevice": + return ec.fieldContext_Vehicle_aftermarketDevice(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Vehicle", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _AftermarketDevice_beneficiary(ctx context.Context, field graphql.CollectedField, obj *model.AftermarketDevice) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_AftermarketDevice_beneficiary(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Beneficiary, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*common.Address) + fc.Result = res + return ec.marshalOAddress2ᚖgithubᚗcomᚋethereumᚋgoᚑethereumᚋcommonᚐAddress(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_AftermarketDevice_beneficiary(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AftermarketDevice", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Address does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _AftermarketDeviceConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.AftermarketDeviceConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_AftermarketDeviceConnection_totalCount(ctx, field) if err != nil { @@ -1031,6 +1161,10 @@ func (ec *executionContext) fieldContext_AftermarketDeviceEdge_node(ctx context. return ec.fieldContext_AftermarketDevice_imei(ctx, field) case "mintedAt": return ec.fieldContext_AftermarketDevice_mintedAt(ctx, field) + case "vehicle": + return ec.fieldContext_AftermarketDevice_vehicle(ctx, field) + case "beneficiary": + return ec.fieldContext_AftermarketDevice_beneficiary(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type AftermarketDevice", field.Name) }, @@ -1633,6 +1767,65 @@ func (ec *executionContext) fieldContext_Vehicle_mintedAt(ctx context.Context, f return fc, nil } +func (ec *executionContext) _Vehicle_aftermarketDevice(ctx context.Context, field graphql.CollectedField, obj *model.Vehicle) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Vehicle_aftermarketDevice(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Vehicle().AftermarketDevice(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.AftermarketDevice) + fc.Result = res + return ec.marshalOAftermarketDevice2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋidentityᚑapiᚋgraphᚋmodelᚐAftermarketDevice(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Vehicle_aftermarketDevice(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Vehicle", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_AftermarketDevice_id(ctx, field) + case "address": + return ec.fieldContext_AftermarketDevice_address(ctx, field) + case "owner": + return ec.fieldContext_AftermarketDevice_owner(ctx, field) + case "serial": + return ec.fieldContext_AftermarketDevice_serial(ctx, field) + case "imei": + return ec.fieldContext_AftermarketDevice_imei(ctx, field) + case "mintedAt": + return ec.fieldContext_AftermarketDevice_mintedAt(ctx, field) + case "vehicle": + return ec.fieldContext_AftermarketDevice_vehicle(ctx, field) + case "beneficiary": + return ec.fieldContext_AftermarketDevice_beneficiary(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AftermarketDevice", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _VehicleConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *model.VehicleConnection) (ret graphql.Marshaler) { fc, err := ec.fieldContext_VehicleConnection_totalCount(ctx, field) if err != nil { @@ -1828,6 +2021,8 @@ func (ec *executionContext) fieldContext_VehicleEdge_node(ctx context.Context, f return ec.fieldContext_Vehicle_year(ctx, field) case "mintedAt": return ec.fieldContext_Vehicle_mintedAt(ctx, field) + case "aftermarketDevice": + return ec.fieldContext_Vehicle_aftermarketDevice(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Vehicle", field.Name) }, @@ -3674,14 +3869,14 @@ func (ec *executionContext) _AftermarketDevice(ctx context.Context, sel ast.Sele case "id": out.Values[i] = ec._AftermarketDevice_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "address": out.Values[i] = ec._AftermarketDevice_address(ctx, field, obj) case "owner": out.Values[i] = ec._AftermarketDevice_owner(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "serial": out.Values[i] = ec._AftermarketDevice_serial(ctx, field, obj) @@ -3690,8 +3885,43 @@ func (ec *executionContext) _AftermarketDevice(ctx context.Context, sel ast.Sele case "mintedAt": out.Values[i] = ec._AftermarketDevice_mintedAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "vehicle": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._AftermarketDevice_vehicle(ctx, field, obj) + return res } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "beneficiary": + out.Values[i] = ec._AftermarketDevice_beneficiary(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -3957,12 +4187,12 @@ func (ec *executionContext) _Vehicle(ctx context.Context, sel ast.SelectionSet, case "id": out.Values[i] = ec._Vehicle_id(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "owner": out.Values[i] = ec._Vehicle_owner(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "make": out.Values[i] = ec._Vehicle_make(ctx, field, obj) @@ -3973,8 +4203,41 @@ func (ec *executionContext) _Vehicle(ctx context.Context, sel ast.SelectionSet, case "mintedAt": out.Values[i] = ec._Vehicle_mintedAt(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) + } + case "aftermarketDevice": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Vehicle_aftermarketDevice(ctx, field, obj) + return res } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4942,6 +5205,13 @@ func (ec *executionContext) marshalOAddress2ᚖgithubᚗcomᚋethereumᚋgoᚑet return res } +func (ec *executionContext) marshalOAftermarketDevice2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋidentityᚑapiᚋgraphᚋmodelᚐAftermarketDevice(ctx context.Context, sel ast.SelectionSet, v *model.AftermarketDevice) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AftermarketDevice(ctx, sel, v) +} + func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) { res, err := graphql.UnmarshalBoolean(v) return res, graphql.ErrorOnPath(ctx, err) @@ -5000,6 +5270,13 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } +func (ec *executionContext) marshalOVehicle2ᚖgithubᚗcomᚋDIMOᚑNetworkᚋidentityᚑapiᚋgraphᚋmodelᚐVehicle(ctx context.Context, sel ast.SelectionSet, v *model.Vehicle) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Vehicle(ctx, sel, v) +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go index 6bd9cbb3..8a8e11b5 100644 --- a/graph/model/models_gen.go +++ b/graph/model/models_gen.go @@ -9,12 +9,14 @@ import ( ) type AftermarketDevice struct { - ID string `json:"id"` - Address *common.Address `json:"address,omitempty"` - Owner common.Address `json:"owner"` - Serial *string `json:"serial,omitempty"` - Imei *string `json:"imei,omitempty"` - MintedAt time.Time `json:"mintedAt"` + ID string `json:"id"` + Address *common.Address `json:"address,omitempty"` + Owner common.Address `json:"owner"` + Serial *string `json:"serial,omitempty"` + Imei *string `json:"imei,omitempty"` + MintedAt time.Time `json:"mintedAt"` + Vehicle *Vehicle `json:"vehicle,omitempty"` + Beneficiary *common.Address `json:"beneficiary,omitempty"` } type AftermarketDeviceConnection struct { @@ -34,12 +36,13 @@ type PageInfo struct { } type Vehicle struct { - ID string `json:"id"` - Owner common.Address `json:"owner"` - Make *string `json:"make,omitempty"` - Model *string `json:"model,omitempty"` - Year *int `json:"year,omitempty"` - MintedAt time.Time `json:"mintedAt"` + ID string `json:"id"` + Owner common.Address `json:"owner"` + Make *string `json:"make,omitempty"` + Model *string `json:"model,omitempty"` + Year *int `json:"year,omitempty"` + MintedAt time.Time `json:"mintedAt"` + AftermarketDevice *AftermarketDevice `json:"aftermarketDevice,omitempty"` } type VehicleConnection struct { diff --git a/graph/schema.graphqls b/graph/schema.graphqls index 4b0e2f57..f8eeeab9 100644 --- a/graph/schema.graphqls +++ b/graph/schema.graphqls @@ -18,6 +18,7 @@ type Vehicle { model: String year: Int mintedAt: Time! + aftermarketDevice: AftermarketDevice } type VehicleEdge { @@ -32,6 +33,8 @@ type AftermarketDevice { serial: String imei: String mintedAt: Time! + vehicle: Vehicle + beneficiary: Address } type AftermarketDeviceConnection { diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index ab3c2b00..5c673711 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -8,9 +8,15 @@ import ( "context" "github.com/DIMO-Network/identity-api/graph/model" + "github.com/DIMO-Network/identity-api/internal/loader" "github.com/ethereum/go-ethereum/common" ) +// Vehicle is the resolver for the vehicle field. +func (r *aftermarketDeviceResolver) Vehicle(ctx context.Context, obj *model.AftermarketDevice) (*model.Vehicle, error) { + return loader.GetLinkedVehicleByID(ctx, obj.ID) +} + // OwnedVehicles is the resolver for the ownedVehicles field. func (r *queryResolver) OwnedVehicles(ctx context.Context, address common.Address, first *int, after *string) (*model.VehicleConnection, error) { return r.Repo.GetOwnedVehicles(ctx, address, first, after) @@ -21,7 +27,22 @@ func (r *queryResolver) OwnedAftermarketDevices(ctx context.Context, address com return r.Repo.GetOwnedAftermarketDevices(ctx, address, first, after) } +// AftermarketDevice is the resolver for the aftermarketDevice field. +func (r *vehicleResolver) AftermarketDevice(ctx context.Context, obj *model.Vehicle) (*model.AftermarketDevice, error) { + return loader.GetAftermarketDeviceByVehicleID(ctx, obj.ID) +} + +// AftermarketDevice returns AftermarketDeviceResolver implementation. +func (r *Resolver) AftermarketDevice() AftermarketDeviceResolver { + return &aftermarketDeviceResolver{r} +} + // Query returns QueryResolver implementation. func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } +// Vehicle returns VehicleResolver implementation. +func (r *Resolver) Vehicle() VehicleResolver { return &vehicleResolver{r} } + +type aftermarketDeviceResolver struct{ *Resolver } type queryResolver struct{ *Resolver } +type vehicleResolver struct{ *Resolver } diff --git a/internal/loader/aftermarket_device_loader.go b/internal/loader/aftermarket_device_loader.go new file mode 100644 index 00000000..1aa809b6 --- /dev/null +++ b/internal/loader/aftermarket_device_loader.go @@ -0,0 +1,72 @@ +package loader + +import ( + "context" + "strconv" + + "github.com/DIMO-Network/identity-api/graph/model" + "github.com/DIMO-Network/identity-api/internal/repositories" + "github.com/DIMO-Network/identity-api/models" + "github.com/DIMO-Network/shared/db" + "github.com/graph-gophers/dataloader/v7" +) + +type AftermarketDeviceLoader struct { + db db.Store +} + +func GetAftermarketDeviceByVehicleID(ctx context.Context, vehicleID string) (*model.AftermarketDevice, error) { + // read loader from context + loaders := ctx.Value(dataLoadersKey).(*Loaders) + // invoke and get thunk + thunk := loaders.AftermarketDeviceByVehicleID.Load(ctx, vehicleID) + // read value from thunk + result, err := thunk() + if err != nil { + return nil, err + } + return result, nil +} + +// BatchGetLinkedAftermarketDeviceByVehicleID implements the dataloader for finding aftermarket devices linked to vehicles and returns +// them in the order requested +func (ad *AftermarketDeviceLoader) BatchGetLinkedAftermarketDeviceByVehicleID(ctx context.Context, vehicleIDs []string) []*dataloader.Result[*model.AftermarketDevice] { + keyOrder := make(map[int]int) + results := make([]*dataloader.Result[*model.AftermarketDevice], len(vehicleIDs)) + var vIDs []int + + for ix, key := range vehicleIDs { + k, err := strconv.Atoi(key) + if err != nil { + results[ix] = &dataloader.Result[*model.AftermarketDevice]{Data: nil, Error: err} + } + keyOrder[k] = ix + vIDs = append(vIDs, k) + } + + devices, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.VehicleID.IN(vIDs), + ).All(ctx, ad.db.DBS().Reader) + if err != nil { + for ix := range vIDs { + results[ix] = &dataloader.Result[*model.AftermarketDevice]{Data: nil, Error: err} + } + return results + } + + for _, device := range devices { + v := &model.AftermarketDevice{ + ID: strconv.Itoa(device.ID), + Address: repositories.BytesToAddr(device.Address), + // Owner: repositories.BytesToAddr(device.Owner), + Serial: device.Serial.Ptr(), + Imei: device.Imei.Ptr(), + // MintedAt: device.MintedAt.Ptr(), + Beneficiary: repositories.BytesToAddr(device.Beneficiary), + } + results[keyOrder[device.VehicleID.Int]] = &dataloader.Result[*model.AftermarketDevice]{Data: v, Error: nil} + delete(keyOrder, device.VehicleID.Int) + } + + return results +} diff --git a/internal/loader/loader.go b/internal/loader/loader.go new file mode 100644 index 00000000..88d460fc --- /dev/null +++ b/internal/loader/loader.go @@ -0,0 +1,51 @@ +package loader + +import ( + "context" + "net/http" + + "github.com/DIMO-Network/identity-api/graph/model" + "github.com/DIMO-Network/shared/db" + "github.com/graph-gophers/dataloader/v7" +) + +type loaderKey struct{} +type loadersString string + +const ( + dataLoadersKey loadersString = "dataLoadersKey" +) + +type Loaders struct { + VehicleByID dataloader.Interface[string, *model.Vehicle] + AftermarketDeviceByVehicleID dataloader.Interface[string, *model.AftermarketDevice] +} + +// NewDataLoader returns the instantiated Loaders struct for use in a request +func NewDataLoader(dbs db.Store) *Loaders { + // instantiate the user dataloader + vehicle := &VehicleLoader{db: dbs} + aftermarketDevice := &AftermarketDeviceLoader{db: dbs} + // return the DataLoader + return &Loaders{ + VehicleByID: dataloader.NewBatchedLoader( + vehicle.BatchGetLinkedVehicleByAftermarketID, + dataloader.WithClearCacheOnBatch[string, *model.Vehicle](), + ), + AftermarketDeviceByVehicleID: dataloader.NewBatchedLoader( + aftermarketDevice.BatchGetLinkedAftermarketDeviceByVehicleID, + dataloader.WithClearCacheOnBatch[string, *model.AftermarketDevice](), + ), + } +} + +// Middleware injects a DataLoader into the request context so it can be +// used later in the schema resolvers +func Middleware(db db.Store, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + loader := NewDataLoader(db) + nextCtx := context.WithValue(r.Context(), dataLoadersKey, loader) + r = r.WithContext(nextCtx) + next.ServeHTTP(w, r) + }) +} diff --git a/internal/loader/vehicle_loader.go b/internal/loader/vehicle_loader.go new file mode 100644 index 00000000..0639330c --- /dev/null +++ b/internal/loader/vehicle_loader.go @@ -0,0 +1,79 @@ +package loader + +import ( + "context" + "strconv" + + "github.com/DIMO-Network/identity-api/graph/model" + "github.com/DIMO-Network/identity-api/models" + "github.com/DIMO-Network/shared/db" + "github.com/ethereum/go-ethereum/common" + "github.com/graph-gophers/dataloader/v7" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +type VehicleLoader struct { + db db.Store +} + +func GetLinkedVehicleByID(ctx context.Context, vehicleID string) (*model.Vehicle, error) { + // read loader from context + loaders := ctx.Value(dataLoadersKey).(*Loaders) + // invoke and get thunk + thunk := loaders.VehicleByID.Load(ctx, vehicleID) + // read value from thunk + result, err := thunk() + if err != nil { + return nil, err + } + return result, nil +} + +// BatchGetLinkedVehicleByAftermarketID implements the dataloader for finding vehicles linked to aftermarket devices and returns +// them in the order requested +func (v *VehicleLoader) BatchGetLinkedVehicleByAftermarketID(ctx context.Context, aftermarketDeviceIDs []string) []*dataloader.Result[*model.Vehicle] { + keyOrder := make(map[int]int) + results := make([]*dataloader.Result[*model.Vehicle], len(aftermarketDeviceIDs)) + var adIDs []int + + for ix, key := range aftermarketDeviceIDs { + k, err := strconv.Atoi(key) + if err != nil { + results[ix] = &dataloader.Result[*model.Vehicle]{Data: nil, Error: err} + } + keyOrder[k] = ix + adIDs = append(adIDs, k) + } + + adVehicleLink, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.IN(adIDs), + qm.Load(models.AftermarketDeviceRels.Vehicle), + ).All(ctx, v.db.DBS().Reader) + if err != nil { + for ix := range adIDs { + results[ix] = &dataloader.Result[*model.Vehicle]{Data: nil, Error: err} + } + return results + } + + for _, device := range adVehicleLink { + if device.R.Vehicle == nil { + results[keyOrder[device.ID]] = &dataloader.Result[*model.Vehicle]{Data: nil, Error: nil} + continue + } + + v := &model.Vehicle{ + ID: strconv.Itoa(device.R.Vehicle.ID), + Owner: common.BytesToAddress(device.R.Vehicle.OwnerAddress), + Make: device.R.Vehicle.Make.Ptr(), + Model: device.R.Vehicle.Model.Ptr(), + Year: device.R.Vehicle.Year.Ptr(), + MintedAt: device.R.Vehicle.MintedAt, + } + results[keyOrder[device.ID]] = &dataloader.Result[*model.Vehicle]{Data: v, Error: nil} + delete(keyOrder, device.ID) + + } + + return results +} diff --git a/internal/repositories/aftermarket_devices.go b/internal/repositories/aftermarket_devices.go index 2e6038b4..932c4585 100644 --- a/internal/repositories/aftermarket_devices.go +++ b/internal/repositories/aftermarket_devices.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "database/sql" "encoding/base64" "errors" "strconv" @@ -23,7 +24,7 @@ func BytesToAddr(addrB null.Bytes) *common.Address { func (v *VehiclesRepo) GetOwnedAftermarketDevices(ctx context.Context, addr common.Address, first *int, after *string) (*gmodel.AftermarketDeviceConnection, error) { ownedADCount, err := models.AftermarketDevices( - models.AftermarketDeviceWhere.Owner.EQ(addr.Bytes()), + // models.AftermarketDeviceWhere.Owner.EQ(addr.Bytes()), ).Count(ctx, v.pdb.DBS().Reader) if err != nil { return nil, err @@ -47,6 +48,7 @@ func (v *VehiclesRepo) GetOwnedAftermarketDevices(ctx context.Context, addr comm queryMods := []qm.QueryMod{ models.AftermarketDeviceWhere.Owner.EQ(addr.Bytes()), + qm.Load(models.AftermarketDeviceRels.Vehicle), // Use limit + 1 here to check if there's a next page. qm.Limit(limit + 1), qm.OrderBy(models.AftermarketDeviceColumns.ID + " DESC"), @@ -73,20 +75,32 @@ func (v *VehiclesRepo) GetOwnedAftermarketDevices(ctx context.Context, addr comm hasNextPage := len(ads) > limit if hasNextPage { - ads = ads[:len(ads)-1] + ads = ads[:limit] } var adEdges []*gmodel.AftermarketDeviceEdge for _, d := range ads { + var vehicle gmodel.Vehicle + if d.R.Vehicle != nil { + vehicle.ID = strconv.Itoa(d.R.Vehicle.ID) + vehicle.Owner = common.BytesToAddress(d.R.Vehicle.OwnerAddress) + vehicle.Make = d.R.Vehicle.Make.Ptr() + vehicle.Model = d.R.Vehicle.Model.Ptr() + vehicle.Year = d.R.Vehicle.Year.Ptr() + vehicle.MintedAt = d.R.Vehicle.MintedAt + } + adEdges = append(adEdges, &gmodel.AftermarketDeviceEdge{ Node: &gmodel.AftermarketDevice{ - ID: strconv.Itoa(d.ID), - Address: BytesToAddr(d.Address), - Owner: common.BytesToAddress(d.Owner), - Serial: d.Serial.Ptr(), - Imei: d.Imei.Ptr(), - MintedAt: d.MintedAt, + ID: strconv.Itoa(d.ID), + Address: BytesToAddr(d.Address), + Owner: common.BytesToAddress(d.Owner), + Serial: d.Serial.Ptr(), + Imei: d.Imei.Ptr(), + MintedAt: d.MintedAt, + Vehicle: &vehicle, + Beneficiary: BytesToAddr(d.Beneficiary), }, Cursor: base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(d.ID))), }, @@ -108,3 +122,32 @@ func (v *VehiclesRepo) GetOwnedAftermarketDevices(ctx context.Context, addr comm res.PageInfo.EndCursor = &adEdges[len(adEdges)-1].Cursor return res, nil } + +func (v *VehiclesRepo) GetLinkedAftermarketDeviceByVehicleID(ctx context.Context, vehicleID string) (*gmodel.AftermarketDevice, error) { + vID, err := strconv.Atoi(vehicleID) + if err != nil { + return nil, err + } + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.VehicleID.EQ(null.IntFrom(vID)), + ).One(ctx, v.pdb.DBS().Reader) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + res := &gmodel.AftermarketDevice{ + ID: strconv.Itoa(ad.ID), + Address: BytesToAddr(ad.Address), + Owner: common.BytesToAddress(ad.Address.Bytes), + Serial: ad.Serial.Ptr(), + Imei: ad.Imei.Ptr(), + MintedAt: ad.MintedAt, + Beneficiary: BytesToAddr(ad.Beneficiary), + } + + return res, nil +} diff --git a/internal/repositories/aftermarket_devices_test.go b/internal/repositories/aftermarket_devices_test.go index d65e0104..7c80c936 100644 --- a/internal/repositories/aftermarket_devices_test.go +++ b/internal/repositories/aftermarket_devices_test.go @@ -1,99 +1,159 @@ -package repositories +package repositories_test import ( "context" "encoding/json" "fmt" - "math/big" - "os" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" "testing" "time" - "github.com/DIMO-Network/identity-api/internal/config" - "github.com/DIMO-Network/identity-api/internal/services" + "github.com/99designs/gqlgen/graphql/handler" + "github.com/99designs/gqlgen/graphql/playground" + "github.com/DIMO-Network/identity-api/graph" + "github.com/DIMO-Network/identity-api/graph/model" + "github.com/DIMO-Network/identity-api/internal/loader" + "github.com/DIMO-Network/identity-api/internal/repositories" "github.com/DIMO-Network/identity-api/internal/test" "github.com/DIMO-Network/identity-api/models" - "github.com/DIMO-Network/shared" - "github.com/Shopify/sarama" - "github.com/Shopify/sarama/mocks" "github.com/ethereum/go-ethereum/common" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" ) -var cloudEvent = shared.CloudEvent[json.RawMessage]{ - ID: "2SiTVhP3WBhfQQnnnpeBdMR7BSY", - Source: "chain/80001", - SpecVersion: "1.0", - Subject: "0x4de1bcf2b7e851e31216fc07989caa902a604784", - Time: time.Now(), - Type: "zone.dimo.contract.event", +var aftermarketDevice = models.AftermarketDevice{ + ID: 1, + Address: null.BytesFrom(common.HexToAddress("46a3A41bd932244Dd08186e4c19F1a7E48cbcDf5").Bytes()), + Owner: common.HexToAddress("46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), + Serial: null.StringFrom("aftermarketDeviceSerial-1"), + Imei: null.StringFrom("aftermarketDeviceIMEI-1"), + MintedAt: time.Now(), + VehicleID: null.IntFrom(11), + Beneficiary: null.BytesFrom(common.HexToAddress("46a3A41bd932244Dd08186e4c19F1a7E48cbcDf3").Bytes()), } -var contractEventData = services.ContractEventData{ - ChainID: 80001, - EventName: "AftermarketDeviceNodeMinted", - Contract: common.HexToAddress("0x4de1bcf2b7e851e31216fc07989caa902a604784"), - TransactionHash: common.HexToHash("0x811a85e24d0129a2018c9a6668652db63d73bc6d1c76f21b07da2162c6bfea7d"), - EventSignature: common.HexToHash("0xd624fd4c3311e1803d230d97ce71fd60c4f658c30a31fbe08edcb211fd90f63f"), +var vehicle = models.Vehicle{ + ID: 11, + OwnerAddress: common.HexToAddress("46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), + Make: null.StringFrom("Ford"), + Model: null.StringFrom("Bronco"), + Year: null.IntFrom(2022), + MintedAt: time.Now(), } -var aftermarketDeviceNodeMintedArgs = services.AftermarketDeviceNodeMintedData{ - AftermarketDeviceAddress: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), - ManufacturerID: big.NewInt(137), - Owner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), - TokenID: big.NewInt(42), -} +func createTestServerAndDB(ctx context.Context, t *testing.T, aftermarketDevices []models.AftermarketDevice, vehicles []models.Vehicle) *httptest.Server { + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + repo := repositories.NewVehiclesRepo(pdb) -func TestAftermarketDeviceNodeMintSingleResponse(t *testing.T) { - ctx := context.Background() - logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + s := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{ + Repo: repo, + }})) + srv := loader.Middleware(pdb, s) - settings := config.Settings{ - DIMORegistryAddr: "0x4de1bcf2b7e851e31216fc07989caa902a604784", - DIMORegistryChainID: 80001, - } + mux := http.NewServeMux() + mux.Handle("/", playground.Handler("GraphQL playground", "/query")) + mux.Handle("/query", srv) - config := mocks.NewTestConfig() - consumer := mocks.NewConsumer(t, config) + app := httptest.NewServer(mux) - pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) - contractEventConsumer := services.NewContractsEventsConsumer(pdb, &logger, &settings) + for _, vehicle := range vehicles { + err := vehicle.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + } - argBytes, err := json.Marshal(aftermarketDeviceNodeMintedArgs) - assert.NoError(t, err) + for _, device := range aftermarketDevices { + err := device.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + } - contractEventData.Arguments = argBytes - ctEventDataBytes, err := json.Marshal(contractEventData) - assert.NoError(t, err) + return app - cloudEvent.Data = ctEventDataBytes - expectedBytes, err := json.Marshal(cloudEvent) - assert.NoError(t, err) +} + +func TestOwnedAftermarketDevices(t *testing.T) { + ctx := context.Background() + app := createTestServerAndDB(ctx, t, []models.AftermarketDevice{aftermarketDevice}, []models.Vehicle{vehicle}) + defer app.Close() + + r := strings.NewReader(`{ + "query": "query ownedAftermarketDevices($address: Address!){ ownedAftermarketDevices(address: $address) { edges { node { id serial owner address imei mintedAt owner } } }}", + "operationName": "ownedAftermarketDevices", + "variables": { + "address": "46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4" + } +}`) - consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + resp, err := http.Post(app.URL+"/query", "application/json", r) + if err != nil { + fmt.Println(err) + } - outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + var respBody model.AftermarketDevice + b, err := io.ReadAll(resp.Body) + defer resp.Body.Close() assert.NoError(t, err) - m := <-outputTest.Messages() - var e shared.CloudEvent[json.RawMessage] - err = json.Unmarshal(m.Value, &e) + err = json.Unmarshal([]byte(gjson.GetBytes(b, "data.ownedAftermarketDevices.edges.0.node").String()), &respBody) assert.NoError(t, err) - err = contractEventConsumer.Process(ctx, &e) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, strconv.Itoa(aftermarketDevice.ID), respBody.ID) + assert.Equal(t, common.BytesToAddress(aftermarketDevice.Address.Bytes), *respBody.Address) + assert.Equal(t, common.BytesToAddress(aftermarketDevice.Owner), respBody.Owner) + assert.Equal(t, aftermarketDevice.Serial.String, *respBody.Serial) + assert.Equal(t, aftermarketDevice.Imei.String, *respBody.Imei) +} + +func TestOwnedAftermarketDeviceAndLinkedVehicle(t *testing.T) { + ctx := context.Background() + app := createTestServerAndDB(ctx, t, []models.AftermarketDevice{aftermarketDevice}, []models.Vehicle{vehicle}) + defer app.Close() + + r := strings.NewReader(`{ + "query": "query ownedAftermarketDevices($address: Address!){ ownedAftermarketDevices(address: $address) { edges { node { id owner address imei serial mintedAt vehicle { id owner make model year mintedAt } } } }}" + , + "operationName": "ownedAftermarketDevices", + "variables": { + "address": "46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4" + } + }`) + + resp, err := http.Post(app.URL+"/query", "application/json", r) + if err != nil { + fmt.Println(err) + } + + var adBody model.AftermarketDevice + var vehicleBody model.Vehicle + b, err := io.ReadAll(resp.Body) + defer resp.Body.Close() assert.NoError(t, err) - ad, err := models.AftermarketDevices(models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDeviceNodeMintedArgs.TokenID.Int64()))).One(ctx, pdb.DBS().Reader) + err = json.Unmarshal([]byte(gjson.GetBytes(b, "data.ownedAftermarketDevices.edges.0.node").String()), &adBody) assert.NoError(t, err) - assert.Equal(t, ad.Address.Bytes, aftermarketDeviceNodeMintedArgs.Owner.Bytes()) - adController := NewVehiclesRepo(pdb) - res, err := adController.GetOwnedAftermarketDevices(ctx, aftermarketDeviceNodeMintedArgs.Owner, nil, nil) + err = json.Unmarshal([]byte(gjson.GetBytes(b, "data.ownedAftermarketDevices.edges.0.node.vehicle").String()), &vehicleBody) assert.NoError(t, err) - assert.Equal(t, *res.Edges[0].Node.Address, aftermarketDeviceNodeMintedArgs.Owner) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, strconv.Itoa(aftermarketDevice.ID), adBody.ID) + assert.Equal(t, common.BytesToAddress(aftermarketDevice.Address.Bytes), *adBody.Address) + assert.Equal(t, common.BytesToAddress(aftermarketDevice.Owner), adBody.Owner) + assert.Equal(t, aftermarketDevice.Serial.String, *adBody.Serial) + assert.Equal(t, aftermarketDevice.Imei.String, *adBody.Imei) + + assert.Equal(t, strconv.Itoa(vehicle.ID), vehicleBody.ID) + assert.Equal(t, common.BytesToAddress(vehicle.OwnerAddress), vehicleBody.Owner) + assert.Equal(t, vehicle.Make.String, *vehicleBody.Make) + assert.Equal(t, vehicle.Model.String, *vehicleBody.Model) + assert.Equal(t, vehicle.Year.Int, *vehicleBody.Year) + assert.Equal(t, vehicle.MintedAt.UTC().Format(time.RFC1123), vehicleBody.MintedAt.UTC().Format(time.RFC1123)) } func TestAftermarketDeviceNodeMintMultiResponse(t *testing.T) { @@ -103,9 +163,8 @@ func TestAftermarketDeviceNodeMintMultiResponse(t *testing.T) { for i := 1; i < 6; i++ { ad := models.AftermarketDevice{ - ID: i, - Owner: aftermarketDeviceNodeMintedArgs.Owner.Bytes(), - MintedAt: time.Now(), + ID: i, + Owner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), } err := ad.Insert(ctx, pdb.DBS().Writer, boil.Infer()) @@ -117,10 +176,10 @@ func TestAftermarketDeviceNodeMintMultiResponse(t *testing.T) { // | // after this - adController := NewVehiclesRepo(pdb) + adController := repositories.NewVehiclesRepo(pdb) first := 2 after := "NA==" // 4 - res, err := adController.GetOwnedAftermarketDevices(ctx, aftermarketDeviceNodeMintedArgs.Owner, &first, &after) + res, err := adController.GetOwnedAftermarketDevices(ctx, common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), &first, &after) assert.NoError(t, err) fmt.Println(res) diff --git a/internal/repositories/owned_vehicles.go b/internal/repositories/owned_vehicles.go index a69082e7..d9420761 100644 --- a/internal/repositories/owned_vehicles.go +++ b/internal/repositories/owned_vehicles.go @@ -28,44 +28,6 @@ func NewVehiclesRepo(pdb db.Store) VehiclesRepo { } } -func (v *VehiclesRepo) createVehiclesResponse(totalCount int64, vehicles []models.Vehicle, hasNext bool) *gmodel.VehicleConnection { - lastItmID := vehicles[len(vehicles)-1].ID - endCursr := base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(lastItmID))) - - var vEdges []*gmodel.VehicleEdge - for _, v := range vehicles { - crs := base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(v.ID))) - cursor := crs - - owner := common.BytesToAddress(v.OwnerAddress) - - edge := &gmodel.VehicleEdge{ - Node: &gmodel.Vehicle{ - ID: strconv.Itoa(v.ID), - Owner: owner, - Make: v.Make.Ptr(), - Model: v.Model.Ptr(), - Year: v.Year.Ptr(), - MintedAt: v.MintedAt, - }, - Cursor: cursor, - } - - vEdges = append(vEdges, edge) - } - - res := &gmodel.VehicleConnection{ - TotalCount: int(totalCount), - PageInfo: &gmodel.PageInfo{ - HasNextPage: hasNext, - EndCursor: &endCursr, - }, - Edges: vEdges, - } - - return res -} - func (v *VehiclesRepo) GetOwnedVehicles(ctx context.Context, addr common.Address, first *int, after *string) (*gmodel.VehicleConnection, error) { totalCount, err := models.Vehicles( models.VehicleWhere.OwnerAddress.EQ(addr.Bytes()), @@ -91,52 +53,107 @@ func (v *VehiclesRepo) GetOwnedVehicles(ctx context.Context, addr common.Address queryMods := []qm.QueryMod{ models.VehicleWhere.OwnerAddress.EQ(addr.Bytes()), + qm.Load(models.VehicleRels.AftermarketDevice), // Use limit + 1 here to check if there's a next page. qm.Limit(limit + 1), qm.OrderBy(models.VehicleColumns.ID + " DESC"), } if after != nil { - lastCursor, err := base64.StdEncoding.DecodeString(*after) + searchAfter, err := strconv.Atoi(string([]byte(*after))) if err != nil { return nil, err } - lastCursorVal, err := strconv.Atoi(string(lastCursor)) - if err != nil { - return nil, err - } - queryMods = append(queryMods, models.VehicleWhere.ID.LT(lastCursorVal)) + queryMods = append(queryMods, models.VehicleWhere.ID.LT(searchAfter)) } - all, err := models.Vehicles(queryMods...).All(ctx, v.pdb.DBS().Reader) + vehicles, err := models.Vehicles(queryMods...).All(ctx, v.pdb.DBS().Reader) if err != nil { return nil, err } - if len(all) == 0 { + if len(vehicles) == 0 { return &gmodel.VehicleConnection{ TotalCount: int(totalCount), Edges: []*gmodel.VehicleEdge{}, }, nil } - vIterate := all - if len(all) > limit { - vIterate = all[:limit] + hasNextPage := len(vehicles) > limit + if hasNextPage { + vehicles = vehicles[:limit] + } + + lastItmID := vehicles[len(vehicles)-1].ID + endCursr := base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(lastItmID))) + + var vEdges []*gmodel.VehicleEdge + for _, v := range vehicles { + edge := &gmodel.VehicleEdge{ + Node: &gmodel.Vehicle{ + ID: strconv.Itoa(v.ID), + Owner: common.BytesToAddress(v.OwnerAddress), + Make: v.Make.Ptr(), + Model: v.Model.Ptr(), + Year: v.Year.Ptr(), + MintedAt: v.MintedAt, + }, + Cursor: base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(v.ID))), + } + + if v.R.AftermarketDevice != nil { + edge.Node.AftermarketDevice = &gmodel.AftermarketDevice{ + ID: strconv.Itoa(v.R.AftermarketDevice.ID), + Address: BytesToAddr(v.R.AftermarketDevice.Address), + // Owner: BytesToAddr(v.R.AftermarketDevice.Owner), + Serial: v.R.AftermarketDevice.Serial.Ptr(), + Imei: v.R.AftermarketDevice.Imei.Ptr(), + // MintedAt: v.R.AftermarketDevice.MintedAt.Ptr(), + Beneficiary: BytesToAddr(v.R.AftermarketDevice.Beneficiary), + } + } + vEdges = append(vEdges, edge) + } + + res := &gmodel.VehicleConnection{ + TotalCount: int(totalCount), + PageInfo: &gmodel.PageInfo{ + HasNextPage: hasNextPage, + EndCursor: &endCursr, + }, + Edges: vEdges, + } + + return res, nil +} + +func (v *VehiclesRepo) GetLinkedVehicleByID(ctx context.Context, aftermarketDevID string) (*gmodel.Vehicle, error) { + adID, err := strconv.Atoi(aftermarketDevID) + if err != nil { + return nil, err + } + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.EQ(adID), + qm.Load(models.AftermarketDeviceRels.Vehicle), + ).One(ctx, v.pdb.DBS().Reader) + if err != nil { + return nil, err + } + + if ad.R.Vehicle == nil { + return nil, nil } - vehicles := []models.Vehicle{} - for _, v := range vIterate { - vehicles = append(vehicles, models.Vehicle{ - ID: v.ID, - OwnerAddress: v.OwnerAddress, - Make: v.Make, - Model: v.Model, - Year: v.Year, - MintedAt: v.MintedAt, - }) + res := &gmodel.Vehicle{ + ID: strconv.Itoa(ad.R.Vehicle.ID), + Owner: common.BytesToAddress(ad.R.Vehicle.OwnerAddress), + Make: ad.R.Vehicle.Make.Ptr(), + Model: ad.R.Vehicle.Model.Ptr(), + Year: ad.R.Vehicle.Year.Ptr(), + MintedAt: ad.R.Vehicle.MintedAt, } - return v.createVehiclesResponse(totalCount, vehicles, len(all) > limit), nil + return res, nil } diff --git a/internal/services/contract_events_consumer_test.go b/internal/services/contract_events_consumer_test.go new file mode 100644 index 00000000..26a65a22 --- /dev/null +++ b/internal/services/contract_events_consumer_test.go @@ -0,0 +1,401 @@ +package services + +import ( + "context" + "encoding/json" + "math/big" + "os" + "testing" + "time" + + "github.com/DIMO-Network/identity-api/internal/config" + "github.com/DIMO-Network/identity-api/internal/test" + "github.com/DIMO-Network/identity-api/models" + "github.com/DIMO-Network/shared" + "github.com/Shopify/sarama" + "github.com/Shopify/sarama/mocks" + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +var mintedAt = time.Now() + +var cloudEvent = shared.CloudEvent[json.RawMessage]{ + ID: "2SiTVhP3WBhfQQnnnpeBdMR7BSY", + Source: "chain/80001", + SpecVersion: "1.0", + Subject: "0x4de1bcf2b7e851e31216fc07989caa902a604784", + Time: mintedAt, + Type: "zone.dimo.contract.event", +} + +var contractEventData = ContractEventData{ + ChainID: 80001, + Contract: common.HexToAddress("0x4de1bcf2b7e851e31216fc07989caa902a604784"), + TransactionHash: common.HexToHash("0x811a85e24d0129a2018c9a6668652db63d73bc6d1c76f21b07da2162c6bfea7d"), + EventSignature: common.HexToHash("0xd624fd4c3311e1803d230d97ce71fd60c4f658c30a31fbe08edcb211fd90f63f"), + Block: Block{ + Time: mintedAt, + }, +} + +func eventBytes(args interface{}, contractEventData ContractEventData, t *testing.T) []byte { + + argBytes, err := json.Marshal(args) + assert.NoError(t, err) + + contractEventData.Arguments = argBytes + ctEventDataBytes, err := json.Marshal(contractEventData) + assert.NoError(t, err) + + cloudEvent.Data = ctEventDataBytes + expectedBytes, err := json.Marshal(cloudEvent) + assert.NoError(t, err) + + return expectedBytes +} + +func TestHandleAftermarketDeviceNodeMintedEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "AftermarketDeviceNodeMinted" + + var aftermarketDeviceNodeMintedArgs = AftermarketDeviceNodeMintedData{ + AftermarketDeviceAddress: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + ManufacturerID: big.NewInt(137), + Owner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + TokenID: big.NewInt(42), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(aftermarketDeviceNodeMintedArgs, contractEventData, t) + + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices(models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDeviceNodeMintedArgs.TokenID.Int64()))).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, aftermarketDeviceNodeMintedArgs.TokenID.Int64(), int64(ad.ID)) + assert.Equal(t, aftermarketDeviceNodeMintedArgs.AftermarketDeviceAddress.Bytes(), ad.Address.Bytes) + assert.Equal(t, aftermarketDeviceNodeMintedArgs.Owner.Bytes(), ad.Owner) + assert.Equal(t, aftermarketDeviceNodeMintedArgs.Owner.Bytes(), ad.Beneficiary.Bytes) + assert.Equal(t, mintedAt.UTC().Format(time.RFC3339), ad.MintedAt.UTC().Format(time.RFC3339)) +} + +func TestHandleAftermarketDeviceAttributeSetEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "AftermarketDeviceAttributeSet" + + var aftermarketDeviceAttributesSerial = AftermarketDeviceAttributeSetData{ + Attribute: "Serial", + Info: "randomgarbagevalue", + TokenID: big.NewInt(43), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(aftermarketDeviceAttributesSerial, contractEventData, t) + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + prepopulateAttribute := models.AftermarketDevice{ + ID: int(aftermarketDeviceAttributesSerial.TokenID.Int64()), + Imei: null.StringFrom("garbage-imei-value"), + MintedAt: mintedAt, + Owner: common.Hex2Bytes("46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + } + err = prepopulateAttribute.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices(models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDeviceAttributesSerial.TokenID.Int64()))).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, aftermarketDeviceAttributesSerial.TokenID.Int64(), int64(ad.ID)) + assert.Equal(t, aftermarketDeviceAttributesSerial.Info, ad.Serial.String) + assert.Equal(t, prepopulateAttribute.Imei.String, ad.Imei.String) + assert.Equal(t, null.Bytes{Bytes: []uint8{}}, ad.Address) + assert.Equal(t, prepopulateAttribute.Owner, ad.Owner) + assert.Equal(t, null.Bytes{Bytes: []uint8{}}, ad.Beneficiary) + assert.Equal(t, prepopulateAttribute.MintedAt.UTC().Format(time.RFC3339), ad.MintedAt.UTC().Format(time.RFC3339)) +} + +func TestHandleAftermarketDevicePairedEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "AftermarketDevicePaired" + + var aftermarketDevicePairData = AftermarketDevicePairData{ + Owner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + AftermarketDeviceNode: big.NewInt(1), + VehicleNode: big.NewInt(11), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(aftermarketDevicePairData, contractEventData, t) + + v := models.Vehicle{ + ID: 11, + OwnerAddress: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), + Make: null.StringFrom("Tesla"), + Model: null.StringFrom("Model-3"), + Year: null.IntFrom(2023), + MintedAt: time.Now(), + } + err := v.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDevicePairData.AftermarketDeviceNode.Int64())), + qm.Load(models.AftermarketDeviceRels.Vehicle), + ).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, aftermarketDevicePairData.AftermarketDeviceNode.Int64(), int64(ad.ID)) + assert.Equal(t, aftermarketDevicePairData.Owner.Bytes(), ad.Owner) + assert.Equal(t, aftermarketDevicePairData.VehicleNode.Int64(), int64(ad.R.Vehicle.ID)) +} + +func TestHandleAftermarketDeviceUnPairedEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "AftermarketDeviceUnpaired" + + var aftermarketDevicePairData = AftermarketDevicePairData{ + Owner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + AftermarketDeviceNode: big.NewInt(1), + VehicleNode: big.NewInt(11), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(aftermarketDevicePairData, contractEventData, t) + + v := models.Vehicle{ + ID: 11, + OwnerAddress: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), + Make: null.StringFrom("Tesla"), + Model: null.StringFrom("Model-3"), + Year: null.IntFrom(2023), + MintedAt: time.Now(), + } + err := v.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDevicePairData.AftermarketDeviceNode.Int64())), + qm.Load(models.AftermarketDeviceRels.Vehicle), + ).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, aftermarketDevicePairData.AftermarketDeviceNode.Int64(), int64(ad.ID)) + assert.Equal(t, null.Bytes(null.Bytes{Bytes: []uint8(nil)}), null.Bytes{}) + + if ad.R.Vehicle != nil { + assert.Fail(t, "failed to unlink vehicle and aftermarket device while unpairing") + } +} + +func TestHandleAftermarketDeviceTransferredEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "AftermarketDeviceTransferred" + + var aftermarketDeviceTransferredData = AftermarketDeviceTransferredEventData{ + OldOwner: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + NewOwner: common.HexToAddress("0x55a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + AftermarketDeviceNode: big.NewInt(100), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(aftermarketDeviceTransferredData, contractEventData, t) + + v := models.Vehicle{ + ID: 11, + OwnerAddress: aftermarketDeviceTransferredData.OldOwner.Bytes(), + MintedAt: mintedAt, + } + err := v.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + d := models.AftermarketDevice{ + ID: 100, + Owner: aftermarketDeviceTransferredData.OldOwner.Bytes(), + Beneficiary: null.BytesFrom(aftermarketDeviceTransferredData.OldOwner.Bytes()), + VehicleID: null.IntFrom(11), + } + err = d.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.EQ(int(aftermarketDeviceTransferredData.AftermarketDeviceNode.Int64())), + ).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, aftermarketDeviceTransferredData.AftermarketDeviceNode.Int64(), int64(ad.ID)) + assert.Equal(t, aftermarketDeviceTransferredData.NewOwner, common.BytesToAddress(ad.Owner)) + assert.Equal(t, aftermarketDeviceTransferredData.NewOwner.Bytes(), ad.Beneficiary.Bytes) + assert.Equal(t, null.Int{}, ad.VehicleID) + +} + +func TestHandleBeneficiarySetEvent(t *testing.T) { + ctx := context.Background() + logger := zerolog.New(os.Stdout).With().Timestamp().Str("app", test.DBSettings.Name).Logger() + contractEventData.EventName = "BeneficiarySet" + + var beneficiarySetData = BeneficiarySetEventData{ + IdProxyAddress: common.HexToAddress("0x46a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + Beneficiary: common.HexToAddress("0x55a3A41bd932244Dd08186e4c19F1a7E48cbcDf4"), + NodeId: big.NewInt(100), + } + + settings := config.Settings{ + DIMORegistryAddr: contractEventData.Contract.String(), + DIMORegistryChainID: contractEventData.ChainID, + } + + config := mocks.NewTestConfig() + consumer := mocks.NewConsumer(t, config) + + pdb, _ := test.StartContainerDatabase(ctx, t, test.MigrationsDirRelPath) + contractEventConsumer := NewContractsEventsConsumer(pdb, &logger, &settings) + expectedBytes := eventBytes(beneficiarySetData, contractEventData, t) + + d := models.AftermarketDevice{ + ID: 100, + Owner: common.HexToAddress("0x22a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes(), + Beneficiary: null.BytesFrom(common.HexToAddress("0x22a3A41bd932244Dd08186e4c19F1a7E48cbcDf4").Bytes()), + } + err := d.Insert(ctx, pdb.DBS().Writer, boil.Infer()) + assert.NoError(t, err) + + consumer.ExpectConsumePartition(settings.ContractsEventTopic, 0, 0).YieldMessage(&sarama.ConsumerMessage{Value: expectedBytes}) + + outputTest, err := consumer.ConsumePartition(settings.ContractsEventTopic, 0, 0) + assert.NoError(t, err) + + m := <-outputTest.Messages() + var e shared.CloudEvent[json.RawMessage] + err = json.Unmarshal(m.Value, &e) + assert.NoError(t, err) + + err = contractEventConsumer.Process(ctx, &e) + assert.NoError(t, err) + + ad, err := models.AftermarketDevices( + models.AftermarketDeviceWhere.ID.EQ(int(beneficiarySetData.NodeId.Int64())), + ).One(ctx, pdb.DBS().Reader) + assert.NoError(t, err) + + assert.Equal(t, beneficiarySetData.NodeId.Int64(), int64(ad.ID)) + assert.Equal(t, beneficiarySetData.Beneficiary.Bytes(), ad.Beneficiary.Bytes) + +} diff --git a/internal/services/contracts_events_consumer.go b/internal/services/contracts_events_consumer.go index 1d76a46c..a93fec09 100644 --- a/internal/services/contracts_events_consumer.go +++ b/internal/services/contracts_events_consumer.go @@ -29,9 +29,14 @@ type ContractsEventsConsumer struct { type EventName string const ( + VehicleNodeMinted EventName = "VehicleNodeMinted" VehicleAttributeSet EventName = "VehicleAttributeSet" - AftermarketDeviceNodeMinted EventName = "AftermarketDeviceNodeMinted" + AftermarketDeviceNodeMintedEvent EventName = "AftermarketDeviceNodeMinted" AftermarketDeviceAttributeSetEvent EventName = "AftermarketDeviceAttributeSet" + AftermarketDevicePairedEvent EventName = "AftermarketDevicePaired" + AftermarketDeviceUnpairedEvent EventName = "AftermarketDeviceUnpaired" + AftermarketDeviceTransferredEvent EventName = "AftermarketDeviceTransferred" + BeneficiarySetEvent EventName = "BeneficiarySet" Transfer EventName = "Transfer" ) @@ -75,12 +80,31 @@ type AftermarketDeviceAttributeSetData struct { Attribute string Info string } + +type AftermarketDevicePairData struct { + AftermarketDeviceNode *big.Int + VehicleNode *big.Int + Owner common.Address +} + type TransferEventData struct { From common.Address To common.Address TokenID *big.Int } +type AftermarketDeviceTransferredEventData struct { + OldOwner common.Address + NewOwner common.Address + AftermarketDeviceNode *big.Int +} + +type BeneficiarySetEventData struct { + IdProxyAddress common.Address + NodeId *big.Int + Beneficiary common.Address +} + func NewContractsEventsConsumer(dbs db.Store, log *zerolog.Logger, settings *config.Settings) *ContractsEventsConsumer { return &ContractsEventsConsumer{ dbs: dbs, @@ -107,16 +131,23 @@ func (c *ContractsEventsConsumer) Process(ctx context.Context, event *shared.Clo return nil } eventName := EventName(data.EventName) - switch data.Contract { case registryAddr: switch eventName { case VehicleAttributeSet: return c.handleVehicleAttributeSetEvent(ctx, &data) + case AftermarketDeviceNodeMintedEvent: + return c.handleAftermarketDeviceNodeMintedEvent(ctx, &data) case AftermarketDeviceAttributeSetEvent: - return c.handleAftermarketDeviceAttributeSetEvent(&data) - case AftermarketDeviceNodeMinted: - return c.handleAftermarketDeviceNodeMintedEvent(&data) + return c.handleAftermarketDeviceAttributeSetEvent(ctx, &data) + case AftermarketDevicePairedEvent: + return c.handleAftermarketDevicePairedEvent(ctx, &data) + case AftermarketDeviceUnpairedEvent: + return c.handleAftermarketDeviceUnpairedEvent(ctx, &data) + case AftermarketDeviceTransferredEvent: + return c.handleAftermarketDeviceTransferredEvent(ctx, &data) + case BeneficiarySetEvent: + return c.handleBeneficiarySetEvent(ctx, &data) } case vehicleNFTAddr: if eventName == Transfer { @@ -193,24 +224,25 @@ func (c *ContractsEventsConsumer) handleVehicleTransferEvent(ctx context.Context return nil } -func (c *ContractsEventsConsumer) handleAftermarketDeviceNodeMintedEvent(e *ContractEventData) error { +func (c *ContractsEventsConsumer) handleAftermarketDeviceNodeMintedEvent(ctx context.Context, e *ContractEventData) error { var args AftermarketDeviceNodeMintedData if err := json.Unmarshal(e.Arguments, &args); err != nil { return err } ad := models.AftermarketDevice{ - ID: int(args.TokenID.Int64()), - Address: null.BytesFrom(args.AftermarketDeviceAddress.Bytes()), - Owner: args.Owner.Bytes(), - MintedAt: e.Block.Time, + ID: int(args.TokenID.Int64()), + Address: null.BytesFrom(args.AftermarketDeviceAddress.Bytes()), + Owner: args.Owner.Bytes(), + Beneficiary: null.BytesFrom(args.Owner.Bytes()), + MintedAt: e.Block.Time, } - if err := ad.Upsert(context.Background(), c.dbs.DBS().Writer, + if err := ad.Upsert(ctx, c.dbs.DBS().Writer, true, []string{models.AftermarketDeviceColumns.ID}, - boil.Whitelist(models.AftermarketDeviceColumns.ID, models.AftermarketDeviceColumns.Address, models.AftermarketDeviceColumns.Owner, models.AftermarketDeviceColumns.MintedAt), - boil.Whitelist(models.AftermarketDeviceColumns.ID, models.AftermarketDeviceColumns.Address, models.AftermarketDeviceColumns.Owner, models.AftermarketDeviceColumns.MintedAt), + boil.Whitelist(models.AftermarketDeviceColumns.ID, models.AftermarketDeviceColumns.Address, models.AftermarketDeviceColumns.Owner, models.AftermarketDeviceColumns.MintedAt, models.AftermarketDeviceColumns.Beneficiary), + boil.Whitelist(models.AftermarketDeviceColumns.ID, models.AftermarketDeviceColumns.Address, models.AftermarketDeviceColumns.Owner, models.AftermarketDeviceColumns.MintedAt, models.AftermarketDeviceColumns.Beneficiary), ); err != nil { return err } @@ -218,7 +250,7 @@ func (c *ContractsEventsConsumer) handleAftermarketDeviceNodeMintedEvent(e *Cont return nil } -func (c *ContractsEventsConsumer) handleAftermarketDeviceAttributeSetEvent(e *ContractEventData) error { +func (c *ContractsEventsConsumer) handleAftermarketDeviceAttributeSetEvent(ctx context.Context, e *ContractEventData) error { var args AftermarketDeviceAttributeSetData if err := json.Unmarshal(e.Arguments, &args); err != nil { return err @@ -227,31 +259,116 @@ func (c *ContractsEventsConsumer) handleAftermarketDeviceAttributeSetEvent(e *Co ad := models.AftermarketDevice{ ID: int(args.TokenID.Int64()), } - switch args.Attribute { case "Serial": ad.Serial = null.StringFrom(args.Info) - if err := ad.Upsert( - context.Background(), + if _, err := ad.Update( + ctx, c.dbs.DBS().Writer, - true, - []string{models.AftermarketDeviceColumns.ID}, - boil.Whitelist(models.AftermarketDeviceColumns.Serial), - boil.Infer()); err != nil { + boil.Whitelist(models.AftermarketDeviceColumns.Serial)); err != nil { return err } case "IMEI": ad.Imei = null.StringFrom(args.Info) - if err := ad.Upsert( - context.Background(), + if _, err := ad.Update( + ctx, c.dbs.DBS().Writer, - true, - []string{models.AftermarketDeviceColumns.ID}, - boil.Whitelist(models.AftermarketDeviceColumns.Imei), - boil.Infer()); err != nil { + boil.Whitelist(models.AftermarketDeviceColumns.Imei)); err != nil { return err } } return nil } + +func (c *ContractsEventsConsumer) handleAftermarketDevicePairedEvent(ctx context.Context, e *ContractEventData) error { + var args AftermarketDevicePairData + if err := json.Unmarshal(e.Arguments, &args); err != nil { + return err + } + + ad := models.AftermarketDevice{ + ID: int(args.AftermarketDeviceNode.Int64()), + VehicleID: null.IntFrom(int(args.VehicleNode.Int64())), + Owner: args.Owner.Bytes(), + Beneficiary: null.BytesFrom(args.Owner.Bytes()), + } + + if err := ad.Upsert( + ctx, + c.dbs.DBS().Writer, + true, + []string{models.AftermarketDeviceColumns.ID}, + boil.Whitelist(models.AftermarketDeviceColumns.VehicleID, models.AftermarketDeviceColumns.Owner), + boil.Infer(), + ); err != nil { + return err + } + + return nil +} + +func (c *ContractsEventsConsumer) handleAftermarketDeviceUnpairedEvent(ctx context.Context, e *ContractEventData) error { + var args AftermarketDevicePairData + if err := json.Unmarshal(e.Arguments, &args); err != nil { + return err + } + + ad := models.AftermarketDevice{ + ID: int(args.AftermarketDeviceNode.Int64()), + VehicleID: null.Int{}, + Owner: args.Owner.Bytes(), + } + + if err := ad.Upsert( + ctx, + c.dbs.DBS().Writer, + true, + []string{models.AftermarketDeviceColumns.ID}, + boil.Whitelist(models.AftermarketDeviceColumns.VehicleID, models.AftermarketDeviceColumns.Owner), + boil.Infer(), + ); err != nil { + return err + } + + return nil +} + +func (c *ContractsEventsConsumer) handleAftermarketDeviceTransferredEvent(ctx context.Context, e *ContractEventData) error { + var args AftermarketDeviceTransferredEventData + if err := json.Unmarshal(e.Arguments, &args); err != nil { + return err + } + + ad := models.AftermarketDevice{ + ID: int(args.AftermarketDeviceNode.Int64()), + Owner: args.NewOwner.Bytes(), + Beneficiary: null.BytesFrom(args.NewOwner.Bytes()), + VehicleID: null.Int{}, + } + + _, err := ad.Update( + ctx, + c.dbs.DBS().Writer, + boil.Whitelist(models.AftermarketDeviceColumns.Owner, models.AftermarketDeviceColumns.VehicleID, models.AftermarketDeviceColumns.Beneficiary)) + return err +} + +func (c *ContractsEventsConsumer) handleBeneficiarySetEvent(ctx context.Context, e *ContractEventData) error { + var args BeneficiarySetEventData + if err := json.Unmarshal(e.Arguments, &args); err != nil { + return err + } + + ad := models.AftermarketDevice{ + ID: int(args.NodeId.Int64()), + Beneficiary: null.BytesFrom(args.Beneficiary.Bytes()), + } + + _, err := ad.Update( + ctx, + c.dbs.DBS().Writer, + boil.Whitelist(models.AftermarketDeviceColumns.Beneficiary), + ) + return err +} diff --git a/migrations/00006_aftermarket_device_vehicle_link.sql b/migrations/00006_aftermarket_device_vehicle_link.sql new file mode 100644 index 00000000..d217a440 --- /dev/null +++ b/migrations/00006_aftermarket_device_vehicle_link.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- +goose StatementBegin + +ALTER TABLE aftermarket_devices +ADD COLUMN vehicle_id int UNIQUE; + +ALTER TABLE aftermarket_devices +ADD CONSTRAINT ad_vehicle_link FOREIGN KEY (vehicle_id) REFERENCES vehicles (id); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +ALTER TABLE aftermarket_devices +DROP CONSTRAINT ad_vehicle_link; + +ALTER TABLE aftermarket_devices +DROP COLUMN vehicle_id; +-- +goose StatementEnd diff --git a/migrations/00007_set_device_beneficiary.sql b/migrations/00007_set_device_beneficiary.sql new file mode 100644 index 00000000..00cfb016 --- /dev/null +++ b/migrations/00007_set_device_beneficiary.sql @@ -0,0 +1,14 @@ +-- +goose Up +-- +goose StatementBegin + +ALTER TABLE aftermarket_devices +ADD COLUMN beneficiary bytea; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +ALTER TABLE aftermarket_devices +DROP COLUMN beneficiary; +-- +goose StatementEnd diff --git a/migrations/00006_required_ad_fields.sql b/migrations/00008_required_ad_fields.sql similarity index 100% rename from migrations/00006_required_ad_fields.sql rename to migrations/00008_required_ad_fields.sql diff --git a/models/aftermarket_devices.go b/models/aftermarket_devices.go index 49bac2db..69f0fb34 100644 --- a/models/aftermarket_devices.go +++ b/models/aftermarket_devices.go @@ -24,47 +24,57 @@ import ( // AftermarketDevice is an object representing the database table. type AftermarketDevice struct { - ID int `boil:"id" json:"id" toml:"id" yaml:"id"` - Address null.Bytes `boil:"address" json:"address,omitempty" toml:"address" yaml:"address,omitempty"` - Owner []byte `boil:"owner" json:"owner" toml:"owner" yaml:"owner"` - Serial null.String `boil:"serial" json:"serial,omitempty" toml:"serial" yaml:"serial,omitempty"` - Imei null.String `boil:"imei" json:"imei,omitempty" toml:"imei" yaml:"imei,omitempty"` - MintedAt time.Time `boil:"minted_at" json:"minted_at" toml:"minted_at" yaml:"minted_at"` + ID int `boil:"id" json:"id" toml:"id" yaml:"id"` + Address null.Bytes `boil:"address" json:"address,omitempty" toml:"address" yaml:"address,omitempty"` + Owner []byte `boil:"owner" json:"owner" toml:"owner" yaml:"owner"` + Serial null.String `boil:"serial" json:"serial,omitempty" toml:"serial" yaml:"serial,omitempty"` + Imei null.String `boil:"imei" json:"imei,omitempty" toml:"imei" yaml:"imei,omitempty"` + MintedAt time.Time `boil:"minted_at" json:"minted_at" toml:"minted_at" yaml:"minted_at"` + VehicleID null.Int `boil:"vehicle_id" json:"vehicle_id,omitempty" toml:"vehicle_id" yaml:"vehicle_id,omitempty"` + Beneficiary null.Bytes `boil:"beneficiary" json:"beneficiary,omitempty" toml:"beneficiary" yaml:"beneficiary,omitempty"` R *aftermarketDeviceR `boil:"-" json:"-" toml:"-" yaml:"-"` L aftermarketDeviceL `boil:"-" json:"-" toml:"-" yaml:"-"` } var AftermarketDeviceColumns = struct { - ID string - Address string - Owner string - Serial string - Imei string - MintedAt string + ID string + Address string + Owner string + Serial string + Imei string + MintedAt string + VehicleID string + Beneficiary string }{ - ID: "id", - Address: "address", - Owner: "owner", - Serial: "serial", - Imei: "imei", - MintedAt: "minted_at", + ID: "id", + Address: "address", + Owner: "owner", + Serial: "serial", + Imei: "imei", + MintedAt: "minted_at", + VehicleID: "vehicle_id", + Beneficiary: "beneficiary", } var AftermarketDeviceTableColumns = struct { - ID string - Address string - Owner string - Serial string - Imei string - MintedAt string + ID string + Address string + Owner string + Serial string + Imei string + MintedAt string + VehicleID string + Beneficiary string }{ - ID: "aftermarket_devices.id", - Address: "aftermarket_devices.address", - Owner: "aftermarket_devices.owner", - Serial: "aftermarket_devices.serial", - Imei: "aftermarket_devices.imei", - MintedAt: "aftermarket_devices.minted_at", + ID: "aftermarket_devices.id", + Address: "aftermarket_devices.address", + Owner: "aftermarket_devices.owner", + Serial: "aftermarket_devices.serial", + Imei: "aftermarket_devices.imei", + MintedAt: "aftermarket_devices.minted_at", + VehicleID: "aftermarket_devices.vehicle_id", + Beneficiary: "aftermarket_devices.beneficiary", } // Generated where @@ -184,28 +194,74 @@ func (w whereHelpertime_Time) GTE(x time.Time) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) } +type whereHelpernull_Int struct{ field string } + +func (w whereHelpernull_Int) EQ(x null.Int) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, false, x) +} +func (w whereHelpernull_Int) NEQ(x null.Int) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, true, x) +} +func (w whereHelpernull_Int) LT(x null.Int) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpernull_Int) LTE(x null.Int) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpernull_Int) GT(x null.Int) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpernull_Int) GTE(x null.Int) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} +func (w whereHelpernull_Int) IN(slice []int) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelpernull_Int) NIN(slice []int) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +func (w whereHelpernull_Int) IsNull() qm.QueryMod { return qmhelper.WhereIsNull(w.field) } +func (w whereHelpernull_Int) IsNotNull() qm.QueryMod { return qmhelper.WhereIsNotNull(w.field) } + var AftermarketDeviceWhere = struct { - ID whereHelperint - Address whereHelpernull_Bytes - Owner whereHelper__byte - Serial whereHelpernull_String - Imei whereHelpernull_String - MintedAt whereHelpertime_Time + ID whereHelperint + Address whereHelpernull_Bytes + Owner whereHelper__byte + Serial whereHelpernull_String + Imei whereHelpernull_String + MintedAt whereHelpertime_Time + VehicleID whereHelpernull_Int + Beneficiary whereHelpernull_Bytes }{ - ID: whereHelperint{field: "\"identity_api\".\"aftermarket_devices\".\"id\""}, - Address: whereHelpernull_Bytes{field: "\"identity_api\".\"aftermarket_devices\".\"address\""}, - Owner: whereHelper__byte{field: "\"identity_api\".\"aftermarket_devices\".\"owner\""}, - Serial: whereHelpernull_String{field: "\"identity_api\".\"aftermarket_devices\".\"serial\""}, - Imei: whereHelpernull_String{field: "\"identity_api\".\"aftermarket_devices\".\"imei\""}, - MintedAt: whereHelpertime_Time{field: "\"identity_api\".\"aftermarket_devices\".\"minted_at\""}, + ID: whereHelperint{field: "\"identity_api\".\"aftermarket_devices\".\"id\""}, + Address: whereHelpernull_Bytes{field: "\"identity_api\".\"aftermarket_devices\".\"address\""}, + Owner: whereHelper__byte{field: "\"identity_api\".\"aftermarket_devices\".\"owner\""}, + Serial: whereHelpernull_String{field: "\"identity_api\".\"aftermarket_devices\".\"serial\""}, + Imei: whereHelpernull_String{field: "\"identity_api\".\"aftermarket_devices\".\"imei\""}, + MintedAt: whereHelpertime_Time{field: "\"identity_api\".\"aftermarket_devices\".\"minted_at\""}, + VehicleID: whereHelpernull_Int{field: "\"identity_api\".\"aftermarket_devices\".\"vehicle_id\""}, + Beneficiary: whereHelpernull_Bytes{field: "\"identity_api\".\"aftermarket_devices\".\"beneficiary\""}, } // AftermarketDeviceRels is where relationship names are stored. var AftermarketDeviceRels = struct { -}{} + Vehicle string +}{ + Vehicle: "Vehicle", +} // aftermarketDeviceR is where relationships are stored. type aftermarketDeviceR struct { + Vehicle *Vehicle `boil:"Vehicle" json:"Vehicle" toml:"Vehicle" yaml:"Vehicle"` } // NewStruct creates a new relationship struct @@ -213,13 +269,20 @@ func (*aftermarketDeviceR) NewStruct() *aftermarketDeviceR { return &aftermarketDeviceR{} } +func (r *aftermarketDeviceR) GetVehicle() *Vehicle { + if r == nil { + return nil + } + return r.Vehicle +} + // aftermarketDeviceL is where Load methods for each relationship are stored. type aftermarketDeviceL struct{} var ( - aftermarketDeviceAllColumns = []string{"id", "address", "owner", "serial", "imei", "minted_at"} + aftermarketDeviceAllColumns = []string{"id", "address", "owner", "serial", "imei", "minted_at", "vehicle_id", "beneficiary"} aftermarketDeviceColumnsWithoutDefault = []string{"id", "owner", "minted_at"} - aftermarketDeviceColumnsWithDefault = []string{"address", "serial", "imei"} + aftermarketDeviceColumnsWithDefault = []string{"address", "serial", "imei", "vehicle_id", "beneficiary"} aftermarketDevicePrimaryKeyColumns = []string{"id"} aftermarketDeviceGeneratedColumns = []string{} ) @@ -502,6 +565,210 @@ func (q aftermarketDeviceQuery) Exists(ctx context.Context, exec boil.ContextExe return count > 0, nil } +// Vehicle pointed to by the foreign key. +func (o *AftermarketDevice) Vehicle(mods ...qm.QueryMod) vehicleQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.VehicleID), + } + + queryMods = append(queryMods, mods...) + + return Vehicles(queryMods...) +} + +// LoadVehicle allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (aftermarketDeviceL) LoadVehicle(ctx context.Context, e boil.ContextExecutor, singular bool, maybeAftermarketDevice interface{}, mods queries.Applicator) error { + var slice []*AftermarketDevice + var object *AftermarketDevice + + if singular { + var ok bool + object, ok = maybeAftermarketDevice.(*AftermarketDevice) + if !ok { + object = new(AftermarketDevice) + ok = queries.SetFromEmbeddedStruct(&object, &maybeAftermarketDevice) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeAftermarketDevice)) + } + } + } else { + s, ok := maybeAftermarketDevice.(*[]*AftermarketDevice) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeAftermarketDevice) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeAftermarketDevice)) + } + } + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &aftermarketDeviceR{} + } + if !queries.IsNil(object.VehicleID) { + args = append(args, object.VehicleID) + } + + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &aftermarketDeviceR{} + } + + for _, a := range args { + if queries.Equal(a, obj.VehicleID) { + continue Outer + } + } + + if !queries.IsNil(obj.VehicleID) { + args = append(args, obj.VehicleID) + } + + } + } + + if len(args) == 0 { + return nil + } + + query := NewQuery( + qm.From(`identity_api.vehicles`), + qm.WhereIn(`identity_api.vehicles.id in ?`, args...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load Vehicle") + } + + var resultSlice []*Vehicle + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice Vehicle") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for vehicles") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for vehicles") + } + + if len(vehicleAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + + if len(resultSlice) == 0 { + return nil + } + + if singular { + foreign := resultSlice[0] + object.R.Vehicle = foreign + if foreign.R == nil { + foreign.R = &vehicleR{} + } + foreign.R.AftermarketDevice = object + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if queries.Equal(local.VehicleID, foreign.ID) { + local.R.Vehicle = foreign + if foreign.R == nil { + foreign.R = &vehicleR{} + } + foreign.R.AftermarketDevice = local + break + } + } + } + + return nil +} + +// SetVehicle of the aftermarketDevice to the related item. +// Sets o.R.Vehicle to related. +// Adds o to related.R.AftermarketDevice. +func (o *AftermarketDevice) SetVehicle(ctx context.Context, exec boil.ContextExecutor, insert bool, related *Vehicle) error { + var err error + if insert { + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } + + updateQuery := fmt.Sprintf( + "UPDATE \"identity_api\".\"aftermarket_devices\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"vehicle_id"}), + strmangle.WhereClause("\"", "\"", 2, aftermarketDevicePrimaryKeyColumns), + ) + values := []interface{}{related.ID, o.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + queries.Assign(&o.VehicleID, related.ID) + if o.R == nil { + o.R = &aftermarketDeviceR{ + Vehicle: related, + } + } else { + o.R.Vehicle = related + } + + if related.R == nil { + related.R = &vehicleR{ + AftermarketDevice: o, + } + } else { + related.R.AftermarketDevice = o + } + + return nil +} + +// RemoveVehicle relationship. +// Sets o.R.Vehicle to nil. +// Removes o from all passed in related items' relationships struct. +func (o *AftermarketDevice) RemoveVehicle(ctx context.Context, exec boil.ContextExecutor, related *Vehicle) error { + var err error + + queries.SetScanner(&o.VehicleID, nil) + if _, err = o.Update(ctx, exec, boil.Whitelist("vehicle_id")); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + if o.R != nil { + o.R.Vehicle = nil + } + if related == nil || related.R == nil { + return nil + } + + related.R.AftermarketDevice = nil + return nil +} + // AftermarketDevices retrieves all the records using an executor. func AftermarketDevices(mods ...qm.QueryMod) aftermarketDeviceQuery { mods = append(mods, qm.From("\"identity_api\".\"aftermarket_devices\"")) diff --git a/models/vehicles.go b/models/vehicles.go index 86b0d00d..35884d44 100644 --- a/models/vehicles.go +++ b/models/vehicles.go @@ -69,44 +69,6 @@ var VehicleTableColumns = struct { // Generated where -type whereHelpernull_Int struct{ field string } - -func (w whereHelpernull_Int) EQ(x null.Int) qm.QueryMod { - return qmhelper.WhereNullEQ(w.field, false, x) -} -func (w whereHelpernull_Int) NEQ(x null.Int) qm.QueryMod { - return qmhelper.WhereNullEQ(w.field, true, x) -} -func (w whereHelpernull_Int) LT(x null.Int) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LT, x) -} -func (w whereHelpernull_Int) LTE(x null.Int) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LTE, x) -} -func (w whereHelpernull_Int) GT(x null.Int) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GT, x) -} -func (w whereHelpernull_Int) GTE(x null.Int) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GTE, x) -} -func (w whereHelpernull_Int) IN(slice []int) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) -} -func (w whereHelpernull_Int) NIN(slice []int) qm.QueryMod { - values := make([]interface{}, 0, len(slice)) - for _, value := range slice { - values = append(values, value) - } - return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) -} - -func (w whereHelpernull_Int) IsNull() qm.QueryMod { return qmhelper.WhereIsNull(w.field) } -func (w whereHelpernull_Int) IsNotNull() qm.QueryMod { return qmhelper.WhereIsNotNull(w.field) } - var VehicleWhere = struct { ID whereHelperint OwnerAddress whereHelper__byte @@ -125,10 +87,14 @@ var VehicleWhere = struct { // VehicleRels is where relationship names are stored. var VehicleRels = struct { -}{} + AftermarketDevice string +}{ + AftermarketDevice: "AftermarketDevice", +} // vehicleR is where relationships are stored. type vehicleR struct { + AftermarketDevice *AftermarketDevice `boil:"AftermarketDevice" json:"AftermarketDevice" toml:"AftermarketDevice" yaml:"AftermarketDevice"` } // NewStruct creates a new relationship struct @@ -136,6 +102,13 @@ func (*vehicleR) NewStruct() *vehicleR { return &vehicleR{} } +func (r *vehicleR) GetAftermarketDevice() *AftermarketDevice { + if r == nil { + return nil + } + return r.AftermarketDevice +} + // vehicleL is where Load methods for each relationship are stored. type vehicleL struct{} @@ -425,6 +398,208 @@ func (q vehicleQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bo return count > 0, nil } +// AftermarketDevice pointed to by the foreign key. +func (o *Vehicle) AftermarketDevice(mods ...qm.QueryMod) aftermarketDeviceQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"vehicle_id\" = ?", o.ID), + } + + queryMods = append(queryMods, mods...) + + return AftermarketDevices(queryMods...) +} + +// LoadAftermarketDevice allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for a 1-1 relationship. +func (vehicleL) LoadAftermarketDevice(ctx context.Context, e boil.ContextExecutor, singular bool, maybeVehicle interface{}, mods queries.Applicator) error { + var slice []*Vehicle + var object *Vehicle + + if singular { + var ok bool + object, ok = maybeVehicle.(*Vehicle) + if !ok { + object = new(Vehicle) + ok = queries.SetFromEmbeddedStruct(&object, &maybeVehicle) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeVehicle)) + } + } + } else { + s, ok := maybeVehicle.(*[]*Vehicle) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeVehicle) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeVehicle)) + } + } + } + + args := make([]interface{}, 0, 1) + if singular { + if object.R == nil { + object.R = &vehicleR{} + } + args = append(args, object.ID) + } else { + Outer: + for _, obj := range slice { + if obj.R == nil { + obj.R = &vehicleR{} + } + + for _, a := range args { + if queries.Equal(a, obj.ID) { + continue Outer + } + } + + args = append(args, obj.ID) + } + } + + if len(args) == 0 { + return nil + } + + query := NewQuery( + qm.From(`identity_api.aftermarket_devices`), + qm.WhereIn(`identity_api.aftermarket_devices.vehicle_id in ?`, args...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load AftermarketDevice") + } + + var resultSlice []*AftermarketDevice + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice AftermarketDevice") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for aftermarket_devices") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for aftermarket_devices") + } + + if len(aftermarketDeviceAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + + if len(resultSlice) == 0 { + return nil + } + + if singular { + foreign := resultSlice[0] + object.R.AftermarketDevice = foreign + if foreign.R == nil { + foreign.R = &aftermarketDeviceR{} + } + foreign.R.Vehicle = object + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if queries.Equal(local.ID, foreign.VehicleID) { + local.R.AftermarketDevice = foreign + if foreign.R == nil { + foreign.R = &aftermarketDeviceR{} + } + foreign.R.Vehicle = local + break + } + } + } + + return nil +} + +// SetAftermarketDevice of the vehicle to the related item. +// Sets o.R.AftermarketDevice to related. +// Adds o to related.R.Vehicle. +func (o *Vehicle) SetAftermarketDevice(ctx context.Context, exec boil.ContextExecutor, insert bool, related *AftermarketDevice) error { + var err error + + if insert { + queries.Assign(&related.VehicleID, o.ID) + + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } else { + updateQuery := fmt.Sprintf( + "UPDATE \"identity_api\".\"aftermarket_devices\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"vehicle_id"}), + strmangle.WhereClause("\"", "\"", 2, aftermarketDevicePrimaryKeyColumns), + ) + values := []interface{}{o.ID, related.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update foreign table") + } + + queries.Assign(&related.VehicleID, o.ID) + } + + if o.R == nil { + o.R = &vehicleR{ + AftermarketDevice: related, + } + } else { + o.R.AftermarketDevice = related + } + + if related.R == nil { + related.R = &aftermarketDeviceR{ + Vehicle: o, + } + } else { + related.R.Vehicle = o + } + return nil +} + +// RemoveAftermarketDevice relationship. +// Sets o.R.AftermarketDevice to nil. +// Removes o from all passed in related items' relationships struct. +func (o *Vehicle) RemoveAftermarketDevice(ctx context.Context, exec boil.ContextExecutor, related *AftermarketDevice) error { + var err error + + queries.SetScanner(&related.VehicleID, nil) + if _, err = related.Update(ctx, exec, boil.Whitelist("vehicle_id")); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + if o.R != nil { + o.R.AftermarketDevice = nil + } + + if related == nil || related.R == nil { + return nil + } + + related.R.Vehicle = nil + + return nil +} + // Vehicles retrieves all the records using an executor. func Vehicles(mods ...qm.QueryMod) vehicleQuery { mods = append(mods, qm.From("\"identity_api\".\"vehicles\""))