~netlandish/links

02e7a59b57be1c16dd3dbc126e26abb0747dc5c9 — Peter Sanchez a month ago d35a122
Moving org links to enums
M api/api_test.go => api/api_test.go +11 -11
@@ 142,7 142,7 @@ func TestDirective(t *testing.T) {

		var resultAddLink GraphQLResponseAddLink
		q = `mutation AddLink($title: String!, $url: String!, $description: String,
							  $visibility: Int!, $unread: Boolean!, $starred: Boolean!,
							  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,


@@ 391,7 391,7 @@ func TestAPI(t *testing.T) {

		var result GraphQLResponse
		q := `mutation AddLink($title: String!, $url: String!, $description: String,
							  $visibility: Int!, $unread: Boolean!, $starred: Boolean!,
							  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,


@@ 440,7 440,7 @@ func TestAPI(t *testing.T) {
		op = gqlclient.NewOperation(q)
		op.Var("title", "testing link")
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", 100)
		op.Var("visibility", "NOT_VALID")
		op.Var("tags", "one, two")
		op.Var("slug", "personal-org")
		op.Var("unread", true)


@@ 448,7 448,7 @@ func TestAPI(t *testing.T) {
		op.Var("archive", false)
		err = links.Execute(ctx, op, &result)
		c.Error(err)
		c.Equal("gqlclient: server failure: Invalid Visibility value.", err.Error())
		c.Equal("gqlclient: HTTP server error (422 Unprocessable Entity): gqlclient: server failure: NOT_VALID is not a valid LinkVisibility", err.Error())
	})

	t.Run("create link", func(t *testing.T) {


@@ 458,7 458,7 @@ func TestAPI(t *testing.T) {

		var result GraphQLResponse
		q := `mutation AddLink($title: String!, $url: String!, $description: String,
							  $visibility: Int!, $unread: Boolean!, $starred: Boolean!,
							  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,


@@ 547,7 547,7 @@ func TestAPI(t *testing.T) {
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $url: String!, $visibility: LinkVisibility!,
								 $tags: String) {
				updateLink(input: {
					title: $title,


@@ 581,11 581,11 @@ func TestAPI(t *testing.T) {
		op.Var("title", "New Title")
		op.Var("hash", ol.Hash)
		op.Var("url", "https://netlandish.com")
		op.Var("visibility", 100)
		op.Var("visibility", "NOT_VALID")
		op.Var("tags", "one, two, three")
		err = links.Execute(ctx, op, &result)
		c.Error(err)
		c.Equal("gqlclient: server failure: Invalid Visibility value.", err.Error())
		c.Equal("gqlclient: HTTP server error (422 Unprocessable Entity): gqlclient: server failure: NOT_VALID is not a valid LinkVisibility", err.Error())
	})

	t.Run("org link update", func(t *testing.T) {


@@ 602,7 602,7 @@ func TestAPI(t *testing.T) {
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $url: String!, $visibility: LinkVisibility!,
								 $tags: String) {
				updateLink(input: {
					title: $title,


@@ 1852,7 1852,7 @@ func TestAPI(t *testing.T) {

		var result GraphQLResponse
		q := `mutation AddLink($title: String!, $url: String!, $description: String,
							  $visibility: Int!, $unread: Boolean!, $starred: Boolean!,
							  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,


@@ 1892,7 1892,7 @@ func TestAPI(t *testing.T) {
		}
		var result GraphQLResponse
		q := `mutation UpdateLink($hash: String!, $title: String!,
								 $url: String!, $visibility: Int!,
								 $url: String!, $visibility: LinkVisibility!,
								 $tags: String) {
				updateLink(input: {
					title: $title,

M api/graph/generated.go => api/graph/generated.go +145 -23
@@ 45,6 45,7 @@ type ResolverRoot interface {
	Mutation() MutationResolver
	OrgLink() OrgLinkResolver
	Organization() OrganizationResolver
	OrganizationSettings() OrganizationSettingsResolver
	Query() QueryResolver
	User() UserResolver
}


@@ 473,10 474,15 @@ type MutationResolver interface {
}
type OrgLinkResolver interface {
	BaseURLID(ctx context.Context, obj *models.OrgLink) (*model.NullInt, error)

	Visibility(ctx context.Context, obj *models.OrgLink) (model.LinkVisibility, error)
}
type OrganizationResolver interface {
	Image(ctx context.Context, obj *models.Organization) (*graphql.Upload, error)
}
type OrganizationSettingsResolver interface {
	DefaultPerm(ctx context.Context, obj *models.OrganizationSettings) (model.LinkVisibility, error)
}
type QueryResolver interface {
	Version(ctx context.Context) (*model.Version, error)
	Me(ctx context.Context) (*models.User, error)


@@ 12681,8 12687,36 @@ func (ec *executionContext) _OrgLink_visibility(ctx context.Context, field graph
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Visibility, nil
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return ec.resolvers.OrgLink().Visibility(rctx, obj)
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS")
			if err != nil {
				return nil, err
			}
			kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO")
			if err != nil {
				return nil, err
			}
			if ec.directives.Access == nil {
				return nil, errors.New("directive access is not implemented")
			}
			return ec.directives.Access(ctx, obj, 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.(model.LinkVisibility); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be links/api/graph/model.LinkVisibility`, tmp)
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 12694,19 12728,19 @@ func (ec *executionContext) _OrgLink_visibility(ctx context.Context, field graph
		}
		return graphql.Null
	}
	res := resTmp.(int)
	res := resTmp.(model.LinkVisibility)
	fc.Result = res
	return ec.marshalNInt2int(ctx, field.Selections, res)
	return ec.marshalNLinkVisibility2linksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_OrgLink_visibility(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "OrgLink",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type Int does not have child fields")
			return nil, errors.New("field of type LinkVisibility does not have child fields")
		},
	}
	return fc, nil


@@ 14187,7 14221,7 @@ func (ec *executionContext) _OrganizationSettings_defaultPerm(ctx context.Contex
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.DefaultPerm, nil
		return ec.resolvers.OrganizationSettings().DefaultPerm(rctx, obj)
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 14199,19 14233,19 @@ func (ec *executionContext) _OrganizationSettings_defaultPerm(ctx context.Contex
		}
		return graphql.Null
	}
	res := resTmp.(int)
	res := resTmp.(model.LinkVisibility)
	fc.Result = res
	return ec.marshalNInt2int(ctx, field.Selections, res)
	return ec.marshalNLinkVisibility2linksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_OrganizationSettings_defaultPerm(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "OrganizationSettings",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type Int does not have child fields")
			return nil, errors.New("field of type LinkVisibility does not have child fields")
		},
	}
	return fc, nil


@@ 22617,7 22651,7 @@ func (ec *executionContext) unmarshalInputLinkInput(ctx context.Context, obj int
			it.Description = data
		case "visibility":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility"))
			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
			data, err := ec.unmarshalOLinkVisibility2ᚖlinksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, v)
			if err != nil {
				return it, err
			}


@@ 22803,7 22837,7 @@ func (ec *executionContext) unmarshalInputNoteInput(ctx context.Context, obj int
			it.Description = data
		case "visibility":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility"))
			data, err := ec.unmarshalNInt2int(ctx, v)
			data, err := ec.unmarshalNLinkVisibility2linksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, v)
			if err != nil {
				return it, err
			}


@@ 23154,7 23188,7 @@ func (ec *executionContext) unmarshalInputUpdateLinkInput(ctx context.Context, o
			it.URL = data
		case "visibility":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("visibility"))
			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
			data, err := ec.unmarshalOLinkVisibility2ᚖlinksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, v)
			if err != nil {
				return it, err
			}


@@ 23479,7 23513,7 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co
			it.DeleteImg = data
		case "defaultPerm":
			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defaultPerm"))
			data, err := ec.unmarshalOInt2ᚖint(ctx, v)
			data, err := ec.unmarshalOLinkVisibility2ᚖlinksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx, v)
			if err != nil {
				return it, err
			}


