Skip to content
This repository has been archived by the owner on Oct 17, 2020. It is now read-only.

Add UpdateURL to graphql schema and resolvers #654

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion backend/app/adapter/gqlapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func TestGraphQlAPI(t *testing.T) {
riskDetector,
)

updater := url.NewUpdaterPersist(
&urlRepo,
&urlRelationRepo,
keyGen,
longLinkValidator,
customAliasValidator,
tm,
riskDetector,
)

s := requester.NewReCaptchaFake(requester.VerifyResponse{})
verifier := requester.NewVerifier(s)
auth := authenticator.NewAuthenticatorFake(time.Now(), time.Hour)
Expand All @@ -61,7 +71,7 @@ func TestGraphQlAPI(t *testing.T) {
changeLogRepo := repository.NewChangeLogFake([]entity.Change{})
userChangeLogRepo := repository.NewUserChangeLogFake(map[string]time.Time{})
changeLog := changelog.NewPersist(keyGen, tm, &changeLogRepo, &userChangeLogRepo)
r := resolver.NewResolver(lg, retriever, creator, changeLog, verifier, auth)
r := resolver.NewResolver(lg, retriever, creator, updater, changeLog, verifier, auth)
graphqlAPI := NewShort(r)
assert.Equal(t, true, graphql.IsGraphQlAPIValid(graphqlAPI))
}
70 changes: 68 additions & 2 deletions backend/app/adapter/gqlapi/resolver/authmutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,39 @@ type AuthMutation struct {
authenticator authenticator.Authenticator
changeLog changelog.ChangeLog
urlCreator url.Creator
urlUpdater url.Updater
}

// URLInput represents possible URL attributes
type URLInput struct {
OriginalURL string
OriginalURL *string
CustomAlias *string
ExpireAt *time.Time
}

// isEmpty checks if the input contains only nil pointers
func (u *URLInput) isEmpty() bool {
return *u == URLInput{}
}

// originalURL returns the URLInput OrignalURL and an empty string if the
// pointer references a nil value.
func (u *URLInput) originalURL() string {
if u.OriginalURL == nil {
return ""
}
return *u.OriginalURL
}

// customAlias returns the URLInput CustomAlias and an empty string if the
// pointer references a nil value.
func (u *URLInput) customAlias() string {
if u.CustomAlias == nil {
return ""
}
return *u.CustomAlias
}

// CreateURLArgs represents the possible parameters for CreateURL endpoint
type CreateURLArgs struct {
URL URLInput
Expand All @@ -50,9 +74,10 @@ func (a AuthMutation) CreateURL(args *CreateURLArgs) (*URL, error) {
return nil, ErrInvalidAuthToken{}
}

originalURL := args.URL.originalURL()
customAlias := args.URL.CustomAlias
u := entity.URL{
OriginalURL: args.URL.OriginalURL,
OriginalURL: originalURL,
ExpireAt: args.URL.ExpireAt,
}

Expand All @@ -77,6 +102,45 @@ func (a AuthMutation) CreateURL(args *CreateURLArgs) (*URL, error) {
}
}

// UpdateURLArgs represents the possible parameters for updateURL endpoint
type UpdateURLArgs struct {
OldAlias string
Url URLInput
}

func (u *URLInput) createUpdate() (*entity.URL, error) {
if u.isEmpty() {
return nil, ErrEmptyUpdate{}
}

return &entity.URL{
Alias: u.customAlias(),
OriginalURL: u.originalURL(),
ExpireAt: u.ExpireAt,
}, nil

}

// UpdateURL updates a short link mapping that belongs to a user
func (a AuthMutation) UpdateURL(args *UpdateURLArgs) (*URL, error) {
user, err := viewer(a.authToken, a.authenticator)
if err != nil {
return nil, ErrInvalidAuthToken{}
}

update, err := args.Url.createUpdate()
if err != nil {
return nil, err
}

newURL, err := a.urlUpdater.UpdateURL(args.OldAlias, *update, user)
if err != nil {
return nil, err
}

return &URL{url: newURL}, nil
}

// CreateChange creates a Change in the change log
func (a AuthMutation) CreateChange(args *CreateChangeArgs) (Change, error) {
change, err := a.changeLog.CreateChange(args.Change.Title, args.Change.SummaryMarkdown)
Expand All @@ -99,11 +163,13 @@ func newAuthMutation(
authenticator authenticator.Authenticator,
changeLog changelog.ChangeLog,
urlCreator url.Creator,
urlUpdater url.Updater,
) AuthMutation {
return AuthMutation{
authToken: authToken,
authenticator: authenticator,
changeLog: changeLog,
urlCreator: urlCreator,
urlUpdater: urlUpdater,
}
}
19 changes: 19 additions & 0 deletions backend/app/adapter/gqlapi/resolver/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
ErrCodeInvalidCustomAlias = "invalidCustomAlias"
ErrCodeMaliciousContent = "maliciousContent"
ErrCodeInvalidAuthToken = "invalidAuthToken"
ErrCodeEmptyUpdate = "emptyUpdate"
)

