~netlandish/links

7e21914d1775d6dd8b1c6193a8b6ac14e92833b1 — Peter Sanchez 5 months ago d99b5c8
I think this is the last of the hash conversions.

Implements: https://todo.code.netlandish.com/~netlandish/links/74
M api/graph/generated.go => api/graph/generated.go +61 -0
@@ 92,6 92,7 @@ type ComplexityRoot struct {
		Counter     func(childComplexity int) int
		CreatedOn   func(childComplexity int) int
		Data        func(childComplexity int) int
		Hash        func(childComplexity int) int
		ID          func(childComplexity int) int
		PublicReady func(childComplexity int) int
		Tags        func(childComplexity int) int


@@ 679,6 680,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.BaseURL.Data(childComplexity), true

	case "BaseURL.hash":
		if e.complexity.BaseURL.Hash == nil {
			break
		}

		return e.complexity.BaseURL.Hash(childComplexity), true

	case "BaseURL.id":
		if e.complexity.BaseURL.ID == nil {
			break


@@ 4854,6 4862,50 @@ func (ec *executionContext) fieldContext_BaseURL_publicReady(ctx context.Context
	return fc, nil
}

func (ec *executionContext) _BaseURL_hash(ctx context.Context, field graphql.CollectedField, obj *models.BaseURL) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BaseURL_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_BaseURL_hash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BaseURL",
		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) _BaseURL_data(ctx context.Context, field graphql.CollectedField, obj *models.BaseURL) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BaseURL_data(ctx, field)
	if err != nil {


@@ 16241,6 16293,8 @@ func (ec *executionContext) fieldContext_Query_getPopularLinks(ctx context.Conte
				return ec.fieldContext_BaseURL_tags(ctx, field)
			case "publicReady":
				return ec.fieldContext_BaseURL_publicReady(ctx, field)
			case "hash":
				return ec.fieldContext_BaseURL_hash(ctx, field)
			case "data":
				return ec.fieldContext_BaseURL_data(ctx, field)
			case "createdOn":


@@ 23584,6 23638,13 @@ func (ec *executionContext) _BaseURL(ctx context.Context, sel ast.SelectionSet, 
			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "hash":

			out.Values[i] = ec._BaseURL_hash(ctx, field, obj)

			if out.Values[i] == graphql.Null {
				invalids++
			}
		case "data":

			out.Values[i] = ec._BaseURL_data(ctx, field, obj)

M api/graph/schema.graphqls => api/graph/schema.graphqls +1 -0
@@ 146,6 146,7 @@ type BaseURL {
    counter: Int!
    tags: [Tag]!
    publicReady: Boolean! @internal
    hash: String!
    data: BaseURLData!
    createdOn: Time!
    updatedOn: Time!

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +1 -1
@@ 39,7 39,7 @@ import (
	"golang.org/x/image/draw"
	"golang.org/x/net/idna"
	"netlandish.com/x/gobwebs"
	oauth2 "netlandish.com/x/gobwebs-oauth2"
	"netlandish.com/x/gobwebs-oauth2"
	gaccounts "netlandish.com/x/gobwebs/accounts"
	"netlandish.com/x/gobwebs/database"
	"netlandish.com/x/gobwebs/email"

M core/routes.go => core/routes.go +5 -4
@@ 1356,11 1356,11 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
	}

	// BaseURL
	id, err := strconv.Atoi(c.QueryParam("burlid"))
	if err == nil {
	hash := c.QueryParam("burlid")
	if hash != "" {
		opts := &database.FilterOptions{
			Filter: sq.And{
				sq.Eq{"b.id": id},
				sq.Eq{"b.hash": hash},
				sq.Eq{"b.public_ready": true},
			},
		}


@@ 1382,7 1382,7 @@ func (s *Service) OrgLinksCreate(c echo.Context) error {
		}
	} else {
		// OrgLink
		hash := c.QueryParam("linkid")
		hash = c.QueryParam("linkid")
		if hash != "" {
			type GraphQLResponse struct {
				Link models.OrgLink `json:"getOrgLink"`


@@ 1441,6 1441,7 @@ func (s *Service) PopularLinkList(c echo.Context) error {
					id
					title
					url
					hash
					counter
					tags {
						id

M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +2 -1
@@ 85,6 85,7 @@ CREATE TABLE base_urls (
  title VARCHAR ( 150 ) DEFAULT '',
  url TEXT UNIQUE NOT NULL,
  public_ready BOOLEAN DEFAULT FALSE,
  hash VARCHAR(128) UNIQUE NOT NULL,
  data JSONB DEFAULT '{}',
  counter INT DEFAULT 0,
  created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,


@@ 100,7 101,7 @@ CREATE TABLE org_links (
  url TEXT NOT NULL,
  description TEXT DEFAULT '',
  "type" INT NOT NULL DEFAULT 0,
  hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27),
  hash VARCHAR(128) UNIQUE NOT NULL,
  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 migrations/test_migration.up.sql => migrations/test_migration.up.sql +5 -5
@@ 7,13 7,13 @@ INSERT INTO organizations (owner_id, name, slug, org_type) VALUES (1, 'business 

INSERT INTO organizations (owner_id, name, slug) VALUES (2, 'api test org', 'api-test-org');

INSERT INTO base_urls (url) VALUES ('http://base.com');
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) VALUES
    ('Public Business url', 'http://base.com?vis=public', 1, 1, 2, 0);
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, 'hijklmn');

INSERT INTO org_links (title, url, base_url_id, user_id, org_id, visibility) VALUES
    ('Private Business url', 'http://base.com?vis=private', 1, 1, 2, 1);
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, 'opqrstu');

INSERT INTO domains (name, lookup_name, org_id, service, status) VALUES ('short domain', 'short.domain.org', 1, 1, 1);
INSERT INTO domains (name, lookup_name, org_id, service, status, level) VALUES ('listing domain', 'list.domain.org', 1, 2, 1, 1);

M models/base_url.go => models/base_url.go +15 -8
@@ 12,6 12,7 @@ import (
	"time"

	sq "github.com/Masterminds/squirrel"
	"github.com/segmentio/ksuid"
	"netlandish.com/x/gobwebs/database"
	"netlandish.com/x/gobwebs/timezone"
)


@@ 53,7 54,8 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL,
	if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
		q := opts.GetBuilder(nil)
		rows, err := q.
			Columns("b.id", "b.url", "b.title", "b.counter", "b.data", "b.public_ready", "b.created_on", "json_agg(t)::jsonb").
			Columns("b.id", "b.url", "b.title", "b.counter", "b.data", "b.public_ready", "b.hash",
				"b.created_on", "json_agg(t)::jsonb").
			From("base_urls b").
			LeftJoin("org_links ol ON ol.base_url_id = b.id").
			LeftJoin("tag_links tl ON tl.org_link_id = ol.id").


@@ 75,7 77,7 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL,
			var url BaseURL
			var tags string
			if err = rows.Scan(&url.ID, &url.URL, &url.Title, &url.Counter,
				&url.Data, &url.PublicReady, &url.CreatedOn, &tags); err != nil {
				&url.Data, &url.PublicReady, &url.Hash, &url.CreatedOn, &tags); err != nil {
				return err
			}
			re := regexp.MustCompile(`(,\s)?null,?`)


@@ 114,12 116,13 @@ func (b *BaseURL) Load(ctx context.Context) error {
	tz := timezone.ForContext(ctx)
	err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
		err := sq.
			Select("id", "title", "url", "counter", "data", "public_ready", "created_on").
			Select("id", "title", "url", "counter", "data", "public_ready", "hash", "created_on").
			From("base_urls").
			Where("id = ?", b.ID).
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
			ScanContext(ctx, &b.ID, &b.Title, &b.URL, &b.Counter, &b.Data, &b.PublicReady, &b.CreatedOn)
			ScanContext(ctx, &b.ID, &b.Title, &b.URL, &b.Counter, &b.Data,
				&b.PublicReady, &b.Hash, &b.CreatedOn)
		if err != nil {
			if err == sql.ErrNoRows {
				return nil


@@ 137,10 140,13 @@ func (b *BaseURL) Store(ctx context.Context) error {
	err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
		var err error
		if b.ID == 0 {
			if b.Hash == "" {
				b.Hash = ksuid.New().String()
			}
			row := tx.QueryRowContext(ctx, `
				WITH ins AS (
				   INSERT INTO base_urls(url)
				   VALUES ($1)
				   INSERT INTO base_urls (url, hash)
				   VALUES ($1, $2)
				   ON     CONFLICT (url) DO UPDATE
				   SET    url = NULL
				   WHERE  FALSE


@@ 150,7 156,7 @@ func (b *BaseURL) Store(ctx context.Context) error {
				UNION  ALL
				SELECT id, url, created_on FROM base_urls
				WHERE  url = $1
				LIMIT  1;`, b.URL)
				LIMIT  1;`, b.URL, b.Hash)
			err := row.Scan(&b.ID, &b.URL, &b.CreatedOn)
			if err != nil {
				return err


@@ 161,6 167,7 @@ func (b *BaseURL) Store(ctx context.Context) error {
				Set("title", b.Title).
				Set("data", b.Data).
				Set("public_ready", b.PublicReady).
				Set("hash", b.Hash).
				Where("id = ?", b.ID).
				Suffix(`RETURNING (updated_on)`).
				PlaceholderFormat(sq.Dollar).


@@ 228,6 235,6 @@ func (b *BaseURL) TagsToString() string {
// to a users bookmarks
func (b *BaseURL) QueryParams() url.Values {
	qs := url.Values{}
	qs.Set("burlid", fmt.Sprint(b.ID))
	qs.Set("burlid", b.Hash)
	return qs
}

M models/models.go => models/models.go +1 -0
@@ 70,6 70,7 @@ type BaseURL struct {
	Counter     int         `db:"counter"`
	Data        BaseURLData `db:"data"`
	PublicReady bool        `db:"public_ready"`
	Hash        string      `db:"hash" json:"hash"`
	CreatedOn   time.Time   `db:"created_on"`
	UpdatedOn   time.Time   `db:"updated_on"`


M models/schema.sql => models/schema.sql +2 -3
@@ 1,5 1,3 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE OR REPLACE FUNCTION update_updated_on_column()
RETURNS TRIGGER AS $$
BEGIN


@@ 87,6 85,7 @@ CREATE TABLE base_urls (
  title VARCHAR ( 150 ) DEFAULT '',
  url TEXT UNIQUE NOT NULL,
  public_ready BOOLEAN DEFAULT FALSE,
  hash VARCHAR(128) UNIQUE NOT NULL,
  data JSONB DEFAULT '{}',
  counter INT DEFAULT 0,
  created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,


@@ 102,7 101,7 @@ CREATE TABLE org_links (
  url TEXT NOT NULL,
  description TEXT DEFAULT '',
  "type" INT NOT NULL DEFAULT 0,
  hash VARCHAR(128) UNIQUE NOT NULL DEFAULT substr(encode(sha256(random()::text::bytea), 'hex'), 0, 27),
  hash VARCHAR(128) UNIQUE NOT NULL,
  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,