~netlandish/links

ref: fd727f96864b9d4bb0652bfba75037592a88dacf links/models/base_url.go -rw-r--r-- 5.6 KiB
fd727f96Peter Sanchez Adding GraphQL calls for audit logs. 10 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
package models

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"time"

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

// HTMLMeta holds a base URL meta data
type HTMLMeta struct {
	Title       string `json:"title"`
	Description string `json:"description"`
	Image       string `json:"image"`
	SiteName    string `json:"site_name"`
}

// BaseURLData holds metadata for the BaseURL model
type BaseURLData struct {
	Meta HTMLMeta `json:"meta"`
}

// Value ...
func (b BaseURLData) Value() (driver.Value, error) {
	return json.Marshal(b)
}

// Scan ...
func (b *BaseURLData) Scan(value interface{}) error {
	d, ok := value.([]byte)
	if !ok {
		return errors.New("type assertion to []byte failed")
	}
	return json.Unmarshal(d, &b)
}

// GetBaseURLs ...
func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL, error) {
	if opts == nil {
		opts = &database.FilterOptions{}
	}
	tz := timezone.ForContext(ctx)
	urls := make([]*BaseURL, 0)
	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.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").
			LeftJoin("tags t ON t.id = tl.tag_id").
			GroupBy("b.id").
			Distinct().
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
			QueryContext(ctx)
		if err != nil {
			if err == sql.ErrNoRows {
				return nil
			}
			return err
		}
		defer rows.Close()

		for rows.Next() {
			var url BaseURL
			var tags string
			if err = rows.Scan(&url.ID, &url.URL, &url.Title, &url.Counter,
				&url.Data, &url.PublicReady, &url.Hash, &url.CreatedOn, &tags); err != nil {
				return err
			}
			re := regexp.MustCompile(`(,\s)?null,?`)
			tags = re.ReplaceAllString(tags, "")
			if tags != "[]" {
				err = json.Unmarshal([]byte(tags), &url.Tags)
				if err != nil {
					return err
				}
			}
			err = url.ToLocalTZ(tz)
			if err != nil {
				return err
			}
			urls = append(urls, &url)
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return urls, nil
}

// GetBaseURL ...
func GetBaseURL(ctx context.Context, id int) (*BaseURL, error) {
	b := &BaseURL{ID: id}
	err := b.Load(ctx)
	return b, err
}

// Load ...
func (b *BaseURL) Load(ctx context.Context) error {
	if b.ID == 0 {
		return fmt.Errorf("No BaseURL ID was given")
	}
	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", "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.Hash, &b.CreatedOn)
		if err != nil {
			if err == sql.ErrNoRows {
				return nil
			}
			return err
		}
		err = b.ToLocalTZ(tz)
		return err
	})
	return err
}

// Store will save an invoice to the db
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, hash)
				   VALUES ($1, $2)
				   ON     CONFLICT (url) DO UPDATE
				   SET    url = NULL
				   WHERE  FALSE
				   RETURNING id, url, public_ready, created_on
				)
				SELECT id, url, public_ready, created_on FROM ins
				UNION  ALL
				SELECT id, url, public_ready, created_on FROM base_urls
				WHERE  url = $1
				LIMIT  1;`, b.URL, b.Hash)
			err := row.Scan(&b.ID, &b.URL, &b.PublicReady, &b.CreatedOn)
			if err != nil {
				return err
			}
		} else {
			err = sq.
				Update("base_urls").
				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).
				RunWith(tx).
				ScanContext(ctx, &b.UpdatedOn)
		}
		return err
	})
	return err
}

// Delete ...
func (b *BaseURL) Delete(ctx context.Context) error {
	if b.ID == 0 {
		return fmt.Errorf("BaseURL object is not populated")
	}
	err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
		_, err := sq.
			Delete("base_urls").
			Where("id = ?", b.ID).
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
			ExecContext(ctx)
		return err
	})
	return err
}

// ToLocalTZ convert UTC date to local tz
func (b *BaseURL) ToLocalTZ(tz string) error {
	loc, err := time.LoadLocation(tz)
	if err != nil {
		return err
	}
	b.CreatedOn = b.CreatedOn.In(loc)
	return nil
}

// UpdateCounter ...
func (b *BaseURL) UpdateCounter(ctx context.Context, add bool) error {
	// XXX Probably should change this to just use a subselect count(*)
	op := "+"
	if !add {
		op = "-"
	}
	query := fmt.Sprintf(`
		UPDATE base_urls
		SET counter = counter%s1
		WHERE id = $1
		RETURNING (updated_on)
	`, op)
	err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
		row := tx.QueryRowContext(ctx, query, b.ID)
		return row.Scan(&b.UpdatedOn)
	})
	return err
}

// TagsToString will convert linked tags to a comma separated string (used in forms)
func (b *BaseURL) TagsToString() string {
	return TagsToString(b.Tags)
}

// QueryParams will return a URL that can auto add information to easily save this BaseURL
// to a users bookmarks
func (b *BaseURL) QueryParams() url.Values {
	qs := url.Values{}
	qs.Set("burlid", b.Hash)
	return qs
}
Do not follow this link