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,