Peter Sanchez: 1 api: add `organizations` and `domains` to the User type 5 files changed, 458 insertions(+), 83 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/227/mbox | git am -3Learn more about email & git
Changelog-added: organizations and domains to GraphQL User type Changelog-updated: api version bumped to 0.13.0 --- api/graph/generated.go | 333 ++++++++++++++++++++++++++++++++++ api/graph/helpers.go | 91 ++++++++++ api/graph/schema.graphqls | 2 + api/graph/schema.resolvers.go | 111 +++--------- models/listing_link.go | 4 +- 5 files changed, 458 insertions(+), 83 deletions(-) create mode 100644 api/graph/helpers.go diff --git a/api/graph/generated.go b/api/graph/generated.go index bcd11e0..af1105f 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -472,12 +472,14 @@ type ComplexityRoot struct { User struct { CreatedOn func(childComplexity int) int + Domains func(childComplexity int) int Email func(childComplexity int) int ID func(childComplexity int) int IsEmailVerified func(childComplexity int) int IsLocked func(childComplexity int) int LockReason func(childComplexity int) int Name func(childComplexity int) int + Organizations func(childComplexity int) int } UserCursor struct { @@ -605,6 +607,9 @@ type QueryResolver interface { } type UserResolver interface { ID(ctx context.Context, obj *models.User) (int, error) + + Organizations(ctx context.Context, obj *models.User) ([]*models.Organization, error) + Domains(ctx context.Context, obj *models.User) ([]*models.Domain, error) } type executableSchema struct { @@ -2906,6 +2911,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.User.CreatedOn(childComplexity), true + case "User.domains": + if e.complexity.User.Domains == nil { + break + } + + return e.complexity.User.Domains(childComplexity), true + case "User.email": if e.complexity.User.Email == nil { break @@ -2948,6 +2960,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.User.Name(childComplexity), true + case "User.organizations": + if e.complexity.User.Organizations == nil { + break + } + + return e.complexity.User.Organizations(childComplexity), true + case "UserCursor.pageInfo": if e.complexity.UserCursor.PageInfo == nil { break @@ -7104,6 +7123,10 @@ func (ec *executionContext) fieldContext_EmailPostUser_user(_ context.Context, f return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -11080,6 +11103,10 @@ func (ec *executionContext) fieldContext_Mutation_register(ctx context.Context, return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -11183,6 +11210,10 @@ func (ec *executionContext) fieldContext_Mutation_completeRegister(ctx context.C return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -11286,6 +11317,10 @@ func (ec *executionContext) fieldContext_Mutation_updateProfile(ctx context.Cont return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -13520,6 +13555,10 @@ func (ec *executionContext) fieldContext_Mutation_updateAdminUser(ctx context.Co return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -18315,6 +18354,10 @@ func (ec *executionContext) fieldContext_Query_me(_ context.Context, field graph return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -19353,6 +19396,10 @@ func (ec *executionContext) fieldContext_Query_getOrgMembers(ctx context.Context return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -20941,6 +20988,10 @@ func (ec *executionContext) fieldContext_Query_getUser(ctx context.Context, fiel return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -22525,6 +22576,212 @@ func (ec *executionContext) fieldContext_User_lockReason(_ context.Context, fiel return fc, nil } +func (ec *executionContext) _User_organizations(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_organizations(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) (any, error) { + directive0 := func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.User().Organizations(rctx, obj) + } + + directive1 := func(ctx context.Context) (any, error) { + scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "ORGS") + if err != nil { + var zeroVal []*models.Organization + return zeroVal, err + } + kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO") + if err != nil { + var zeroVal []*models.Organization + return zeroVal, err + } + if ec.directives.Access == nil { + var zeroVal []*models.Organization + return zeroVal, errors.New("directive access is not implemented") + } + return ec.directives.Access(ctx, obj, directive0, scope, kind) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.Organization); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*links/models.Organization`, tmp) + }) + 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.([]*models.Organization) + fc.Result = res + return ec.marshalNOrganization2ᚕᚖlinksᚋmodelsᚐOrganizationᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_organizations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + 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_Organization_id(ctx, field) + case "ownerId": + return ec.fieldContext_Organization_ownerId(ctx, field) + case "orgType": + return ec.fieldContext_Organization_orgType(ctx, field) + case "name": + return ec.fieldContext_Organization_name(ctx, field) + case "slug": + return ec.fieldContext_Organization_slug(ctx, field) + case "image": + return ec.fieldContext_Organization_image(ctx, field) + case "imageUrl": + return ec.fieldContext_Organization_imageUrl(ctx, field) + case "timezone": + return ec.fieldContext_Organization_timezone(ctx, field) + case "settings": + return ec.fieldContext_Organization_settings(ctx, field) + case "isActive": + return ec.fieldContext_Organization_isActive(ctx, field) + case "visibility": + return ec.fieldContext_Organization_visibility(ctx, field) + case "createdOn": + return ec.fieldContext_Organization_createdOn(ctx, field) + case "updatedOn": + return ec.fieldContext_Organization_updatedOn(ctx, field) + case "ownerName": + return ec.fieldContext_Organization_ownerName(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _User_domains(ctx context.Context, field graphql.CollectedField, obj *models.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_domains(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) (any, error) { + directive0 := func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.User().Domains(rctx, obj) + } + + directive1 := func(ctx context.Context) (any, error) { + scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "DOMAINS") + if err != nil { + var zeroVal []*models.Domain + return zeroVal, err + } + kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO") + if err != nil { + var zeroVal []*models.Domain + return zeroVal, err + } + if ec.directives.Access == nil { + var zeroVal []*models.Domain + return zeroVal, errors.New("directive access is not implemented") + } + return ec.directives.Access(ctx, obj, directive0, scope, kind) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*models.Domain); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*links/models.Domain`, tmp) + }) + 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.([]*models.Domain) + fc.Result = res + return ec.marshalNDomain2ᚕᚖlinksᚋmodelsᚐDomain(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_domains(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + 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_Domain_id(ctx, field) + case "name": + return ec.fieldContext_Domain_name(ctx, field) + case "lookupName": + return ec.fieldContext_Domain_lookupName(ctx, field) + case "orgId": + return ec.fieldContext_Domain_orgId(ctx, field) + case "orgSlug": + return ec.fieldContext_Domain_orgSlug(ctx, field) + case "service": + return ec.fieldContext_Domain_service(ctx, field) + case "level": + return ec.fieldContext_Domain_level(ctx, field) + case "status": + return ec.fieldContext_Domain_status(ctx, field) + case "isActive": + return ec.fieldContext_Domain_isActive(ctx, field) + case "createdOn": + return ec.fieldContext_Domain_createdOn(ctx, field) + case "updatedOn": + return ec.fieldContext_Domain_updatedOn(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Domain", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _UserCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.UserCursor) (ret graphql.Marshaler) { fc, err := ec.fieldContext_UserCursor_result(ctx, field) if err != nil { @@ -22578,6 +22835,10 @@ func (ec *executionContext) fieldContext_UserCursor_result(_ context.Context, fi return ec.fieldContext_User_isLocked(ctx, field) case "lockReason": return ec.fieldContext_User_lockReason(ctx, field) + case "organizations": + return ec.fieldContext_User_organizations(ctx, field) + case "domains": + return ec.fieldContext_User_domains(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -31170,6 +31431,78 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "organizations": + 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._User_organizations(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 "domains": + 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._User_domains(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) }) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/api/graph/helpers.go b/api/graph/helpers.go new file mode 100644 index 0000000..5dc7d08 --- /dev/null +++ b/api/graph/helpers.go @@ -0,0 +1,91 @@ +package graph + +import ( + "context" + "links" + "links/api/graph/model" + "links/models" + + sq "github.com/Masterminds/squirrel" + "netlandish.com/x/gobwebs/database" +) + +// freshDBContext returns a context with a fresh DBI instance, preventing +// concurrent reuse of the request's shared transaction. Required when gqlgen +// resolves multiple DB-hitting fields in parallel on the same parent type. +func freshDBContext(ctx context.Context) context.Context { + dbi := database.DBIForContext(ctx) + if dbi != nil { + ctx = database.DBIContext(ctx, database.NewDBI(dbi.GetDB())) + } + return ctx +} + +func getOrganizationsForUser(ctx context.Context, userID int, search *string) ([]*models.Organization, error) { + opts := &database.FilterOptions{ + Filter: sq.Eq{"o.owner_id": userID}, + OrderBy: "o.created_on ASC", + } + if search != nil && *search != "" { + s := links.ParseSearch(*search) + opts.Filter = sq.And{ + opts.Filter, + sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug ) + @@ to_tsquery('simple', ?)`, s), + } + } + return models.GetOrganizations(ctx, opts) +} + +func getDomainsForUser(ctx context.Context, user *models.User, orgSlug *string, service *model.DomainService) ([]*models.Domain, bool, error) { + var ( + org *models.Organization + orgSent bool + ) + orgs, err := user.GetOrgs(ctx, models.OrgUserPermissionRead) + if err != nil { + return nil, false, err + } + if orgSlug == nil || *orgSlug == "" { + for _, o := range orgs { + if o.OwnerID == int(user.ID) && o.OrgType == models.OrgTypeUser { + org = o + break + } + } + } else { + orgSent = true + for _, o := range orgs { + if o.Slug == *orgSlug { + org = o + break + } + } + } + if org == nil { + return nil, true, nil + } + opts := &database.FilterOptions{ + Filter: sq.Eq{"d.org_id": org.ID}, + } + if !orgSent { + opts.Filter = sq.Or{ + opts.Filter, + sq.And{ + sq.Eq{"d.org_id": nil}, + sq.Eq{"d.level": models.DomainLevelSystem}, + }, + } + } + if service != nil { + opts.Filter = sq.And{ + opts.Filter, + sq.Eq{"d.service": *service}, + } + } + domains, err := models.GetDomains(ctx, opts) + if err != nil { + return nil, false, err + } + return domains, false, nil +} diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index a4d3713..efe1df7 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -170,6 +170,8 @@ type User { isEmailVerified: Boolean! @access(scope: PROFILE, kind: RO) isLocked: Boolean! @access(scope: PROFILE, kind: RO) lockReason: String! @access(scope: PROFILE, kind: RO) + organizations: [Organization!]! @access(scope: ORGS, kind: RO) + domains: [Domain]! @access(scope: DOMAINS, kind: RO) } type BillingSettings { diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 3dc1210..bdb6ccc 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -4479,6 +4479,7 @@ func (r *mutationResolver) RenameTag(ctx context.Context, input model.TagInput) return payload, nil } +// DeleteAccount is the resolver for the deleteAccount field. func (r *mutationResolver) DeleteAccount(ctx context.Context, input model.DeleteAccountInput) (*model.DeletePayload, error) { tokenUser := oauth2.ForContext(ctx) if tokenUser == nil { @@ -5197,7 +5198,7 @@ func (r *qRCodeResolver) ImageURL(ctx context.Context, obj *models.QRCode) (*str func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) { return &model.Version{ Major: 0, - Minor: 12, + Minor: 13, Patch: 0, DeprecationDate: nil, }, nil @@ -5220,36 +5221,7 @@ func (r *queryResolver) GetOrganizations(ctx context.Context, input *model.GetOr return nil, valid.ErrAuthorization } user := tokenUser.User.(*models.User) - opts := &database.FilterOptions{ - Filter: sq.Eq{"o.owner_id": user.ID}, - OrderBy: "o.created_on ASC", - } - // XXX Uncomment when we decide on a path for org members with admin write permissions - //opts := &database.FilterOptions{ - // Filter: sq.Or{ - // sq.Eq{"o.owner_id": user.ID}, - // sq.And{ - // sq.Eq{"ou.user_id": user.ID}, - // sq.GtOrEq{"ou.permission": models.OrgUserPermissionAdminWrite}, - // sq.Eq{"ou.is_active": true}, - // }, - // }, - // OrderBy: "o.created_on ASC", - //} - if input.Search != nil && *input.Search != "" { - s := links.ParseSearch(*input.Search) - opts.Filter = sq.And{ - opts.Filter, - sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug ) - @@ to_tsquery('simple', ?)`, s), - } - } - - orgs, err := models.GetOrganizations(ctx, opts) - if err != nil { - return nil, err - } - return orgs, err + return getOrganizationsForUser(ctx, int(user.ID), input.Search) } // GetOrganization is the resolver for the getOrganization field. @@ -6036,67 +6008,19 @@ func (r *queryResolver) GetDomains(ctx context.Context, orgSlug *string, service user := tokenUser.User.(*models.User) lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user) lt := localizer.GetLocalizer(lang) - ctx = timezone.Context(ctx, links.GetUserTZ(user)) - var ( - org *models.Organization - orgSent bool - ) - orgs, err := user.GetOrgs(ctx, models.OrgUserPermissionRead) + domains, notFound, err := getDomainsForUser(ctx, user, orgSlug, service) if err != nil { return nil, err } - if orgSlug == nil || *orgSlug == "" { - // No org given, default to user - for _, o := range orgs { - if o.OwnerID == int(user.ID) && o.OrgType == models.OrgTypeUser { - org = o - break - } - } - } else { - orgSent = true - for _, o := range orgs { - if o.Slug == *orgSlug { - org = o - break - } - } - } - - if org == nil { + if notFound { validator := valid.New(ctx) validator.Error( "%s", lt.Translate("Unable to find suitable organization")). WithCode(valid.ErrNotFoundCode) return nil, nil } - - opts := &database.FilterOptions{ - Filter: sq.Eq{"d.org_id": org.ID}, - } - if !orgSent { - // If no organization is specified then include system level domains as well - opts.Filter = sq.Or{ - opts.Filter, - sq.And{ - sq.Eq{"d.org_id": nil}, - sq.Eq{"d.level": models.DomainLevelSystem}, - }, - } - } - if service != nil { - opts.Filter = sq.And{ - opts.Filter, - sq.Eq{"d.service": *service}, - } - } - - domains, err := models.GetDomains(ctx, opts) - if err != nil { - return nil, err - } return domains, nil } @@ -7701,6 +7625,31 @@ func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) { return int(obj.ID), nil } +// Organizations is the resolver for the organizations field. +func (r *userResolver) Organizations(ctx context.Context, obj *models.User) ([]*models.Organization, error) { + return getOrganizationsForUser(freshDBContext(ctx), int(obj.ID), nil) +} + +// Domains is the resolver for the domains field. +func (r *userResolver) Domains(ctx context.Context, obj *models.User) ([]*models.Domain, error) { + ctx = freshDBContext(ctx) + lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), obj) + lt := localizer.GetLocalizer(lang) + ctx = timezone.Context(ctx, links.GetUserTZ(obj)) + + domains, notFound, err := getDomainsForUser(ctx, obj, nil, nil) + if err != nil { + return nil, err + } + if notFound { + validator := valid.New(ctx) + validator.Error("%s", lt.Translate("Unable to find suitable organization")). + WithCode(valid.ErrNotFoundCode) + return nil, nil + } + return domains, nil +} + // AuditLog returns AuditLogResolver implementation. func (r *Resolver) AuditLog() AuditLogResolver { return &auditLogResolver{r} } diff --git a/models/listing_link.go b/models/listing_link.go index 36f7cec..addbec1 100644 --- a/models/listing_link.go +++ b/models/listing_link.go @@ -162,7 +162,7 @@ func GetListingLinksAnalytics(ctx context.Context, opts *database.FilterOptions) if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error { q := opts.GetBuilder(nil) rows, err := q. - Columns("ll.id", "ll.title", "ll.url", "SUM(dt.clicks) AS counter"). + Columns("ll.id", "ll.title", "ll.url", "ll.created_on", "ll.updated_on", "SUM(dt.clicks) AS counter"). From("listing_links ll"). Join("daily_totals dt ON ll.id = dt.listing_link_id"). GroupBy("ll.id"). @@ -180,7 +180,7 @@ func GetListingLinksAnalytics(ctx context.Context, opts *database.FilterOptions) for rows.Next() { var l ListingLink - if err = rows.Scan(&l.ID, &l.Title, &l.URL, &l.Clicks); err != nil { + if err = rows.Scan(&l.ID, &l.Title, &l.URL, &l.CreatedOn, &l.UpdatedOn, &l.Clicks); err != nil { return err } err = l.ToLocalTZ(tz) -- 2.52.0
Applied. To git@git.code.netlandish.com:~netlandish/links 6a64e3b..4a69bb1 master -> master