M api/api_test.go => api/api_test.go +11 -3
@@ 625,13 625,21 @@ func TestAPI(t *testing.T) {
c.NoError(err)
org := orgs[0]
+ // We know this has already been saved earlier. We need the hash to fetch it so
+ // let's query the db
+ ols, err := models.GetOrgLinks(dbCtx,
+ &database.FilterOptions{Filter: sq.Eq{"ol.id": 3}, Limit: 1},
+ )
+ c.NoError(err)
+ ol := ols[0]
+
type GraphQLResponse struct {
Link models.OrgLink `json:"getOrgLink"`
}
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($id: Int!) {
- getOrgLink(id: $id) {
+ `query getOrgLink($hash: String!) {
+ getOrgLink(hash: $hash) {
id
title
url
@@ 651,7 659,7 @@ func TestAPI(t *testing.T) {
}
}
}`)
- op.Var("id", 3)
+ op.Var("hash", ol.Hash)
err = links.Execute(ctx, op, &result)
c.NoError(err)
c.Equal("New title", result.Link.Title)
M api/graph/generated.go => api/graph/generated.go +75 -78
@@ 259,8 259,8 @@ type ComplexityRoot struct {
BaseURLID func(childComplexity int) int
CreatedOn func(childComplexity int) int
Description func(childComplexity int) int
+ Hash func(childComplexity int) int
ID func(childComplexity int) int
- NoteHash func(childComplexity int) int
OrgID func(childComplexity int) int
OrgSlug func(childComplexity int) int
Starred func(childComplexity int) int
@@ 368,7 368,7 @@ type ComplexityRoot struct {
GetListing func(childComplexity int, input *model.GetListingDetailInput) int
GetListingLink func(childComplexity int, slug string, id int, domainID int) int
GetListings func(childComplexity int, input *model.GetListingInput) int
- GetOrgLink func(childComplexity int, id *int, hash *string) int
+ GetOrgLink func(childComplexity int, hash string) int
GetOrgLinks func(childComplexity int, input *model.GetLinkInput) int
GetOrgMembers func(childComplexity int, orgSlug string) int
GetOrganization func(childComplexity int, id int) int
@@ 477,7 477,7 @@ type QueryResolver interface {
GetOrganization(ctx context.Context, id int) (*models.Organization, error)
GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error)
GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) ([]*models.BaseURL, error)
- GetOrgLink(ctx context.Context, id *int, hash *string) (*models.OrgLink, error)
+ GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error)
GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error)
GetOrgMembers(ctx context.Context, orgSlug string) ([]*models.User, error)
GetDomains(ctx context.Context, orgSlug *string, service *int) ([]*models.Domain, error)
@@ 1625,19 1625,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.OrgLink.Description(childComplexity), true
- case "OrgLink.id":
- if e.complexity.OrgLink.ID == nil {
+ case "OrgLink.hash":
+ if e.complexity.OrgLink.Hash == nil {
break
}
- return e.complexity.OrgLink.ID(childComplexity), true
+ return e.complexity.OrgLink.Hash(childComplexity), true
- case "OrgLink.noteHash":
- if e.complexity.OrgLink.NoteHash == nil {
+ case "OrgLink.id":
+ if e.complexity.OrgLink.ID == nil {
break
}
- return e.complexity.OrgLink.NoteHash(childComplexity), true
+ return e.complexity.OrgLink.ID(childComplexity), true
case "OrgLink.orgId":
if e.complexity.OrgLink.OrgID == nil {
@@ 2232,7 2232,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
- return e.complexity.Query.GetOrgLink(childComplexity, args["id"].(*int), args["hash"].(*string)), true
+ return e.complexity.Query.GetOrgLink(childComplexity, args["hash"].(string)), true
case "Query.getOrgLinks":
if e.complexity.Query.GetOrgLinks == nil {
@@ 3405,24 3405,15 @@ func (ec *executionContext) field_Query_getListings_args(ctx context.Context, ra
func (ec *executionContext) field_Query_getOrgLink_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 {
- ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
- arg0, err = ec.unmarshalOInt2ᚖint(ctx, tmp)
- if err != nil {
- return nil, err
- }
- }
- args["id"] = arg0
- var arg1 *string
+ var arg0 string
if tmp, ok := rawArgs["hash"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("hash"))
- arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+ arg0, err = ec.unmarshalNString2string(ctx, tmp)
if err != nil {
return nil, err
}
}
- args["hash"] = arg1
+ args["hash"] = arg0
return args, nil
}
@@ 8899,6 8890,8 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f
return ec.fieldContext_OrgLink_description(ctx, field)
case "url":
return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
case "baseUrlId":
return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
case "orgId":
@@ 8915,8 8908,6 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f
return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
case "type":
return ec.fieldContext_OrgLink_type(ctx, field)
- case "noteHash":
- return ec.fieldContext_OrgLink_noteHash(ctx, field)
case "tags":
return ec.fieldContext_OrgLink_tags(ctx, field)
case "author":
@@ 9022,6 9013,8 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context
return ec.fieldContext_OrgLink_description(ctx, field)
case "url":
return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
case "baseUrlId":
return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
case "orgId":
@@ 9038,8 9031,6 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context
return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
case "type":
return ec.fieldContext_OrgLink_type(ctx, field)
- case "noteHash":
- return ec.fieldContext_OrgLink_noteHash(ctx, field)
case "tags":
return ec.fieldContext_OrgLink_tags(ctx, field)
case "author":
@@ 9234,6 9225,8 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f
return ec.fieldContext_OrgLink_description(ctx, field)
case "url":
return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
case "baseUrlId":
return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
case "orgId":
@@ 9250,8 9243,6 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f
return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
case "type":
return ec.fieldContext_OrgLink_type(ctx, field)
- case "noteHash":
- return ec.fieldContext_OrgLink_noteHash(ctx, field)
case "tags":
return ec.fieldContext_OrgLink_tags(ctx, field)
case "author":
@@ 12114,6 12105,50 @@ func (ec *executionContext) fieldContext_OrgLink_url(ctx context.Context, field
return fc, nil
}
+func (ec *executionContext) _OrgLink_hash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_OrgLink_hash(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Hash, 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.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_OrgLink_hash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "OrgLink",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _OrgLink_baseUrlId(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_OrgLink_baseUrlId(ctx, field)
if err != nil {
@@ 12609,47 12644,6 @@ func (ec *executionContext) fieldContext_OrgLink_type(ctx context.Context, field
return fc, nil
}
-func (ec *executionContext) _OrgLink_noteHash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
- fc, err := ec.fieldContext_OrgLink_noteHash(ctx, field)
- if err != nil {
- return graphql.Null
- }
- ctx = graphql.WithFieldContext(ctx, fc)
- defer func() {
- if r := recover(); r != nil {
- ec.Error(ctx, ec.Recover(ctx, r))
- ret = graphql.Null
- }
- }()
- resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
- ctx = rctx // use context from middleware stack in children
- return obj.NoteHash, nil
- })
- if err != nil {
- ec.Error(ctx, err)
- return graphql.Null
- }
- if resTmp == nil {
- return graphql.Null
- }
- res := resTmp.(string)
- fc.Result = res
- return ec.marshalOString2string(ctx, field.Selections, res)
-}
-
-func (ec *executionContext) fieldContext_OrgLink_noteHash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
- fc = &graphql.FieldContext{
- Object: "OrgLink",
- Field: field,
- IsMethod: false,
- IsResolver: false,
- Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
- return nil, errors.New("field of type String does not have child fields")
- },
- }
- return fc, nil
-}
-
func (ec *executionContext) _OrgLink_tags(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_OrgLink_tags(ctx, field)
if err != nil {
@@ 13003,6 12997,8 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(ctx context.Contex
return ec.fieldContext_OrgLink_description(ctx, field)
case "url":
return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
case "baseUrlId":
return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
case "orgId":
@@ 13019,8 13015,6 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(ctx context.Contex
return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
case "type":
return ec.fieldContext_OrgLink_type(ctx, field)
- case "noteHash":
- return ec.fieldContext_OrgLink_noteHash(ctx, field)
case "tags":
return ec.fieldContext_OrgLink_tags(ctx, field)
case "author":
@@ 16286,7 16280,7 @@ func (ec *executionContext) _Query_getOrgLink(ctx context.Context, field graphql
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
- return ec.resolvers.Query().GetOrgLink(rctx, fc.Args["id"].(*int), fc.Args["hash"].(*string))
+ return ec.resolvers.Query().GetOrgLink(rctx, fc.Args["hash"].(string))
}
directive1 := func(ctx context.Context) (interface{}, error) {
scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS")
@@ 16343,6 16337,8 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
return ec.fieldContext_OrgLink_description(ctx, field)
case "url":
return ec.fieldContext_OrgLink_url(ctx, field)
+ case "hash":
+ return ec.fieldContext_OrgLink_hash(ctx, field)
case "baseUrlId":
return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
case "orgId":
@@ 16359,8 16355,6 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
case "type":
return ec.fieldContext_OrgLink_type(ctx, field)
- case "noteHash":
- return ec.fieldContext_OrgLink_noteHash(ctx, field)
case "tags":
return ec.fieldContext_OrgLink_tags(ctx, field)
case "author":
@@ 24797,6 24791,13 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
+ case "hash":
+
+ out.Values[i] = ec._OrgLink_hash(ctx, field, obj)
+
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&invalids, 1)
+ }
case "baseUrlId":
field := field
@@ 24863,10 24864,6 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
- case "noteHash":
-
- out.Values[i] = ec._OrgLink_noteHash(ctx, field, obj)
-
case "tags":
out.Values[i] = ec._OrgLink_tags(ctx, field, obj)
M api/graph/schema.graphqls => api/graph/schema.graphqls +2 -2
@@ 163,6 163,7 @@ type OrgLink {
title: String!
description: String!
url: String!
+ hash: String!
baseUrlId: NullInt! @access(scope: LINKS, kind: RO)
orgId: Int! @access(scope: LINKS, kind: RO)
userId: Int @access(scope: LINKS, kind: RO)
@@ 171,7 172,6 @@ type OrgLink {
starred: Boolean! @access(scope: LINKS, kind: RO)
archiveUrl: String!
type: Int!
- noteHash: String
tags: [Tag]!
author: String!
orgSlug: String! @access(scope: LINKS, kind: RO)
@@ 688,7 688,7 @@ type Query {
getPopularLinks(input: PopularLinksInput): [BaseURL]! @access(scope: LINKS, kind: RO)
"Returns a specific organization link"
- getOrgLink(id: Int, hash: String): OrgLink @access(scope: LINKS, kind: RO)
+ getOrgLink(hash: String!): OrgLink @access(scope: LINKS, kind: RO)
"Returns an array of organization links"
getOrgLinks(input: GetLinkInput): OrgLinkCursor! @access(scope: LINKS, kind: RO)
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +19 -41
@@ 269,6 269,7 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput)
URL: input.URL,
UserID: int(user.ID),
Type: models.OrgLinkType,
+ Hash: ksuid.New().String(),
}
if input.Description != nil {
OrgLink.Description = *input.Description
@@ 595,7 596,8 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput)
return nil, valid.ErrAuthorization
}
user := tokenUser.User.(*models.User)
- lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
+ c := server.EchoForContext(ctx)
+ lang := links.GetLangFromRequest(c.Request(), user)
lt := localizer.GetLocalizer(lang)
validator := valid.New(ctx)
@@ 650,7 652,6 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput)
WithCode(valid.ErrNotFoundCode)
return nil, nil
}
-
org := orgs[0]
// If the owner if not creating the note, add extra validation
@@ 672,7 673,6 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput)
}
// Create the note url
- c := server.EchoForContext(ctx)
noteURL, noteHash := links.CreateNoteURL(c)
// If the note is public we create a based link
@@ 701,7 701,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput)
URL: noteURL,
UserID: int(user.ID),
Type: models.NoteType,
- NoteHash: noteHash,
+ Hash: noteHash,
}
err = OrgLinkNote.Store(ctx)
@@ 4533,7 4533,7 @@ func (r *queryResolver) GetPopularLinks(ctx context.Context, input *model.Popula
}
// GetOrgLink is the resolver for the getOrgLink field.
-func (r *queryResolver) GetOrgLink(ctx context.Context, id *int, hash *string) (*models.OrgLink, error) {
+func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error) {
tokenUser := oauth2.ForContext(ctx)
if tokenUser == nil {
return nil, valid.ErrAuthorization
@@ 4544,49 4544,27 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, id *int, hash *string) (
ctx = timezone.Context(ctx, links.GetUserTZ(user))
- if id == nil && hash == nil {
- validator := valid.New(ctx)
- validator.Error(lt.Translate("No identifier id or hash was provided")).
- WithCode(valid.ErrValidationGlobalCode)
- return nil, nil
- }
-
opts := &database.FilterOptions{
- Filter: sq.Or{
- sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner
- // if the user is a member of the org is able to see private and public links
- sq.And{
- sq.Eq{"ou.user_id": user.ID},
- sq.Eq{"ou.is_active": true},
- sq.Eq{"ol.visibility": []int{
- models.OrgLinkVisibilityPublic,
- models.OrgLinkVisibilityPrivate},
+ Filter: sq.And{
+ sq.Eq{"ol.hash": hash},
+ sq.Or{
+ sq.Eq{"o.owner_id": user.ID}, // If the user is the org owner
+ // if the user is a member of the org is able to see private and public links
+ sq.And{
+ sq.Eq{"ou.user_id": user.ID},
+ sq.Eq{"ou.is_active": true},
+ sq.Eq{"ol.visibility": []int{
+ models.OrgLinkVisibilityPublic,
+ models.OrgLinkVisibilityPrivate},
+ },
},
+ // Otherwise the user is only able to see public links
+ sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
},
- // Otherwise the user is only able to see public links
- sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
},
Limit: 1,
}
- if id != nil {
- opts.Filter = sq.And{
- opts.Filter,
- sq.And{
- sq.Eq{"ol.id": *id},
- sq.Eq{"ol.type": models.OrgLinkType},
- },
- }
- } else {
- opts.Filter = sq.And{
- opts.Filter,
- sq.And{
- sq.Eq{"ol.note_hash": *hash},
- sq.Eq{"ol.type": models.NoteType},
- },
- }
- }
-
orgLinks, err := models.GetOrgLinks(ctx, opts)
if err != nil {
return nil, err
M core/import.go => core/import.go +12 -12
@@ 36,7 36,7 @@ const (
// import helper funcs
type importObj interface {
GetURL() string
- GetNoteHash() string
+ GetHash() string
GetTitle() string
GetDescription() string
IsPublic() bool
@@ 44,7 44,7 @@ type importObj interface {
IsNote() bool
SetURL(string)
SetIsNote(bool)
- SetNoteHash(string)
+ SetHash(string)
}
// Especific pinboard object representation
@@ 60,8 60,8 @@ type pinBoardObj struct {
Tags string
// We are not parsing this data from the json
- noteHash string
- isNote bool
+ hash string
+ isNote bool
}
func (p pinBoardObj) GetURL() string {
@@ 92,12 92,12 @@ func (p *pinBoardObj) SetURL(url string) {
p.Href = url
}
-func (p *pinBoardObj) SetNoteHash(hash string) {
- p.noteHash = hash
+func (p *pinBoardObj) SetHash(hash string) {
+ p.hash = hash
}
-func (p pinBoardObj) GetNoteHash() string {
- return p.noteHash
+func (p pinBoardObj) GetHash() string {
+ return p.hash
}
func (p pinBoardObj) IsUnread() bool {
@@ 140,12 140,12 @@ func (h *htmlObj) SetURL(_ string) {
// method importer
}
-func (h *htmlObj) SetNoteHash(_ string) {
+func (h *htmlObj) SetHash(_ string) {
// NOTE we don't need to implement this
// method importer
}
-func (h htmlObj) GetNoteHash() string {
+func (h htmlObj) GetHash() string {
return ""
}
@@ 188,7 188,7 @@ func processBaseURLs(obj importObj, tmpURLMap map[string]bool, urlList []string,
noteURL, noteHash := links.CreateNoteURL(c)
obj.SetURL(noteURL)
obj.SetIsNote(true)
- obj.SetNoteHash(noteHash)
+ obj.SetHash(noteHash)
}
// Get a list of base url to make a query
// of existing ones
@@ 277,7 277,7 @@ func processOrgLinks(obj importObj, baseURLMap map[string]int,
UserID: int(user.ID),
Type: linkType,
Visibility: vis,
- NoteHash: obj.GetNoteHash(),
+ Hash: obj.GetHash(),
Unread: obj.IsUnread(),
}
}
M core/routes.go => core/routes.go +32 -31
@@ 65,8 65,8 @@ func (s *Service) RegisterRoutes() {
s.eg.GET("/q/:hash", s.QRRedirect).Name = s.RouteName("qr_redirect")
s.eg.GET("/tour", s.FeatureTour).Name = s.RouteName("feature_tour")
s.eg.GET("/note/:hash", s.NoteDetail).Name = s.RouteName("note_detail")
- s.eg.GET("/link/:id", s.OrgLinkDetail).Name = s.RouteName("link_detail")
- s.eg.GET("/click/:id", s.OrgLinkRedirect).Name = s.RouteName("link_redirect")
+ s.eg.GET("/link/:hash", s.OrgLinkDetail).Name = s.RouteName("link_detail")
+ s.eg.GET("/click/:hash", s.OrgLinkRedirect).Name = s.RouteName("link_redirect")
s.eg.GET("/tag-autocomplete", s.TagAutocomplete).Name = s.RouteName("tag_autocomplete")
s.eg.Use(auth.AuthRequired())
@@ 1298,7 1298,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
archive: $archive,
orgSlug: $slug,
tags: $tags}) {
- id
+ hash
}
}`)
op.Var("title", form.Title)
@@ 1329,7 1329,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
if form.Redirect != "" {
redirect = form.Redirect
} else {
- redirect = c.Echo().Reverse(s.RouteName("link_detail"), result.Link.ID)
+ redirect = c.Echo().Reverse(s.RouteName("link_detail"), result.Link.Hash)
}
return c.Redirect(http.StatusMovedPermanently, redirect)
}
@@ 1382,16 1382,16 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
}
} else {
// OrgLink
- id, err = strconv.Atoi(c.QueryParam("linkid"))
- if err == nil {
+ hash := c.QueryParam("linkid")
+ if hash != "" {
type GraphQLResponse struct {
Link models.OrgLink `json:"getOrgLink"`
}
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($id: Int) {
- getOrgLink(id: $id) {
+ `query getOrgLink($hash: String!) {
+ getOrgLink(hash: $hash) {
title
description
url
@@ 1402,7 1402,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
}
}
}`)
- op.Var("id", id)
+ op.Var("hash", hash)
err = links.Execute(c.Request().Context(), op, &result)
if err != nil {
if graphError, ok := err.(*gqlclient.Error); ok {
@@ 1674,7 1674,7 @@ func (s *Service) UserFeed(c echo.Context) error {
desc = l.BaseURLData.Meta.Description
}
item := links.Item{
- GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.ID)),
+ GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.Hash)),
Title: l.Title,
Description: desc,
Link: l.URL,
@@ 1764,7 1764,7 @@ func (s *Service) OrgLinksList(c echo.Context) error {
starred
archiveUrl
type
- noteHash
+ hash
tags {
id
name
@@ 1997,7 1997,7 @@ func (s *Service) OrgLinksList(c echo.Context) error {
desc = l.BaseURLData.Meta.Description
}
item := links.Item{
- GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.ID)),
+ GUID: fmt.Sprintf("%s%s", domain, c.Echo().Reverse(s.RouteName("link_detail"), l.Hash)),
Title: l.Title,
Description: desc,
Link: l.URL,
@@ 2072,8 2072,8 @@ func (s *Service) OrgLinksList(c echo.Context) error {
}
func (s *Service) OrgLinkRedirect(c echo.Context) error {
- id, err := strconv.Atoi(c.Param("id"))
- if err != nil {
+ hash := c.Param("hash")
+ if hash == "" {
return echo.NotFoundHandler(c)
}
type GraphQLResponse struct {
@@ 2082,14 2082,14 @@ func (s *Service) OrgLinkRedirect(c echo.Context) error {
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($id: Int) {
- getOrgLink(id: $id) {
- id
+ `query getOrgLink($hash: String!) {
+ getOrgLink(hash: $hash) {
+ hash
url
}
}`)
- op.Var("id", id)
- err = links.Execute(c.Request().Context(), op, &result)
+ op.Var("hash", hash)
+ err := links.Execute(c.Request().Context(), op, &result)
if err != nil {
if graphError, ok := err.(*gqlclient.Error); ok {
err = links.ParseInputErrors(c, graphError, gobwebs.Map{})
@@ 2101,8 2101,8 @@ func (s *Service) OrgLinkRedirect(c echo.Context) error {
// OrgLinkDetail ...
func (s *Service) OrgLinkDetail(c echo.Context) error {
- id, err := strconv.Atoi(c.Param("id"))
- if err != nil {
+ hash := c.Param("hash")
+ if hash == "" {
return echo.NotFoundHandler(c)
}
type GraphQLResponse struct {
@@ 2111,14 2111,15 @@ func (s *Service) OrgLinkDetail(c echo.Context) error {
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($id: Int) {
- getOrgLink(id: $id) {
+ `query getOrgLink($hash: String!) {
+ getOrgLink(hash: $hash) {
id
title
description
url
userId
type
+ hash
createdOn
tags {
id
@@ 2127,8 2128,8 @@ func (s *Service) OrgLinkDetail(c echo.Context) error {
}
}
}`)
- op.Var("id", id)
- err = links.Execute(c.Request().Context(), op, &result)
+ op.Var("hash", hash)
+ err := links.Execute(c.Request().Context(), op, &result)
if err != nil {
if graphError, ok := err.(*gqlclient.Error); ok {
err = links.ParseInputErrors(c, graphError, gobwebs.Map{})
@@ 2367,7 2368,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {
}
messages.Success(c, lt.Translate("Link successfully updated."))
return c.Redirect(http.StatusMovedPermanently,
- c.Echo().Reverse(s.RouteName("link_detail"), result.Link.ID))
+ c.Echo().Reverse(s.RouteName("link_detail"), result.Link.Hash))
}
var canEdit bool
@@ 3218,7 3219,7 @@ func (s *Service) NoteDetail(c echo.Context) error {
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($hash: String) {
+ `query getOrgLink($hash: String!) {
getOrgLink(hash: $hash) {
id
title
@@ 3375,15 3376,15 @@ func (s *Service) NoteCreate(c echo.Context) error {
OrgSlug: formSlug,
}
- id := c.QueryParam("linkid")
- if id != "" {
+ hash := c.QueryParam("linkid")
+ if hash != "" {
type GraphQLResponse struct {
Link models.OrgLink `json:"getOrgLink"`
}
var result GraphQLResponse
op := gqlclient.NewOperation(
- `query getOrgLink($hash: String) {
+ `query getOrgLink($hash: String!) {
getOrgLink(hash: $hash) {
title
description
@@ 3394,7 3395,7 @@ func (s *Service) NoteCreate(c echo.Context) error {
}
}
}`)
- op.Var("hash", id)
+ op.Var("hash", hash)
err = links.Execute(c.Request().Context(), op, &result)
if err != nil {
if graphError, ok := err.(*gqlclient.Error); ok {
M core/routes_test.go => core/routes_test.go +4 -4
@@ 302,14 302,14 @@ func TestHandlers(t *testing.T) {
Context: e.NewContext(request, recorder),
User: loggedInUser,
}
- ctx.SetPath("/link/:id/edit")
- ctx.SetParamNames("id")
- ctx.SetParamValues("1")
+ ctx.SetPath("/link/:hash")
+ ctx.SetParamNames("hash")
+ ctx.SetParamValues("abcdefg") // link hash is "abcdefg" in mock json file
// We get the error from the API call
err = test.MakeRequestWithDomain(srv, coreService.OrgLinkDetail, ctx, domains[0])
c.NoError(err)
htmlBody := recorder.Body.String()
- c.True(strings.Contains(htmlBody, "/click/30")) // link ID is 30 in mock json file
+ c.True(strings.Contains(htmlBody, "/click/abcdefg"))
})
t.Run("link delete error", func(t *testing.T) {
M core/samples/detail_link.json => core/samples/detail_link.json +1 -0
@@ 2,6 2,7 @@
"data": {
"getOrgLink": {
"id": 30,
+ "hash": "abcdefg",
"title": "Detail org",
"url": "https://www.detail.org",
"baseUrlId": {
M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +1 -1
@@ 100,7 100,7 @@ CREATE TABLE org_links (
url TEXT NOT NULL,
description TEXT DEFAULT '',
"type" INT NOT NULL DEFAULT 0,
- note_hash TEXT DEFAULT '',
+ hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27),
base_url_id INT REFERENCES base_urls (id) ON DELETE CASCADE,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
M models/models.go => models/models.go +2 -2
@@ 90,7 90,7 @@ type OrgLink struct {
Starred bool `db:"starred" json:"starred"`
ArchiveURL string `db:"archive_url" json:"archiveUrl"`
Type int `db:"type" json:"type"`
- NoteHash string `db:"note_hash" json:"noteHash"`
+ Hash string `db:"hash" json:"hash"`
CreatedOn time.Time `db:"created_on" json:"createdOn"`
UpdatedOn time.Time `db:"updated_on" json:"updatedOn"`
@@ 346,7 346,7 @@ type ExportOrgLink struct {
Visibility int `db:"visibility" json:"visibility"`
Unread bool `db:"unread" json:"unread"`
Starred bool `db:"starred" json:"starred"`
- NoteHash string `db:"note_hash" json:"note_hash"`
+ Hash string `db:"hash" json:"hash"`
Tags []ExportTag `db:"-" json:"tags"`
CreatedOn time.Time `db:"created_on" json:"created_on"`
}
M models/org_link.go => models/org_link.go +12 -16
@@ 36,7 36,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
q := opts.GetBuilder(nil)
rows, err := q.
Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.base_url_id", "ol.org_id", "ol.user_id",
- "ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.note_hash",
+ "ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.hash",
"ol.created_on", "ol.updated_on", "o.slug", "u.full_name", "json_agg(t)::jsonb", "b.data").
From("org_links ol").
Join("organizations o ON o.id = ol.org_id").
@@ 63,7 63,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
var o OrgLink
var tags string
if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID, &o.OrgID,
- &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.NoteHash,
+ &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.Hash,
&o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags, &o.BaseURLData); err != nil {
return err
}
@@ 104,7 104,7 @@ func (o *OrgLink) Load(ctx context.Context) error {
err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
err := sq.
Select("id", "title", "url", "description", "base_url_id", "org_id", "user_id",
- "visibility", "unread", "starred", "archive_url", "type", "note_hash",
+ "visibility", "unread", "starred", "archive_url", "type", "hash",
"created_on", "updated_on").
From("org_links").
Where("id = ?", o.ID).
@@ 112,7 112,7 @@ func (o *OrgLink) Load(ctx context.Context) error {
RunWith(tx).
ScanContext(ctx, &o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID,
&o.OrgID, &o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL,
- &o.Type, &o.NoteHash, &o.CreatedOn, &o.UpdatedOn)
+ &o.Type, &o.Hash, &o.CreatedOn, &o.UpdatedOn)
if err != nil {
if err == sql.ErrNoRows {
return nil
@@ 133,9 133,9 @@ func (o *OrgLink) Store(ctx context.Context) error {
err = sq.
Insert("org_links").
Columns("title", "url", "description", "base_url_id", "org_id", "user_id", "visibility",
- "unread", "starred", "archive_url", "type", "note_hash").
+ "unread", "starred", "archive_url", "type", "hash").
Values(o.Title, o.URL, o.Description, o.BaseURLID, o.OrgID, o.UserID, o.Visibility,
- o.Unread, o.Starred, o.ArchiveURL, o.Type, o.NoteHash).
+ o.Unread, o.Starred, o.ArchiveURL, o.Type, o.Hash).
Suffix(`RETURNING id, created_on, updated_on`).
PlaceholderFormat(sq.Dollar).
RunWith(tx).
@@ 154,7 154,7 @@ func (o *OrgLink) Store(ctx context.Context) error {
Set("starred", o.Starred).
Set("archive_url", o.ArchiveURL).
Set("type", o.Type).
- Set("note_hash", o.NoteHash).
+ Set("hash", o.Hash).
Where("id = ?", o.ID).
Suffix(`RETURNING (updated_on)`).
PlaceholderFormat(sq.Dollar).
@@ 208,11 208,7 @@ func (o *OrgLink) TagsToString() string {
// to a users bookmarks
func (o *OrgLink) QueryParams() url.Values {
qs := url.Values{}
- if o.Type == NoteType {
- qs.Set("linkid", o.NoteHash)
- } else {
- qs.Set("linkid", fmt.Sprint(o.ID))
- }
+ qs.Set("linkid", o.Hash)
return qs
}
@@ 265,11 261,11 @@ func OrgLinkStoreBatch(ctx context.Context, links []*OrgLink) error {
batch := sq.
Insert("org_links").
Columns("title", "url", "description", "base_url_id", "org_id", "user_id", "visibility",
- "note_hash", "type", "unread")
+ "hash", "type", "unread")
for _, link := range links {
batch = batch.Values(link.Title, link.URL, link.Description, link.BaseURLID, link.OrgID,
- link.UserID, link.Visibility, link.NoteHash, link.Type, link.Unread)
+ link.UserID, link.Visibility, link.Hash, link.Type, link.Unread)
}
_, err := batch.
PlaceholderFormat(sq.Dollar).
@@ 290,7 286,7 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor
q := opts.GetBuilder(nil)
rows, err := q.
Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.visibility",
- "ol.unread", "ol.starred", "ol.note_hash", "ol.created_on", "json_agg(t)::jsonb").
+ "ol.unread", "ol.starred", "ol.hash", "ol.created_on", "json_agg(t)::jsonb").
From("org_links ol").
LeftJoin("tag_links tl ON tl.org_link_id = ol.id").
LeftJoin("tags t ON t.id = tl.tag_id").
@@ 311,7 307,7 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor
var o ExportOrgLink
var tags string
if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.Visibility,
- &o.Unread, &o.Starred, &o.NoteHash, &o.CreatedOn, &tags); err != nil {
+ &o.Unread, &o.Starred, &o.Hash, &o.CreatedOn, &tags); err != nil {
return err
}
re := regexp.MustCompile(`(,\s)?null,?`)
M models/schema.sql => models/schema.sql +3 -1
@@ 1,3 1,5 @@
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
CREATE OR REPLACE FUNCTION update_updated_on_column()
RETURNS TRIGGER AS $$
BEGIN
@@ 100,7 102,7 @@ CREATE TABLE org_links (
url TEXT NOT NULL,
description TEXT DEFAULT '',
"type" INT NOT NULL DEFAULT 0,
- note_hash TEXT DEFAULT '',
+ hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27),
base_url_id INT REFERENCES base_urls (id) ON DELETE CASCADE,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
M templates/link_detail.html => templates/link_detail.html +2 -2
@@ 10,7 10,7 @@
<header class="d-flex flex-row justify-between items-center">
<div class="d-flex flex-row items-center">
<div class="ml-1">
- <h2><a class="text-dark" href="{{reverse "core:link_redirect" .link.ID}}">{{.link.Title}}</a></h2>
+ <h2><a class="text-dark" href="{{reverse "core:link_redirect" .link.Hash}}">{{.link.Title}}</a></h2>
<div class="d-flex flex-row items-center">
{{if .IsPrivate}}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:20px; margin-right: 7px;">
@@ 29,7 29,7 @@
</div>
</header>
{{if eq .link.Type 0}}
- <p><a href="{{reverse "core:link_redirect" .link.ID}}" target="_blank">{{stripCommonProtocol .link.URL}}</a></p>
+ <p><a href="{{reverse "core:link_redirect" .link.Hash}}" target="_blank">{{stripCommonProtocol .link.URL}}</a></p>
{{end}}
<p>{{.link.Description}}</p>
{{if .link.Tags}}