-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.go
More file actions
232 lines (204 loc) · 6.4 KB
/
bootstrap.go
File metadata and controls
232 lines (204 loc) · 6.4 KB
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
package pg
import (
"database/sql"
"fmt"
"os"
"strings"
"github.com/go-ap/errors"
"github.com/jackc/pgx/v5"
)
const (
createImmutableTSFunc = `
CREATE OR REPLACE FUNCTION text2ts(text) RETURNS timestamp with time zone LANGUAGE sql IMMUTABLE AS
$$
SELECT CASE WHEN $1 ~ '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d+)?' -- extract date-time part of the RFC3339 value
THEN CAST($1 AS timestamp with time zone)
at time zone coalesce((regexp_match($1, '(Z|[+\-]?\d\d:\d\d)$'))[1], 'utc') -- extract tz part of the RFC3339 value
END
$$;
`
createSchemas = `CREATE SCHEMA pub;
CREATE SCHEMA oauth2;`
createObjectsQuery = `
CREATE TABLE pub.object (
"raw" jsonb,
"iri" varchar NOT NULL constraint object_key unique,
"id" varchar GENERATED ALWAYS AS (raw ->> 'id') STORED,
"type" varchar GENERATED ALWAYS AS (raw ->> 'type') STORED,
"to" varchar GENERATED ALWAYS AS (raw ->> 'to') STORED,
"bto" varchar GENERATED ALWAYS AS (raw ->> 'bto') STORED,
"cc" varchar GENERATED ALWAYS AS (raw ->> 'cc') STORED,
"bcc" varchar GENERATED ALWAYS AS (raw ->> 'bcc') STORED,
"published" timestamptz GENERATED ALWAYS AS (text2ts(raw ->> 'published')) STORED,
"updated" timestamptz GENERATED ALWAYS AS (text2ts(raw ->> 'updated')) STORED,
"url" varchar GENERATED ALWAYS AS (raw ->> 'url') STORED,
"name" varchar GENERATED ALWAYS AS (raw ->> 'name') STORED,
"preferred_username" text GENERATED ALWAYS AS (raw ->> 'preferredUsername') STORED,
"summary" varchar GENERATED ALWAYS AS (raw ->> 'summary') STORED,
"content" varchar GENERATED ALWAYS AS (raw ->> 'content') STORED,
"actor" varchar GENERATED ALWAYS AS (raw ->> 'actor') STORED,
"object" varchar GENERATED ALWAYS AS (raw ->> 'object') STORED
);
CREATE INDEX object_type ON pub.object(type);
CREATE INDEX object_names ON pub.object USING GIN (tsvector_concat(to_tsvector('english', name), to_tsvector('english', preferred_username)));
CREATE INDEX object_contents ON pub.object USING GIN (to_tsvector('english', summary));
CREATE INDEX object_published ON pub.object(published);
-- CREATE INDEX object_contents ON object USING GIN (to_tsvector('english', content));
`
createCollectionsQuery = `
CREATE TABLE pub.collection (
"id" varchar references pub.object(iri),
"iri" varchar NOT NULL,
"added" timestamptz default (now() at time zone 'utc')
);
--CREATE TRIGGER collections_updated_published AFTER UPDATE ON pub.collection BEGIN
--UPDATE pub.object SET updated = (now() at time zone 'utc') WHERE iri = old.id;
--END;
`
createMetaDataQuery = `CREATE TABLE pub.meta (
"iri" varchar NOT NULL constraint meta_key unique,
"raw" jsonb NOT NULL DEFAULT '{}'
);`
createClientTable = `CREATE TABLE IF NOT EXISTS oauth2.client (
"code" varchar constraint client_code_pkey PRIMARY KEY,
"secret" varchar NOT NULL,
"redirect_uri" varchar NOT NULL,
"extra" varchar
);
`
createAuthorizeTable = `CREATE TABLE IF NOT EXISTS oauth2.authorize (
"client" varchar REFERENCES oauth2.client(code),
"code" varchar constraint authorize_code_pkey PRIMARY KEY,
"expires_in" INTEGER,
"scope" varchar,
"redirect_uri" varchar NOT NULL,
"state" varchar,
"code_challenge" varchar DEFAULT NULL,
"code_challenge_method" varchar DEFAULT NULL,
"created_at" timestamptz DEFAULT (now() at time zone 'utc'),
"extra" varchar
);
`
createAccessTable = `CREATE TABLE IF NOT EXISTS oauth2.access (
"token" varchar constraint access_token_pkey PRIMARY KEY,
"client" varchar REFERENCES oauth2.client(code),
"authorize" varchar REFERENCES oauth2.authorize(code),
"previous" varchar,
"refresh_token" varchar NOT NULL,
"expires_in" INTEGER,
"scope" varchar DEFAULT NULL,
"redirect_uri" varchar NOT NULL,
"created_at" timestamptz DEFAULT (now() at time zone 'utc'),
"extra" varchar
);
`
createRefreshTable = `CREATE TABLE IF NOT EXISTS oauth2.refresh (
"token" varchar PRIMARY KEY NOT NULL,
"access_token" varchar NOT NULL REFERENCES oauth2.access(token) ON DELETE CASCADE
);
`
)
func stringClean(qSql string) string {
return strings.ReplaceAll(qSql, "\n", "")
}
func ParseConfig(connStr string) (Config, error) {
pconf, err := pgx.ParseConfig(connStr)
if err != nil {
return Config{}, err
}
return Config{
Host: pconf.Host,
Port: pconf.Port,
Database: pconf.Database,
User: pconf.User,
Password: pconf.Password,
}, nil
}
func Bootstrap(conf Config) error {
dsn := conf.DSN()
if dsn == "" {
return errInvalidConnection
}
r, err := New(conf)
if err != nil {
return err
}
if err = r.open(dsn); err != nil {
return err
}
defer r.Close()
if err = r.conn.Ping(); err != nil {
return err
}
exec := func(conn *sql.DB, qRaw string, par ...any) error {
qSql := fmt.Sprintf(qRaw, par...)
r.logFn("Executing %s", stringClean(qSql))
if _, err := conn.Exec(qSql); err != nil {
r.errFn("Failed: %s", err)
return errors.Annotatef(err, "unable to execute: %s", stringClean(qSql))
}
r.logFn("Success!")
return nil
}
err = exec(r.conn, createImmutableTSFunc)
err = exec(r.conn, createSchemas)
err = exec(r.conn, createObjectsQuery)
err = exec(r.conn, createCollectionsQuery)
err = exec(r.conn, createMetaDataQuery)
err = exec(r.conn, createClientTable)
err = exec(r.conn, createAuthorizeTable)
err = exec(r.conn, createAccessTable)
err = exec(r.conn, createRefreshTable)
return err
}
var errInvalidConnection = os.ErrNotExist
var tables = []string{
"pub.object", "pub.collection",
"pub.meta",
"oauth2.client", "oauth2.authorize", "oauth2.access", "oauth2.refresh",
}
func (r *repo) Reset() {
if r.conn == nil {
r.errFn("connection is not open")
return
}
tx, err := r.conn.Begin()
if err != nil {
r.errFn("unable to start transaction: %s", err)
return
}
for _, table := range tables {
s := `TRUNCATE TABLE ` + table + ` CASCADE;`
if _, err = tx.Exec(s); err != nil {
_ = tx.Rollback()
r.errFn("unable to truncate table %s: %s", table, err)
}
}
if err := tx.Commit(); err != nil {
_ = tx.Rollback()
r.errFn("unable to commit transaction item: %+s", err)
}
}
func Clean(conf Config) error {
dsn := conf.DSN()
if dsn == "" {
return errInvalidConnection
}
r, err := New(conf)
if err != nil {
return err
}
if err = r.open(dsn); err != nil {
return err
}
if err = r.conn.Ping(); err != nil {
return err
}
defer r.Close()
for _, table := range tables {
if _, err = r.conn.Exec(`DROP TABLE IF EXISTS ` + table + ` CASCADE`); err != nil {
r.errFn("unable to drop table %s: %s", table, err)
}
}
return nil
}