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