From 0581965b330ff79fc9621c1e3eda7ab927c591f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Bj=C3=B8rnstad?= Date: Mon, 7 Oct 2024 09:40:31 +0200 Subject: [PATCH] Stuff Roger does when alone... (#66) * Refactor GraphQL schema and resolvers to add support for member lookup by email * make fmt * Refactor team_members.sql and team_members.sql.go to sort names case-insensitively * Refactor GetMemberByEmail to use teamMemberRoleFromSqlTeamRole for role assignment * Refactor generated.go and repository.graphqls to fix null edge issue in RepositoryConnection --- internal/v1/graphv1/gengqlv1/generated.go | 306 +++++++++++++++--- .../v1/graphv1/schema/repository.graphqls | 2 +- internal/v1/graphv1/schema/teams.graphqls | 3 + internal/v1/graphv1/teams.resolvers.go | 4 + internal/v1/team/queries.go | 17 + internal/v1/team/queries/team_members.sql | 17 +- internal/v1/team/teamsql/querier.go | 1 + internal/v1/team/teamsql/team_members.sql.go | 43 ++- 8 files changed, 336 insertions(+), 57 deletions(-) diff --git a/internal/v1/graphv1/gengqlv1/generated.go b/internal/v1/graphv1/gengqlv1/generated.go index fa318670f..1f66f88ad 100644 --- a/internal/v1/graphv1/gengqlv1/generated.go +++ b/internal/v1/graphv1/gengqlv1/generated.go @@ -822,6 +822,7 @@ type ComplexityRoot struct { Jobs func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *job.JobOrder) int KafkaTopics func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *kafkatopic.KafkaTopicOrder) int LastSuccessfulSync func(childComplexity int) int + Member func(childComplexity int, email string) int Members func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *team.TeamMemberOrder) int OpenSearchInstances func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *opensearch.OpenSearchOrder) int Purpose func(childComplexity int) int @@ -1260,6 +1261,7 @@ type SqlInstanceResolver interface { Users(ctx context.Context, obj *sqlinstance.SQLInstance, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *sqlinstance.SQLInstanceUserOrder) (*pagination.Connection[*sqlinstance.SQLInstanceUser], error) } type TeamResolver interface { + Member(ctx context.Context, obj *team.Team, email string) (*team.TeamMember, error) Members(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *team.TeamMemberOrder) (*pagination.Connection[*team.TeamMember], error) ViewerIsOwner(ctx context.Context, obj *team.Team) (bool, error) @@ -4348,6 +4350,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Team.LastSuccessfulSync(childComplexity), true + case "Team.member": + if e.complexity.Team.Member == nil { + break + } + + args, err := ec.field_Team_member_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Team.Member(childComplexity, args["email"].(string)), true + case "Team.members": if e.complexity.Team.Members == nil { break @@ -6456,7 +6470,6 @@ extend type Team { orderBy: OpenSearchOrder ): OpenSearchConnection! - "Google Cloud Storage buckets owned by the team." buckets( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -6475,7 +6488,6 @@ extend type Team { orderBy: BucketOrder ): BucketConnection! - "Kafka topics owned by the team." kafkaTopics( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -6697,7 +6709,6 @@ type Bucket implements Persistence & Node { status: BucketStatus! } - type BucketStatus { state: String! } @@ -6950,8 +6961,6 @@ type BucketEdge { node: Bucket! } - - type KafkaTopicEdge { cursor: Cursor! node: KafkaTopic! @@ -7320,7 +7329,7 @@ type RepositoryConnection { nodes: [Repository!]! "List of edges." - edges: [RepositoryEdge] + edges: [RepositoryEdge!]! } type RepositoryEdge { @@ -7612,6 +7621,9 @@ type Team implements Node { "The CDN bucket for the team. This value is managed by the Google CDN reconciler." cdnBucket: String + "Get a specific member of the team." + member(email: String!): TeamMember! + "Team members." members( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -12556,6 +12568,38 @@ func (ec *executionContext) field_Team_kafkaTopics_argsOrderBy( return zeroVal, nil } +func (ec *executionContext) field_Team_member_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + arg0, err := ec.field_Team_member_argsEmail(ctx, rawArgs) + if err != nil { + return nil, err + } + args["email"] = arg0 + return args, nil +} +func (ec *executionContext) field_Team_member_argsEmail( + ctx context.Context, + rawArgs map[string]interface{}, +) (string, error) { + // We won't call the directive if the argument is null. + // Set call_argument_directives_with_null to true to call directives + // even if the argument is null. + _, ok := rawArgs["email"] + if !ok { + var zeroVal string + return zeroVal, nil + } + + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + if tmp, ok := rawArgs["email"]; ok { + return ec.unmarshalNString2string(ctx, tmp) + } + + var zeroVal string + return zeroVal, nil +} + func (ec *executionContext) field_Team_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -14017,6 +14061,8 @@ func (ec *executionContext) fieldContext_Application_team(_ context.Context, fie return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -15947,6 +15993,8 @@ func (ec *executionContext) fieldContext_BigQueryDataset_team(_ context.Context, return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -17259,6 +17307,8 @@ func (ec *executionContext) fieldContext_Bucket_team(_ context.Context, field gr return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -18724,6 +18774,8 @@ func (ec *executionContext) fieldContext_CreateTeamPayload_team(_ context.Contex return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -20130,6 +20182,8 @@ func (ec *executionContext) fieldContext_Job_team(_ context.Context, field graph return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -22410,6 +22464,8 @@ func (ec *executionContext) fieldContext_KafkaTopic_team(_ context.Context, fiel return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -22933,6 +22989,8 @@ func (ec *executionContext) fieldContext_KafkaTopicAcl_team(_ context.Context, f return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -25131,6 +25189,8 @@ func (ec *executionContext) fieldContext_NetworkPolicyRule_targetTeam(_ context. return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -25371,6 +25431,8 @@ func (ec *executionContext) fieldContext_OpenSearch_team(_ context.Context, fiel return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -27102,6 +27164,8 @@ func (ec *executionContext) fieldContext_Query_team(ctx context.Context, field g return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -28486,6 +28550,8 @@ func (ec *executionContext) fieldContext_RedisInstance_team(_ context.Context, f return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -29674,6 +29740,8 @@ func (ec *executionContext) fieldContext_RemoveTeamMemberPayload_team(_ context. return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -29870,6 +29938,8 @@ func (ec *executionContext) fieldContext_Repository_team(_ context.Context, fiel return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -30054,11 +30124,14 @@ func (ec *executionContext) _RepositoryConnection_edges(ctx context.Context, fie return graphql.Null } if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } return graphql.Null } res := resTmp.([]pagination.Edge[*repository.Repository]) fc.Result = res - return ec.marshalORepositoryEdge2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx, field.Selections, res) + return ec.marshalNRepositoryEdge2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdgeᚄ(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_RepositoryConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -30897,6 +30970,8 @@ func (ec *executionContext) fieldContext_SqlDatabase_team(_ context.Context, fie return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -31332,6 +31407,8 @@ func (ec *executionContext) fieldContext_SqlInstance_team(_ context.Context, fie return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -33699,6 +33776,8 @@ func (ec *executionContext) fieldContext_SynchronizeTeamPayload_team(_ context.C return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -34131,6 +34210,69 @@ func (ec *executionContext) fieldContext_Team_cdnBucket(_ context.Context, field return fc, nil } +func (ec *executionContext) _Team_member(ctx context.Context, field graphql.CollectedField, obj *team.Team) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Team_member(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.Team().Member(rctx, obj, fc.Args["email"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*team.TeamMember) + fc.Result = res + return ec.marshalNTeamMember2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋteamᚐTeamMember(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Team_member(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Team", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "team": + return ec.fieldContext_TeamMember_team(ctx, field) + case "user": + return ec.fieldContext_TeamMember_user(ctx, field) + case "role": + return ec.fieldContext_TeamMember_role(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TeamMember", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Team_member_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Team_members(ctx context.Context, field graphql.CollectedField, obj *team.Team) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Team_members(ctx, field) if err != nil { @@ -35923,6 +36065,8 @@ func (ec *executionContext) fieldContext_TeamConnection_nodes(_ context.Context, return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -37356,6 +37500,8 @@ func (ec *executionContext) fieldContext_TeamDeleteKey_team(_ context.Context, f return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -37508,6 +37654,8 @@ func (ec *executionContext) fieldContext_TeamEdge_node(_ context.Context, field return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -37789,6 +37937,8 @@ func (ec *executionContext) fieldContext_TeamEnvironment_team(_ context.Context, return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -38553,6 +38703,8 @@ func (ec *executionContext) fieldContext_TeamMember_team(_ context.Context, fiel return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -41309,6 +41461,8 @@ func (ec *executionContext) fieldContext_TeamUtilizationData_team(_ context.Cont return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -42086,6 +42240,8 @@ func (ec *executionContext) fieldContext_UpdateTeamPayload_team(_ context.Contex return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) case "cdnBucket": return ec.fieldContext_Team_cdnBucket(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) case "members": return ec.fieldContext_Team_members(ctx, field) case "lastSuccessfulSync": @@ -53668,6 +53824,9 @@ func (ec *executionContext) _RepositoryConnection(ctx context.Context, sel ast.S } case "edges": out.Values[i] = ec._RepositoryConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -54994,6 +55153,42 @@ func (ec *executionContext) _Team(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = ec._Team_googleArtifactRegistry(ctx, field, obj) case "cdnBucket": out.Values[i] = ec._Team_cdnBucket(ctx, field, obj) + case "member": + 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._Team_member(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + 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 "members": field := field @@ -61897,6 +62092,54 @@ func (ec *executionContext) marshalNRepositoryConnection2ᚖgithubᚗcomᚋnais return ec._RepositoryConnection(ctx, sel, v) } +func (ec *executionContext) marshalNRepositoryEdge2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx context.Context, sel ast.SelectionSet, v pagination.Edge[*repository.Repository]) graphql.Marshaler { + return ec._RepositoryEdge(ctx, sel, &v) +} + +func (ec *executionContext) marshalNRepositoryEdge2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []pagination.Edge[*repository.Repository]) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNRepositoryEdge2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) unmarshalNRequestTeamDeletionInput2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋteamᚐRequestTeamDeletionInput(ctx context.Context, v interface{}) (team.RequestTeamDeletionInput, error) { res, err := ec.unmarshalInputRequestTeamDeletionInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -62980,6 +63223,10 @@ func (ec *executionContext) marshalNTeamEnvironment2ᚖgithubᚗcomᚋnaisᚋapi return ec._TeamEnvironment(ctx, sel, v) } +func (ec *executionContext) marshalNTeamMember2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋteamᚐTeamMember(ctx context.Context, sel ast.SelectionSet, v team.TeamMember) graphql.Marshaler { + return ec._TeamMember(ctx, sel, &v) +} + func (ec *executionContext) marshalNTeamMember2ᚕᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋteamᚐTeamMemberᚄ(ctx context.Context, sel ast.SelectionSet, v []*team.TeamMember) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -64218,51 +64465,6 @@ func (ec *executionContext) unmarshalORedisInstanceOrder2ᚖgithubᚗcomᚋnais return &res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalORepositoryEdge2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx context.Context, sel ast.SelectionSet, v pagination.Edge[*repository.Repository]) graphql.Marshaler { - return ec._RepositoryEdge(ctx, sel, &v) -} - -func (ec *executionContext) marshalORepositoryEdge2ᚕgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx context.Context, sel ast.SelectionSet, v []pagination.Edge[*repository.Repository]) graphql.Marshaler { - if v == nil { - return graphql.Null - } - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalORepositoryEdge2githubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋgraphv1ᚋpaginationᚐEdge(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() - - return ret -} - func (ec *executionContext) unmarshalOSearchType2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋv1ᚋsearchv1ᚐSearchType(ctx context.Context, v interface{}) (*searchv1.SearchType, error) { if v == nil { return nil, nil diff --git a/internal/v1/graphv1/schema/repository.graphqls b/internal/v1/graphv1/schema/repository.graphqls index 69df5dca7..804956ffd 100644 --- a/internal/v1/graphv1/schema/repository.graphqls +++ b/internal/v1/graphv1/schema/repository.graphqls @@ -56,7 +56,7 @@ type RepositoryConnection { nodes: [Repository!]! "List of edges." - edges: [RepositoryEdge] + edges: [RepositoryEdge!]! } type RepositoryEdge { diff --git a/internal/v1/graphv1/schema/teams.graphqls b/internal/v1/graphv1/schema/teams.graphqls index 55e98bde0..78850bb33 100644 --- a/internal/v1/graphv1/schema/teams.graphqls +++ b/internal/v1/graphv1/schema/teams.graphqls @@ -133,6 +133,9 @@ type Team implements Node { "The CDN bucket for the team. This value is managed by the Google CDN reconciler." cdnBucket: String + "Get a specific member of the team." + member(email: String!): TeamMember! + "Team members." members( "Get the first n items in the connection. This can be used in combination with the after parameter." diff --git a/internal/v1/graphv1/teams.resolvers.go b/internal/v1/graphv1/teams.resolvers.go index dc44882f7..06fcc54f1 100644 --- a/internal/v1/graphv1/teams.resolvers.go +++ b/internal/v1/graphv1/teams.resolvers.go @@ -244,6 +244,10 @@ func (r *removeTeamMemberPayloadResolver) Team(ctx context.Context, obj *team.Re return team.Get(ctx, obj.TeamSlug) } +func (r *teamResolver) Member(ctx context.Context, obj *team.Team, email string) (*team.TeamMember, error) { + return team.GetMemberByEmail(ctx, obj.Slug, email) +} + func (r *teamResolver) Members(ctx context.Context, obj *team.Team, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *team.TeamMemberOrder) (*pagination.Connection[*team.TeamMember], error) { page, err := pagination.ParsePage(first, after, last, before) if err != nil { diff --git a/internal/v1/team/queries.go b/internal/v1/team/queries.go index 94fb53637..51188a972 100644 --- a/internal/v1/team/queries.go +++ b/internal/v1/team/queries.go @@ -187,6 +187,23 @@ func ListForUser(ctx context.Context, userID uuid.UUID, page *pagination.Paginat return pagination.NewConvertConnection(ret, page, int32(total), toGraphUserTeam), nil } +func GetMemberByEmail(ctx context.Context, teamSlug slug.Slug, email string) (*TeamMember, error) { + q := db(ctx) + + m, err := q.GetMemberByEmail(ctx, teamsql.GetMemberByEmailParams{ + TeamSlug: teamSlug, + Email: email, + }) + if err != nil { + return nil, err + } + return &TeamMember{ + Role: teamMemberRoleFromSqlTeamRole(m.RoleName), + TeamSlug: teamSlug, + UserID: m.ID, + }, nil +} + func ListMembers(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagination, orderBy *TeamMemberOrder) (*TeamMemberConnection, error) { q := db(ctx) diff --git a/internal/v1/team/queries/team_members.sql b/internal/v1/team/queries/team_members.sql index 4cef54908..f6d445b54 100644 --- a/internal/v1/team/queries/team_members.sql +++ b/internal/v1/team/queries/team_members.sql @@ -10,10 +10,10 @@ WHERE user_roles.target_team_slug = @team_slug::slug ORDER BY CASE - WHEN @order_by::TEXT = 'name:asc' THEN users.name + WHEN @order_by::TEXT = 'name:asc' THEN LOWER(users.name) END ASC, CASE - WHEN @order_by::TEXT = 'name:desc' THEN users.name + WHEN @order_by::TEXT = 'name:desc' THEN LOWER(users.name) END DESC, CASE WHEN @order_by::TEXT = 'email:asc' THEN users.email @@ -92,6 +92,19 @@ WHERE AND user_roles.user_id = @user_id ; +-- name: GetMemberByEmail :one +SELECT + users.*, + user_roles.role_name +FROM + user_roles + JOIN teams ON teams.slug = user_roles.target_team_slug + JOIN users ON users.id = user_roles.user_id +WHERE + user_roles.target_team_slug = @team_slug::slug + AND users.email = @email +; + -- name: AddMember :exec INSERT INTO user_roles (user_id, role_name, target_team_slug) diff --git a/internal/v1/team/teamsql/querier.go b/internal/v1/team/teamsql/querier.go index 541e624e1..a1619ddc9 100644 --- a/internal/v1/team/teamsql/querier.go +++ b/internal/v1/team/teamsql/querier.go @@ -20,6 +20,7 @@ type Querier interface { Get(ctx context.Context, argSlug slug.Slug) (*Team, error) GetDeleteKey(ctx context.Context, arg GetDeleteKeyParams) (*TeamDeleteKey, error) GetMember(ctx context.Context, arg GetMemberParams) (*GetMemberRow, error) + GetMemberByEmail(ctx context.Context, arg GetMemberByEmailParams) (*GetMemberByEmailRow, error) List(ctx context.Context, arg ListParams) ([]*Team, error) ListBySlugs(ctx context.Context, slugs []slug.Slug) ([]*Team, error) ListEnvironmentsBySlug(ctx context.Context, argSlug slug.Slug) ([]*TeamAllEnvironment, error) diff --git a/internal/v1/team/teamsql/team_members.sql.go b/internal/v1/team/teamsql/team_members.sql.go index 5a510e84b..eae58b563 100644 --- a/internal/v1/team/teamsql/team_members.sql.go +++ b/internal/v1/team/teamsql/team_members.sql.go @@ -102,6 +102,45 @@ func (q *Queries) GetMember(ctx context.Context, arg GetMemberParams) (*GetMembe return &i, err } +const getMemberByEmail = `-- name: GetMemberByEmail :one +SELECT + users.id, users.email, users.name, users.external_id, + user_roles.role_name +FROM + user_roles + JOIN teams ON teams.slug = user_roles.target_team_slug + JOIN users ON users.id = user_roles.user_id +WHERE + user_roles.target_team_slug = $1::slug + AND users.email = $2 +` + +type GetMemberByEmailParams struct { + TeamSlug slug.Slug + Email string +} + +type GetMemberByEmailRow struct { + ID uuid.UUID + Email string + Name string + ExternalID string + RoleName RoleName +} + +func (q *Queries) GetMemberByEmail(ctx context.Context, arg GetMemberByEmailParams) (*GetMemberByEmailRow, error) { + row := q.db.QueryRow(ctx, getMemberByEmail, arg.TeamSlug, arg.Email) + var i GetMemberByEmailRow + err := row.Scan( + &i.ID, + &i.Email, + &i.Name, + &i.ExternalID, + &i.RoleName, + ) + return &i, err +} + const listForUser = `-- name: ListForUser :many SELECT users.id, users.email, users.name, users.external_id, @@ -185,10 +224,10 @@ WHERE user_roles.target_team_slug = $1::slug ORDER BY CASE - WHEN $2::TEXT = 'name:asc' THEN users.name + WHEN $2::TEXT = 'name:asc' THEN LOWER(users.name) END ASC, CASE - WHEN $2::TEXT = 'name:desc' THEN users.name + WHEN $2::TEXT = 'name:desc' THEN LOWER(users.name) END DESC, CASE WHEN $2::TEXT = 'email:asc' THEN users.email