@@ 25228,10 25262,41 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet, 
		case "userId":
			out.Values[i] = ec._OrgLink_userId(ctx, field, obj)
		case "visibility":
			out.Values[i] = ec._OrgLink_visibility(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&out.Invalids, 1)
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._OrgLink_visibility(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			if field.Deferrable != nil {
				dfs, ok := deferred[field.Deferrable.Label]
				di := 0
				if ok {
					dfs.AddField(field)
					di = len(dfs.Values) - 1
				} else {
					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
					deferred[field.Deferrable.Label] = dfs
				}
				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
					return innerFunc(ctx, dfs)
				})

				// don't run the out.Concurrently() call below
				out.Values[i] = graphql.Null
				continue
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
		case "unread":
			out.Values[i] = ec._OrgLink_unread(ctx, field, obj)
			if out.Values[i] == graphql.Null {


@@ 25521,10 25586,41 @@ func (ec *executionContext) _OrganizationSettings(ctx context.Context, sel ast.S
		case "__typename":
			out.Values[i] = graphql.MarshalString("OrganizationSettings")
		case "defaultPerm":
			out.Values[i] = ec._OrganizationSettings_defaultPerm(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._OrganizationSettings_defaultPerm(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			if field.Deferrable != nil {
				dfs, ok := deferred[field.Deferrable.Label]
				di := 0
				if ok {
					dfs.AddField(field)
					di = len(dfs.Values) - 1
				} else {
					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
					deferred[field.Deferrable.Label] = dfs
				}
				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
					return innerFunc(ctx, dfs)
				})

				// don't run the out.Concurrently() call below
				out.Values[i] = graphql.Null
				continue
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
		case "billing":
			out.Values[i] = ec._OrganizationSettings_billing(ctx, field, obj)
		default:


@@ 27611,6 27707,16 @@ func (ec *executionContext) marshalNLinkShortCursor2ᚖlinksᚋapiᚋgraphᚋmod
	return ec._LinkShortCursor(ctx, sel, v)
}

func (ec *executionContext) unmarshalNLinkVisibility2linksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx context.Context, v interface{}) (model.LinkVisibility, error) {
	var res model.LinkVisibility
	err := res.UnmarshalGQL(v)
	return res, graphql.ErrorOnPath(ctx, err)
}

func (ec *executionContext) marshalNLinkVisibility2linksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx context.Context, sel ast.SelectionSet, v model.LinkVisibility) graphql.Marshaler {
	return v
}

func (ec *executionContext) marshalNListing2linksᚋmodelsᚐListing(ctx context.Context, sel ast.SelectionSet, v models.Listing) graphql.Marshaler {
	return ec._Listing(ctx, sel, &v)
}


