M api/graph/generated.go => api/graph/generated.go +63 -0
@@ 115,6 115,7 @@ type ComplexityRoot struct {
}
DeletePayload struct {
+ Message func(childComplexity int) int
ObjectID func(childComplexity int) int
Success func(childComplexity int) int
}
@@ 772,6 773,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.BillingSettings.Status(childComplexity), true
+ case "DeletePayload.message":
+ if e.complexity.DeletePayload.Message == nil {
+ break
+ }
+
+ return e.complexity.DeletePayload.Message(childComplexity), true
+
case "DeletePayload.objectId":
if e.complexity.DeletePayload.ObjectID == nil {
break
@@ 6581,6 6589,47 @@ func (ec *executionContext) fieldContext_DeletePayload_objectId(_ context.Contex
return fc, nil
}
+func (ec *executionContext) _DeletePayload_message(ctx context.Context, field graphql.CollectedField, obj *model.DeletePayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_DeletePayload_message(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.Message, 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.marshalOString2áš–string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_DeletePayload_message(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "DeletePayload",
+ 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) _Domain_id(ctx context.Context, field graphql.CollectedField, obj *models.Domain) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Domain_id(ctx, field)
if err != nil {
@@ 10644,6 10693,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteLink(ctx context.Context
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 11666,6 11717,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteDomain(ctx context.Conte
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 11981,6 12034,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteLinkShort(ctx context.Co
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 12526,6 12581,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteListing(ctx context.Cont
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 12619,6 12676,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteListingLink(ctx context.
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 12819,6 12878,8 @@ func (ec *executionContext) fieldContext_Mutation_deleteQRCode(ctx context.Conte
return ec.fieldContext_DeletePayload_success(ctx, field)
case "objectId":
return ec.fieldContext_DeletePayload_objectId(ctx, field)
+ case "message":
+ return ec.fieldContext_DeletePayload_message(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeletePayload", field.Name)
},
@@ 25736,6 25797,8 @@ func (ec *executionContext) _DeletePayload(ctx context.Context, sel ast.Selectio
if out.Values[i] == graphql.Null {
out.Invalids++
}
+ case "message":
+ out.Values[i] = ec._DeletePayload_message(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
M api/graph/model/models_gen.go => api/graph/model/models_gen.go +3 -2
@@ 110,8 110,9 @@ type CompleteRegisterInput struct {
}
type DeletePayload struct {
- Success bool `json:"success"`
- ObjectID string `json:"objectId"`
+ Success bool `json:"success"`
+ ObjectID string `json:"objectId"`
+ Message *string `json:"message,omitempty"`
}
type DomainCursor struct {
M api/graph/schema.graphqls => api/graph/schema.graphqls +1 -0
@@ 714,6 714,7 @@ input AddQRCodeInput {
type DeletePayload {
success: Boolean!
objectId: ID!
+ message: String
}
type AddMemberPayload {
M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +21 -6
@@ 1932,7 1932,7 @@ func (r *mutationResolver) DeleteDomain(ctx context.Context, id int) (*model.Del
return nil, err
}
if len(domains) == 0 {
- return nil, fmt.Errorf(lt.Translate("invalid domain ID [%d] given", id))
+ return nil, fmt.Errorf(lt.Translate("invalid domain ID"))
}
domain := domains[0]
@@ 1956,6 1956,12 @@ func (r *mutationResolver) DeleteDomain(ctx context.Context, id int) (*model.Del
}
}
+ if !domain.CanDelete(ctx) {
+ msg := lt.Translate("Unable to delete. Domain has active short links or link listings")
+ deletePayload.Message = &msg
+ return deletePayload, nil
+ }
+
err = domain.Delete(ctx)
if err != nil {
return nil, err
@@ 3984,6 3990,13 @@ func (r *mutationResolver) UpdateAdminDomain(ctx context.Context, input model.Up
ldomain := domains[0]
+ if string(input.Service) != ldomain.Service && !ldomain.CanDelete(ctx) {
+ validator.Error(lt.Translate("Domain in use. Can not change service.")).
+ WithField("service").
+ WithCode(valid.ErrValidationCode)
+ return nil, nil
+ }
+
var org *models.Organization
if input.OrgSlug != nil {
opts := &database.FilterOptions{
@@ 5354,7 5367,14 @@ func (r *queryResolver) GetListing(ctx context.Context, input *model.GetListingD
if tokenUser == nil {
return nil, valid.ErrAuthorization
}
+
validator := valid.New(ctx)
+ if input.After != nil && input.Before != nil {
+ validator.Error("You can not send both after and before cursors").
+ WithCode(valid.ErrValidationGlobalCode)
+ return nil, nil
+ }
+
opts := &database.FilterOptions{
Filter: sq.And{
sq.Eq{"l.domain_id": input.DomainID},
@@ 5405,11 5425,6 @@ func (r *queryResolver) GetListing(ctx context.Context, input *model.GetListingD
},
OrderBy: "ll.link_order ASC",
}
- if input.After != nil && input.Before != nil {
- validator.Error("You can not send both after and before cursors").
- WithCode(valid.ErrValidationGlobalCode)
- return nil, nil
- }
numElements := 10
var hasPrevPage bool
M core/routes.go => core/routes.go +3 -1
@@ 555,6 555,7 @@ func (s *Service) DomainDelete(c echo.Context) error {
DeleteDomain struct {
Success bool `json:"success"`
ID string `json:"objectId"`
+ Message string `json:"message"`
} `json:"deleteDomain"`
}
@@ 564,6 565,7 @@ func (s *Service) DomainDelete(c echo.Context) error {
deleteDomain(id: $id) {
success
objectId
+ message
}
}`)
op.Var("id", id)
@@ 572,7 574,7 @@ func (s *Service) DomainDelete(c echo.Context) error {
return err
}
if !result.DeleteDomain.Success {
- messages.Error(c, lt.Translate("Something went wrong. The domain could not be deleted."))
+ messages.Error(c, lt.Translate("%s", result.DeleteDomain.Message))
redirect := c.Request().Header.Get("Referer")
if redirect == "" {
redirect = c.Echo().Reverse(s.RouteName("domain_list"))
M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +2 -2
@@ 383,7 383,7 @@ CREATE TABLE link_shorts (
title VARCHAR ( 150 ) DEFAULT '',
url TEXT NOT NULL,
short_code TEXT DEFAULT '',
- domain_id INT REFERENCES domains (id) ON DELETE CASCADE NOT NULL,
+ domain_id INT REFERENCES domains (id) ON DELETE RESTRICT NOT NULL,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ 415,7 415,7 @@ CREATE TABLE listings (
slug VARCHAR ( 150 ) NOT NULL,
image VARCHAR(1024) DEFAULT '',
metadata JSONB DEFAULT '{}',
- domain_id INT REFERENCES domains (id) ON DELETE CASCADE NOT NULL,
+ domain_id INT REFERENCES domains (id) ON DELETE RESTRICT NOT NULL,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
is_default BOOLEAN DEFAULT FALSE,
M models/domains.go => models/domains.go +31 -0
@@ 8,6 8,7 @@ import (
"time"
sq "github.com/Masterminds/squirrel"
+ "github.com/labstack/echo/v4"
"golang.org/x/net/idna"
"netlandish.com/x/gobwebs/database"
"netlandish.com/x/gobwebs/timezone"
@@ 266,3 267,33 @@ func (d *Domain) IsStatusApproved() bool {
func (d *Domain) IsStatusError() bool {
return d.Status == DomainStatusError
}
+
+// Hack for templates to avoid more hacks
+func (d *Domain) TmplCanDelete(c echo.Context) bool {
+ return d.CanDelete(c.Request().Context())
+}
+
+func (d *Domain) CanDelete(ctx context.Context) bool {
+ if d.ID == 0 {
+ return false
+ }
+
+ if d.IsServiceLink() {
+ return true
+ }
+
+ canDelete := true // Default to true so default return value is false
+ query := `SELECT EXISTS(
+ SELECT 1
+ FROM domains d
+ LEFT JOIN listings l ON l.domain_id = d.id
+ LEFT JOIN link_shorts s ON s.domain_id = d.id
+ WHERE l.domain_id=$1 OR s.domain_id=$1);`
+
+ // Ignore any error here and just return false
+ database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
+ row := tx.QueryRowContext(ctx, query, d.ID)
+ return row.Scan(&canDelete)
+ })
+ return !canDelete
+}
M models/schema.sql => models/schema.sql +2 -2
@@ 338,7 338,7 @@ CREATE TABLE link_shorts (
title VARCHAR ( 150 ) DEFAULT '',
url TEXT NOT NULL,
short_code TEXT DEFAULT '',
- domain_id INT REFERENCES domains (id) ON DELETE CASCADE NOT NULL,
+ domain_id INT REFERENCES domains (id) ON DELETE RESTRICT NOT NULL,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ 370,7 370,7 @@ CREATE TABLE listings (
slug VARCHAR ( 150 ) NOT NULL,
image VARCHAR(1024) DEFAULT '',
metadata JSONB DEFAULT '{}',
- domain_id INT REFERENCES domains (id) ON DELETE CASCADE NOT NULL,
+ domain_id INT REFERENCES domains (id) ON DELETE RESTRICT NOT NULL,
org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
user_id INT REFERENCES users (id) ON DELETE SET NULL,
is_default BOOLEAN DEFAULT FALSE,
M templates/domain_list.html => templates/domain_list.html +1 -1
@@ 34,7 34,7 @@
{{end}}
</td>
<td>
- <a class="button primary outline is-small" href="{{ reverse "core:domain_delete" $.slug .ID }}">{{$.pd.Data.delete}}</a>
+ {{ if .TmplCanDelete $.context }}<a class="button primary outline is-small" href="{{ reverse "core:domain_delete" $.slug .ID }}">{{$.pd.Data.delete}}</a>{{ end }}
</td>
</tr>
{{end}}
M values.go => values.go +2 -5
@@ 84,6 84,8 @@ var InvalidSlugs = []string{
"api",
"faq",
"status",
+ "help",
+ "getting-started",
}
// Interval used to display data in analytics engagement chart
@@ 110,11 112,6 @@ const (
// comments. Also for our case, we want to ensure at least one dot ('.') is
// included in the given domain name.
func IsDomainName(s string) bool {
- // The root domain name is valid. See golang.org/issue/45715.
- if s == "." {
- return true
- }
-
// See RFC 1035, RFC 3696.
// Presentation format has dots before every label except the first, and the
// terminal empty label is optional here because we assume fully-qualified