mirror of
https://github.com/aykhans/slash-e.git
synced 2025-06-14 20:07:50 +00:00
feat: init project
This commit is contained in:
72
store/cache.go
Normal file
72
store/cache.go
Normal file
@ -0,0 +1,72 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
"github.com/boojack/corgi/api"
|
||||
)
|
||||
|
||||
var (
|
||||
// 64 MiB.
|
||||
cacheSize = 1024 * 1024 * 64
|
||||
_ api.CacheService = (*CacheService)(nil)
|
||||
)
|
||||
|
||||
// CacheService implements a cache.
|
||||
type CacheService struct {
|
||||
cache *fastcache.Cache
|
||||
}
|
||||
|
||||
// NewCacheService creates a cache service.
|
||||
func NewCacheService() *CacheService {
|
||||
return &CacheService{
|
||||
cache: fastcache.New(cacheSize),
|
||||
}
|
||||
}
|
||||
|
||||
// FindCache finds the value in cache.
|
||||
func (s *CacheService) FindCache(namespace api.CacheNamespace, id int, entry interface{}) (bool, error) {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
buf2, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
|
||||
if has {
|
||||
dec := gob.NewDecoder(bytes.NewReader(buf2))
|
||||
if err := dec.Decode(entry); err != nil {
|
||||
return false, fmt.Errorf("failed to decode entry for cache namespace: %s, error: %w", namespace, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// UpsertCache upserts the value to cache.
|
||||
func (s *CacheService) UpsertCache(namespace api.CacheNamespace, id int, entry interface{}) error {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
var buf2 bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf2)
|
||||
if err := enc.Encode(entry); err != nil {
|
||||
return fmt.Errorf("failed to encode entry for cache namespace: %s, error: %w", namespace, err)
|
||||
}
|
||||
s.cache.Set(append([]byte(namespace), buf1...), buf2.Bytes())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCache deletes the cache.
|
||||
func (s *CacheService) DeleteCache(namespace api.CacheNamespace, id int) {
|
||||
buf1 := []byte{0, 0, 0, 0, 0, 0, 0, 0}
|
||||
binary.LittleEndian.PutUint64(buf1, uint64(id))
|
||||
|
||||
_, has := s.cache.HasGet(nil, append([]byte(namespace), buf1...))
|
||||
if has {
|
||||
s.cache.Del(append([]byte(namespace), buf1...))
|
||||
}
|
||||
}
|
258
store/db/db.go
Normal file
258
store/db/db.go
Normal file
@ -0,0 +1,258 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/boojack/corgi/server/profile"
|
||||
"github.com/boojack/corgi/server/version"
|
||||
)
|
||||
|
||||
//go:embed migration
|
||||
var migrationFS embed.FS
|
||||
|
||||
//go:embed seed
|
||||
var seedFS embed.FS
|
||||
|
||||
type DB struct {
|
||||
// sqlite db connection instance
|
||||
Db *sql.DB
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
// NewDB returns a new instance of DB associated with the given datasource name.
|
||||
func NewDB(profile *profile.Profile) *DB {
|
||||
db := &DB{
|
||||
profile: profile,
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *DB) Open(ctx context.Context) (err error) {
|
||||
// Ensure a DSN is set before attempting to open the database.
|
||||
if db.profile.DSN == "" {
|
||||
return fmt.Errorf("dsn required")
|
||||
}
|
||||
|
||||
// Connect to the database.
|
||||
sqlDB, err := sql.Open("sqlite3", db.profile.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
|
||||
}
|
||||
|
||||
db.Db = sqlDB
|
||||
// If mode is dev, we should migrate and seed the database.
|
||||
if db.profile.Mode == "dev" {
|
||||
if err := db.applyLatestSchema(ctx); err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
if err := db.seed(ctx); err != nil {
|
||||
return fmt.Errorf("failed to seed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// If db file not exists, we should migrate the database.
|
||||
if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
err := db.applyLatestSchema(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := db.createMigrationHistoryTable(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration_history table: %w", err)
|
||||
}
|
||||
|
||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if migrationHistory == nil {
|
||||
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||
Version: currentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if version.IsVersionGreaterThan(version.GetSchemaVersion(currentVersion), migrationHistory.Version) {
|
||||
minorVersionList := getMinorVersionList()
|
||||
|
||||
// backup the raw database file before migration
|
||||
rawBytes, err := os.ReadFile(db.profile.DSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
||||
}
|
||||
backupDBFilePath := fmt.Sprintf("%s/corgi_%s_%d_backup.db", db.profile.Data, db.profile.Version, time.Now().Unix())
|
||||
if err := os.WriteFile(backupDBFilePath, rawBytes, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write raw database file, err: %w", err)
|
||||
}
|
||||
|
||||
println("succeed to copy a backup database file")
|
||||
println("start migrate")
|
||||
for _, minorVersion := range minorVersionList {
|
||||
normalizedVersion := minorVersion + ".0"
|
||||
if version.IsVersionGreaterThan(normalizedVersion, migrationHistory.Version) && version.IsVersionGreaterOrEqualThan(currentVersion, normalizedVersion) {
|
||||
println("applying migration for", normalizedVersion)
|
||||
if err := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
latestSchemaFileName = "LATEST__SCHEMA.sql"
|
||||
)
|
||||
|
||||
func (db *DB) applyLatestSchema(ctx context.Context) error {
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
|
||||
buf, err := migrationFS.ReadFile(latestSchemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *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 err
|
||||
}
|
||||
|
||||
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 fmt.Errorf("failed to read minor version migration file, filename=%s err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
migrationStmt += stmt
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("migrate error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// upsert the newest version to migration_history
|
||||
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
|
||||
Version: minorVersion + ".0",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (db *DB) seed(ctx context.Context) error {
|
||||
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 fmt.Errorf("failed to read seed file, filename=%s err=%w", filename, err)
|
||||
}
|
||||
stmt := string(buf)
|
||||
if err := db.execute(ctx, stmt); err != nil {
|
||||
return fmt.Errorf("seed error: statement:%s err=%w", stmt, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// execute runs a single SQL statement within a transaction.
|
||||
func (db *DB) execute(ctx context.Context, stmt string) error {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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.Strings(minorVersionList)
|
||||
|
||||
return minorVersionList
|
||||
}
|
||||
|
||||
// createMigrationHistoryTable creates the migration_history table if it doesn't exist.
|
||||
func (db *DB) createMigrationHistoryTable(ctx context.Context) error {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if err := createTable(ctx, tx, `
|
||||
CREATE TABLE IF NOT EXISTS migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
159
store/db/migration/dev/LATEST__SCHEMA.sql
Normal file
159
store/db/migration/dev/LATEST__SCHEMA.sql
Normal file
@ -0,0 +1,159 @@
|
||||
-- drop all tables
|
||||
DROP TABLE IF EXISTS `activity`;
|
||||
DROP TABLE IF EXISTS `shortcut_organizer`;
|
||||
DROP TABLE IF EXISTS `shortcut`;
|
||||
DROP TABLE IF EXISTS `workspace_user`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
DROP TABLE IF EXISTS `workspace_setting`;
|
||||
DROP TABLE IF EXISTS `workspace`;
|
||||
|
||||
-- workspace
|
||||
CREATE TABLE workspace (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('workspace', 10);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_workspace_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `workspace` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`workspace`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- workspace_setting
|
||||
CREATE TABLE workspace_setting (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX workspace_setting_key_workspace_id_index ON workspace_setting(key, workspace_id);
|
||||
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('user', 100);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `user` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`user`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- user_setting
|
||||
CREATE TABLE user_setting (
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
|
||||
-- workspace_user
|
||||
CREATE TABLE workspace_user (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER',
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX workspace_user_workspace_id_user_id_index ON workspace_user(workspace_id, user_id);
|
||||
|
||||
-- shortcut
|
||||
CREATE TABLE shortcut (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
workspace_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (row_status IN ('PRIVATE', 'WORKSPACE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX shortcut_workspace_id_name_index ON shortcut(workspace_id, name);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('shortcut', 1000);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `shortcut` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`shortcut`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- shortcut_organizer
|
||||
CREATE TABLE shortcut_organizer (
|
||||
shortcut_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
||||
FOREIGN KEY(shortcut_id) REFERENCES shortcut(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
UNIQUE(shortcut_id, user_id)
|
||||
);
|
||||
|
||||
-- activity
|
||||
CREATE TABLE activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
type TEXT NOT NULL,
|
||||
comment TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('activity', 10000);
|
157
store/db/migration/prod/LATEST__SCHEMA.sql
Normal file
157
store/db/migration/prod/LATEST__SCHEMA.sql
Normal file
@ -0,0 +1,157 @@
|
||||
-- drop all tables
|
||||
DROP TABLE IF EXISTS `activity`;
|
||||
DROP TABLE IF EXISTS `shortcut_organizer`;
|
||||
DROP TABLE IF EXISTS `shortcut`;
|
||||
DROP TABLE IF EXISTS `workspace_user`;
|
||||
DROP TABLE IF EXISTS `user_setting`;
|
||||
DROP TABLE IF EXISTS `user`;
|
||||
DROP TABLE IF EXISTS `workspace_setting`;
|
||||
DROP TABLE IF EXISTS `workspace`;
|
||||
|
||||
-- workspace
|
||||
CREATE TABLE workspace (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('workspace', 10);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_workspace_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `workspace` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`workspace`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- workspace_setting
|
||||
CREATE TABLE workspace_setting (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX workspace_setting_key_workspace_id_index ON workspace_setting(key, workspace_id);
|
||||
|
||||
-- user
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('user', 100);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `user` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`user`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- user_setting
|
||||
CREATE TABLE user_setting (
|
||||
user_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
|
||||
-- workspace_user
|
||||
CREATE TABLE workspace_user (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('ADMIN', 'USER')) DEFAULT 'USER',
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX workspace_user_workspace_id_user_id_index ON workspace_user(workspace_id, user_id);
|
||||
|
||||
-- shortcut
|
||||
CREATE TABLE shortcut (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||
workspace_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (row_status IN ('PRIVATE', 'WORKSPACE')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX shortcut_workspace_id_name_index ON shortcut(workspace_id, name);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('shortcut', 1000);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time`
|
||||
AFTER
|
||||
UPDATE
|
||||
ON `shortcut` FOR EACH ROW BEGIN
|
||||
UPDATE
|
||||
`shortcut`
|
||||
SET
|
||||
updated_ts = (strftime('%s', 'now'))
|
||||
WHERE
|
||||
rowid = old.rowid;
|
||||
END;
|
||||
|
||||
-- shortcut_organizer
|
||||
CREATE TABLE shortcut_organizer (
|
||||
shortcut_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
||||
FOREIGN KEY(shortcut_id) REFERENCES shortcut(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
UNIQUE(shortcut_id, user_id)
|
||||
);
|
||||
|
||||
-- activity
|
||||
CREATE TABLE activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
type TEXT NOT NULL,
|
||||
comment TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('activity', 10000);
|
134
store/db/migration_history.go
Normal file
134
store/db/migration_history.go
Normal file
@ -0,0 +1,134 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MigrationHistory struct {
|
||||
Version string
|
||||
CreatedTs int64
|
||||
}
|
||||
|
||||
type MigrationHistoryUpsert struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type MigrationHistoryFind struct {
|
||||
Version *string
|
||||
}
|
||||
|
||||
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findMigrationHistoryList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
migrationHistory := list[0]
|
||||
return migrationHistory, nil
|
||||
}
|
||||
|
||||
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
migrationHistory, err := upsertMigrationHistory(ctx, tx, upsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return migrationHistory, nil
|
||||
}
|
||||
|
||||
func findMigrationHistoryList(ctx context.Context, tx *sql.Tx, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.Version; v != nil {
|
||||
where, args = append(where, "version = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
version,
|
||||
created_ts
|
||||
FROM
|
||||
migration_history
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY version DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
migrationHistoryList := make([]*MigrationHistory, 0)
|
||||
for rows.Next() {
|
||||
var migrationHistory MigrationHistory
|
||||
if err := rows.Scan(
|
||||
&migrationHistory.Version,
|
||||
&migrationHistory.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
migrationHistoryList = append(migrationHistoryList, &migrationHistory)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return migrationHistoryList, nil
|
||||
}
|
||||
|
||||
func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
query := `
|
||||
INSERT INTO migration_history (
|
||||
version
|
||||
)
|
||||
VALUES (?)
|
||||
ON CONFLICT(version) DO UPDATE
|
||||
SET
|
||||
version=EXCLUDED.version
|
||||
RETURNING version, created_ts
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, upsert.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
row.Next()
|
||||
var migrationHistory MigrationHistory
|
||||
if err := row.Scan(
|
||||
&migrationHistory.Version,
|
||||
&migrationHistory.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migrationHistory, nil
|
||||
}
|
8
store/db/seed/10000__reset.sql
Normal file
8
store/db/seed/10000__reset.sql
Normal file
@ -0,0 +1,8 @@
|
||||
DELETE FROM activity;
|
||||
DELETE FROM shortcut_organizer;
|
||||
DELETE FROM shortcut;
|
||||
DELETE FROM workspace_user;
|
||||
DELETE FROM user_setting;
|
||||
DELETE FROM user;
|
||||
DELETE FROM workspace_setting;
|
||||
DELETE FROM workspace;
|
65
store/db/table.go
Normal file
65
store/db/table.go
Normal file
@ -0,0 +1,65 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
Name string
|
||||
SQL string
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Ignore unused function temporarily for debugging
|
||||
//nolint:all
|
||||
func findTable(ctx context.Context, tx *sql.Tx, tableName string) (*Table, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
where, args = append(where, "type = ?"), append(args, "table")
|
||||
where, args = append(where, "name = ?"), append(args, tableName)
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
tbl_name,
|
||||
sql
|
||||
FROM sqlite_schema
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tableList := make([]*Table, 0)
|
||||
for rows.Next() {
|
||||
var table Table
|
||||
if err := rows.Scan(
|
||||
&table.Name,
|
||||
&table.SQL,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableList = append(tableList, &table)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tableList) == 0 {
|
||||
return nil, nil
|
||||
} else {
|
||||
return tableList[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
func createTable(ctx context.Context, tx *sql.Tx, stmt string) error {
|
||||
_, err := tx.ExecContext(ctx, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
19
store/error.go
Normal file
19
store/error.go
Normal file
@ -0,0 +1,19 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func FormatError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
return errors.New("data not found")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
328
store/shortcut.go
Normal file
328
store/shortcut.go
Normal file
@ -0,0 +1,328 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
"github.com/boojack/corgi/common"
|
||||
)
|
||||
|
||||
// shortcutRaw is the store model for an Shortcut.
|
||||
// Fields have exactly the same meanings as Shortcut.
|
||||
type shortcutRaw struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
WorkspaceID int
|
||||
RowStatus api.RowStatus
|
||||
|
||||
// Domain specific fields
|
||||
Name string
|
||||
Link string
|
||||
Visibility api.Visibility
|
||||
}
|
||||
|
||||
func (raw *shortcutRaw) toShortcut() *api.Shortcut {
|
||||
return &api.Shortcut{
|
||||
ID: raw.ID,
|
||||
|
||||
CreatorID: raw.CreatorID,
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
WorkspaceID: raw.WorkspaceID,
|
||||
RowStatus: raw.RowStatus,
|
||||
|
||||
Name: raw.Name,
|
||||
Link: raw.Link,
|
||||
Visibility: raw.Visibility,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateShortcut(ctx context.Context, create *api.ShortcutCreate) (*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRaw, err := createShortcut(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
shortcut := shortcutRaw.toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchShortcut(ctx context.Context, patch *api.ShortcutPatch) (*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRaw, err := patchShortcut(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
shortcut := shortcutRaw.toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindShortcutList(ctx context.Context, find *api.ShortcutFind) ([]*api.Shortcut, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
shortcutRawList, err := findShortcutList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.Shortcut{}
|
||||
for _, raw := range shortcutRawList {
|
||||
list = append(list, raw.toShortcut())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindShortcut(ctx context.Context, find *api.ShortcutFind) (*api.Shortcut, error) {
|
||||
if find.ID != nil {
|
||||
shortcut := &api.Shortcut{}
|
||||
has, err := s.cache.FindCache(api.ShortcutCache, *find.ID, shortcut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return shortcut, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findShortcutList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
|
||||
}
|
||||
|
||||
shortcut := list[0].toShortcut()
|
||||
|
||||
if err := s.cache.UpsertCache(api.ShortcutCache, shortcut.ID, shortcut); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteShortcut(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.ShortcutCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) (*shortcutRaw, error) {
|
||||
query := `
|
||||
INSERT INTO shortcut (
|
||||
creator_id,
|
||||
workspace_id,
|
||||
name,
|
||||
link,
|
||||
visibility
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING id, creator_id, created_ts, updated_ts, workspace_id, row_status, name, link, visibility
|
||||
`
|
||||
var shortcutRaw shortcutRaw
|
||||
if err := tx.QueryRowContext(ctx, query, create.CreatorID, create.WorkspaceID, create.Name, create.Link, create.Visibility).Scan(
|
||||
&shortcutRaw.ID,
|
||||
&shortcutRaw.CreatorID,
|
||||
&shortcutRaw.CreatedTs,
|
||||
&shortcutRaw.UpdatedTs,
|
||||
&shortcutRaw.WorkspaceID,
|
||||
&shortcutRaw.RowStatus,
|
||||
&shortcutRaw.Name,
|
||||
&shortcutRaw.Link,
|
||||
&shortcutRaw.Visibility,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &shortcutRaw, nil
|
||||
}
|
||||
|
||||
func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Link; v != nil {
|
||||
set, args = append(set, "link = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Visibility; v != nil {
|
||||
set, args = append(set, "visibility = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
query := `
|
||||
UPDATE shortcut
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, creator_id, created_ts, updated_ts, workspace_id, row_status, name, link, visibility
|
||||
`
|
||||
var shortcutRaw shortcutRaw
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&shortcutRaw.ID,
|
||||
&shortcutRaw.CreatorID,
|
||||
&shortcutRaw.CreatedTs,
|
||||
&shortcutRaw.UpdatedTs,
|
||||
&shortcutRaw.WorkspaceID,
|
||||
&shortcutRaw.RowStatus,
|
||||
&shortcutRaw.Name,
|
||||
&shortcutRaw.Link,
|
||||
&shortcutRaw.Visibility,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &shortcutRaw, nil
|
||||
}
|
||||
|
||||
func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) ([]*shortcutRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.CreatorID; v != nil {
|
||||
where, args = append(where, "creator_id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.WorkspaceID; v != nil {
|
||||
where, args = append(where, "workspace_id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Name; v != nil {
|
||||
where, args = append(where, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Link; v != nil {
|
||||
where, args = append(where, "link = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Visibility; v != nil {
|
||||
where, args = append(where, "visibility = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT
|
||||
id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
workspace_id,
|
||||
row_status,
|
||||
name,
|
||||
link,
|
||||
visibility
|
||||
FROM shortcut
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY created_ts DESC`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
shortcutRawList := make([]*shortcutRaw, 0)
|
||||
for rows.Next() {
|
||||
var shortcutRaw shortcutRaw
|
||||
if err := rows.Scan(
|
||||
&shortcutRaw.ID,
|
||||
&shortcutRaw.CreatorID,
|
||||
&shortcutRaw.CreatedTs,
|
||||
&shortcutRaw.UpdatedTs,
|
||||
&shortcutRaw.WorkspaceID,
|
||||
&shortcutRaw.RowStatus,
|
||||
&shortcutRaw.Name,
|
||||
&shortcutRaw.Link,
|
||||
&shortcutRaw.Visibility,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
shortcutRawList = append(shortcutRawList, &shortcutRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return shortcutRawList, nil
|
||||
}
|
||||
|
||||
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM shortcut WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
26
store/store.go
Normal file
26
store/store.go
Normal file
@ -0,0 +1,26 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
"github.com/boojack/corgi/server/profile"
|
||||
)
|
||||
|
||||
// Store provides database access to all raw objects.
|
||||
type Store struct {
|
||||
db *sql.DB
|
||||
profile *profile.Profile
|
||||
cache api.CacheService
|
||||
}
|
||||
|
||||
// New creates a new instance of Store.
|
||||
func New(db *sql.DB, profile *profile.Profile) *Store {
|
||||
cacheService := NewCacheService()
|
||||
|
||||
return &Store{
|
||||
db: db,
|
||||
profile: profile,
|
||||
cache: cacheService,
|
||||
}
|
||||
}
|
328
store/user.go
Normal file
328
store/user.go
Normal file
@ -0,0 +1,328 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
"github.com/boojack/corgi/common"
|
||||
)
|
||||
|
||||
// userRaw is the store model for an User.
|
||||
// Fields have exactly the same meanings as User.
|
||||
type userRaw struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
RowStatus api.RowStatus
|
||||
|
||||
// Domain specific fields
|
||||
Email string
|
||||
Name string
|
||||
PasswordHash string
|
||||
}
|
||||
|
||||
func (raw *userRaw) toUser() *api.User {
|
||||
return &api.User{
|
||||
ID: raw.ID,
|
||||
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
RowStatus: raw.RowStatus,
|
||||
|
||||
Email: raw.Email,
|
||||
Name: raw.Name,
|
||||
PasswordHash: raw.PasswordHash,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, create *api.UserCreate) (*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRaw, err := createUser(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
user := userRaw.toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchUser(ctx context.Context, patch *api.UserPatch) (*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRaw, err := patchUser(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
user := userRaw.toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.User, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userRawList, err := findUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.User{}
|
||||
for _, raw := range userRawList {
|
||||
list = append(list, raw.toUser())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, error) {
|
||||
if find.ID != nil {
|
||||
user := &api.User{}
|
||||
has, err := s.cache.FindCache(api.UserCache, *find.ID, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found user with filter %+v", find)}
|
||||
} else if len(list) > 1 {
|
||||
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d users with filter %+v, expect 1", len(list), find)}
|
||||
}
|
||||
|
||||
user := list[0].toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteUser(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.UserCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userRaw, error) {
|
||||
query := `
|
||||
INSERT INTO user (
|
||||
email,
|
||||
name,
|
||||
password_hash
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id, email, name, password_hash, created_ts, updated_ts, row_status
|
||||
`
|
||||
var userRaw userRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
create.Email,
|
||||
create.Name,
|
||||
create.PasswordHash,
|
||||
).Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.CreatedTs,
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &userRaw, nil
|
||||
}
|
||||
|
||||
func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.RowStatus; v != nil {
|
||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Email; v != nil {
|
||||
set, args = append(set, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.PasswordHash; v != nil {
|
||||
set, args = append(set, "password_hash = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
query := `
|
||||
UPDATE user
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, created_ts, updated_ts, row_status, email, name, password_hash
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if row.Next() {
|
||||
var userRaw userRaw
|
||||
if err := row.Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.CreatedTs,
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.PasswordHash,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userRaw, nil
|
||||
}
|
||||
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", patch.ID)}
|
||||
}
|
||||
|
||||
func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Email; v != nil {
|
||||
where, args = append(where, "email = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Name; v != nil {
|
||||
where, args = append(where, "name = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status,
|
||||
email,
|
||||
name,
|
||||
password_hash
|
||||
FROM user
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY updated_ts DESC, created_ts DESC, row_status DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userRawList := make([]*userRaw, 0)
|
||||
for rows.Next() {
|
||||
var userRaw userRaw
|
||||
if err := rows.Scan(
|
||||
&userRaw.ID,
|
||||
&userRaw.CreatedTs,
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.PasswordHash,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
userRawList = append(userRawList, &userRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return userRawList, nil
|
||||
}
|
||||
|
||||
func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM user WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
151
store/user_setting.go
Normal file
151
store/user_setting.go
Normal file
@ -0,0 +1,151 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
)
|
||||
|
||||
type userSettingRaw struct {
|
||||
UserID int
|
||||
Key api.UserSettingKey
|
||||
Value string
|
||||
}
|
||||
|
||||
func (raw *userSettingRaw) toUserSetting() *api.UserSetting {
|
||||
return &api.UserSetting{
|
||||
UserID: raw.UserID,
|
||||
Key: raw.Key,
|
||||
Value: raw.Value,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UpsertUserSetting(ctx context.Context, upsert *api.UserSettingUpsert) (*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userSettingRaw, err := upsertUserSetting(ctx, tx, upsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userSetting := userSettingRaw.toUserSetting()
|
||||
|
||||
return userSetting, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserSettingList(ctx context.Context, find *api.UserSettingFind) ([]*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
userSettingRawList, err := findUserSettingList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.UserSetting{}
|
||||
for _, raw := range userSettingRawList {
|
||||
list = append(list, raw.toUserSetting())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserSetting(ctx context.Context, find *api.UserSettingFind) (*api.UserSetting, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findUserSettingList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userSetting := list[0].toUserSetting()
|
||||
|
||||
return userSetting, nil
|
||||
}
|
||||
|
||||
func upsertUserSetting(ctx context.Context, tx *sql.Tx, upsert *api.UserSettingUpsert) (*userSettingRaw, error) {
|
||||
query := `
|
||||
INSERT INTO user_setting (
|
||||
user_id, key, value
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, key) DO UPDATE
|
||||
SET
|
||||
value = EXCLUDED.value
|
||||
RETURNING user_id, key, value
|
||||
`
|
||||
var userSettingRaw userSettingRaw
|
||||
if err := tx.QueryRowContext(ctx, query, upsert.UserID, upsert.Key, upsert.Value).Scan(
|
||||
&userSettingRaw.UserID,
|
||||
&userSettingRaw.Key,
|
||||
&userSettingRaw.Value,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &userSettingRaw, nil
|
||||
}
|
||||
|
||||
func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingFind) ([]*userSettingRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.Key; v != nil {
|
||||
where, args = append(where, "key = ?"), append(args, v.String())
|
||||
}
|
||||
|
||||
where, args = append(where, "user_id = ?"), append(args, find.UserID)
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
user_id,
|
||||
key,
|
||||
value
|
||||
FROM user_setting
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userSettingRawList := make([]*userSettingRaw, 0)
|
||||
for rows.Next() {
|
||||
var userSettingRaw userSettingRaw
|
||||
if err := rows.Scan(
|
||||
&userSettingRaw.UserID,
|
||||
&userSettingRaw.Key,
|
||||
&userSettingRaw.Value,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
userSettingRawList = append(userSettingRawList, &userSettingRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return userSettingRawList, nil
|
||||
}
|
327
store/workspace.go
Normal file
327
store/workspace.go
Normal file
@ -0,0 +1,327 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
"github.com/boojack/corgi/common"
|
||||
)
|
||||
|
||||
// workspaceRaw is the store model for Workspace.
|
||||
type workspaceRaw struct {
|
||||
ID int
|
||||
|
||||
// Standard fields
|
||||
CreatorID int
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
RowStatus api.RowStatus
|
||||
|
||||
// Domain specific fields
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (raw *workspaceRaw) toWorkspace() *api.Workspace {
|
||||
return &api.Workspace{
|
||||
ID: raw.ID,
|
||||
|
||||
CreatorID: raw.CreatorID,
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
RowStatus: raw.RowStatus,
|
||||
|
||||
Name: raw.Name,
|
||||
Description: raw.Description,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) CreateWorkspace(ctx context.Context, create *api.WorkspaceCreate) (*api.Workspace, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
workspaceRaw, err := createWorkspace(ctx, tx, create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
workspace := workspaceRaw.toWorkspace()
|
||||
|
||||
if err := s.cache.UpsertCache(api.WorkspaceCache, workspace.ID, workspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (s *Store) PatchWorkspace(ctx context.Context, patch *api.WorkspacePatch) (*api.Workspace, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
workspaceRaw, err := patchWorkspace(ctx, tx, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
workspace := workspaceRaw.toWorkspace()
|
||||
|
||||
if err := s.cache.UpsertCache(api.WorkspaceCache, workspace.ID, workspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindWordspaceList(ctx context.Context, find *api.WorkspaceFind) ([]*api.Workspace, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
workspaceRawList, err := findWorkspaceList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.Workspace{}
|
||||
for _, raw := range workspaceRawList {
|
||||
list = append(list, raw.toWorkspace())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindWorkspace(ctx context.Context, find *api.WorkspaceFind) (*api.Workspace, error) {
|
||||
if find.ID != nil {
|
||||
workspace := &api.Workspace{}
|
||||
has, err := s.cache.FindCache(api.WorkspaceCache, *find.ID, workspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if has {
|
||||
return workspace, nil
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findWorkspaceList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found workspace with filter %+v", find)}
|
||||
} else if len(list) > 1 {
|
||||
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d workspaces with filter %+v, expect 1", len(list), find)}
|
||||
}
|
||||
|
||||
workspace := list[0].toWorkspace()
|
||||
|
||||
if err := s.cache.UpsertCache(api.WorkspaceCache, workspace.ID, workspace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteWorkspace(ctx context.Context, delete *api.WorkspaceDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteWorkspace(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
s.cache.DeleteCache(api.WorkspaceCache, delete.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWorkspace(ctx context.Context, tx *sql.Tx, create *api.WorkspaceCreate) (*workspaceRaw, error) {
|
||||
query := `
|
||||
INSERT INTO workspace (
|
||||
creator_id,
|
||||
name,
|
||||
description
|
||||
)
|
||||
VALUES (?, ?, ?)
|
||||
RETURNING id, creator_id, created_ts, updated_ts, row_status, name, description
|
||||
`
|
||||
var workspaceRaw workspaceRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
create.CreatorID,
|
||||
create.Name,
|
||||
create.Description,
|
||||
).Scan(
|
||||
&workspaceRaw.ID,
|
||||
&workspaceRaw.CreatorID,
|
||||
&workspaceRaw.CreatedTs,
|
||||
&workspaceRaw.UpdatedTs,
|
||||
&workspaceRaw.RowStatus,
|
||||
&workspaceRaw.Name,
|
||||
&workspaceRaw.Description,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &workspaceRaw, nil
|
||||
}
|
||||
|
||||
func patchWorkspace(ctx context.Context, tx *sql.Tx, patch *api.WorkspacePatch) (*workspaceRaw, error) {
|
||||
set, args := []string{}, []interface{}{}
|
||||
|
||||
if v := patch.RowStatus; v != nil {
|
||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Name; v != nil {
|
||||
set, args = append(set, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.Description; v != nil {
|
||||
set, args = append(set, "description = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
args = append(args, patch.ID)
|
||||
|
||||
query := `
|
||||
UPDATE workspace
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, creator_id, created_ts, updated_ts, row_status, name, description
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if row.Next() {
|
||||
var workspaceRaw workspaceRaw
|
||||
if err := row.Scan(
|
||||
&workspaceRaw.ID,
|
||||
&workspaceRaw.CreatorID,
|
||||
&workspaceRaw.CreatedTs,
|
||||
&workspaceRaw.UpdatedTs,
|
||||
&workspaceRaw.RowStatus,
|
||||
&workspaceRaw.Name,
|
||||
&workspaceRaw.Description,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &workspaceRaw, nil
|
||||
}
|
||||
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("workspace ID not found: %d", patch.ID)}
|
||||
}
|
||||
|
||||
func findWorkspaceList(ctx context.Context, tx *sql.Tx, find *api.WorkspaceFind) ([]*workspaceRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.ID; v != nil {
|
||||
where, args = append(where, "id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.RowStatus; v != nil {
|
||||
where, args = append(where, "row_status = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.Name; v != nil {
|
||||
where, args = append(where, "name = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.MemberID; v != nil {
|
||||
where, args = append(where, "id IN (SELECT workspace_id FROM workspace_user WHERE user_id = ? )"), append(args, *v)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
id,
|
||||
creator_id,
|
||||
created_ts,
|
||||
updated_ts,
|
||||
row_status,
|
||||
name,
|
||||
description
|
||||
FROM workspace
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY created_ts DESC, row_status DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
workspaceRawList := make([]*workspaceRaw, 0)
|
||||
for rows.Next() {
|
||||
var workspaceRaw workspaceRaw
|
||||
if err := rows.Scan(
|
||||
&workspaceRaw.ID,
|
||||
&workspaceRaw.CreatorID,
|
||||
&workspaceRaw.CreatedTs,
|
||||
&workspaceRaw.UpdatedTs,
|
||||
&workspaceRaw.RowStatus,
|
||||
&workspaceRaw.Name,
|
||||
&workspaceRaw.Description,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
workspaceRawList = append(workspaceRawList, &workspaceRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return workspaceRawList, nil
|
||||
}
|
||||
|
||||
func deleteWorkspace(ctx context.Context, tx *sql.Tx, delete *api.WorkspaceDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM workspace WHERE id = ?
|
||||
`, delete.ID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("workspace ID not found: %d", delete.ID)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
213
store/workspace_user.go
Normal file
213
store/workspace_user.go
Normal file
@ -0,0 +1,213 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/boojack/corgi/api"
|
||||
"github.com/boojack/corgi/common"
|
||||
)
|
||||
|
||||
// workspaceUserRaw is the store model for WorkspaceUser.
|
||||
type workspaceUserRaw struct {
|
||||
WorkspaceID int
|
||||
UserID int
|
||||
Role api.Role
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
}
|
||||
|
||||
func (raw *workspaceUserRaw) toWorkspaceUser() *api.WorkspaceUser {
|
||||
return &api.WorkspaceUser{
|
||||
WorkspaceID: raw.WorkspaceID,
|
||||
UserID: raw.UserID,
|
||||
Role: raw.Role,
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UpsertWorkspaceUser(ctx context.Context, upsert *api.WorkspaceUserUpsert) (*api.WorkspaceUser, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
workspaceUserRaw, err := upsertWorkspaceUser(ctx, tx, upsert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
workspaceUser := workspaceUserRaw.toWorkspaceUser()
|
||||
|
||||
return workspaceUser, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindWordspaceUserList(ctx context.Context, find *api.WorkspaceUserFind) ([]*api.WorkspaceUser, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
workspaceUserRawList, err := findWorkspaceUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*api.WorkspaceUser{}
|
||||
for _, raw := range workspaceUserRawList {
|
||||
list = append(list, raw.toWorkspaceUser())
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindWordspaceUser(ctx context.Context, find *api.WorkspaceUserFind) (*api.WorkspaceUser, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
list, err := findWorkspaceUserList(ctx, tx, find)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found workspace user with filter %+v", find)}
|
||||
} else if len(list) > 1 {
|
||||
return nil, &common.Error{Code: common.Conflict, Err: fmt.Errorf("found %d workspaces user with filter %+v, expect 1", len(list), find)}
|
||||
}
|
||||
|
||||
workspaceUser := list[0].toWorkspaceUser()
|
||||
return workspaceUser, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteWorkspaceUser(ctx context.Context, delete *api.WorkspaceUserDelete) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
err = deleteWorkspaceUser(ctx, tx, delete)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertWorkspaceUser(ctx context.Context, tx *sql.Tx, upsert *api.WorkspaceUserUpsert) (*workspaceUserRaw, error) {
|
||||
query := `
|
||||
INSERT INTO workspace_user (
|
||||
workspace_id,
|
||||
user_id,
|
||||
role,
|
||||
updated_ts
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(workspace_id, user_id) DO UPDATE
|
||||
SET
|
||||
role = EXCLUDED.role,
|
||||
updated_ts = EXCLUDED.updated_ts
|
||||
RETURNING workspace_id, user_id, role, created_ts, updated_ts
|
||||
`
|
||||
var workspaceUserRaw workspaceUserRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
upsert.WorkspaceID,
|
||||
upsert.UserID,
|
||||
upsert.Role,
|
||||
upsert.UpdatedTs,
|
||||
).Scan(
|
||||
&workspaceUserRaw.WorkspaceID,
|
||||
&workspaceUserRaw.UserID,
|
||||
&workspaceUserRaw.Role,
|
||||
&workspaceUserRaw.CreatedTs,
|
||||
&workspaceUserRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return &workspaceUserRaw, nil
|
||||
}
|
||||
|
||||
func findWorkspaceUserList(ctx context.Context, tx *sql.Tx, find *api.WorkspaceUserFind) ([]*workspaceUserRaw, error) {
|
||||
where, args := []string{"1 = 1"}, []interface{}{}
|
||||
|
||||
if v := find.WorkspaceID; v != nil {
|
||||
where, args = append(where, "workspace_id = ?"), append(args, *v)
|
||||
}
|
||||
if v := find.UserID; v != nil {
|
||||
where, args = append(where, "user_id = ?"), append(args, *v)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
workspace_id,
|
||||
user_id,
|
||||
role,
|
||||
created_ts,
|
||||
updated_ts
|
||||
FROM workspace
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY updated_ts DESC, created_ts DESC, row_status DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
workspaceUserRawList := make([]*workspaceUserRaw, 0)
|
||||
for rows.Next() {
|
||||
var workspaceUserRaw workspaceUserRaw
|
||||
if err := rows.Scan(
|
||||
&workspaceUserRaw.WorkspaceID,
|
||||
&workspaceUserRaw.UserID,
|
||||
&workspaceUserRaw.Role,
|
||||
&workspaceUserRaw.CreatedTs,
|
||||
&workspaceUserRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
workspaceUserRawList = append(workspaceUserRawList, &workspaceUserRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
||||
return workspaceUserRawList, nil
|
||||
}
|
||||
|
||||
func deleteWorkspaceUser(ctx context.Context, tx *sql.Tx, delete *api.WorkspaceUserDelete) error {
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
PRAGMA foreign_keys = ON;
|
||||
DELETE FROM workspace_user WHERE workspace_id = ? AND user_id = ?
|
||||
`, delete.WorkspaceID, delete.UserID)
|
||||
if err != nil {
|
||||
return FormatError(err)
|
||||
}
|
||||
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("workspace user not found")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user