Peter Sanchez: 1 api: add `getBaseURL` query. 10 files changed, 261 insertions(+), 12 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/220/mbox | git am -3Learn more about email & git
also fixed issue with a naming conflict when running `make schema` Changelog-added: `getBaseURL` query to api Changelog-updated: api version to 0.11.0 Changelog-fixed: naming conflict with `models.TagInput` and api generated `TagInput` --- api/api_test.go | 51 ++++++++++++ api/graph/generated.go | 152 ++++++++++++++++++++++++++++++++++ api/graph/schema.graphqls | 3 + api/graph/schema.resolvers.go | 46 +++++++++- helpers.go | 6 +- models/base_url.go | 5 +- models/models.go | 4 +- models/tag_link_shorts.go | 2 +- models/tag_links.go | 2 +- models/tag_listing.go | 2 +- 10 files changed, 261 insertions(+), 12 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index 9b97a7a..edf5ab6 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3578,4 +3578,55 @@ func TestAPI(t *testing.T) { c.NoError(err) c.Equal(0, len(tagLinksAAfter), "Link should have 0 tags after update with empty tags") }) + + t.Run("get base url by hash", func(t *testing.T) { + _, err := sq.Update("base_urls"). + Set("public_ready", true). + Where("id = ?", 1). + PlaceholderFormat(database.GetPlaceholderFormat()). + RunWith(srv.DB). + Exec() + c.NoError(err) + + type GraphQLResponse struct { + BaseURL *models.BaseURL `json:"getBaseURL"` + } + var result GraphQLResponse + op := gqlclient.NewOperation(`query GetBaseURL($hash: String!) { + getBaseURL(hash: $hash) { + id + url + hash + counter + visibility + createdOn + updatedOn + } + }`) + op.Var("hash", "abcdefg") + err = links.Execute(ctx, op, &result) + c.NoError(err) + c.NotNil(result.BaseURL) + c.Equal("http://base.com", result.BaseURL.URL) + c.Equal("abcdefg", result.BaseURL.Hash) + }) + + t.Run("get base url not found", func(t *testing.T) { + type GraphQLResponse struct { + BaseURL *models.BaseURL `json:"getBaseURL"` + } + var result GraphQLResponse + op := gqlclient.NewOperation(`query GetBaseURL($hash: String!) { + getBaseURL(hash: $hash) { + id + url + hash + } + }`) + op.Var("hash", "nonexistent-hash") + err := links.Execute(ctx, op, &result) + c.Error(err) + c.Contains(err.Error(), "BaseURL Not Found") + }) + } diff --git a/api/graph/generated.go b/api/graph/generated.go index 9b78c73..78b7cce 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -414,6 +414,7 @@ type ComplexityRoot struct { GetAdminOrgStats func(childComplexity int, id int) int GetAdminOrganizations func(childComplexity int, input *model.GetAdminOrganizationsInput) int GetAuditLogs func(childComplexity int, input *model.AuditLogInput) int + GetBaseURL func(childComplexity int, hash string) int GetBookmarks func(childComplexity int, hash string, tags *string) int GetDomain func(childComplexity int, id int) int GetDomains func(childComplexity int, orgSlug *string, service *model.DomainService) int @@ -574,6 +575,7 @@ type QueryResolver interface { GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error) GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) (*model.PopularLinkCursor, error) GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error) + GetBaseURL(ctx context.Context, hash string) (*models.BaseURL, error) GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error) GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error) GetTags(ctx context.Context, input model.GetTagsInput) (*model.TagCursor, error) @@ -2481,6 +2483,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Query.GetAuditLogs(childComplexity, args["input"].(*model.AuditLogInput)), true + case "Query.getBaseURL": + if e.complexity.Query.GetBaseURL == nil { + break + } + + args, err := ec.field_Query_getBaseURL_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.GetBaseURL(childComplexity, args["hash"].(string)), true + case "Query.getBookmarks": if e.complexity.Query.GetBookmarks == nil { break @@ -3606,6 +3620,17 @@ func (ec *executionContext) field_Query_getAuditLogs_args(ctx context.Context, r return args, nil } +func (ec *executionContext) field_Query_getBaseURL_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "hash", ec.unmarshalNString2string) + if err != nil { + return nil, err + } + args["hash"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_getBookmarks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -18739,6 +18764,114 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f return fc, nil } +func (ec *executionContext) _Query_getBaseURL(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_getBaseURL(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.Query().GetBaseURL(rctx, fc.Args["hash"].(string)) + } + + directive1 := func(ctx context.Context) (any, error) { + scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS") + if err != nil { + var zeroVal *models.BaseURL + return zeroVal, err + } + kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO") + if err != nil { + var zeroVal *models.BaseURL + return zeroVal, err + } + if ec.directives.Access == nil { + var zeroVal *models.BaseURL + return zeroVal, errors.New("directive access is not implemented") + } + return ec.directives.Access(ctx, nil, 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.BaseURL); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/models.BaseURL`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*models.BaseURL) + fc.Result = res + return ec.marshalOBaseURL2ᚖlinksᚋmodelsᚐBaseURL(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_getBaseURL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + 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_BaseURL_id(ctx, field) + case "title": + return ec.fieldContext_BaseURL_title(ctx, field) + case "url": + return ec.fieldContext_BaseURL_url(ctx, field) + case "counter": + return ec.fieldContext_BaseURL_counter(ctx, field) + case "tags": + return ec.fieldContext_BaseURL_tags(ctx, field) + case "publicReady": + return ec.fieldContext_BaseURL_publicReady(ctx, field) + case "hash": + return ec.fieldContext_BaseURL_hash(ctx, field) + case "data": + return ec.fieldContext_BaseURL_data(ctx, field) + case "visibility": + return ec.fieldContext_BaseURL_visibility(ctx, field) + case "createdOn": + return ec.fieldContext_BaseURL_createdOn(ctx, field) + case "updatedOn": + return ec.fieldContext_BaseURL_updatedOn(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type BaseURL", 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_Query_getBaseURL_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_getBookmarks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_getBookmarks(ctx, field) if err != nil { @@ -30067,6 +30200,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "getBaseURL": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_getBaseURL(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "getBookmarks": field := field diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 361ffb3..f590b98 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -870,6 +870,9 @@ type Query { "Returns a specific organization link" getOrgLink(hash: String!): OrgLink @access(scope: LINKS, kind: RO) + "Returns BaseURL. Accepts BaseURL hash or full url" + getBaseURL(hash: String!): BaseURL @access(scope: LINKS, kind: RO) + "Returns saved links (bookmarks) for a given URL. Accepts BaseURL hash or full url" getBookmarks(hash: String!, tags: String): BookmarkCursor! @access(scope: LINKS, kind: RO) diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index aab1ba5..6146a0e 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -5126,8 +5126,8 @@ 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: 10, - Patch: 3, + Minor: 11, + Patch: 0, DeprecationDate: nil, }, nil } @@ -5456,6 +5456,48 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.Or return orgLinks[0], nil } +// GetBaseURL is the resolver for the getBaseURL field. +func (r *queryResolver) GetBaseURL(ctx context.Context, hash string) (*models.BaseURL, error) { + tokenUser := oauth2.ForContext(ctx) + if tokenUser == nil { + return nil, valid.ErrAuthorization + } + user := tokenUser.User.(*models.User) + lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user) + lt := localizer.GetLocalizer(lang) + + ctx = timezone.Context(ctx, links.GetUserTZ(user)) + validator := valid.New(ctx) + + opts := &database.FilterOptions{ + Filter: sq.Eq{"b.public_ready": true}, + Limit: 1, + } + _, err := url.Parse(hash) + if err != nil { + opts.Filter = sq.And{ + opts.Filter, + sq.Eq{"b.url": links.StripURLFragment(hash)}, + } + } else { + opts.Filter = sq.And{ + opts.Filter, + sq.Eq{"b.hash": hash}, + } + } + + burls, err := models.GetBaseURLs(ctx, opts) + if err != nil { + return nil, err + } + if len(burls) == 0 { + validator.Error("%s", lt.Translate("BaseURL Not Found")). + WithCode(valid.ErrNotFoundCode) + return nil, nil + } + return burls[0], nil +} + // GetBookmarks is the resolver for the getBookmarks field. func (r *queryResolver) GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error) { tokenUser := oauth2.ForContext(ctx) diff --git a/helpers.go b/helpers.go index eb77b00..44c4c91 100644 --- a/helpers.go +++ b/helpers.go @@ -762,8 +762,8 @@ func LangForContext(ctx context.Context) string { return lang } -func ProcessTags(ctx context.Context, tags []string) ([]models.TagInput, error) { - tagInputs := make([]models.TagInput, 0) +func ProcessTags(ctx context.Context, tags []string) ([]models.ProcessedTag, error) { + tagInputs := make([]models.ProcessedTag, 0) for _, tag := range tags { originalName := strings.TrimSpace(tag) originalName = strings.TrimPrefix(originalName, "#") @@ -780,7 +780,7 @@ func ProcessTags(ctx context.Context, tags []string) ([]models.TagInput, error) if err != nil { return nil, err } - tagInputs = append(tagInputs, models.TagInput{ + tagInputs = append(tagInputs, models.ProcessedTag{ ID: Tag.ID, Name: originalName, }) diff --git a/models/base_url.go b/models/base_url.go index 5510991..140fdd0 100644 --- a/models/base_url.go +++ b/models/base_url.go @@ -64,7 +64,7 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL, q := opts.GetBuilder(nil) rows, err := q. Columns("b.id", "b.url", "b.title", "b.counter", "b.data", "b.public_ready", "b.hash", - "b.parse_attempts", "b.last_parse_attempt", "b.created_on", "b.visibility", + "b.parse_attempts", "b.last_parse_attempt", "b.created_on", "b.updated_on", "b.visibility", fmt.Sprintf("json_agg(CASE WHEN t.id IS NOT NULL THEN json_build_object('id', t.id, 'name', tl.name, 'slug', t.slug, 'createdOn', t.created_on) END ORDER BY %s)::jsonb", tagOrder)). From("base_urls b"). LeftJoin("org_links ol ON ol.base_url_id = b.id"). @@ -89,7 +89,8 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL, var tags string if err = rows.Scan(&url.ID, &url.URL, &url.Title, &url.Counter, &url.Data, &url.PublicReady, &url.Hash, &url.ParseAttempts, - &url.LastParseAttempt, &url.CreatedOn, &url.Visibility, &tags); err != nil { + &url.LastParseAttempt, &url.CreatedOn, &url.UpdatedOn, + &url.Visibility, &tags); err != nil { return err } diff --git a/models/models.go b/models/models.go index bb61912..87ea649 100644 --- a/models/models.go +++ b/models/models.go @@ -138,8 +138,8 @@ type Tag struct { Count int `db:"-" json:"count"` } -// TagInput represents a tag with its ID and user's original name -type TagInput struct { +// ProcessedTag represents a tag after processing - ID resolved + user's display name +type ProcessedTag struct { ID int Name string } diff --git a/models/tag_link_shorts.go b/models/tag_link_shorts.go index 50ff3c8..b968bde 100644 --- a/models/tag_link_shorts.go +++ b/models/tag_link_shorts.go @@ -60,7 +60,7 @@ func GetTagLinkShort(ctx context.Context, id int) (*TagLinkShort, error) { return tl, err } -func CreateBatchTagLinkShorts(ctx context.Context, linkShortID int, tags []TagInput) error { +func CreateBatchTagLinkShorts(ctx context.Context, linkShortID int, tags []ProcessedTag) error { if len(tags) == 0 { return nil } diff --git a/models/tag_links.go b/models/tag_links.go index 81f804c..7f17e65 100644 --- a/models/tag_links.go +++ b/models/tag_links.go @@ -60,7 +60,7 @@ func GetTagLink(ctx context.Context, id int) (*TagLink, error) { return tl, err } -func CreateBatchTagLinks(ctx context.Context, linkID int, tags []TagInput) error { +func CreateBatchTagLinks(ctx context.Context, linkID int, tags []ProcessedTag) error { if len(tags) == 0 { return nil } diff --git a/models/tag_listing.go b/models/tag_listing.go index dc0939d..c543620 100644 --- a/models/tag_listing.go +++ b/models/tag_listing.go @@ -60,7 +60,7 @@ func GetTagListing(ctx context.Context, id int) (*TagListing, error) { return tl, err } -func CreateBatchTagListings(ctx context.Context, listingID int, tags []TagInput) error { +func CreateBatchTagListings(ctx context.Context, listingID int, tags []ProcessedTag) error { if len(tags) == 0 { return nil } -- 2.52.0
Applied. To git@git.code.netlandish.com:~netlandish/links 3d34761..35b62fa master -> master