// GraphQlError represents a GraphAPI error.
Expand Down Expand Up @@ -151,3 +152,21 @@ func (e ErrMaliciousContent) Extensions() map[string]interface{} {
func (e ErrMaliciousContent) Error() string {
return "contains malicious content"
}

// ErrEmptyUpdate signifies that input does not contain any URL fields to update.
type ErrEmptyUpdate struct{}

var _ GraphQlError = (*ErrEmptyUpdate)(nil)

// Extensions keeps structured error metadata so that the clients can reliably
// handle the error.
func (e ErrEmptyUpdate) Extensions() map[string]interface{} {
return map[string]interface{}{
"code": ErrCodeEmptyUpdate,
}
}

// Error retrieves the human readable error message.
func (e ErrEmptyUpdate) Error() string {
return "all input fields empty"
}
5 changes: 4 additions & 1 deletion backend/app/adapter/gqlapi/resolver/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type Mutation struct {
logger logger.Logger
urlCreator url.Creator
urlUpdater url.Updater
requesterVerifier requester.Verifier
authenticator authenticator.Authenticator
changeLog changelog.ChangeLog
Expand All @@ -35,21 +36,23 @@ func (m Mutation) AuthMutation(args *AuthMutationArgs) (*AuthMutation, error) {
return nil, ErrNotHuman{}
}

authMutation := newAuthMutation(args.AuthToken, m.authenticator, m.changeLog, m.urlCreator)
authMutation := newAuthMutation(args.AuthToken, m.authenticator, m.changeLog, m.urlCreator, m.urlUpdater)
return &authMutation, nil
}

func newMutation(
logger logger.Logger,
changeLog changelog.ChangeLog,
urlCreator url.Creator,
urlUpdater url.Updater,
requesterVerifier requester.Verifier,
authenticator authenticator.Authenticator,
) Mutation {
return Mutation{
logger: logger,
changeLog: changeLog,
urlCreator: urlCreator,
urlUpdater: urlUpdater,
requesterVerifier: requesterVerifier,
authenticator: authenticator,
}
Expand Down
2 changes: 2 additions & 0 deletions backend/app/adapter/gqlapi/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func NewResolver(
logger logger.Logger,
urlRetriever url.Retriever,
urlCreator url.Creator,
urlUpdater url.Updater,
changeLog changelog.ChangeLog,
requesterVerifier requester.Verifier,
authenticator authenticator.Authenticator,
Expand All @@ -29,6 +30,7 @@ func NewResolver(
logger,
changeLog,
urlCreator,
urlUpdater,
requesterVerifier,
authenticator,
),
Expand Down
3 changes: 2 additions & 1 deletion backend/app/adapter/gqlapi/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ type Change {

type AuthMutation {
createURL(url: URLInput!, isPublic: Boolean!): URL
updateURL(oldAlias: String!, url: URLInput!): URL
createChange(change: ChangeInput!): Change!
viewChangeLog: Time!
}

input URLInput {
originalURL: String!
originalURL: String
customAlias: String
expireAt: Time
}
Expand Down
2 changes: 1 addition & 1 deletion backend/app/adapter/sqldb/short_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ WHERE "%s"=$5;`,
table.URL.ColumnOriginalURL,
table.URL.ColumnExpireAt,
table.URL.ColumnUpdatedAt,
oldAlias,
table.URL.ColumnAlias,
)

_, err := u.db.Exec(
Expand Down
99 changes: 99 additions & 0 deletions backend/app/usecase/url/urlupdater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package url

import (
"github.com/short-d/app/fw/timer"
"github.com/short-d/short/backend/app/entity"
"github.com/short-d/short/backend/app/usecase/keygen"
"github.com/short-d/short/backend/app/usecase/repository"
"github.com/short-d/short/backend/app/usecase/risk"
"github.com/short-d/short/backend/app/usecase/validator"
)

var _ Updater = (*UpdaterPersist)(nil)

// Updater represents a short link attribute updater.
type Updater interface {
UpdateURL(oldAlias string, update entity.URL, user entity.User) (entity.URL, error)
}

// UpdaterPersist represents a short link updater that persists the given changes
type UpdaterPersist struct {
urlRepo repository.URL
userURLRelationRepo repository.UserURLRelation
keyGen keygen.KeyGenerator
longLinkValidator validator.LongLink
aliasValidator validator.CustomAlias
timer timer.Timer
riskDetector risk.Detector
}

// UpdateURL persists mutations for a given short link in the repository.
func (u UpdaterPersist) UpdateURL(
oldAlias string,
update entity.URL,
user entity.User,
) (entity.URL, error) {
url, err := u.urlRepo.GetByAlias(oldAlias)

if err != nil {
return entity.URL{}, err
}

url = u.updateAlias(url, update)
url = u.updateLongLink(url, update)

if u.riskDetector.IsURLMalicious(url.OriginalURL) {
return entity.URL{}, ErrMaliciousLongLink(url.OriginalURL)
}

if !u.aliasValidator.IsValid(&url.Alias) {
return entity.URL{}, ErrInvalidCustomAlias(url.Alias)
}

if !u.longLinkValidator.IsValid(&url.OriginalURL) {
return entity.URL{}, ErrInvalidLongLink(url.OriginalURL)
}

return u.urlRepo.UpdateURL(oldAlias, url)
}

func (u UpdaterPersist) updateAlias(url, update entity.URL) entity.URL {
newAlias := update.Alias

if newAlias != "" {
url.Alias = newAlias
}

return url
}

func (u *UpdaterPersist) updateLongLink(url, update entity.URL) entity.URL {
newLongLink := update.OriginalURL

if newLongLink != "" {
url.OriginalURL = newLongLink
}

return url
}

// NewUpdaterPersist creates a new UpdaterPersist instance.
func NewUpdaterPersist(
urlRepo repository.URL,
userURLRelationRepo repository.UserURLRelation,
keyGen keygen.KeyGenerator,
longLinkValidator validator.LongLink,
aliasValidator validator.CustomAlias,
timer timer.Timer,
riskDetector risk.Detector,
) UpdaterPersist {
return UpdaterPersist{
urlRepo,
userURLRelationRepo,
keyGen,
longLinkValidator,
aliasValidator,
timer,
riskDetector,
}
}
Loading