mirror of
https://github.com/aykhans/slash-e.git
synced 2025-06-14 20:07:50 +00:00
feat: refactor code structure
This commit is contained in:
161
store/db/db.go
161
store/db/db.go
@ -24,8 +24,8 @@ var seedFS embed.FS
|
||||
|
||||
type DB struct {
|
||||
// sqlite db connection instance
|
||||
Db *sql.DB
|
||||
profile *profile.Profile
|
||||
DBInstance *sql.DB
|
||||
profile *profile.Profile
|
||||
}
|
||||
|
||||
// NewDB returns a new instance of DB associated with the given datasource name.
|
||||
@ -42,83 +42,87 @@ func (db *DB) Open(ctx context.Context) (err error) {
|
||||
return fmt.Errorf("dsn required")
|
||||
}
|
||||
|
||||
// Connect to the database.
|
||||
sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=1")
|
||||
// Connect to the database without foreign_key.
|
||||
sqliteDB, err := sql.Open("sqlite3", db.profile.DSN+"?cache=shared&_foreign_keys=0&_journal_mode=WAL")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err)
|
||||
}
|
||||
db.DBInstance = sqliteDB
|
||||
|
||||
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.profile.Mode == "prod" {
|
||||
// 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 {
|
||||
if err := db.applyLatestSchema(ctx); err != nil {
|
||||
return fmt.Errorf("failed to apply latest schema: %w", err)
|
||||
}
|
||||
} else {
|
||||
err := db.createMigrationHistoryTable(ctx)
|
||||
}
|
||||
|
||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistoryList, err := db.FindMigrationHistoryList(ctx, &MigrationHistoryFind{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find migration history, err: %w", err)
|
||||
}
|
||||
if len(migrationHistoryList) == 0 {
|
||||
_, err := db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||
Version: currentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration_history table: %w", err)
|
||||
return fmt.Errorf("failed to upsert migration history, err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
currentVersion := version.GetCurrentVersion(db.profile.Mode)
|
||||
migrationHistory, err := db.FindMigrationHistory(ctx, &MigrationHistoryFind{})
|
||||
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(db.profile.DSN)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to read raw database file, err: %w", err)
|
||||
}
|
||||
if migrationHistory == nil {
|
||||
migrationHistory, err = db.UpsertMigrationHistory(ctx, &MigrationHistoryUpsert{
|
||||
Version: currentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return 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")
|
||||
|
||||
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("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 := db.applyMigrationForMinorVersion(ctx, minorVersion); err != nil {
|
||||
return fmt.Errorf("failed to apply minor version migration: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
println("end migrate")
|
||||
|
||||
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))
|
||||
}
|
||||
// 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(db.profile.DSN); errors.Is(err, os.ErrNotExist) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
@ -126,7 +130,11 @@ const (
|
||||
)
|
||||
|
||||
func (db *DB) applyLatestSchema(ctx context.Context) error {
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", db.profile.Mode, latestSchemaFileName)
|
||||
schemaMode := "dev"
|
||||
if db.profile.Mode == "prod" {
|
||||
schemaMode = "prod"
|
||||
}
|
||||
latestSchemaPath := fmt.Sprintf("%s/%s/%s", "migration", schemaMode, latestSchemaFileName)
|
||||
buf, err := migrationFS.ReadFile(latestSchemaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read latest schema %q, error %w", latestSchemaPath, err)
|
||||
@ -141,7 +149,7 @@ func (db *DB) applyLatestSchema(ctx context.Context) error {
|
||||
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
|
||||
return fmt.Errorf("failed to read ddl files, err: %w", err)
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
@ -160,17 +168,18 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
|
||||
}
|
||||
}
|
||||
|
||||
tx, err := db.Db.Begin()
|
||||
tx, err := db.DBInstance.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// upsert the newest version to migration_history
|
||||
version := minorVersion + ".0"
|
||||
if _, err = upsertMigrationHistory(ctx, tx, &MigrationHistoryUpsert{
|
||||
Version: minorVersion + ".0",
|
||||
Version: version,
|
||||
}); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to upsert migration history with version: %s, err: %w", version, err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
@ -179,7 +188,7 @@ func (db *DB) applyMigrationForMinorVersion(ctx context.Context, minorVersion st
|
||||
func (db *DB) seed(ctx context.Context) error {
|
||||
filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s/*.sql", "seed"))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to read seed files, err: %w", err)
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
@ -200,14 +209,14 @@ func (db *DB) seed(ctx context.Context) error {
|
||||
|
||||
// execute runs a single SQL statement within a transaction.
|
||||
func (db *DB) execute(ctx context.Context, stmt string) error {
|
||||
tx, err := db.Db.Begin()
|
||||
tx, err := db.DBInstance.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, stmt); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to execute statement, err: %w", err)
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
@ -232,27 +241,7 @@ func getMinorVersionList() []string {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sort.Strings(minorVersionList)
|
||||
sort.Sort(version.SortVersion(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()
|
||||
}
|
||||
|
@ -1,52 +1,9 @@
|
||||
-- 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
|
||||
-- migration_history
|
||||
CREATE TABLE migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
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,
|
||||
@ -54,7 +11,7 @@ CREATE TABLE user (
|
||||
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,
|
||||
display_name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
@ -64,47 +21,46 @@ INSERT INTO
|
||||
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;
|
||||
|
||||
CREATE INDEX user_id_index ON user(id);
|
||||
|
||||
CREATE UNIQUE INDEX user_email_index ON user(email);
|
||||
|
||||
CREATE UNIQUE INDEX user_open_id_index ON user(open_id);
|
||||
|
||||
-- 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
|
||||
UNIQUE(user_id, key)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
-- 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 ''
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('workspace', 10);
|
||||
|
||||
-- workspace_setting
|
||||
CREATE TABLE workspace_setting (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
UNIQUE(workspace_id, key)
|
||||
);
|
||||
|
||||
-- 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
|
||||
UNIQUE(workspace_id, user_id)
|
||||
);
|
||||
|
||||
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,
|
||||
@ -116,52 +72,18 @@ CREATE TABLE shortcut (
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE',
|
||||
FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace(id) ON DELETE CASCADE
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||
);
|
||||
|
||||
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);
|
||||
);
|
@ -1,52 +1,9 @@
|
||||
-- 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
|
||||
-- migration_history
|
||||
CREATE TABLE migration_history (
|
||||
version TEXT NOT NULL PRIMARY KEY,
|
||||
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
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,
|
||||
@ -54,7 +11,7 @@ CREATE TABLE user (
|
||||
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,
|
||||
display_name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
open_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
@ -64,47 +21,46 @@ INSERT INTO
|
||||
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;
|
||||
|
||||
CREATE INDEX user_id_index ON user(id);
|
||||
|
||||
CREATE UNIQUE INDEX user_email_index ON user(email);
|
||||
|
||||
CREATE UNIQUE INDEX user_open_id_index ON user(open_id);
|
||||
|
||||
-- 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
|
||||
UNIQUE(user_id, key)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id);
|
||||
-- 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 ''
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
sqlite_sequence (name, seq)
|
||||
VALUES
|
||||
('workspace', 10);
|
||||
|
||||
-- workspace_setting
|
||||
CREATE TABLE workspace_setting (
|
||||
workspace_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
UNIQUE(workspace_id, key)
|
||||
);
|
||||
|
||||
-- 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
|
||||
UNIQUE(workspace_id, user_id)
|
||||
);
|
||||
|
||||
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,
|
||||
@ -116,52 +72,18 @@ CREATE TABLE shortcut (
|
||||
name TEXT NOT NULL,
|
||||
link TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
visibility TEXT NOT NULL CHECK (visibility 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
|
||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||
);
|
||||
|
||||
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);
|
||||
);
|
@ -19,8 +19,8 @@ type MigrationHistoryFind struct {
|
||||
Version *string
|
||||
}
|
||||
|
||||
func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFind) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
func (db *DB) FindMigrationHistoryList(ctx context.Context, find *MigrationHistoryFind) ([]*MigrationHistory, error) {
|
||||
tx, err := db.DBInstance.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -31,16 +31,11 @@ func (db *DB) FindMigrationHistory(ctx context.Context, find *MigrationHistoryFi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
migrationHistory := list[0]
|
||||
return migrationHistory, nil
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (db *DB) UpsertMigrationHistory(ctx context.Context, upsert *MigrationHistoryUpsert) (*MigrationHistory, error) {
|
||||
tx, err := db.Db.Begin()
|
||||
tx, err := db.DBInstance.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -111,24 +106,13 @@ func upsertMigrationHistory(ctx context.Context, tx *sql.Tx, upsert *MigrationHi
|
||||
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(
|
||||
if err := tx.QueryRowContext(ctx, query, upsert.Version).Scan(
|
||||
&migrationHistory.Version,
|
||||
&migrationHistory.CreatedTs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &migrationHistory, nil
|
||||
}
|
||||
|
@ -1,8 +1,20 @@
|
||||
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;
|
||||
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;
|
@ -1,14 +1,14 @@
|
||||
INSERT INTO
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`id`,
|
||||
`email`,
|
||||
`name`,
|
||||
`display_name`,
|
||||
`password_hash`,
|
||||
`open_id`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
101,
|
||||
101,
|
||||
'frank@iamcorgi.com',
|
||||
'Frank',
|
||||
-- raw password: secret
|
||||
@ -16,20 +16,20 @@ VALUES
|
||||
'frank_open_id'
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
INSERT INTO
|
||||
user (
|
||||
`id`,
|
||||
`id`,
|
||||
`email`,
|
||||
`name`,
|
||||
`display_name`,
|
||||
`password_hash`,
|
||||
`open_id`
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
102,
|
||||
102,
|
||||
'bob@iamcorgi.com',
|
||||
'Bob',
|
||||
-- raw password: secret
|
||||
'$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK',
|
||||
'bob_open_id'
|
||||
);
|
||||
);
|
@ -1,65 +0,0 @@
|
||||
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
|
||||
}
|
@ -22,7 +22,7 @@ type userRaw struct {
|
||||
|
||||
// Domain specific fields
|
||||
Email string
|
||||
Name string
|
||||
DisplayName string
|
||||
PasswordHash string
|
||||
OpenID string
|
||||
}
|
||||
@ -36,7 +36,7 @@ func (raw *userRaw) toUser() *api.User {
|
||||
RowStatus: raw.RowStatus,
|
||||
|
||||
Email: raw.Email,
|
||||
Name: raw.Name,
|
||||
DisplayName: raw.DisplayName,
|
||||
PasswordHash: raw.PasswordHash,
|
||||
OpenID: raw.OpenID,
|
||||
}
|
||||
@ -113,20 +113,9 @@ func (s *Store) FindUserList(ctx context.Context, find *api.UserFind) ([]*api.Us
|
||||
}
|
||||
|
||||
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)
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
@ -142,7 +131,6 @@ func (s *Store) FindUser(ctx context.Context, find *api.UserFind) (*api.User, er
|
||||
}
|
||||
|
||||
user := list[0].toUser()
|
||||
|
||||
if err := s.cache.UpsertCache(api.UserCache, user.ID, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -175,17 +163,17 @@ func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userR
|
||||
query := `
|
||||
INSERT INTO user (
|
||||
email,
|
||||
name,
|
||||
display_name,
|
||||
password_hash,
|
||||
open_id
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING id, created_ts, updated_ts, row_status, email, name, password_hash, open_id
|
||||
RETURNING id, created_ts, updated_ts, row_status, email, display_name, password_hash, open_id
|
||||
`
|
||||
var userRaw userRaw
|
||||
if err := tx.QueryRowContext(ctx, query,
|
||||
create.Email,
|
||||
create.Name,
|
||||
create.DisplayName,
|
||||
create.PasswordHash,
|
||||
create.OpenID,
|
||||
).Scan(
|
||||
@ -194,7 +182,7 @@ func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userR
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.DisplayName,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
); err != nil {
|
||||
@ -213,8 +201,8 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
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.DisplayName; v != nil {
|
||||
set, args = append(set, "display_name = ?"), append(args, *v)
|
||||
}
|
||||
if v := patch.PasswordHash; v != nil {
|
||||
set, args = append(set, "password_hash = ?"), append(args, *v)
|
||||
@ -229,7 +217,7 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
UPDATE user
|
||||
SET ` + strings.Join(set, ", ") + `
|
||||
WHERE id = ?
|
||||
RETURNING id, created_ts, updated_ts, row_status, email, name, password_hash, open_id
|
||||
RETURNING id, created_ts, updated_ts, row_status, email, display_name, password_hash, open_id
|
||||
`
|
||||
row, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
@ -245,7 +233,7 @@ func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw,
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.DisplayName,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
); err != nil {
|
||||
@ -271,9 +259,6 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
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)
|
||||
}
|
||||
if v := find.OpenID; v != nil {
|
||||
where, args = append(where, "open_id = ?"), append(args, *v)
|
||||
}
|
||||
@ -285,16 +270,16 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
updated_ts,
|
||||
row_status,
|
||||
email,
|
||||
name,
|
||||
display_name,
|
||||
password_hash,
|
||||
open_id
|
||||
FROM user
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY updated_ts DESC, created_ts DESC, row_status DESC
|
||||
ORDER BY updated_ts DESC, created_ts DESC
|
||||
`
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@ -307,18 +292,17 @@ func findUserList(ctx context.Context, tx *sql.Tx, find *api.UserFind) ([]*userR
|
||||
&userRaw.UpdatedTs,
|
||||
&userRaw.RowStatus,
|
||||
&userRaw.Email,
|
||||
&userRaw.Name,
|
||||
&userRaw.DisplayName,
|
||||
&userRaw.PasswordHash,
|
||||
&userRaw.OpenID,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userRawList = append(userRawList, &userRaw)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, FormatError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userRawList, nil
|
||||
|
@ -15,8 +15,6 @@ type workspaceUserRaw struct {
|
||||
WorkspaceID int
|
||||
UserID int
|
||||
Role api.Role
|
||||
CreatedTs int64
|
||||
UpdatedTs int64
|
||||
}
|
||||
|
||||
func (raw *workspaceUserRaw) toWorkspaceUser() *api.WorkspaceUser {
|
||||
@ -24,8 +22,6 @@ func (raw *workspaceUserRaw) toWorkspaceUser() *api.WorkspaceUser {
|
||||
WorkspaceID: raw.WorkspaceID,
|
||||
UserID: raw.UserID,
|
||||
Role: raw.Role,
|
||||
CreatedTs: raw.CreatedTs,
|
||||
UpdatedTs: raw.UpdatedTs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +34,7 @@ func (s *Store) ComposeWorkspaceUser(ctx context.Context, workspaceUser *api.Wor
|
||||
}
|
||||
|
||||
workspaceUser.Email = user.Email
|
||||
workspaceUser.Name = user.Name
|
||||
workspaceUser.DisplayName = user.DisplayName
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -159,17 +155,14 @@ func upsertWorkspaceUser(ctx context.Context, tx *sql.Tx, upsert *api.WorkspaceU
|
||||
VALUES (` + strings.Join(placeholder, ",") + `)
|
||||
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
|
||||
role = EXCLUDED.role
|
||||
RETURNING workspace_id, user_id, role
|
||||
`
|
||||
var workspaceUserRaw workspaceUserRaw
|
||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
&workspaceUserRaw.WorkspaceID,
|
||||
&workspaceUserRaw.UserID,
|
||||
&workspaceUserRaw.Role,
|
||||
&workspaceUserRaw.CreatedTs,
|
||||
&workspaceUserRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
@ -191,13 +184,9 @@ func findWorkspaceUserList(ctx context.Context, tx *sql.Tx, find *api.WorkspaceU
|
||||
SELECT
|
||||
workspace_id,
|
||||
user_id,
|
||||
role,
|
||||
created_ts,
|
||||
updated_ts
|
||||
role
|
||||
FROM workspace_user
|
||||
WHERE ` + strings.Join(where, " AND ") + `
|
||||
ORDER BY updated_ts DESC, created_ts DESC
|
||||
`
|
||||
WHERE ` + strings.Join(where, " AND ")
|
||||
rows, err := tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, FormatError(err)
|
||||
@ -211,8 +200,6 @@ func findWorkspaceUserList(ctx context.Context, tx *sql.Tx, find *api.WorkspaceU
|
||||
&workspaceUserRaw.WorkspaceID,
|
||||
&workspaceUserRaw.UserID,
|
||||
&workspaceUserRaw.Role,
|
||||
&workspaceUserRaw.CreatedTs,
|
||||
&workspaceUserRaw.UpdatedTs,
|
||||
); err != nil {
|
||||
return nil, FormatError(err)
|
||||
}
|
||||
|
Reference in New Issue
Block a user