mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-20 22:07:15 +00:00
feat: initial postgres driver
This commit is contained in:
parent
41cb597f03
commit
a7d48e8059
@ -65,6 +65,8 @@ linters-settings:
|
|||||||
disabled: true
|
disabled: true
|
||||||
- name: early-return
|
- name: early-return
|
||||||
disabled: true
|
disabled: true
|
||||||
|
- name: use-any
|
||||||
|
disabled: true
|
||||||
- name: exported
|
- name: exported
|
||||||
arguments:
|
arguments:
|
||||||
- "disableStutteringCheck"
|
- "disableStutteringCheck"
|
||||||
|
1
go.mod
1
go.mod
@ -74,6 +74,7 @@ require (
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/improbable-eng/grpc-web v0.15.0
|
github.com/improbable-eng/grpc-web v0.15.0
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mssola/useragent v1.0.0
|
github.com/mssola/useragent v1.0.0
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
2
go.sum
2
go.sum
@ -292,6 +292,8 @@ github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrP
|
|||||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/yourselfhosted/slash/server/profile"
|
"github.com/yourselfhosted/slash/server/profile"
|
||||||
"github.com/yourselfhosted/slash/store"
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
"github.com/yourselfhosted/slash/store/db/postgres"
|
||||||
"github.com/yourselfhosted/slash/store/db/sqlite"
|
"github.com/yourselfhosted/slash/store/db/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ func NewDBDriver(profile *profile.Profile) (store.Driver, error) {
|
|||||||
switch profile.Driver {
|
switch profile.Driver {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
driver, err = sqlite.NewDB(profile)
|
driver, err = sqlite.NewDB(profile)
|
||||||
|
case "postgres":
|
||||||
|
driver, err = postgres.NewDB(profile)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown db driver")
|
return nil, errors.New("unknown db driver")
|
||||||
}
|
}
|
||||||
|
88
store/db/postgres/activity.go
Normal file
88
store/db/postgres/activity.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO activity (
|
||||||
|
creator_id,
|
||||||
|
type,
|
||||||
|
level,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, created_ts
|
||||||
|
`
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt,
|
||||||
|
create.CreatorID,
|
||||||
|
create.Type.String(),
|
||||||
|
create.Level.String(),
|
||||||
|
create.Payload,
|
||||||
|
).Scan(
|
||||||
|
&create.ID,
|
||||||
|
&create.CreatedTs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activity := create
|
||||||
|
return activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
if find.Type != "" {
|
||||||
|
where, args = append(where, "type = $"+fmt.Sprint(len(args)+1)), append(args, find.Type.String())
|
||||||
|
}
|
||||||
|
if find.Level != "" {
|
||||||
|
where, args = append(where, "level = $"+fmt.Sprint(len(args)+1)), append(args, find.Level.String())
|
||||||
|
}
|
||||||
|
if find.Where != nil {
|
||||||
|
where = append(where, find.Where...)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
created_ts,
|
||||||
|
type,
|
||||||
|
level,
|
||||||
|
payload
|
||||||
|
FROM activity
|
||||||
|
WHERE ` + strings.Join(where, " AND ")
|
||||||
|
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := []*store.Activity{}
|
||||||
|
for rows.Next() {
|
||||||
|
activity := &store.Activity{}
|
||||||
|
if err := rows.Scan(
|
||||||
|
&activity.ID,
|
||||||
|
&activity.CreatorID,
|
||||||
|
&activity.CreatedTs,
|
||||||
|
&activity.Type,
|
||||||
|
&activity.Level,
|
||||||
|
&activity.Payload,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
194
store/db/postgres/collection.go
Normal file
194
store/db/postgres/collection.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/internal/util"
|
||||||
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) CreateCollection(ctx context.Context, create *storepb.Collection) (*storepb.Collection, error) {
|
||||||
|
set := []string{"creator_id", "name", "title", "description", "shortcut_ids", "visibility"}
|
||||||
|
args := []any{create.CreatorId, create.Name, create.Title, create.Description, strings.Trim(strings.Join(strings.Fields(fmt.Sprint(create.ShortcutIds)), ","), "[]"), create.Visibility.String()}
|
||||||
|
placeholder := []string{"$1", "$2", "$3", "$4", "$5", "$6"}
|
||||||
|
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO collection (
|
||||||
|
` + strings.Join(set, ", ") + `
|
||||||
|
)
|
||||||
|
VALUES (` + strings.Join(placeholder, ",") + `)
|
||||||
|
RETURNING id, created_ts, updated_ts
|
||||||
|
`
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&create.Id,
|
||||||
|
&create.CreatedTs,
|
||||||
|
&create.UpdatedTs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
collection := create
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateCollection(ctx context.Context, update *store.UpdateCollection) (*storepb.Collection, error) {
|
||||||
|
set, args := []string{}, []any{}
|
||||||
|
if update.Name != nil {
|
||||||
|
set, args = append(set, "name = $1"), append(args, *update.Name)
|
||||||
|
}
|
||||||
|
if update.Title != nil {
|
||||||
|
set, args = append(set, "title = $2"), append(args, *update.Title)
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
set, args = append(set, "description = $3"), append(args, *update.Description)
|
||||||
|
}
|
||||||
|
if update.ShortcutIDs != nil {
|
||||||
|
set, args = append(set, "shortcut_ids = $4"), append(args, strings.Trim(strings.Join(strings.Fields(fmt.Sprint(update.ShortcutIDs)), ","), "[]"))
|
||||||
|
}
|
||||||
|
if update.Visibility != nil {
|
||||||
|
set, args = append(set, "visibility = $5"), append(args, update.Visibility.String())
|
||||||
|
}
|
||||||
|
if len(set) == 0 {
|
||||||
|
return nil, errors.New("no update specified")
|
||||||
|
}
|
||||||
|
args = append(args, update.ID)
|
||||||
|
|
||||||
|
stmt := `
|
||||||
|
UPDATE collection
|
||||||
|
SET
|
||||||
|
` + strings.Join(set, ", ") + `
|
||||||
|
WHERE
|
||||||
|
id = $6
|
||||||
|
RETURNING id, creator_id, created_ts, updated_ts, name, title, description, shortcut_ids, visibility
|
||||||
|
`
|
||||||
|
collection := &storepb.Collection{}
|
||||||
|
var shortcutIDs, visibility string
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&collection.Id,
|
||||||
|
&collection.CreatorId,
|
||||||
|
&collection.CreatedTs,
|
||||||
|
&collection.UpdatedTs,
|
||||||
|
&collection.Name,
|
||||||
|
&collection.Title,
|
||||||
|
&collection.Description,
|
||||||
|
&shortcutIDs,
|
||||||
|
&visibility,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.ShortcutIds = []int32{}
|
||||||
|
if shortcutIDs != "" {
|
||||||
|
for _, idStr := range strings.Split(shortcutIDs, ",") {
|
||||||
|
shortcutID, err := util.ConvertStringToInt32(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to convert shortcut id")
|
||||||
|
}
|
||||||
|
collection.ShortcutIds = append(collection.ShortcutIds, shortcutID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collection.Visibility = convertVisibilityStringToStorepb(visibility)
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListCollections(ctx context.Context, find *store.FindCollection) ([]*storepb.Collection, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
if v := find.ID; v != nil {
|
||||||
|
where, args = append(where, "id = $1"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.CreatorID; v != nil {
|
||||||
|
where, args = append(where, "creator_id = $2"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.Name; v != nil {
|
||||||
|
where, args = append(where, "name = $3"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.VisibilityList; len(v) != 0 {
|
||||||
|
list := []string{}
|
||||||
|
for i, visibility := range v {
|
||||||
|
list = append(list, fmt.Sprintf("$%d", len(args)+i+1))
|
||||||
|
args = append(args, visibility)
|
||||||
|
}
|
||||||
|
where = append(where, fmt.Sprintf("visibility IN (%s)", strings.Join(list, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
created_ts,
|
||||||
|
updated_ts,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
shortcut_ids,
|
||||||
|
visibility
|
||||||
|
FROM collection
|
||||||
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
|
ORDER BY created_ts DESC`,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]*storepb.Collection, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
collection := &storepb.Collection{}
|
||||||
|
var shortcutIDs, visibility string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&collection.Id,
|
||||||
|
&collection.CreatorId,
|
||||||
|
&collection.CreatedTs,
|
||||||
|
&collection.UpdatedTs,
|
||||||
|
&collection.Name,
|
||||||
|
&collection.Title,
|
||||||
|
&collection.Description,
|
||||||
|
&shortcutIDs,
|
||||||
|
&visibility,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
collection.ShortcutIds = []int32{}
|
||||||
|
if shortcutIDs != "" {
|
||||||
|
for _, idStr := range strings.Split(shortcutIDs, ",") {
|
||||||
|
shortcutID, err := util.ConvertStringToInt32(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to convert shortcut id")
|
||||||
|
}
|
||||||
|
collection.ShortcutIds = append(collection.ShortcutIds, shortcutID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collection.Visibility = storepb.Visibility(storepb.Visibility_value[visibility])
|
||||||
|
list = append(list, collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteCollection(ctx context.Context, delete *store.DeleteCollection) error {
|
||||||
|
if _, err := d.db.ExecContext(ctx, `DELETE FROM collection WHERE id = $1`, delete.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func vacuumCollection(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
stmt := `DELETE FROM collection WHERE creator_id NOT IN (SELECT id FROM user)`
|
||||||
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
208
store/db/postgres/memo.go
Normal file
208
store/db/postgres/memo.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) CreateMemo(ctx context.Context, create *storepb.Memo) (*storepb.Memo, error) {
|
||||||
|
set := []string{"creator_id", "name", "title", "content", "visibility", "tag"}
|
||||||
|
args := []any{create.CreatorId, create.Name, create.Title, create.Content, create.Visibility.String(), strings.Join(create.Tags, " ")}
|
||||||
|
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO memo (
|
||||||
|
` + strings.Join(set, ", ") + `
|
||||||
|
)
|
||||||
|
VALUES (` + placeholders(len(args)) + `)
|
||||||
|
RETURNING id, created_ts, updated_ts, row_status
|
||||||
|
`
|
||||||
|
|
||||||
|
var rowStatus string
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&create.Id,
|
||||||
|
&create.CreatedTs,
|
||||||
|
&create.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
create.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
memo := create
|
||||||
|
return memo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) (*storepb.Memo, error) {
|
||||||
|
set, args := []string{}, []any{}
|
||||||
|
if update.RowStatus != nil {
|
||||||
|
set = append(set, fmt.Sprintf("row_status = $%d", len(set)+1))
|
||||||
|
args = append(args, update.RowStatus.String())
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
set = append(set, fmt.Sprintf("name = $%d", len(set)+1))
|
||||||
|
args = append(args, *update.Name)
|
||||||
|
}
|
||||||
|
if update.Title != nil {
|
||||||
|
set = append(set, fmt.Sprintf("title = $%d", len(set)+1))
|
||||||
|
args = append(args, *update.Title)
|
||||||
|
}
|
||||||
|
if update.Content != nil {
|
||||||
|
set = append(set, fmt.Sprintf("content = $%d", len(set)+1))
|
||||||
|
args = append(args, *update.Content)
|
||||||
|
}
|
||||||
|
if update.Visibility != nil {
|
||||||
|
set = append(set, fmt.Sprintf("visibility = $%d", len(set)+1))
|
||||||
|
args = append(args, update.Visibility.String())
|
||||||
|
}
|
||||||
|
if update.Tag != nil {
|
||||||
|
set = append(set, fmt.Sprintf("tag = $%d", len(set)+1))
|
||||||
|
args = append(args, *update.Tag)
|
||||||
|
}
|
||||||
|
if len(set) == 0 {
|
||||||
|
return nil, errors.New("no update specified")
|
||||||
|
}
|
||||||
|
args = append(args, update.ID)
|
||||||
|
|
||||||
|
stmt := `
|
||||||
|
UPDATE memo
|
||||||
|
SET
|
||||||
|
` + strings.Join(set, ", ") + `
|
||||||
|
WHERE
|
||||||
|
id = $` + fmt.Sprint(len(set)+1) + `
|
||||||
|
RETURNING id, creator_id, created_ts, updated_ts, row_status, name, title, content, visibility, tag
|
||||||
|
`
|
||||||
|
|
||||||
|
memo := &storepb.Memo{}
|
||||||
|
var rowStatus, visibility, tags string
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&memo.Id,
|
||||||
|
&memo.CreatorId,
|
||||||
|
&memo.CreatedTs,
|
||||||
|
&memo.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
&memo.Name,
|
||||||
|
&memo.Title,
|
||||||
|
&memo.Content,
|
||||||
|
&visibility,
|
||||||
|
&tags,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
memo.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
memo.Visibility = convertVisibilityStringToStorepb(visibility)
|
||||||
|
memo.Tags = filterTags(strings.Split(tags, " "))
|
||||||
|
return memo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*storepb.Memo, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
if v := find.ID; v != nil {
|
||||||
|
where, args = append(where, "id = $1"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.CreatorID; v != nil {
|
||||||
|
where, args = append(where, "creator_id = $2"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.RowStatus; v != nil {
|
||||||
|
where, args = append(where, "row_status = $3"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.Name; v != nil {
|
||||||
|
where, args = append(where, "name = $4"), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.VisibilityList; len(v) != 0 {
|
||||||
|
list := []string{}
|
||||||
|
for i, visibility := range v {
|
||||||
|
list = append(list, fmt.Sprintf("$%d", len(args)+i+1))
|
||||||
|
args = append(args, visibility)
|
||||||
|
}
|
||||||
|
where = append(where, fmt.Sprintf("visibility IN (%s)", strings.Join(list, ",")))
|
||||||
|
}
|
||||||
|
if v := find.Tag; v != nil {
|
||||||
|
where, args = append(where, "tag LIKE $"+fmt.Sprint(len(args)+1)), append(args, "%"+*v+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
created_ts,
|
||||||
|
updated_ts,
|
||||||
|
row_status,
|
||||||
|
name,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
visibility,
|
||||||
|
tag
|
||||||
|
FROM memo
|
||||||
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
|
ORDER BY created_ts DESC`,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]*storepb.Memo, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
memo := &storepb.Memo{}
|
||||||
|
var rowStatus, visibility, tags string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&memo.Id,
|
||||||
|
&memo.CreatorId,
|
||||||
|
&memo.CreatedTs,
|
||||||
|
&memo.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
&memo.Name,
|
||||||
|
&memo.Title,
|
||||||
|
&memo.Content,
|
||||||
|
&visibility,
|
||||||
|
&tags,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
memo.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
memo.Visibility = storepb.Visibility(storepb.Visibility_value[visibility])
|
||||||
|
memo.Tags = filterTags(strings.Split(tags, " "))
|
||||||
|
list = append(list, memo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error {
|
||||||
|
if _, err := d.db.ExecContext(ctx, `DELETE FROM memo WHERE id = $1`, delete.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
stmt := `DELETE FROM memo WHERE creator_id NOT IN (SELECT id FROM user)`
|
||||||
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeholders(n int) string {
|
||||||
|
placeholder := ""
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if i == 0 {
|
||||||
|
placeholder = fmt.Sprintf("$%d", i+1)
|
||||||
|
} else {
|
||||||
|
placeholder = fmt.Sprintf("%s, $%d", placeholder, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placeholder
|
||||||
|
}
|
92
store/db/postgres/migration/dev/LATEST__SCHEMA.sql
Normal file
92
store/db/postgres/migration/dev/LATEST__SCHEMA.sql
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
-- migration_history
|
||||||
|
CREATE TABLE migration_history (
|
||||||
|
version TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- workspace_setting
|
||||||
|
CREATE TABLE workspace_setting (
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- user
|
||||||
|
CREATE TABLE user (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
nickname TEXT NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_email ON user(email);
|
||||||
|
|
||||||
|
-- user_setting
|
||||||
|
CREATE TABLE user_setting (
|
||||||
|
user_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- shortcut
|
||||||
|
CREATE TABLE shortcut (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
link TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
|
||||||
|
tag TEXT NOT NULL DEFAULT '',
|
||||||
|
og_metadata TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shortcut_name ON shortcut(name);
|
||||||
|
|
||||||
|
-- activity
|
||||||
|
CREATE TABLE activity (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
type TEXT NOT NULL DEFAULT '',
|
||||||
|
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
|
||||||
|
payload TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- collection
|
||||||
|
CREATE TABLE collection (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
shortcut_ids INTEGER ARRAY NOT NULL,
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_collection_name ON collection(name);
|
||||||
|
|
||||||
|
-- memo
|
||||||
|
CREATE TABLE memo (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
content TEXT NOT NULL DEFAULT '',
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
|
||||||
|
tag TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_memo_name ON memo(name);
|
92
store/db/postgres/migration/prod/LATEST__SCHEMA.sql
Normal file
92
store/db/postgres/migration/prod/LATEST__SCHEMA.sql
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
-- migration_history
|
||||||
|
CREATE TABLE migration_history (
|
||||||
|
version TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())
|
||||||
|
);
|
||||||
|
|
||||||
|
-- workspace_setting
|
||||||
|
CREATE TABLE workspace_setting (
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- user
|
||||||
|
CREATE TABLE user (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
nickname TEXT NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_email ON user(email);
|
||||||
|
|
||||||
|
-- user_setting
|
||||||
|
CREATE TABLE user_setting (
|
||||||
|
user_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- shortcut
|
||||||
|
CREATE TABLE shortcut (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
link TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
|
||||||
|
tag TEXT NOT NULL DEFAULT '',
|
||||||
|
og_metadata TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shortcut_name ON shortcut(name);
|
||||||
|
|
||||||
|
-- activity
|
||||||
|
CREATE TABLE activity (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
type TEXT NOT NULL DEFAULT '',
|
||||||
|
level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO',
|
||||||
|
payload TEXT NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- collection
|
||||||
|
CREATE TABLE collection (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
shortcut_ids INTEGER ARRAY NOT NULL,
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_collection_name ON collection(name);
|
||||||
|
|
||||||
|
-- memo
|
||||||
|
CREATE TABLE memo (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
creator_id INTEGER REFERENCES user(id) NOT NULL,
|
||||||
|
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
|
||||||
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
content TEXT NOT NULL DEFAULT '',
|
||||||
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
|
||||||
|
tag TEXT NOT NULL DEFAULT ''
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_memo_name ON memo(name);
|
57
store/db/postgres/migration_history.go
Normal file
57
store/db/postgres/migration_history.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO migration_history (
|
||||||
|
version
|
||||||
|
)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT(version) DO UPDATE
|
||||||
|
SET
|
||||||
|
version=EXCLUDED.version
|
||||||
|
RETURNING version, created_ts
|
||||||
|
`
|
||||||
|
var migrationHistory store.MigrationHistory
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan(
|
||||||
|
&migrationHistory.Version,
|
||||||
|
&migrationHistory.CreatedTs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &migrationHistory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListMigrationHistories(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) {
|
||||||
|
query := "SELECT version, created_ts FROM migration_history ORDER BY created_ts DESC"
|
||||||
|
rows, err := d.db.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]*store.MigrationHistory, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var migrationHistory store.MigrationHistory
|
||||||
|
if err := rows.Scan(
|
||||||
|
&migrationHistory.Version,
|
||||||
|
&migrationHistory.CreatedTs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
list = append(list, &migrationHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
233
store/db/postgres/migrator.go
Normal file
233
store/db/postgres/migrator.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/server/version"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed migration
|
||||||
|
var migrationFS embed.FS
|
||||||
|
|
||||||
|
//go:embed seed
|
||||||
|
var seedFS embed.FS
|
||||||
|
|
||||||
|
// Migrate applies the latest schema to the database.
|
||||||
|
func (d *DB) Migrate(ctx context.Context) error {
|
||||||
|
currentVersion := version.GetCurrentVersion(d.profile.Mode)
|
||||||
|
if d.profile.Mode == "prod" {
|
||||||
|
_, err := os.Stat(d.profile.DSN)
|
||||||
|
if err != nil {
|
||||||
|
// If db file not exists, we should create a new one with latest schema.
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := d.applyLatestSchema(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply latest schema")
|
||||||
|
}
|
||||||
|
// Upsert the newest version to migration_history.
|
||||||
|
if _, err := d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
|
||||||
|
Version: currentVersion,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to upsert migration history")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.Wrap(err, "failed to get db file stat")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If db file exists, we should check if we need to migrate the database.
|
||||||
|
migrationHistoryList, err := d.ListMigrationHistories(ctx, &store.FindMigrationHistory{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to find migration history")
|
||||||
|
}
|
||||||
|
// If no migration history, we should apply the latest version migration and upsert the migration history.
|
||||||
|
if len(migrationHistoryList) == 0 {
|
||||||
|
minorVersion := version.GetMinorVersion(currentVersion)
|
||||||
|
if err := d.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to apply version %s migration", minorVersion)
|
||||||
|
}
|
||||||
|
_, err := d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
|
||||||
|
Version: currentVersion,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to upsert migration history")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationHistoryVersionList := []string{}
|
||||||
|
for _, migrationHistory := range migrationHistoryList {
|
||||||
|
migrationHistoryVersionList = append(migrationHistoryVersionList, migrationHistory.Version)
|
||||||
|
}
|
||||||
|
sort.Sort(version.SortVersion(migrationHistoryVersionList))
|
||||||
|
latestMigrationHistoryVersion := migrationHistoryVersionList[len(migrationHistoryVersionList)-1]
|
||||||
|
|
||||||
|
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), latestMigrationHistoryVersion) {
|
||||||
|
minorVersionList := getMinorVersionList()
|
||||||
|
// backup the raw database file before migration
|
||||||
|
rawBytes, err := os.ReadFile(d.profile.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read raw database file")
|
||||||
|
}
|
||||||
|
backupDBFilePath := fmt.Sprintf("%s/memos_%s_%d_backup.db", d.profile.Data, d.profile.Version, time.Now().Unix())
|
||||||
|
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write raw database file")
|
||||||
|
}
|
||||||
|
println("succeed to copy a backup database file")
|
||||||
|
println("start migrate")
|
||||||
|
for _, minorVersion := range minorVersionList {
|
||||||
|
normalizedVersion := minorVersion + ".0"
|
||||||
|
if version.IsVersionGreaterThan(normalizedVersion, latestMigrationHistoryVersion) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||||
|
println("applying migration for", normalizedVersion)
|
||||||
|
if err := d.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply minor version migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println("end migrate")
|
||||||
|
|
||||||
|
// remove the created backup db file after migrate succeed
|
||||||
|
if err := os.Remove(backupDBFilePath); err != nil {
|
||||||
|
println(fmt.Sprintf("Failed to remove temp database file, err %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In non-prod mode, we should always migrate the database.
|
||||||
|
if _, err := os.Stat(d.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := d.applyLatestSchema(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to apply latest schema")
|
||||||
|
}
|
||||||
|
// In demo mode, we should seed the database.
|
||||||
|
if d.profile.Mode == "demo" {
|
||||||
|
if err := d.seed(ctx); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to seed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) applyLatestSchema(ctx context.Context) error {
|
||||||
|
schemaMode := "dev"
|
||||||
|
if d.profile.Mode == "prod" {
|
||||||
|
schemaMode = "prod"
|
||||||
|
}
|
||||||
|
latestSchemaPath := fmt.Sprintf("migration/%s/%s", schemaMode, latestSchemaFileName)
|
||||||
|
buf, err := migrationFS.ReadFile(latestSchemaPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to read latest schema %q", latestSchemaPath)
|
||||||
|
}
|
||||||
|
stmt := string(buf)
|
||||||
|
if err := d.execute(ctx, stmt); err != nil {
|
||||||
|
return errors.Wrapf(err, "migrate error: %s", stmt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion string) error {
|
||||||
|
filenames, err := fs.Glob(migrationFS, fmt.Sprintf("%s/%s/*.sql", "migration/prod", minorVersion))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read ddl files")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(filenames)
|
||||||
|
migrationStmt := ""
|
||||||
|
|
||||||
|
// Loop over all migration files and execute them in order.
|
||||||
|
for _, filename := range filenames {
|
||||||
|
buf, err := migrationFS.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to read minor version migration file, filename=%s", filename)
|
||||||
|
}
|
||||||
|
stmt := string(buf)
|
||||||
|
migrationStmt += stmt
|
||||||
|
if err := d.execute(ctx, stmt); err != nil {
|
||||||
|
return errors.Wrapf(err, "migrate error: %s", stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert the newest version to migration_history.
|
||||||
|
version := minorVersion + ".0"
|
||||||
|
if _, err = d.UpsertMigrationHistory(ctx, &store.UpsertMigrationHistory{
|
||||||
|
Version: version,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to upsert migration history with version: %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) seed(ctx context.Context) error {
|
||||||
|
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read seed files")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(filenames)
|
||||||
|
|
||||||
|
// Loop over all seed files and execute them in order.
|
||||||
|
for _, filename := range filenames {
|
||||||
|
buf, err := seedFS.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to read seed file, filename=%s", filename)
|
||||||
|
}
|
||||||
|
stmt := string(buf)
|
||||||
|
if err := d.execute(ctx, stmt); err != nil {
|
||||||
|
return errors.Wrapf(err, "seed error: %s", stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute runs a single SQL statement within a transaction.
|
||||||
|
func (d *DB) execute(ctx context.Context, stmt string) error {
|
||||||
|
tx, err := d.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to execute statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// minorDirRegexp is a regular expression for minor version directory.
|
||||||
|
var minorDirRegexp = regexp.MustCompile(`^migration/prod/[0-9]+\.[0-9]+$`)
|
||||||
|
|
||||||
|
func getMinorVersionList() []string {
|
||||||
|
minorVersionList := []string{}
|
||||||
|
|
||||||
|
if err := fs.WalkDir(migrationFS, "migration", func(path string, file fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if file.IsDir() && minorDirRegexp.MatchString(path) {
|
||||||
|
minorVersionList = append(minorVersionList, file.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(version.SortVersion(minorVersionList))
|
||||||
|
return minorVersionList
|
||||||
|
}
|
45
store/db/postgres/postgres.go
Normal file
45
store/db/postgres/postgres.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
// Import the PostgreSQL driver.
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/server/profile"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *sql.DB
|
||||||
|
profile *profile.Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDB(profile *profile.Profile) (store.Driver, error) {
|
||||||
|
if profile == nil {
|
||||||
|
return nil, errors.New("profile is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the PostgreSQL connection
|
||||||
|
db, err := sql.Open("postgres", profile.DSN)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to open database: %s", err)
|
||||||
|
return nil, errors.Wrapf(err, "failed to open database: %s", profile.DSN)
|
||||||
|
}
|
||||||
|
|
||||||
|
var driver store.Driver = &DB{
|
||||||
|
db: db,
|
||||||
|
profile: profile,
|
||||||
|
}
|
||||||
|
return driver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) GetDB() *sql.DB {
|
||||||
|
return d.db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
9
store/db/postgres/seed/10000__reset.sql
Normal file
9
store/db/postgres/seed/10000__reset.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
DELETE FROM activity;
|
||||||
|
|
||||||
|
DELETE FROM shortcut;
|
||||||
|
|
||||||
|
DELETE FROM user_setting;
|
||||||
|
|
||||||
|
DELETE FROM user;
|
||||||
|
|
||||||
|
DELETE FROM workspace_setting;
|
228
store/db/postgres/shortcut.go
Normal file
228
store/db/postgres/shortcut.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) CreateShortcut(ctx context.Context, create *storepb.Shortcut) (*storepb.Shortcut, error) {
|
||||||
|
set := []string{"creator_id", "name", "link", "title", "description", "visibility", "tag"}
|
||||||
|
args := []any{create.CreatorId, create.Name, create.Link, create.Title, create.Description, create.Visibility.String(), strings.Join(create.Tags, " ")}
|
||||||
|
if create.OgMetadata != nil {
|
||||||
|
set = append(set, "og_metadata")
|
||||||
|
openGraphMetadataBytes, err := protojson.Marshal(create.OgMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, string(openGraphMetadataBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`
|
||||||
|
INSERT INTO shortcut (%s)
|
||||||
|
VALUES (%s)
|
||||||
|
RETURNING id, created_ts, updated_ts, row_status
|
||||||
|
`, strings.Join(set, ","), placeholders(len(args)))
|
||||||
|
|
||||||
|
var rowStatus string
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&create.Id,
|
||||||
|
&create.CreatedTs,
|
||||||
|
&create.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
create.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
shortcut := create
|
||||||
|
return shortcut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateShortcut(ctx context.Context, update *store.UpdateShortcut) (*storepb.Shortcut, error) {
|
||||||
|
set, args := []string{}, []any{}
|
||||||
|
if update.RowStatus != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("row_status = $%d", len(args)+1)), append(args, update.RowStatus.String())
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("name = $%d", len(args)+1)), append(args, *update.Name)
|
||||||
|
}
|
||||||
|
if update.Link != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("link = $%d", len(args)+1)), append(args, *update.Link)
|
||||||
|
}
|
||||||
|
if update.Title != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("title = $%d", len(args)+1)), append(args, *update.Title)
|
||||||
|
}
|
||||||
|
if update.Description != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("description = $%d", len(args)+1)), append(args, *update.Description)
|
||||||
|
}
|
||||||
|
if update.Visibility != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("visibility = $%d", len(args)+1)), append(args, update.Visibility.String())
|
||||||
|
}
|
||||||
|
if update.Tag != nil {
|
||||||
|
set, args = append(set, fmt.Sprintf("tag = $%d", len(args)+1)), append(args, *update.Tag)
|
||||||
|
}
|
||||||
|
if update.OpenGraphMetadata != nil {
|
||||||
|
openGraphMetadataBytes, err := protojson.Marshal(update.OpenGraphMetadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Failed to marshal activity payload")
|
||||||
|
}
|
||||||
|
set, args = append(set, fmt.Sprintf("og_metadata = $%d", len(args)+1)), append(args, string(openGraphMetadataBytes))
|
||||||
|
}
|
||||||
|
if len(set) == 0 {
|
||||||
|
return nil, errors.New("no update specified")
|
||||||
|
}
|
||||||
|
args = append(args, update.ID)
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`
|
||||||
|
UPDATE shortcut
|
||||||
|
SET %s
|
||||||
|
WHERE id = $%d
|
||||||
|
RETURNING id, creator_id, created_ts, updated_ts, row_status, name, link, title, description, visibility, tag, og_metadata
|
||||||
|
`, strings.Join(set, ","), len(args))
|
||||||
|
|
||||||
|
shortcut := &storepb.Shortcut{}
|
||||||
|
var rowStatus, visibility, tags, openGraphMetadataString string
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&shortcut.Id,
|
||||||
|
&shortcut.CreatorId,
|
||||||
|
&shortcut.CreatedTs,
|
||||||
|
&shortcut.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
&shortcut.Name,
|
||||||
|
&shortcut.Link,
|
||||||
|
&shortcut.Title,
|
||||||
|
&shortcut.Description,
|
||||||
|
&visibility,
|
||||||
|
&tags,
|
||||||
|
&openGraphMetadataString,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shortcut.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
shortcut.Visibility = convertVisibilityStringToStorepb(visibility)
|
||||||
|
shortcut.Tags = filterTags(strings.Split(tags, " "))
|
||||||
|
var ogMetadata storepb.OpenGraphMetadata
|
||||||
|
if err := protojson.Unmarshal([]byte(openGraphMetadataString), &ogMetadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shortcut.OgMetadata = &ogMetadata
|
||||||
|
return shortcut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListShortcuts(ctx context.Context, find *store.FindShortcut) ([]*storepb.Shortcut, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
if v := find.ID; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("id = $%d", len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.CreatorID; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("creator_id = $%d", len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.RowStatus; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("row_status = $%d", len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.Name; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("name = $%d", len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.VisibilityList; len(v) != 0 {
|
||||||
|
list := []string{}
|
||||||
|
for _, visibility := range v {
|
||||||
|
list = append(list, fmt.Sprintf("$%d", len(args)+1))
|
||||||
|
args = append(args, visibility)
|
||||||
|
}
|
||||||
|
where = append(where, fmt.Sprintf("visibility IN (%s)", strings.Join(list, ",")))
|
||||||
|
}
|
||||||
|
if v := find.Tag; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("tag LIKE $%d", len(args)+1)), append(args, "%"+*v+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := d.db.QueryContext(ctx, fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
created_ts,
|
||||||
|
updated_ts,
|
||||||
|
row_status,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
visibility,
|
||||||
|
tag,
|
||||||
|
og_metadata
|
||||||
|
FROM shortcut
|
||||||
|
WHERE %s
|
||||||
|
ORDER BY created_ts DESC
|
||||||
|
`, strings.Join(where, " AND ")), args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]*storepb.Shortcut, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
shortcut := &storepb.Shortcut{}
|
||||||
|
var rowStatus, visibility, tags, openGraphMetadataString string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&shortcut.Id,
|
||||||
|
&shortcut.CreatorId,
|
||||||
|
&shortcut.CreatedTs,
|
||||||
|
&shortcut.UpdatedTs,
|
||||||
|
&rowStatus,
|
||||||
|
&shortcut.Name,
|
||||||
|
&shortcut.Link,
|
||||||
|
&shortcut.Title,
|
||||||
|
&shortcut.Description,
|
||||||
|
&visibility,
|
||||||
|
&tags,
|
||||||
|
&openGraphMetadataString,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shortcut.RowStatus = store.ConvertRowStatusStringToStorepb(rowStatus)
|
||||||
|
shortcut.Visibility = storepb.Visibility(storepb.Visibility_value[visibility])
|
||||||
|
shortcut.Tags = filterTags(strings.Split(tags, " "))
|
||||||
|
var ogMetadata storepb.OpenGraphMetadata
|
||||||
|
if err := protojson.Unmarshal([]byte(openGraphMetadataString), &ogMetadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shortcut.OgMetadata = &ogMetadata
|
||||||
|
list = append(list, shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteShortcut(ctx context.Context, delete *store.DeleteShortcut) error {
|
||||||
|
_, err := d.db.ExecContext(ctx, "DELETE FROM shortcut WHERE id = $1", delete.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
stmt := `DELETE FROM shortcut WHERE creator_id NOT IN (SELECT id FROM "user")`
|
||||||
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterTags(tags []string) []string {
|
||||||
|
result := []string{}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag != "" {
|
||||||
|
result = append(result, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertVisibilityStringToStorepb(visibility string) storepb.Visibility {
|
||||||
|
return storepb.Visibility(storepb.Visibility_value[visibility])
|
||||||
|
}
|
182
store/db/postgres/user.go
Normal file
182
store/db/postgres/user.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO "user" (
|
||||||
|
email,
|
||||||
|
nickname,
|
||||||
|
password_hash,
|
||||||
|
role
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, created_ts, updated_ts, row_status
|
||||||
|
`
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt,
|
||||||
|
create.Email,
|
||||||
|
create.Nickname,
|
||||||
|
create.PasswordHash,
|
||||||
|
create.Role,
|
||||||
|
).Scan(
|
||||||
|
&create.ID,
|
||||||
|
&create.CreatedTs,
|
||||||
|
&create.UpdatedTs,
|
||||||
|
&create.RowStatus,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := create
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) {
|
||||||
|
set, args := []string{}, []any{}
|
||||||
|
if v := update.RowStatus; v != nil {
|
||||||
|
set, args = append(set, "row_status = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := update.Email; v != nil {
|
||||||
|
set, args = append(set, "email = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := update.Nickname; v != nil {
|
||||||
|
set, args = append(set, "nickname = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := update.PasswordHash; v != nil {
|
||||||
|
set, args = append(set, "password_hash = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := update.Role; v != nil {
|
||||||
|
set, args = append(set, "role = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(set) == 0 {
|
||||||
|
return nil, errors.New("no fields to update")
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := `
|
||||||
|
UPDATE "user"
|
||||||
|
SET ` + strings.Join(set, ", ") + `
|
||||||
|
WHERE id = $` + placeholder(len(args)+1) + `
|
||||||
|
RETURNING id, created_ts, updated_ts, row_status, email, nickname, password_hash, role
|
||||||
|
`
|
||||||
|
args = append(args, update.ID)
|
||||||
|
user := &store.User{}
|
||||||
|
if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.CreatedTs,
|
||||||
|
&user.UpdatedTs,
|
||||||
|
&user.RowStatus,
|
||||||
|
&user.Email,
|
||||||
|
&user.Nickname,
|
||||||
|
&user.PasswordHash,
|
||||||
|
&user.Role,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
|
if v := find.ID; v != nil {
|
||||||
|
where, args = append(where, "id = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.RowStatus; v != nil {
|
||||||
|
where, args = append(where, "row_status = $"+placeholder(len(args)+1)), append(args, v.String())
|
||||||
|
}
|
||||||
|
if v := find.Email; v != nil {
|
||||||
|
where, args = append(where, "email = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.Nickname; v != nil {
|
||||||
|
where, args = append(where, "nickname = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
if v := find.Role; v != nil {
|
||||||
|
where, args = append(where, "role = $"+placeholder(len(args)+1)), append(args, *v)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
created_ts,
|
||||||
|
updated_ts,
|
||||||
|
row_status,
|
||||||
|
email,
|
||||||
|
nickname,
|
||||||
|
password_hash,
|
||||||
|
role
|
||||||
|
FROM "user"
|
||||||
|
WHERE ` + strings.Join(where, " AND ") + `
|
||||||
|
ORDER BY updated_ts DESC, created_ts DESC
|
||||||
|
`
|
||||||
|
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := make([]*store.User, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
user := &store.User{}
|
||||||
|
if err := rows.Scan(
|
||||||
|
&user.ID,
|
||||||
|
&user.CreatedTs,
|
||||||
|
&user.UpdatedTs,
|
||||||
|
&user.RowStatus,
|
||||||
|
&user.Email,
|
||||||
|
&user.Nickname,
|
||||||
|
&user.PasswordHash,
|
||||||
|
&user.Role,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list = append(list, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error {
|
||||||
|
tx, err := d.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
DELETE FROM "user" WHERE id = $1
|
||||||
|
`, delete.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := vacuumUserSetting(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := vacuumShortcut(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := vacuumMemo(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := vacuumCollection(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeholder(n int) string {
|
||||||
|
return "$" + fmt.Sprint(n)
|
||||||
|
}
|
122
store/db/postgres/user_setting.go
Normal file
122
store/db/postgres/user_setting.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting) (*storepb.UserSetting, error) {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO user_setting (
|
||||||
|
user_id, key, value
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT(user_id, key) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value
|
||||||
|
RETURNING user_id, key, value
|
||||||
|
`
|
||||||
|
|
||||||
|
var valueString string
|
||||||
|
if upsert.Key == storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS {
|
||||||
|
valueBytes, err := protojson.Marshal(upsert.GetAccessTokens())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueString = string(valueBytes)
|
||||||
|
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_LOCALE {
|
||||||
|
valueString = upsert.GetLocale().String()
|
||||||
|
} else if upsert.Key == storepb.UserSettingKey_USER_SETTING_COLOR_THEME {
|
||||||
|
valueString = upsert.GetColorTheme().String()
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid user setting key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.db.ExecContext(ctx, stmt, upsert.UserId, upsert.Key.String(), valueString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userSettingMessage := upsert
|
||||||
|
return userSettingMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*storepb.UserSetting, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
|
if v := find.Key; v != storepb.UserSettingKey_USER_SETTING_KEY_UNSPECIFIED {
|
||||||
|
where, args = append(where, fmt.Sprintf("key = $%d", len(args)+1)), append(args, v.String())
|
||||||
|
}
|
||||||
|
if v := find.UserID; v != nil {
|
||||||
|
where, args = append(where, fmt.Sprintf("user_id = $%d", len(args)+1)), append(args, *find.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
FROM user_setting
|
||||||
|
WHERE ` + strings.Join(where, " AND ")
|
||||||
|
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
userSettingList := make([]*storepb.UserSetting, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
userSetting := &storepb.UserSetting{}
|
||||||
|
var keyString, valueString string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&userSetting.UserId,
|
||||||
|
&keyString,
|
||||||
|
&valueString,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userSetting.Key = storepb.UserSettingKey(storepb.UserSettingKey_value[keyString])
|
||||||
|
if userSetting.Key == storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS {
|
||||||
|
accessTokensUserSetting := &storepb.AccessTokensUserSetting{}
|
||||||
|
if err := protojson.Unmarshal([]byte(valueString), accessTokensUserSetting); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userSetting.Value = &storepb.UserSetting_AccessTokens{
|
||||||
|
AccessTokens: accessTokensUserSetting,
|
||||||
|
}
|
||||||
|
} else if userSetting.Key == storepb.UserSettingKey_USER_SETTING_LOCALE {
|
||||||
|
userSetting.Value = &storepb.UserSetting_Locale{
|
||||||
|
Locale: storepb.LocaleUserSetting(storepb.LocaleUserSetting_value[valueString]),
|
||||||
|
}
|
||||||
|
} else if userSetting.Key == storepb.UserSettingKey_USER_SETTING_COLOR_THEME {
|
||||||
|
userSetting.Value = &storepb.UserSetting_ColorTheme{
|
||||||
|
ColorTheme: storepb.ColorThemeUserSetting(storepb.ColorThemeUserSetting_value[valueString]),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid user setting key")
|
||||||
|
}
|
||||||
|
userSettingList = append(userSettingList, userSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userSettingList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {
|
||||||
|
stmt := `DELETE FROM user_setting WHERE user_id NOT IN (SELECT id FROM "user")`
|
||||||
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
116
store/db/postgres/workspace_setting.go
Normal file
116
store/db/postgres/workspace_setting.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||||
|
"github.com/yourselfhosted/slash/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *DB) UpsertWorkspaceSetting(ctx context.Context, upsert *storepb.WorkspaceSetting) (*storepb.WorkspaceSetting, error) {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO workspace_setting (
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT(key) DO UPDATE
|
||||||
|
SET value = EXCLUDED.value
|
||||||
|
`
|
||||||
|
var valueString string
|
||||||
|
if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY {
|
||||||
|
valueString = upsert.GetLicenseKey()
|
||||||
|
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_SECRET_SESSION {
|
||||||
|
valueString = upsert.GetSecretSession()
|
||||||
|
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP {
|
||||||
|
valueString = strconv.FormatBool(upsert.GetEnableSignup())
|
||||||
|
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_CUSTOM_STYLE {
|
||||||
|
valueString = upsert.GetCustomStyle()
|
||||||
|
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_CUSTOM_SCRIPT {
|
||||||
|
valueString = upsert.GetCustomScript()
|
||||||
|
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_AUTO_BACKUP {
|
||||||
|
valueBytes, err := protojson.Marshal(upsert.GetAutoBackup())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueString = string(valueBytes)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("invalid workspace setting key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.db.ExecContext(ctx, stmt, upsert.Key.String(), valueString); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceSetting := upsert
|
||||||
|
return workspaceSetting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) ListWorkspaceSettings(ctx context.Context, find *store.FindWorkspaceSetting) ([]*storepb.WorkspaceSetting, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []interface{}{}
|
||||||
|
|
||||||
|
if find.Key != storepb.WorkspaceSettingKey_WORKSPACE_SETTING_KEY_UNSPECIFIED {
|
||||||
|
where, args = append(where, "key = $"+placeholder(len(args)+1)), append(args, find.Key.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
FROM workspace_setting
|
||||||
|
WHERE ` + strings.Join(where, " AND ")
|
||||||
|
rows, err := d.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := []*storepb.WorkspaceSetting{}
|
||||||
|
for rows.Next() {
|
||||||
|
workspaceSetting := &storepb.WorkspaceSetting{}
|
||||||
|
var keyString, valueString string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&keyString,
|
||||||
|
&valueString,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workspaceSetting.Key = storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[keyString])
|
||||||
|
if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_LICENSE_KEY {
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_LicenseKey{LicenseKey: valueString}
|
||||||
|
} else if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_SECRET_SESSION {
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_SecretSession{SecretSession: valueString}
|
||||||
|
} else if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP {
|
||||||
|
enableSignup, err := strconv.ParseBool(valueString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_EnableSignup{EnableSignup: enableSignup}
|
||||||
|
} else if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_CUSTOM_STYLE {
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_CustomStyle{CustomStyle: valueString}
|
||||||
|
} else if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_CUSTOM_SCRIPT {
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_CustomScript{CustomScript: valueString}
|
||||||
|
} else if workspaceSetting.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_AUTO_BACKUP {
|
||||||
|
autoBackupSetting := &storepb.AutoBackupWorkspaceSetting{}
|
||||||
|
if err := protojson.Unmarshal([]byte(valueString), autoBackupSetting); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workspaceSetting.Value = &storepb.WorkspaceSetting_AutoBackup{AutoBackup: autoBackupSetting}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, workspaceSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user