~sircmpwn/meta.sr.ht

b5cb6e72c85b4d11a6220398e79ed461cf000aa5 — Drew DeVault 2 months ago 4d7e1b0
API: rig up pgpkey { user }; rework some endpoints

You can never read invoices or audit logs for another user, so I've
reworked these a bit and updated top-level routing accordingly.
7 files changed, 258 insertions(+), 814 deletions(-)

M api/graph/api/generated.go
M api/graph/model/models_gen.go
M api/graph/schema.graphqls
M api/graph/schema.resolvers.go
M api/loaders/middleware.go
D api/loaders/usersbypgpkeyloader_gen.go
D api/loaders/usersbysshkeyloader_gen.go
M api/graph/api/generated.go => api/graph/api/generated.go +224 -325
@@ 38,7 38,6 @@ type Config struct {
}

type ResolverRoot interface {
	Invoice() InvoiceResolver
	Mutation() MutationResolver
	PGPKey() PGPKeyResolver
	Query() QueryResolver


@@ 61,7 60,6 @@ type ComplexityRoot struct {
		EventType func(childComplexity int) int
		ID        func(childComplexity int) int
		IPAddress func(childComplexity int) int
		User      func(childComplexity int) int
	}

	Invoice struct {


@@ 69,7 67,6 @@ type ComplexityRoot struct {
		Created   func(childComplexity int) int
		ID        func(childComplexity int) int
		Source    func(childComplexity int) int
		User      func(childComplexity int) int
		ValidThru func(childComplexity int) int
	}



@@ 101,13 98,15 @@ type ComplexityRoot struct {
	}

	Query struct {
		Me           func(childComplexity int) int
		UserByEmail  func(childComplexity int, email string) int
		UserByID     func(childComplexity int, id int) int
		UserByName   func(childComplexity int, username string) int
		UserByPGPKey func(childComplexity int, fingerprint string) int
		UserBySSHKey func(childComplexity int, key string) int
		Version      func(childComplexity int) int
		AuditLog            func(childComplexity int, cursor *model.Cursor) int
		Invoices            func(childComplexity int, cursor *model.Cursor) int
		Me                  func(childComplexity int) int
		PgpKeyByKeyID       func(childComplexity int, keyID string) int
		SSHKeyByFingerprint func(childComplexity int, fingerprint string) int
		UserByEmail         func(childComplexity int, email string) int
		UserByID            func(childComplexity int, id int) int
		UserByName          func(childComplexity int, username string) int
		Version             func(childComplexity int) int
	}

	SSHKey struct {


@@ 126,13 125,11 @@ type ComplexityRoot struct {
	}

	User struct {
		AuditLog      func(childComplexity int, cursor *model.Cursor) int
		Bio           func(childComplexity int) int
		CanonicalName func(childComplexity int) int
		Created       func(childComplexity int) int
		Email         func(childComplexity int) int
		ID            func(childComplexity int) int
		Invoices      func(childComplexity int, cursor *model.Cursor) int
		Location      func(childComplexity int) int
		PgpKeys       func(childComplexity int, cursor *model.Cursor) int
		SSHKeys       func(childComplexity int, cursor *model.Cursor) int


@@ 149,9 146,6 @@ type ComplexityRoot struct {
	}
}

type InvoiceResolver interface {
	User(ctx context.Context, obj *model1.Invoice) (*model1.User, error)
}
type MutationResolver interface {
	UpdateUser(ctx context.Context, input map[string]interface{}) (*model1.User, error)
	CreatePGPKey(ctx context.Context, key string) (*model1.PGPKey, error)


@@ 168,8 162,10 @@ type QueryResolver interface {
	UserByID(ctx context.Context, id int) (*model1.User, error)
	UserByName(ctx context.Context, username string) (*model1.User, error)
	UserByEmail(ctx context.Context, email string) (*model1.User, error)
	UserByPGPKey(ctx context.Context, fingerprint string) (*model1.User, error)
	UserBySSHKey(ctx context.Context, key string) (*model1.User, error)
	SSHKeyByFingerprint(ctx context.Context, fingerprint string) (*model1.SSHKey, error)
	PgpKeyByKeyID(ctx context.Context, keyID string) (*model1.PGPKey, error)
	Invoices(ctx context.Context, cursor *model.Cursor) (*model1.InvoiceCursor, error)
	AuditLog(ctx context.Context, cursor *model.Cursor) (*model1.AuditLogCursor, error)
}
type SSHKeyResolver interface {
	User(ctx context.Context, obj *model1.SSHKey) (*model1.User, error)


@@ 177,8 173,6 @@ type SSHKeyResolver interface {
type UserResolver interface {
	SSHKeys(ctx context.Context, obj *model1.User, cursor *model.Cursor) (*model1.SSHKeyCursor, error)
	PgpKeys(ctx context.Context, obj *model1.User, cursor *model.Cursor) (*model1.PGPKeyCursor, error)
	Invoices(ctx context.Context, obj *model1.User, cursor *model.Cursor) (*model1.InvoiceCursor, error)
	AuditLog(ctx context.Context, obj *model1.User, cursor *model.Cursor) (*model1.AuditLogCursor, error)
}

type executableSchema struct {


@@ 245,13 239,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.AuditLogEntry.IPAddress(childComplexity), true

	case "AuditLogEntry.user":
		if e.complexity.AuditLogEntry.User == nil {
			break
		}

		return e.complexity.AuditLogEntry.User(childComplexity), true

	case "Invoice.cents":
		if e.complexity.Invoice.Cents == nil {
			break


@@ 280,13 267,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.Invoice.Source(childComplexity), true

	case "Invoice.user":
		if e.complexity.Invoice.User == nil {
			break
		}

		return e.complexity.Invoice.User(childComplexity), true

	case "Invoice.validThru":
		if e.complexity.Invoice.ValidThru == nil {
			break


@@ 424,6 404,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.PGPKeyCursor.Results(childComplexity), true

	case "Query.auditLog":
		if e.complexity.Query.AuditLog == nil {
			break
		}

		args, err := ec.field_Query_auditLog_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.AuditLog(childComplexity, args["cursor"].(*model.Cursor)), true

	case "Query.invoices":
		if e.complexity.Query.Invoices == nil {
			break
		}

		args, err := ec.field_Query_invoices_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.Invoices(childComplexity, args["cursor"].(*model.Cursor)), true

	case "Query.me":
		if e.complexity.Query.Me == nil {
			break


@@ 431,65 435,65 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.Query.Me(childComplexity), true

	case "Query.userByEmail":
		if e.complexity.Query.UserByEmail == nil {
	case "Query.pgpKeyByKeyID":
		if e.complexity.Query.PgpKeyByKeyID == nil {
			break
		}

		args, err := ec.field_Query_userByEmail_args(context.TODO(), rawArgs)
		args, err := ec.field_Query_pgpKeyByKeyID_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.UserByEmail(childComplexity, args["email"].(string)), true
		return e.complexity.Query.PgpKeyByKeyID(childComplexity, args["keyID"].(string)), true

	case "Query.userByID":
		if e.complexity.Query.UserByID == nil {
	case "Query.sshKeyByFingerprint":
		if e.complexity.Query.SSHKeyByFingerprint == nil {
			break
		}

		args, err := ec.field_Query_userByID_args(context.TODO(), rawArgs)
		args, err := ec.field_Query_sshKeyByFingerprint_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.UserByID(childComplexity, args["id"].(int)), true
		return e.complexity.Query.SSHKeyByFingerprint(childComplexity, args["fingerprint"].(string)), true

	case "Query.userByName":
		if e.complexity.Query.UserByName == nil {
	case "Query.userByEmail":
		if e.complexity.Query.UserByEmail == nil {
			break
		}

		args, err := ec.field_Query_userByName_args(context.TODO(), rawArgs)
		args, err := ec.field_Query_userByEmail_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.UserByName(childComplexity, args["username"].(string)), true
		return e.complexity.Query.UserByEmail(childComplexity, args["email"].(string)), true

	case "Query.userByPGPKey":
		if e.complexity.Query.UserByPGPKey == nil {
	case "Query.userByID":
		if e.complexity.Query.UserByID == nil {
			break
		}

		args, err := ec.field_Query_userByPGPKey_args(context.TODO(), rawArgs)
		args, err := ec.field_Query_userByID_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.UserByPGPKey(childComplexity, args["fingerprint"].(string)), true
		return e.complexity.Query.UserByID(childComplexity, args["id"].(int)), true

	case "Query.userBySSHKey":
		if e.complexity.Query.UserBySSHKey == nil {
	case "Query.userByName":
		if e.complexity.Query.UserByName == nil {
			break
		}

		args, err := ec.field_Query_userBySSHKey_args(context.TODO(), rawArgs)
		args, err := ec.field_Query_userByName_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.UserBySSHKey(childComplexity, args["key"].(string)), true
		return e.complexity.Query.UserByName(childComplexity, args["username"].(string)), true

	case "Query.version":
		if e.complexity.Query.Version == nil {


@@ 561,18 565,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.SSHKeyCursor.Results(childComplexity), true

	case "User.auditLog":
		if e.complexity.User.AuditLog == nil {
			break
		}

		args, err := ec.field_User_auditLog_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.User.AuditLog(childComplexity, args["cursor"].(*model.Cursor)), true

	case "User.bio":
		if e.complexity.User.Bio == nil {
			break


@@ 608,18 600,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.User.ID(childComplexity), true

	case "User.invoices":
		if e.complexity.User.Invoices == nil {
			break
		}

		args, err := ec.field_User_invoices_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.User.Invoices(childComplexity, args["cursor"].(*model.Cursor)), true

	case "User.location":
		if e.complexity.User.Location == nil {
			break


@@ 800,8 780,6 @@ type User implements Entity {

  sshKeys(cursor: Cursor): SSHKeyCursor!
  pgpKeys(cursor: Cursor): PGPKeyCursor!
  invoices(cursor: Cursor): InvoiceCursor!
  auditLog(cursor: Cursor): AuditLogCursor!
}

type SSHKey {


@@ 847,7 825,6 @@ type Invoice {
  id: Int!
  created: Time!
  cents: Int!
  user: User!
  validThru: Time!
  source: String
}


@@ 865,7 842,6 @@ type InvoiceCursor {
type AuditLogEntry {
  id: Int!
  created: Time!
  user: User!
  ipAddress: String!
  eventType: String!
  details: String


@@ 892,8 868,18 @@ type Query {
  userByID(id: Int!): User
  userByName(username: String!): User
  userByEmail(email: String!): User
  userByPGPKey(fingerprint: String!): User
  userBySSHKey(key: String!): User

  # Returns a specific SSH key
  sshKeyByFingerprint(fingerprint: String!): SSHKey

  # Returns a specific PGP key
  pgpKeyByKeyID(keyID: String!): PGPKey

  # Returns invoices for the authenticated user
  invoices(cursor: Cursor): InvoiceCursor!

  # Returns the audit log for the authenticated user
  auditLog(cursor: Cursor): AuditLogCursor!
}

input UserInput {


@@ 1009,49 995,49 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
	return args, nil
}

func (ec *executionContext) field_Query_userByEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_auditLog_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 string
	if tmp, ok := rawArgs["email"]; ok {
		arg0, err = ec.unmarshalNString2string(ctx, tmp)
	var arg0 *model.Cursor
	if tmp, ok := rawArgs["cursor"]; ok {
		arg0, err = ec.unmarshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgqlᚗsrᚗhtᚋmodelᚐCursor(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["email"] = arg0
	args["cursor"] = arg0
	return args, nil
}

func (ec *executionContext) field_Query_userByID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_invoices_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 int
	if tmp, ok := rawArgs["id"]; ok {
		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
	var arg0 *model.Cursor
	if tmp, ok := rawArgs["cursor"]; ok {
		arg0, err = ec.unmarshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgqlᚗsrᚗhtᚋmodelᚐCursor(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["id"] = arg0
	args["cursor"] = arg0
	return args, nil
}

func (ec *executionContext) field_Query_userByName_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_pgpKeyByKeyID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 string
	if tmp, ok := rawArgs["username"]; ok {
	if tmp, ok := rawArgs["keyID"]; ok {
		arg0, err = ec.unmarshalNString2string(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["username"] = arg0
	args["keyID"] = arg0
	return args, nil
}

func (ec *executionContext) field_Query_userByPGPKey_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_sshKeyByFingerprint_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 string


@@ 1065,45 1051,45 @@ func (ec *executionContext) field_Query_userByPGPKey_args(ctx context.Context, r
	return args, nil
}

func (ec *executionContext) field_Query_userBySSHKey_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_userByEmail_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 string
	if tmp, ok := rawArgs["key"]; ok {
	if tmp, ok := rawArgs["email"]; ok {
		arg0, err = ec.unmarshalNString2string(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["key"] = arg0
	args["email"] = arg0
	return args, nil
}

func (ec *executionContext) field_User_auditLog_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_userByID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 *model.Cursor
	if tmp, ok := rawArgs["cursor"]; ok {
		arg0, err = ec.unmarshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgqlᚗsrᚗhtᚋmodelᚐCursor(ctx, tmp)
	var arg0 int
	if tmp, ok := rawArgs["id"]; ok {
		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["cursor"] = arg0
	args["id"] = arg0
	return args, nil
}

func (ec *executionContext) field_User_invoices_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
func (ec *executionContext) field_Query_userByName_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	var arg0 *model.Cursor
	if tmp, ok := rawArgs["cursor"]; ok {
		arg0, err = ec.unmarshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgqlᚗsrᚗhtᚋmodelᚐCursor(ctx, tmp)
	var arg0 string
	if tmp, ok := rawArgs["username"]; ok {
		arg0, err = ec.unmarshalNString2string(ctx, tmp)
		if err != nil {
			return nil, err
		}
	}
	args["cursor"] = arg0
	args["username"] = arg0
	return args, nil
}



@@ 1304,40 1290,6 @@ func (ec *executionContext) _AuditLogEntry_created(ctx context.Context, field gr
	return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res)
}

func (ec *executionContext) _AuditLogEntry_user(ctx context.Context, field graphql.CollectedField, obj *model1.AuditLogEntry) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "AuditLogEntry",
		Field:    field,
		Args:     nil,
		IsMethod: false,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.User, nil
	})
	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.(*model1.User)
	fc.Result = res
	return ec.marshalNUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
}

func (ec *executionContext) _AuditLogEntry_ipAddress(ctx context.Context, field graphql.CollectedField, obj *model1.AuditLogEntry) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 1539,40 1491,6 @@ func (ec *executionContext) _Invoice_cents(ctx context.Context, field graphql.Co
	return ec.marshalNInt2int(ctx, field.Selections, res)
}

func (ec *executionContext) _Invoice_user(ctx context.Context, field graphql.CollectedField, obj *model1.Invoice) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "Invoice",
		Field:    field,
		Args:     nil,
		IsMethod: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Invoice().User(rctx, obj)
	})
	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.(*model1.User)
	fc.Result = res
	return ec.marshalNUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
}

func (ec *executionContext) _Invoice_validThru(ctx context.Context, field graphql.CollectedField, obj *model1.Invoice) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 2359,7 2277,7 @@ func (ec *executionContext) _Query_userByEmail(ctx context.Context, field graphq
	return ec.marshalOUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
}

func (ec *executionContext) _Query_userByPGPKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
func (ec *executionContext) _Query_sshKeyByFingerprint(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))


@@ 2375,7 2293,7 @@ func (ec *executionContext) _Query_userByPGPKey(ctx context.Context, field graph

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_Query_userByPGPKey_args(ctx, rawArgs)
	args, err := ec.field_Query_sshKeyByFingerprint_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null


@@ 2383,7 2301,7 @@ func (ec *executionContext) _Query_userByPGPKey(ctx context.Context, field graph
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Query().UserByPGPKey(rctx, args["fingerprint"].(string))
		return ec.resolvers.Query().SSHKeyByFingerprint(rctx, args["fingerprint"].(string))
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 2392,12 2310,12 @@ func (ec *executionContext) _Query_userByPGPKey(ctx context.Context, field graph
	if resTmp == nil {
		return graphql.Null
	}
	res := resTmp.(*model1.User)
	res := resTmp.(*model1.SSHKey)
	fc.Result = res
	return ec.marshalOUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
	return ec.marshalOSSHKey2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐSSHKey(ctx, field.Selections, res)
}

func (ec *executionContext) _Query_userBySSHKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
func (ec *executionContext) _Query_pgpKeyByKeyID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))


@@ 2413,7 2331,7 @@ func (ec *executionContext) _Query_userBySSHKey(ctx context.Context, field graph

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_Query_userBySSHKey_args(ctx, rawArgs)
	args, err := ec.field_Query_pgpKeyByKeyID_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null


@@ 2421,7 2339,7 @@ func (ec *executionContext) _Query_userBySSHKey(ctx context.Context, field graph
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Query().UserBySSHKey(rctx, args["key"].(string))
		return ec.resolvers.Query().PgpKeyByKeyID(rctx, args["keyID"].(string))
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 2430,9 2348,91 @@ func (ec *executionContext) _Query_userBySSHKey(ctx context.Context, field graph
	if resTmp == nil {
		return graphql.Null
	}
	res := resTmp.(*model1.User)
	res := resTmp.(*model1.PGPKey)
	fc.Result = res
	return ec.marshalOUser2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐUser(ctx, field.Selections, res)
	return ec.marshalOPGPKey2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐPGPKey(ctx, field.Selections, res)
}

func (ec *executionContext) _Query_invoices(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "Query",
		Field:    field,
		Args:     nil,
		IsMethod: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_Query_invoices_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Query().Invoices(rctx, args["cursor"].(*model.Cursor))
	})
	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.(*model1.InvoiceCursor)
	fc.Result = res
	return ec.marshalNInvoiceCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐInvoiceCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Query_auditLog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "Query",
		Field:    field,
		Args:     nil,
		IsMethod: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_Query_auditLog_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Query().AuditLog(rctx, args["cursor"].(*model.Cursor))
	})
	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.(*model1.AuditLogCursor)
	fc.Result = res
	return ec.marshalNAuditLogCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAuditLogCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {


@@ 3183,88 3183,6 @@ func (ec *executionContext) _User_pgpKeys(ctx context.Context, field graphql.Col
	return ec.marshalNPGPKeyCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐPGPKeyCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _User_invoices(ctx context.Context, field graphql.CollectedField, obj *model1.User) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "User",
		Field:    field,
		Args:     nil,
		IsMethod: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_User_invoices_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.User().Invoices(rctx, obj, args["cursor"].(*model.Cursor))
	})
	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.(*model1.InvoiceCursor)
	fc.Result = res
	return ec.marshalNInvoiceCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐInvoiceCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _User_auditLog(ctx context.Context, field graphql.CollectedField, obj *model1.User) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	fc := &graphql.FieldContext{
		Object:   "User",
		Field:    field,
		Args:     nil,
		IsMethod: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	rawArgs := field.ArgumentMap(ec.Variables)
	args, err := ec.field_User_auditLog_args(ctx, rawArgs)
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	fc.Args = args
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.User().AuditLog(rctx, obj, args["cursor"].(*model.Cursor))
	})
	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.(*model1.AuditLogCursor)
	fc.Result = res
	return ec.marshalNAuditLogCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋmetaᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAuditLogCursor(ctx, field.Selections, res)
}

func (ec *executionContext) _Version_major(ctx context.Context, field graphql.CollectedField, obj *model1.Version) (ret graphql.Marshaler) {
	defer func() {
		if r := recover(); r != nil {


@@ 4527,11 4445,6 @@ func (ec *executionContext) _AuditLogEntry(ctx context.Context, sel ast.Selectio
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "user":
			out.Values[i] = ec._AuditLogEntry_user(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "ipAddress":
			out.Values[i] = ec._AuditLogEntry_ipAddress(ctx, field, obj)
			if out.Values[i] == graphql.Null {


@@ 4569,36 4482,22 @@ func (ec *executionContext) _Invoice(ctx context.Context, sel ast.SelectionSet, 
		case "id":
			out.Values[i] = ec._Invoice_id(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
				invalids++
			}
		case "created":
			out.Values[i] = ec._Invoice_created(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
				invalids++
			}
		case "cents":
			out.Values[i] = ec._Invoice_cents(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
				invalids++
			}
		case "user":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Invoice_user(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "validThru":
			out.Values[i] = ec._Invoice_validThru(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&invalids, 1)
				invalids++
			}
		case "source":
			out.Values[i] = ec._Invoice_source(ctx, field, obj)


@@ 4859,7 4758,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
				res = ec._Query_userByEmail(ctx, field)
				return res
			})
		case "userByPGPKey":
		case "sshKeyByFingerprint":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {


@@ 4867,10 4766,10 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_userByPGPKey(ctx, field)
				res = ec._Query_sshKeyByFingerprint(ctx, field)
				return res
			})
		case "userBySSHKey":
		case "pgpKeyByKeyID":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {


@@ 4878,7 4777,35 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_userBySSHKey(ctx, field)
				res = ec._Query_pgpKeyByKeyID(ctx, field)
				return res
			})
		case "invoices":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_invoices(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "auditLog":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_auditLog(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "__type":


@@ 5063,34 4990,6 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
				}
				return res
			})
		case "invoices":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._User_invoices(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		case "auditLog":
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._User_auditLog(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +0 -1
@@ 20,7 20,6 @@ type AuditLogCursor struct {
type AuditLogEntry struct {
	ID        int       `json:"id"`
	Created   time.Time `json:"created"`
	User      *User     `json:"user"`
	IPAddress string    `json:"ipAddress"`
	EventType string    `json:"eventType"`
	Details   *string   `json:"details"`

M api/graph/schema.graphqls => api/graph/schema.graphqls +12 -6
@@ 34,8 34,6 @@ type User implements Entity {

  sshKeys(cursor: Cursor): SSHKeyCursor!
  pgpKeys(cursor: Cursor): PGPKeyCursor!
  invoices(cursor: Cursor): InvoiceCursor!
  auditLog(cursor: Cursor): AuditLogCursor!
}

type SSHKey {


@@ 81,7 79,6 @@ type Invoice {
  id: Int!
  created: Time!
  cents: Int!
  user: User!
  validThru: Time!
  source: String
}


@@ 99,7 96,6 @@ type InvoiceCursor {
type AuditLogEntry {
  id: Int!
  created: Time!
  user: User!
  ipAddress: String!
  eventType: String!
  details: String


@@ 126,8 122,18 @@ type Query {
  userByID(id: Int!): User
  userByName(username: String!): User
  userByEmail(email: String!): User
  userByPGPKey(fingerprint: String!): User
  userBySSHKey(key: String!): User

  # Returns a specific SSH key
  sshKeyByFingerprint(fingerprint: String!): SSHKey

  # Returns a specific PGP key
  pgpKeyByKeyID(keyID: String!): PGPKey

  # Returns invoices for the authenticated user
  invoices(cursor: Cursor): InvoiceCursor!

  # Returns the audit log for the authenticated user
  auditLog(cursor: Cursor): AuditLogCursor!
}

input UserInput {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +22 -30
@@ 15,10 15,6 @@ import (
	"git.sr.ht/~sircmpwn/meta.sr.ht/api/loaders"
)

func (r *invoiceResolver) User(ctx context.Context, obj *model.Invoice) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *mutationResolver) UpdateUser(ctx context.Context, input map[string]interface{}) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
}


@@ 40,7 36,7 @@ func (r *mutationResolver) DeleteSSHKey(ctx context.Context, key string) (*model
}

func (r *pGPKeyResolver) User(ctx context.Context, obj *model.PGPKey) (*model.User, error) {
	panic(fmt.Errorf("not implemented"))
	return loaders.ForContext(ctx).UsersByID.Load(obj.UserID)
}

func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {


@@ 78,11 74,30 @@ func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.U
	return loaders.ForContext(ctx).UsersByEmail.Load(email)
}

func (r *queryResolver) UserByPGPKey(ctx context.Context, fingerprint string) (*model.User, error) {
func (r *queryResolver) SSHKeyByFingerprint(ctx context.Context, fingerprint string) (*model.SSHKey, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) UserBySSHKey(ctx context.Context, key string) (*model.User, error) {
func (r *queryResolver) PgpKeyByKeyID(ctx context.Context, keyID string) (*model.PGPKey, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Invoices(ctx context.Context, cursor *gqlmodel.Cursor) (*model.InvoiceCursor, error) {
	if cursor == nil {
		cursor = gqlmodel.NewCursor(nil)
	}

	inv := (&model.Invoice{})
	query := database.
		Select(ctx, inv).
		From(`invoice`).
		Where(`user_id = ?`, auth.ForContext(ctx).ID)

	invoices, cursor := inv.QueryWithCursor(ctx, database.ForContext(ctx), query, cursor)
	return &model.InvoiceCursor{invoices, cursor}, nil
}

func (r *queryResolver) AuditLog(ctx context.Context, cursor *gqlmodel.Cursor) (*model.AuditLogCursor, error) {
	panic(fmt.Errorf("not implemented"))
}



@@ 120,28 135,6 @@ func (r *userResolver) PgpKeys(ctx context.Context, obj *model.User, cursor *gql
	return &model.PGPKeyCursor{keys, cursor}, nil
}

func (r *userResolver) Invoices(ctx context.Context, obj *model.User, cursor *gqlmodel.Cursor) (*model.InvoiceCursor, error) {
	if cursor == nil {
		cursor = gqlmodel.NewCursor(nil)
	}

	inv := (&model.Invoice{})
	query := database.
		Select(ctx, inv).
		From(`invoice`).
		Where(`user_id = ?`, obj.ID)

	invoices, cursor := inv.QueryWithCursor(ctx, database.ForContext(ctx), query, cursor)
	return &model.InvoiceCursor{invoices, cursor}, nil
}

func (r *userResolver) AuditLog(ctx context.Context, obj *model.User, cursor *gqlmodel.Cursor) (*model.AuditLogCursor, error) {
	panic(fmt.Errorf("not implemented"))
}

// Invoice returns api.InvoiceResolver implementation.
func (r *Resolver) Invoice() api.InvoiceResolver { return &invoiceResolver{r} }

// Mutation returns api.MutationResolver implementation.
func (r *Resolver) Mutation() api.MutationResolver { return &mutationResolver{r} }



@@ 157,7 150,6 @@ func (r *Resolver) SSHKey() api.SSHKeyResolver { return &sSHKeyResolver{r} }
// User returns api.UserResolver implementation.
func (r *Resolver) User() api.UserResolver { return &userResolver{r} }

type invoiceResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type pGPKeyResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

M api/loaders/middleware.go => api/loaders/middleware.go +0 -4
@@ 3,8 3,6 @@ package loaders
//go:generate ./gen UsersByIDLoader int api/graph/model.User
//go:generate ./gen UsersByNameLoader string api/graph/model.User
//go:generate ./gen UsersByEmailLoader string api/graph/model.User
//go:generate ./gen UsersByPGPKeyLoader string api/graph/model.User
//go:generate ./gen UsersBySSHKeyLoader string api/graph/model.User

import (
	"context"


@@ 30,8 28,6 @@ type Loaders struct {
	UsersByID     UsersByIDLoader
	UsersByName   UsersByNameLoader
	UsersByEmail  UsersByEmailLoader
	UsersByPGPKey UsersByPGPKeyLoader
	UsersBySSHKey UsersBySSHKeyLoader
}

func fetchUsersByID(ctx context.Context,

D api/loaders/usersbypgpkeyloader_gen.go => api/loaders/usersbypgpkeyloader_gen.go +0 -224
@@ 1,224 0,0 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.

package loaders

import (
	"sync"
	"time"

	"git.sr.ht/~sircmpwn/meta.sr.ht/api/graph/model"
)

// UsersByPGPKeyLoaderConfig captures the config to create a new UsersByPGPKeyLoader
type UsersByPGPKeyLoaderConfig struct {
	// Fetch is a method that provides the data for the loader
	Fetch func(keys []string) ([]*model.User, []error)

	// Wait is how long wait before sending a batch
	Wait time.Duration

	// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
	MaxBatch int
}

// NewUsersByPGPKeyLoader creates a new UsersByPGPKeyLoader given a fetch, wait, and maxBatch
func NewUsersByPGPKeyLoader(config UsersByPGPKeyLoaderConfig) *UsersByPGPKeyLoader {
	return &UsersByPGPKeyLoader{
		fetch:    config.Fetch,
		wait:     config.Wait,
		maxBatch: config.MaxBatch,
	}
}

// UsersByPGPKeyLoader batches and caches requests
type UsersByPGPKeyLoader struct {
	// this method provides the data for the loader
	fetch func(keys []string) ([]*model.User, []error)

	// how long to done before sending a batch
	wait time.Duration

	// this will limit the maximum number of keys to send in one batch, 0 = no limit
	maxBatch int

	// INTERNAL

	// lazily created cache
	cache map[string]*model.User

	// the current batch. keys will continue to be collected until timeout is hit,
	// then everything will be sent to the fetch method and out to the listeners
	batch *usersByPGPKeyLoaderBatch

	// mutex to prevent races
	mu sync.Mutex
}

type usersByPGPKeyLoaderBatch struct {
	keys    []string
	data    []*model.User
	error   []error
	closing bool
	done    chan struct{}
}

// Load a User by key, batching and caching will be applied automatically
func (l *UsersByPGPKeyLoader) Load(key string) (*model.User, error) {
	return l.LoadThunk(key)()
}

// LoadThunk returns a function that when called will block waiting for a User.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *UsersByPGPKeyLoader) LoadThunk(key string) func() (*model.User, error) {
	l.mu.Lock()
	if it, ok := l.cache[key]; ok {
		l.mu.Unlock()
		return func() (*model.User, error) {
			return it, nil
		}
	}
	if l.batch == nil {
		l.batch = &usersByPGPKeyLoaderBatch{done: make(chan struct{})}
	}
	batch := l.batch
	pos := batch.keyIndex(l, key)
	l.mu.Unlock()

	return func() (*model.User, error) {
		<-batch.done

		var data *model.User
		if pos < len(batch.data) {
			data = batch.data[pos]
		}

		var err error
		// its convenient to be able to return a single error for everything
		if len(batch.error) == 1 {
			err = batch.error[0]
		} else if batch.error != nil {
			err = batch.error[pos]
		}

		if err == nil {
			l.mu.Lock()
			l.unsafeSet(key, data)
			l.mu.Unlock()
		}

		return data, err
	}
}

// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *UsersByPGPKeyLoader) LoadAll(keys []string) ([]*model.User, []error) {
	results := make([]func() (*model.User, error), len(keys))

	for i, key := range keys {
		results[i] = l.LoadThunk(key)
	}

	users := make([]*model.User, len(keys))
	errors := make([]error, len(keys))
	for i, thunk := range results {
		users[i], errors[i] = thunk()
	}
	return users, errors
}

// LoadAllThunk returns a function that when called will block waiting for a Users.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *UsersByPGPKeyLoader) LoadAllThunk(keys []string) func() ([]*model.User, []error) {
	results := make([]func() (*model.User, error), len(keys))
	for i, key := range keys {
		results[i] = l.LoadThunk(key)
	}
	return func() ([]*model.User, []error) {
		users := make([]*model.User, len(keys))
		errors := make([]error, len(keys))
		for i, thunk := range results {
			users[i], errors[i] = thunk()
		}
		return users, errors
	}
}

// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *UsersByPGPKeyLoader) Prime(key string, value *model.User) bool {
	l.mu.Lock()
	var found bool
	if _, found = l.cache[key]; !found {
		// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
		// and end up with the whole cache pointing to the same value.
		cpy := *value
		l.unsafeSet(key, &cpy)
	}
	l.mu.Unlock()
	return !found
}

// Clear the value at key from the cache, if it exists
func (l *UsersByPGPKeyLoader) Clear(key string) {
	l.mu.Lock()
	delete(l.cache, key)
	l.mu.Unlock()
}

func (l *UsersByPGPKeyLoader) unsafeSet(key string, value *model.User) {
	if l.cache == nil {
		l.cache = map[string]*model.User{}
	}
	l.cache[key] = value
}

// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *usersByPGPKeyLoaderBatch) keyIndex(l *UsersByPGPKeyLoader, key string) int {
	for i, existingKey := range b.keys {
		if key == existingKey {
			return i
		}
	}

	pos := len(b.keys)
	b.keys = append(b.keys, key)
	if pos == 0 {
		go b.startTimer(l)
	}

	if l.maxBatch != 0 && pos >= l.maxBatch-1 {
		if !b.closing {
			b.closing = true
			l.batch = nil
			go b.end(l)
		}
	}

	return pos
}

func (b *usersByPGPKeyLoaderBatch) startTimer(l *UsersByPGPKeyLoader) {
	time.Sleep(l.wait)
	l.mu.Lock()

	// we must have hit a batch limit and are already finalizing this batch
	if b.closing {
		l.mu.Unlock()
		return
	}

	l.batch = nil
	l.mu.Unlock()

	b.end(l)
}

func (b *usersByPGPKeyLoaderBatch) end(l *UsersByPGPKeyLoader) {
	b.data, b.error = l.fetch(b.keys)
	close(b.done)
}

D api/loaders/usersbysshkeyloader_gen.go => api/loaders/usersbysshkeyloader_gen.go +0 -224
@@ 1,224 0,0 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.

package loaders

import (
	"sync"
	"time"

	"git.sr.ht/~sircmpwn/meta.sr.ht/api/graph/model"
)

// UsersBySSHKeyLoaderConfig captures the config to create a new UsersBySSHKeyLoader
type UsersBySSHKeyLoaderConfig struct {
	// Fetch is a method that provides the data for the loader
	Fetch func(keys []string) ([]*model.User, []error)

	// Wait is how long wait before sending a batch
	Wait time.Duration

	// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
	MaxBatch int
}

// NewUsersBySSHKeyLoader creates a new UsersBySSHKeyLoader given a fetch, wait, and maxBatch
func NewUsersBySSHKeyLoader(config UsersBySSHKeyLoaderConfig) *UsersBySSHKeyLoader {
	return &UsersBySSHKeyLoader{
		fetch:    config.Fetch,
		wait:     config.Wait,
		maxBatch: config.MaxBatch,
	}
}

// UsersBySSHKeyLoader batches and caches requests
type UsersBySSHKeyLoader struct {
	// this method provides the data for the loader
	fetch func(keys []string) ([]*model.User, []error)

	// how long to done before sending a batch
	wait time.Duration

	// this will limit the maximum number of keys to send in one batch, 0 = no limit
	maxBatch int

	// INTERNAL

	// lazily created cache
	cache map[string]*model.User

	// the current batch. keys will continue to be collected until timeout is hit,
	// then everything will be sent to the fetch method and out to the listeners
	batch *usersBySSHKeyLoaderBatch

	// mutex to prevent races
	mu sync.Mutex
}

type usersBySSHKeyLoaderBatch struct {
	keys    []string
	data    []*model.User
	error   []error
	closing bool
	done    chan struct{}
}

// Load a User by key, batching and caching will be applied automatically
func (l *UsersBySSHKeyLoader) Load(key string) (*model.User, error) {
	return l.LoadThunk(key)()
}

// LoadThunk returns a function that when called will block waiting for a User.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *UsersBySSHKeyLoader) LoadThunk(key string) func() (*model.User, error) {
	l.mu.Lock()
	if it, ok := l.cache[key]; ok {
		l.mu.Unlock()
		return func() (*model.User, error) {
			return it, nil
		}
	}
	if l.batch == nil {
		l.batch = &usersBySSHKeyLoaderBatch{done: make(chan struct{})}
	}
	batch := l.batch
	pos := batch.keyIndex(l, key)
	l.mu.Unlock()

	return func() (*model.User, error) {
		<-batch.done

		var data *model.User
		if pos < len(batch.data) {
			data = batch.data[pos]
		}

		var err error
		// its convenient to be able to return a single error for everything
		if len(batch.error) == 1 {
			err = batch.error[0]
		} else if batch.error != nil {
			err = batch.error[pos]
		}

		if err == nil {
			l.mu.Lock()
			l.unsafeSet(key, data)
			l.mu.Unlock()
		}

		return data, err
	}
}

// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *UsersBySSHKeyLoader) LoadAll(keys []string) ([]*model.User, []error) {
	results := make([]func() (*model.User, error), len(keys))

	for i, key := range keys {
		results[i] = l.LoadThunk(key)
	}

	users := make([]*model.User, len(keys))
	errors := make([]error, len(keys))
	for i, thunk := range results {
		users[i], errors[i] = thunk()
	}
	return users, errors
}

// LoadAllThunk returns a function that when called will block waiting for a Users.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *UsersBySSHKeyLoader) LoadAllThunk(keys []string) func() ([]*model.User, []error) {
	results := make([]func() (*model.User, error), len(keys))
	for i, key := range keys {
		results[i] = l.LoadThunk(key)
	}
	return func() ([]*model.User, []error) {
		users := make([]*model.User, len(keys))
		errors := make([]error, len(keys))
		for i, thunk := range results {
			users[i], errors[i] = thunk()
		}
		return users, errors
	}
}

// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *UsersBySSHKeyLoader) Prime(key string, value *model.User) bool {
	l.mu.Lock()
	var found bool
	if _, found = l.cache[key]; !found {
		// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
		// and end up with the whole cache pointing to the same value.
		cpy := *value
		l.unsafeSet(key, &cpy)
	}
	l.mu.Unlock()
	return !found
}

// Clear the value at key from the cache, if it exists
func (l *UsersBySSHKeyLoader) Clear(key string) {
	l.mu.Lock()
	delete(l.cache, key)
	l.mu.Unlock()
}

func (l *UsersBySSHKeyLoader) unsafeSet(key string, value *model.User) {
	if l.cache == nil {
		l.cache = map[string]*model.User{}
	}
	l.cache[key] = value
}

// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *usersBySSHKeyLoaderBatch) keyIndex(l *UsersBySSHKeyLoader, key string) int {
	for i, existingKey := range b.keys {
		if key == existingKey {
			return i
		}
	}

	pos := len(b.keys)
	b.keys = append(b.keys, key)
	if pos == 0 {
		go b.startTimer(l)
	}

	if l.maxBatch != 0 && pos >= l.maxBatch-1 {
		if !b.closing {
			b.closing = true
			l.batch = nil
			go b.end(l)
		}
	}

	return pos
}

func (b *usersBySSHKeyLoaderBatch) startTimer(l *UsersBySSHKeyLoader) {
	time.Sleep(l.wait)
	l.mu.Lock()

	// we must have hit a batch limit and are already finalizing this batch
	if b.closing {
		l.mu.Unlock()
		return
	}

	l.batch = nil
	l.mu.Unlock()

	b.end(l)
}

func (b *usersBySSHKeyLoaderBatch) end(l *UsersBySSHKeyLoader) {
	b.data, b.error = l.fetch(b.keys)
	close(b.done)
}