@@ 28766,6 28872,22 @@ func (ec *executionContext) unmarshalOLinkShortInput2ᚖlinksᚋapiᚋgraphᚋmo
	return &res, graphql.ErrorOnPath(ctx, err)
}

func (ec *executionContext) unmarshalOLinkVisibility2ᚖlinksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx context.Context, v interface{}) (*model.LinkVisibility, error) {
	if v == nil {
		return nil, nil
	}
	var res = new(model.LinkVisibility)
	err := res.UnmarshalGQL(v)
	return res, graphql.ErrorOnPath(ctx, err)
}

func (ec *executionContext) marshalOLinkVisibility2ᚖlinksᚋapiᚋgraphᚋmodelᚐLinkVisibility(ctx context.Context, sel ast.SelectionSet, v *model.LinkVisibility) graphql.Marshaler {
	if v == nil {
		return graphql.Null
	}
	return v
}

func (ec *executionContext) marshalOListing2ᚖlinksᚋmodelsᚐListing(ctx context.Context, sel ast.SelectionSet, v *models.Listing) graphql.Marshaler {
	if v == nil {
		return graphql.Null

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +67 -24
@@ 223,15 223,15 @@ type GetUserInput struct {
}

type LinkInput struct {
	URL         string  `json:"url"`
	Description *string `json:"description,omitempty"`
	Visibility  *int    `json:"visibility,omitempty"`
	Unread      bool    `json:"unread"`
	Starred     bool    `json:"starred"`
	Archive     bool    `json:"archive"`
	OrgSlug     string  `json:"orgSlug"`
	Title       string  `json:"title"`
	Tags        *string `json:"tags,omitempty"`
	URL         string          `json:"url"`
	Description *string         `json:"description,omitempty"`
	Visibility  *LinkVisibility `json:"visibility,omitempty"`
	Unread      bool            `json:"unread"`
	Starred     bool            `json:"starred"`
	Archive     bool            `json:"archive"`
	OrgSlug     string          `json:"orgSlug"`
	Title       string          `json:"title"`
	Tags        *string         `json:"tags,omitempty"`
}

type LinkShortCursor struct {


@@ 269,12 269,12 @@ type Mutation struct {
}

type NoteInput struct {
	Title       string  `json:"title"`
	Description string  `json:"description"`
	Visibility  int     `json:"visibility"`
	Tags        *string `json:"tags,omitempty"`
	Starred     bool    `json:"starred"`
	OrgSlug     string  `json:"orgSlug"`
	Title       string         `json:"title"`
	Description string         `json:"description"`
	Visibility  LinkVisibility `json:"visibility"`
	Tags        *string        `json:"tags,omitempty"`
	Starred     bool           `json:"starred"`
	OrgSlug     string         `json:"orgSlug"`
}

type NullInt struct {


@@ 384,14 384,14 @@ type UpdateAdminDomainInput struct {
}

type UpdateLinkInput struct {
	Hash        string  `json:"hash"`
	Title       *string `json:"title,omitempty"`
	Description *string `json:"description,omitempty"`
	URL         *string `json:"url,omitempty"`
	Visibility  *int    `json:"visibility,omitempty"`
	Unread      *bool   `json:"unread,omitempty"`
	Starred     *bool   `json:"starred,omitempty"`
	Tags        *string `json:"tags,omitempty"`
	Hash        string          `json:"hash"`
	Title       *string         `json:"title,omitempty"`
	Description *string         `json:"description,omitempty"`
	URL         *string         `json:"url,omitempty"`
	Visibility  *LinkVisibility `json:"visibility,omitempty"`
	Unread      *bool           `json:"unread,omitempty"`
	Starred     *bool           `json:"starred,omitempty"`
	Tags        *string         `json:"tags,omitempty"`
}

type UpdateLinkShortInput struct {


@@ 435,7 435,7 @@ type UpdateOrganizationInput struct {
	Image       *graphql.Upload `json:"image,omitempty"`
	IsActive    *bool           `json:"isActive,omitempty"`
	DeleteImg   *bool           `json:"deleteImg,omitempty"`
	DefaultPerm *int            `json:"defaultPerm,omitempty"`
	DefaultPerm *LinkVisibility `json:"defaultPerm,omitempty"`
}

type UpdateUserInput struct {


@@ 682,3 682,46 @@ func (e *DomainStatus) UnmarshalGQL(v interface{}) error {
func (e DomainStatus) MarshalGQL(w io.Writer) {
	fmt.Fprint(w, strconv.Quote(e.String()))
}

type LinkVisibility string

const (
	LinkVisibilityPublic     LinkVisibility = "PUBLIC"
	LinkVisibilityPrivate    LinkVisibility = "PRIVATE"
	LinkVisibilityRestricted LinkVisibility = "RESTRICTED"
)

var AllLinkVisibility = []LinkVisibility{
	LinkVisibilityPublic,
	LinkVisibilityPrivate,
	LinkVisibilityRestricted,
}

func (e LinkVisibility) IsValid() bool {
	switch e {
	case LinkVisibilityPublic, LinkVisibilityPrivate, LinkVisibilityRestricted:
		return true
	}
	return false
}

func (e LinkVisibility) String() string {
	return string(e)
}

func (e *LinkVisibility) UnmarshalGQL(v interface{}) error {
	str, ok := v.(string)
	if !ok {
		return fmt.Errorf("enums must be strings")
	}

	*e = LinkVisibility(str)
	if !e.IsValid() {
		return fmt.Errorf("%s is not a valid LinkVisibility", str)
	}
	return nil
}

func (e LinkVisibility) MarshalGQL(w io.Writer) {
	fmt.Fprint(w, strconv.Quote(e.String()))
}

M api/graph/schema.graphqls => api/graph/schema.graphqls +12 -6
@@ 80,6 80,12 @@ enum DomainStatus {
  ERROR
}

enum LinkVisibility {
  PUBLIC
  PRIVATE
  RESTRICTED
}


# Considering removing these Null* fields:
# https://todo.code.netlandish.com/~netlandish/links/75


@@ 112,7 118,7 @@ type BillingSettings {
}

type OrganizationSettings {
    defaultPerm: Int!
    defaultPerm: LinkVisibility!
    billing: BillingSettings
}



@@ 184,7 190,7 @@ type OrgLink {
    baseUrlId: NullInt! @access(scope: LINKS, kind: RO)
    orgId: Int! @access(scope: LINKS, kind: RO)
    userId: Int @access(scope: LINKS, kind: RO)
    visibility: Int!
    visibility: LinkVisibility! @access(scope: LINKS, kind: RO)
    unread: Boolean! @access(scope: LINKS, kind: RO)
    starred: Boolean! @access(scope: LINKS, kind: RO)
    archiveUrl: String!


@@ 373,7 379,7 @@ type QRObject {
input LinkInput {
    url: String!
    description: String
    visibility: Int
    visibility: LinkVisibility
    unread: Boolean!
    starred: Boolean!
    archive: Boolean!


@@ 385,7 391,7 @@ input LinkInput {
input NoteInput {
    title: String!
    description: String!
    visibility: Int!
    visibility: LinkVisibility!
    tags: String
    starred: Boolean!
    orgSlug: String!


@@ 540,7 546,7 @@ input UpdateLinkInput{
    title: String
    description: String
    url: String
    visibility: Int
    visibility: LinkVisibility
    unread: Boolean
    starred: Boolean
    tags: String


@@ 621,7 627,7 @@ input UpdateOrganizationInput {
    image: Upload
    isActive: Boolean
    deleteImg: Boolean
    defaultPerm: Int
    defaultPerm: LinkVisibility
}

input RegisterInput {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +40 -27
@@ 186,8 186,7 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) 
	validator := valid.New(ctx)

	if input.Visibility != nil {
		validator.Expect(*input.Visibility == models.OrgLinkVisibilityPublic ||
			*input.Visibility == models.OrgLinkVisibilityPrivate,
		validator.Expect(models.ValidateLinkVisibility(string(*input.Visibility)),
			lt.Translate("Invalid Visibility value.")).
			WithField("visibility").
			WithCode(valid.ErrValidationCode)


@@ 253,11 252,11 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) 
		return nil, nil
	}

	var visibility int
	var visibility string
	if input.Visibility == nil {
		visibility = org.Settings.DefaultPerm
	} else {
		visibility = *input.Visibility
		visibility = string(*input.Visibility)
	}

	srv := server.ForContext(ctx)


@@ 358,8 357,7 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
	}

	if input.Visibility != nil {
		validator.Expect(*input.Visibility == models.OrgLinkVisibilityPublic ||
			*input.Visibility == models.OrgLinkVisibilityPrivate,
		validator.Expect(models.ValidateLinkVisibility(string(*input.Visibility)),
			lt.Translate("Invalid Visibility value.")).
			WithField("visibility").
			WithCode(valid.ErrValidationCode)


@@ 441,14 439,14 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi

	if input.Visibility != nil {
		// We want to restrict private links only to paid users
		if *input.Visibility == models.OrgLinkVisibilityPrivate &&
		if string(*input.Visibility) == models.OrgLinkVisibilityPrivate &&
			links.BillingEnabled(srv.Config) &&
			org.IsRestricted([]int{models.BillingStatusFree}) {
			validator.Error(lt.Translate("Free organizations are not allowed to create private links. Please upgrade")).
				WithField("visibility").
				WithCode(valid.ErrValidationCode)
			return nil, nil
		} else if *input.Visibility == models.OrgLinkVisibilityPublic && orgLink.Type == models.NoteType {
		} else if string(*input.Visibility) == models.OrgLinkVisibilityPublic && orgLink.Type == models.NoteType {
			// NOTE: when making a note public we want to process its url and creata a base url obj
			BaseURL := &models.BaseURL{
				URL: orgLink.URL,


@@ 463,7 461,7 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi
			}
			srv.QueueTask("general", core.ParseBaseURLTask(srv, BaseURL))
		}
		orgLink.Visibility = *input.Visibility
		orgLink.Visibility = string(*input.Visibility)
	}

	if input.Title != nil {


@@ 634,8 632,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) 
	validator.Expect(input.Description != "", lt.Translate("Description is required.")).
		WithField("description").
		WithCode(valid.ErrValidationCode)
	validator.Expect(input.Visibility == models.OrgLinkVisibilityPublic ||
		input.Visibility == models.OrgLinkVisibilityPrivate,
	validator.Expect(models.ValidateLinkVisibility(string(input.Visibility)),
		lt.Translate("Invalid Visibility value.")).
		WithField("visibility").
		WithCode(valid.ErrValidationCode)


@@ 686,7 683,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) 

	srv := server.ForContext(ctx)
	// We want to restrict private links only to paid users
	if input.Visibility == models.OrgLinkVisibilityPrivate &&
	if string(input.Visibility) == models.OrgLinkVisibilityPrivate &&
		links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree}) {
		validator.Error(lt.Translate("Free organizations are not allowed to create private notes. Please upgrade")).
			WithField("visibility").


@@ 699,14 696,15 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) 

	// If the note is public we create a based link
	var BaseURL *models.BaseURL
	if input.Visibility == models.OrgLinkVisibilityPublic {
		BaseURL = &models.BaseURL{
			URL: noteURL,
		}
		err = BaseURL.Store(ctx)
		if err != nil {
			return nil, err
		}
	BaseURL = &models.BaseURL{
		URL: noteURL,
	}
	err = BaseURL.Store(ctx)
	if err != nil {
		return nil, err
	}

	if string(input.Visibility) == models.OrgLinkVisibilityPublic {
		err = BaseURL.UpdateCounter(ctx, true)
		if err != nil {
			return nil, err


@@ 718,7 716,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) 
		OrgID:       org.ID,
		Description: input.Description,
		BaseURLID:   sql.NullInt64{Valid: BaseURL.ID > 0, Int64: int64(BaseURL.ID)},
		Visibility:  input.Visibility,
		Visibility:  string(input.Visibility),
		Starred:     input.Starred,
		URL:         noteURL,
		UserID:      int(user.ID),


@@ 1702,7 1700,7 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model.

	if input.DefaultPerm != nil {
		if org.Settings.Billing.Status == models.BillingStatusFree &&
			*input.DefaultPerm == models.OrgLinkVisibilityPrivate {
			string(*input.DefaultPerm) == models.OrgLinkVisibilityPrivate {
			validator.Error(lt.Translate(
				"Free organizations can not use Private permission. Please upgrade to use " +
					"Private permission")).


@@ 1710,14 1708,13 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model.
				WithCode(valid.ErrValidationCode)
			return nil, nil
		}
		if *input.DefaultPerm < models.OrgLinkVisibilityPublic ||
			*input.DefaultPerm > models.OrgLinkVisibilityPrivate {
		if !models.ValidateLinkVisibility(string(*input.DefaultPerm)) {
			validator.Error(lt.Translate("Invalid default permission value")).
				WithField("defaultPerm").
				WithCode(valid.ErrValidationCode)
			return nil, nil
		}
		org.Settings.DefaultPerm = *input.DefaultPerm
		org.Settings.DefaultPerm = string(*input.DefaultPerm)
	}

	err = org.Store(ctx)


@@ 4195,11 4192,21 @@ func (r *orgLinkResolver) BaseURLID(ctx context.Context, obj *models.OrgLink) (*
	return &model.NullInt{Int64: int(obj.BaseURLID.Int64), Valid: obj.BaseURLID.Valid}, nil
}

// Visibility is the resolver for the visibility field.
func (r *orgLinkResolver) Visibility(ctx context.Context, obj *models.OrgLink) (model.LinkVisibility, error) {
	return model.LinkVisibility(obj.Visibility), nil
}

// Image is the resolver for the image field.
func (r *organizationResolver) Image(ctx context.Context, obj *models.Organization) (*graphql.Upload, error) {
	panic(fmt.Errorf("not implemented: Image - image"))
}

// DefaultPerm is the resolver for the defaultPerm field.
func (r *organizationSettingsResolver) DefaultPerm(ctx context.Context, obj *models.OrganizationSettings) (model.LinkVisibility, error) {
	return model.LinkVisibility(obj.DefaultPerm), nil
}

// Version is the resolver for the version field.
func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
	return &model.Version{


@@ 4554,7 4561,7 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.Or
				sq.And{
					sq.Eq{"ou.user_id": user.ID},
					sq.Eq{"ou.is_active": true},
					sq.Eq{"ol.visibility": []int{
					sq.Eq{"ol.visibility": []string{
						models.OrgLinkVisibilityPublic,
						models.OrgLinkVisibilityPrivate},
					},


@@ 4630,7 4637,7 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp
		if user.IsAuthenticated() && org.CanRead(ctx, user) {
			linkOpts.Filter = sq.And{
				linkOpts.Filter,
				sq.Eq{"ol.visibility": []int{
				sq.Eq{"ol.visibility": []string{
					models.OrgLinkVisibilityPrivate, models.OrgLinkVisibilityPublic}},
			}
		} else {


@@ 6686,6 6693,11 @@ func (r *Resolver) OrgLink() OrgLinkResolver { return &orgLinkResolver{r} }
// Organization returns OrganizationResolver implementation.
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }

// OrganizationSettings returns OrganizationSettingsResolver implementation.
func (r *Resolver) OrganizationSettings() OrganizationSettingsResolver {
	return &organizationSettingsResolver{r}
}

// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }



@@ 6696,6 6708,7 @@ type domainResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type orgLinkResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type organizationSettingsResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type userResolver struct{ *Resolver }


M core/import.go => core/import.go +1 -1
@@ 262,7 262,7 @@ func processOrgLinks(obj importObj, baseURLMap map[string]int,
		return nil
	}

	var vis int
	var vis string
	if obj.IsPublic() {
		vis = models.OrgLinkVisibilityPublic
	} else if billEnabled && org.IsRestricted([]int{models.BillingStatusFree}) {

M core/inputs.go => core/inputs.go +6 -6
@@ 55,7 55,7 @@ type UpdateOrganizationForm struct {
	Slug        string `form:"slug" validate:"required"`
	Enabled     bool   `from:"is_enabled"`
	DeleteImage bool   `form:"delete"`
	DefaultPerm int    `form:"default_perm" validate:"number,gte=0"`
	DefaultPerm string `form:"default_perm" validate:"oneof=PUBLIC PRIVATE RESTRICTED"`
}

// Validate ...


@@ 65,7 65,7 @@ func (uo *UpdateOrganizationForm) Validate(c echo.Context) error {
		String("slug", &uo.Slug).
		Bool("is_enabled", &uo.Enabled).
		Bool("delete", &uo.DeleteImage).
		Int("default_perm", &uo.DefaultPerm).
		String("default_perm", &uo.DefaultPerm).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)


@@ 100,7 100,7 @@ type LinkForm struct {
	Title       string `form:"title" validate:"required"`
	Description string `form:"description"`
	OrgSlug     string `form:"org_slug" validate:"required"`
	Visibility  int    `form:"visibility" validate:"number,gte=0"`
	Visibility  string `form:"visibility" validate:"oneof=PUBLIC PRIVATE RESTRICTED"`
	Unread      bool   `form:"unread"`
	Starred     bool   `form:"starred"`
	Archive     bool   `form:"archive"`


@@ 122,7 122,7 @@ func (l *LinkForm) Validate(c echo.Context) error {
		Bool("unread", &l.Unread).
		Bool("starred", &l.Starred).
		Bool("archive", &l.Archive).
		Int("visibility", &l.Visibility).
		String("visibility", &l.Visibility).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)


@@ 230,7 230,7 @@ type NoteForm struct {
	Title       string `form:"title" validate:"required"`
	Description string `form:"description" validate:"required"`
	OrgSlug     string `form:"org_slug" validate:"required"`
	Visibility  int    `form:"visibility" validate:"number,gte=0"`
	Visibility  string `form:"visibility" validate:"oneof=PUBLIC PRIVATE RESTRICTED"`
	Starred     bool   `form:"starred"`
	Tags        string `form:"tags"`
}


@@ 243,7 243,7 @@ func (n *NoteForm) Validate(c echo.Context) error {
		String("org_slug", &n.OrgSlug).
		String("tags", &n.Tags).
		Bool("starred", &n.Starred).
		Int("visibility", &n.Visibility).
		String("visibility", &n.Visibility).
		BindErrors()
	if errs != nil {
		return validate.GetInputErrors(errs)

M core/routes.go => core/routes.go +16 -12
@@ 734,7 734,7 @@ func (s *Service) OrgCreate(c echo.Context) error {
		"Sorry, you have exceeded the amount of free accounts available. Please update your current free account to create one more")

	form := &OrganizationForm{}
	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 883,7 883,7 @@ func (s *Service) OrgUpdate(c echo.Context) error {
	pd.Data["visibility"] = lt.Translate("Default Bookmark Visibility")

	form := &UpdateOrganizationForm{}
	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 1376,7 1376,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
	pd.Data["back"] = lt.Translate("Back")
	pd.Data["archive"] = lt.Translate("Archive URL")

	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 1410,7 1410,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation AddLink($title: String!, $url: String!, $description: String,
							  $visibility: Int!, $unread: Boolean!, $starred: Boolean!,
							  $visibility: LinkVisibility!, $unread: Boolean!, $starred: Boolean!,
							  $archive: Boolean! $slug: String!, $tags: String) {
					addLink(input: {
								title: $title,


@@ 2274,6 2274,7 @@ func (s *Service) OrgLinkDetail(c echo.Context) error {
					userId
					type
					hash
					visibility
					createdOn
					tags {
						id


@@ 2453,7 2454,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {
	pd.Data["confirm_message"] = lt.Translate("Do you want to delete")
	pd.Data["archive"] = lt.Translate("Archive")
	pd.Data["tags_helper"] = lt.Translate("Use commas to separate your tags. Example: tag 1, tag 2, tag 3")
	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 2487,7 2488,7 @@ func (s *Service) OrgLinkUpdate(c echo.Context) error {
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateLink($hash: String!, $title: String!, $description: String,
								 $url: String!, $visibility: Int!, $unread: Boolean,
								 $url: String!, $visibility: LinkVisibility!, $unread: Boolean,
								 $starred: Boolean, $tags: String) {
					updateLink(input: {
						title: $title,


@@ 3273,7 3274,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
	pd.Data["starred"] = lt.Translate("Starred")
	pd.Data["confirm_message"] = lt.Translate("Do you want to delete")
	pd.Data["tags_helper"] = lt.Translate("Use commas to separate your tags. Example: tag 1, tag 2, tag 3")
	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 3307,7 3308,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation UpdateLink($hash: String!, $title: String!, $description: String,
								 $visibility: Int!, $starred: Boolean, $tags: String) {
								 $visibility: LinkVisibility!, $starred: Boolean, $tags: String) {
					updateLink(input: {
						hash: $hash,
						title: $title,


@@ 3317,6 3318,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
						tags: $tags,
					}) {
						id
						hash
						url
					}
				}`)


@@ 3340,7 3342,7 @@ func (s *Service) NoteUpdate(c echo.Context) error {
			return err
		}
		messages.Success(c, lt.Translate("Link successfully updated."))
		return c.Redirect(http.StatusMovedPermanently, result.Link.URL)
		return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("core:note_detail", result.Link.Hash))

	}
	var canEdit bool


@@ 3385,6 3387,7 @@ func (s *Service) NoteDetail(c echo.Context) error {
					userId
					type
					visibility
					hash
					createdOn
					updatedOn
					tags {


@@ 3470,7 3473,7 @@ func (s *Service) NoteCreate(c echo.Context) error {
	pd.Data["back"] = lt.Translate("Back")
	pd.Data["organization"] = lt.Translate("Organization")

	visibilityOpt := map[int]string{
	visibilityOpt := map[string]string{
		models.OrgLinkVisibilityPublic:  lt.Translate("Public"),
		models.OrgLinkVisibilityPrivate: lt.Translate("Private"),
	}


@@ 3502,7 3505,7 @@ func (s *Service) NoteCreate(c echo.Context) error {

		var result GraphQLResponse
		op := gqlclient.NewOperation(
			`mutation AddNote($title: String!, $description: String!, $visibility: Int!,
			`mutation AddNote($title: String!, $description: String!, $visibility: LinkVisibility!,
							  $starred: Boolean!, $slug: String!, $tags: String) {
					addNote(input: {
								title: $title,


@@ 3512,6 3515,7 @@ func (s *Service) NoteCreate(c echo.Context) error {
								orgSlug: $slug,
								tags: $tags}) {
						id
						hash
						url
					}
				}`)


@@ 3536,7 3540,7 @@ func (s *Service) NoteCreate(c echo.Context) error {
		}

		messages.Success(c, lt.Translate("An note was successfully created."))
		return c.Redirect(http.StatusMovedPermanently, result.Link.URL)
		return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("core:note_detail", result.Link.Hash))
	}

	form := &NoteForm{

M core/routes_test.go => core/routes_test.go +4 -4
@@ 164,7 164,7 @@ func TestHandlers(t *testing.T) {
		f.Set("url", "http://foo.com")
		f.Set("title", "Title")
		f.Set("org_slug", "slug")
		f.Set("visibility", "1")
		f.Set("visibility", "PRIVATE")
		request := httptest.NewRequest(http.MethodPost, "/add", strings.NewReader(f.Encode()))
		request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
		recorder := httptest.NewRecorder()


@@ 192,7 192,7 @@ func TestHandlers(t *testing.T) {
		f.Set("url", "http://foo.com")
		f.Set("title", "Title")
		f.Set("org_slug", "slug")
		f.Set("visibility", "1")
		f.Set("visibility", "PRIVATE")
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
		request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
		recorder := httptest.NewRecorder()


@@ 234,7 234,7 @@ func TestHandlers(t *testing.T) {
		f.Set("url", "http://foo.bar.com")
		f.Set("title", "Title")
		f.Set("org_slug", "slug")
		f.Set("visibility", "1")
		f.Set("visibility", "PRIVATE")
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
		request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
		recorder := httptest.NewRecorder()


@@ 269,7 269,7 @@ func TestHandlers(t *testing.T) {
		f.Set("url", "http://foo.com")
		f.Set("title", "Title")
		f.Set("org_slug", "slug")
		f.Set("visibility", "1")
		f.Set("visibility", "PRIVATE")
		request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode()))
		request.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
		recorder := httptest.NewRecorder()

M core/samples/create_link.json => core/samples/create_link.json +1 -1
@@ 9,7 9,7 @@
            },
            "orgId": 1,
            "userId": 1,
            "visibility": 0,
            "visibility": "PUBLIC",
            "createdOn": "2023-06-08T22:44:32.35373Z",
            "updatedOn": "2023-06-08T22:44:32.35373Z"
        }

M core/samples/detail_link.json => core/samples/detail_link.json +1 -1
@@ 12,7 12,7 @@
            },
            "orgId": 1,
            "userId": 1,
            "visibility": 0,
            "visibility": "PUBLIC",
            "createdOn": "2023-05-30T18:41:38.891196Z",
            "updatedOn": "2023-05-30T18:41:38.891196Z"
        }

M core/samples/update_link.json => core/samples/update_link.json +1 -1
@@ 9,7 9,7 @@
            },
            "orgId": 1,
            "userId": 1,
            "visibility": 0,
            "visibility": "PUBLIC",
            "createdOn": "2023-05-23T17:27:21.013293Z",
            "updatedOn": "2023-06-08T23:47:11.521328Z"
        }

M migrations/0001_initial.down.sql => migrations/0001_initial.down.sql +1 -0
@@ 36,3 36,4 @@ DROP TYPE IF EXISTS domain_level;
DROP TYPE IF EXISTS domain_service;
DROP TYPE IF EXISTS domain_status;
DROP TYPE IF EXISTS invoice_status;
DROP TYPE IF EXISTS org_link_visibility;

M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +11 -1
@@ 51,6 51,16 @@ EXCEPTION
    WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
  CREATE TYPE org_link_visibility AS ENUM (
    'PUBLIC',
    'PRIVATE',
    'RESTRICTED'
  );
EXCEPTION
    WHEN duplicate_object THEN null;
END $$;


CREATE TABLE users (
  id SERIAL PRIMARY KEY,


@@ 146,7 156,7 @@ CREATE TABLE org_links (
  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,
  visibility INT DEFAULT 0,
  visibility org_link_visibility DEFAULT 'PUBLIC',
  unread BOOLEAN DEFAULT FALSE NOT NULL,
  starred BOOLEAN DEFAULT FALSE NOT NULL,
  archive_url TEXT DEFAULT '',

M migrations/test_migration.up.sql => migrations/test_migration.up.sql +2 -2
@@ 10,10 10,10 @@ INSERT INTO organizations (owner_id, name, slug) VALUES (2, 'api test org', 'api
INSERT INTO base_urls (url, hash) VALUES ('http://base.com', 'abcdefg');

INSERT INTO org_links (title, url, base_url_id, user_id, org_id, visibility, hash) VALUES
    ('Public Business url', 'http://base.com?vis=public', 1, 1, 2, 0, 'hash1');
    ('Public Business url', 'http://base.com?vis=public', 1, 1, 2, 'PUBLIC', 'hash1');

INSERT INTO org_links (title, url, base_url_id, user_id, org_id, visibility, hash) VALUES
    ('Private Business url', 'http://base.com?vis=private', 1, 1, 2, 1, 'hash2');
    ('Private Business url', 'http://base.com?vis=private', 1, 1, 2, 'PRIVATE', 'hash2');

INSERT INTO domains (name, lookup_name, org_id, level, service, status) VALUES ('short domain', 'short.domain.org', 1, 'SYSTEM', 'SHORT', 'APPROVED');
INSERT INTO domains (name, lookup_name, org_id, service, status, level) VALUES ('listing domain', 'list.domain.org', 1, 'LIST', 'APPROVED', 'USER');

M models/models.go => models/models.go +1 -1
@@ 76,7 76,7 @@ type OrgLink struct {
	BaseURLID   sql.NullInt64 `db:"base_url_id" json:"baseURLId"`
	OrgID       int           `db:"org_id" json:"orgId"`
	UserID      int           `db:"user_id" json:"userId"`
	Visibility  int           `db:"visibility" json:"visibility"`
	Visibility  string        `db:"visibility" json:"visibility"`
	Unread      bool          `db:"unread" json:"unread"`
	Starred     bool          `db:"starred" json:"starred"`
	ArchiveURL  string        `db:"archive_url" json:"archiveUrl"`

M models/org_link.go => models/org_link.go +20 -4
@@ 16,9 16,9 @@ import (
)

const (
	OrgLinkVisibilityPublic int = iota
	OrgLinkVisibilityPrivate
	OrgLinkVisibilityRestricted
	OrgLinkVisibilityPublic     string = "PUBLIC"
	OrgLinkVisibilityPrivate    string = "PRIVATE"
	OrgLinkVisibilityRestricted string = "RESTRICTED"
)

const (


@@ 134,6 134,10 @@ func (o *OrgLink) Store(ctx context.Context) error {
			if o.Hash == "" {
				o.Hash = ksuid.New().String()
			}
			// Added for fail safes after migration to enums. Can be removed later
			if o.Visibility == "" {
				o.Visibility = OrgLinkVisibilityPublic
			}
			err = sq.
				Insert("org_links").
				Columns("title", "url", "description", "base_url_id", "org_id", "user_id", "visibility",


@@ 348,7 352,7 @@ func ExportOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*Expor
	return links, nil
}

func ToggleOrgLinksVisibilityBatch(ctx context.Context, opts *database.FilterOptions, vis int) error {
func ToggleOrgLinksVisibilityBatch(ctx context.Context, opts *database.FilterOptions, vis string) error {
	err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
		var err error
		_, err = sq.


@@ 396,3 400,15 @@ func GetOrgLinksCount(ctx context.Context, opts *database.FilterOptions) (int, e
	}
	return count, nil
}

// ValidateLinkVisibility ensures proper value for OrgLink.Visibility
func ValidateLinkVisibility(vis string) bool {
	visMap := map[string]bool{
		OrgLinkVisibilityPublic:     true,
		OrgLinkVisibilityPrivate:    true,
		OrgLinkVisibilityRestricted: true,
	}
	_, ok := visMap[vis]
	return ok

}

M models/organization.go => models/organization.go +1 -1
@@ 37,7 37,7 @@ type BillingSettings struct {

// OrganizationSettings ...
type OrganizationSettings struct {
	DefaultPerm int             `json:"default_perm"` // default: OrgLinkVisibilityPublic
	DefaultPerm string          `json:"default_perm"` // default: OrgLinkVisibilityPublic
	Billing     BillingSettings `json:"billing"`
}


M models/schema.sql => models/schema.sql +7 -0
@@ 34,6 34,12 @@ CREATE TYPE invoice_status AS ENUM (
  'PARTIAL_REFUND'
);

CREATE TYPE org_link_visibility AS ENUM (
  'PUBLIC',
  'PRIVATE',
  'RESTRICTED'
);

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  full_name VARCHAR ( 150 ) NOT NULL,


@@ 129,6 135,7 @@ CREATE TABLE org_links (
  org_id INT REFERENCES organizations (id) ON DELETE CASCADE NOT NULL,
  user_id INT REFERENCES users (id) ON DELETE SET NULL,
  visibility INT DEFAULT 0,
  visibility org_link_visibility DEFAULT 'PUBLIC',
  unread BOOLEAN DEFAULT FALSE NOT NULL,
  starred BOOLEAN DEFAULT FALSE NOT NULL,
  archive_url TEXT DEFAULT '',