diff --git a/api/shortcut.go b/api/shortcut.go index 1f677cb2..7d596e06 100644 --- a/api/shortcut.go +++ b/api/shortcut.go @@ -27,6 +27,7 @@ type ShortcutPatch struct { ID int // Standard fields + UpdatedTs *int64 RowStatus *RowStatus `json:"rowStatus"` // Domain specific fields diff --git a/api/user.go b/api/user.go index 606fcc1b..8360051b 100644 --- a/api/user.go +++ b/api/user.go @@ -71,6 +71,7 @@ type UserPatch struct { ID int // Standard fields + UpdatedTs *int64 RowStatus *RowStatus `json:"rowStatus"` // Domain specific fields diff --git a/server/shortcut.go b/server/shortcut.go index 36d9355c..e672c8ea 100644 --- a/server/shortcut.go +++ b/server/shortcut.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/usememos/memos/api" "github.com/usememos/memos/common" @@ -45,8 +46,10 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err) } + currentTs := time.Now().Unix() shortcutPatch := &api.ShortcutPatch{ - ID: shortcutID, + ID: shortcutID, + UpdatedTs: ¤tTs, } if err := json.NewDecoder(c.Request().Body).Decode(shortcutPatch); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err) diff --git a/server/user.go b/server/user.go index 99c1de35..7e70576c 100644 --- a/server/user.go +++ b/server/user.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "time" "github.com/usememos/memos/api" "github.com/usememos/memos/common" @@ -185,8 +186,10 @@ func (s *Server) registerUserRoutes(g *echo.Group) { return echo.NewHTTPError(http.StatusForbidden, "Access forbidden for current session user").SetInternal(err) } + currentTs := time.Now().Unix() userPatch := &api.UserPatch{ - ID: userID, + ID: userID, + UpdatedTs: ¤tTs, } if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch user request").SetInternal(err) diff --git a/server/version/version.go b/server/version/version.go index 903b5fef..52af0e71 100644 --- a/server/version/version.go +++ b/server/version/version.go @@ -10,7 +10,7 @@ import ( var Version = "0.6.1" // DevVersion is the service current development version. -var DevVersion = "0.6.1" +var DevVersion = "0.7.0" func GetCurrentVersion(mode string) string { if mode == "dev" { diff --git a/store/db/db.go b/store/db/db.go index ecded2b5..19085add 100644 --- a/store/db/db.go +++ b/store/db/db.go @@ -42,13 +42,13 @@ func (db *DB) Open(ctx context.Context) (err error) { return fmt.Errorf("dsn required") } - // Connect to the database without foreign_keys config. - tempDB, err := sql.Open("sqlite3", db.profile.DSN) + // Connect to the database without foreign_key. + sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=0") if err != nil { return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err) } + db.Db = sqlDB - db.Db = tempDB // If mode is dev, we should migrate and seed the database. if db.profile.Mode == "dev" { if _, err := os.Stat(db.profile.DSN); errors.Is(err, os.ErrNotExist) { @@ -118,19 +118,7 @@ func (db *DB) Open(ctx context.Context) (err error) { } } - if err := tempDB.Close(); err != nil { - return fmt.Errorf("failed to close temp db without foreign_keys, err: %w", err) - } - - // Connect to the database with foreign_keys config. - sqlDB, err := sql.Open("sqlite3", db.profile.DSN+"?_foreign_keys=1") - if err != nil { - return fmt.Errorf("failed to open db with dsn: %s, err: %w", db.profile.DSN, err) - } - - db.Db = sqlDB - - return err + return nil } const ( diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index b91e2c2f..f388f2ad 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -1,13 +1,3 @@ --- drop all tables -DROP TABLE IF EXISTS `system_setting`; -DROP TABLE IF EXISTS `memo_resource`; -DROP TABLE IF EXISTS `memo_organizer`; -DROP TABLE IF EXISTS `memo`; -DROP TABLE IF EXISTS `shortcut`; -DROP TABLE IF EXISTS `resource`; -DROP TABLE IF EXISTS `user_setting`; -DROP TABLE IF EXISTS `user`; - -- user CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -21,23 +11,6 @@ CREATE TABLE user ( open_id TEXT NOT NULL UNIQUE ); -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; - -- memo CREATE TABLE memo ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -46,43 +19,18 @@ CREATE TABLE memo ( updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', content TEXT NOT NULL DEFAULT '', - visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE', - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE' ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('memo', 1000); - -CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time` -AFTER -UPDATE - ON `memo` FOR EACH ROW BEGIN -UPDATE - `memo` -SET - updated_ts = (strftime('%s', 'now')) -WHERE - rowid = old.rowid; -END; - -- memo_organizer CREATE TABLE memo_organizer ( id INTEGER PRIMARY KEY AUTOINCREMENT, memo_id INTEGER NOT NULL, user_id INTEGER NOT NULL, pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, - FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, - FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE, UNIQUE(memo_id, user_id) ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('memo_organizer', 1000); - -- shortcut CREATE TABLE shortcut ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -91,27 +39,9 @@ CREATE TABLE shortcut ( updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', title TEXT NOT NULL DEFAULT '', - payload TEXT NOT NULL DEFAULT '{}', - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + payload TEXT NOT NULL DEFAULT '{}' ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('shortcut', 10000); - -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; - -- resource CREATE TABLE resource ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -122,33 +52,14 @@ CREATE TABLE resource ( blob BLOB DEFAULT NULL, external_link TEXT NOT NULL DEFAULT '', type TEXT NOT NULL DEFAULT '', - size INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + size INTEGER NOT NULL DEFAULT 0 ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('resource', 10000); - -CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time` -AFTER -UPDATE - ON `resource` FOR EACH ROW BEGIN -UPDATE - `resource` -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, UNIQUE(user_id, key) ); @@ -158,8 +69,6 @@ CREATE TABLE memo_resource ( resource_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), - FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, - FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE, UNIQUE(memo_id, resource_id) ); diff --git a/store/db/migration/prod/0.7/00__remove_fk.sql b/store/db/migration/prod/0.7/00__remove_fk.sql new file mode 100644 index 00000000..e22297b8 --- /dev/null +++ b/store/db/migration/prod/0.7/00__remove_fk.sql @@ -0,0 +1,130 @@ +PRAGMA foreign_keys=off; + +DROP TABLE IF EXISTS _user_old; + +ALTER TABLE user RENAME TO _user_old; + +-- 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, + role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER', + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + open_id TEXT NOT NULL UNIQUE +); + +INSERT INTO user SELECT * FROM _user_old; + +DROP TABLE IF EXISTS _user_old; + +DROP TABLE IF EXISTS _memo_old; + +ALTER TABLE memo RENAME TO _memo_old; + +-- memo +CREATE TABLE memo ( + 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', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE' +); + +INSERT INTO memo SELECT * FROM _memo_old; + +DROP TABLE IF EXISTS _memo_old; + +DROP TABLE IF EXISTS _memo_organizer_old; + +ALTER TABLE memo_organizer RENAME TO _memo_organizer_old; + +-- memo_organizer +CREATE TABLE memo_organizer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + UNIQUE(memo_id, user_id) +); + +INSERT INTO memo_organizer SELECT * FROM _memo_organizer_old; + +DROP TABLE IF EXISTS _memo_organizer_old; + +DROP TABLE IF EXISTS _shortcut_old; + +ALTER TABLE shortcut RENAME TO _shortcut_old; + +-- 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', + title TEXT NOT NULL DEFAULT '', + payload TEXT NOT NULL DEFAULT '{}' +); + +INSERT INTO shortcut SELECT * FROM _shortcut_old; + +DROP TABLE IF EXISTS _shortcut_old; + +DROP TABLE IF EXISTS _resource_old; + +ALTER TABLE resource RENAME TO _resource_old; + +-- resource +CREATE TABLE resource ( + 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')), + filename TEXT NOT NULL DEFAULT '', + blob BLOB DEFAULT NULL, + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO resource SELECT * FROM _resource_old; + +DROP TABLE IF EXISTS _resource_old; + +DROP TABLE IF EXISTS _user_setting_old; + +ALTER TABLE user_setting RENAME TO _user_setting_old; + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(user_id, key) +); + +INSERT INTO user_setting SELECT * FROM _user_setting_old; + +DROP TABLE IF EXISTS _user_setting_old; + +DROP TABLE IF EXISTS _memo_resource_old; + +ALTER TABLE memo_resource RENAME TO _memo_resource_old; + +-- memo_resource +CREATE TABLE memo_resource ( + memo_id INTEGER NOT NULL, + resource_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + UNIQUE(memo_id, resource_id) +); + +INSERT INTO memo_resource SELECT * FROM _memo_resource_old; + +DROP TABLE IF EXISTS _memo_resource_old; diff --git a/store/db/migration/prod/0.7/01__remove_triggers.sql b/store/db/migration/prod/0.7/01__remove_triggers.sql new file mode 100644 index 00000000..b0dc7cde --- /dev/null +++ b/store/db/migration/prod/0.7/01__remove_triggers.sql @@ -0,0 +1,4 @@ +DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`; +DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`; +DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`; +DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`; diff --git a/store/db/migration/prod/LATEST__SCHEMA.sql b/store/db/migration/prod/LATEST__SCHEMA.sql index b91e2c2f..f388f2ad 100644 --- a/store/db/migration/prod/LATEST__SCHEMA.sql +++ b/store/db/migration/prod/LATEST__SCHEMA.sql @@ -1,13 +1,3 @@ --- drop all tables -DROP TABLE IF EXISTS `system_setting`; -DROP TABLE IF EXISTS `memo_resource`; -DROP TABLE IF EXISTS `memo_organizer`; -DROP TABLE IF EXISTS `memo`; -DROP TABLE IF EXISTS `shortcut`; -DROP TABLE IF EXISTS `resource`; -DROP TABLE IF EXISTS `user_setting`; -DROP TABLE IF EXISTS `user`; - -- user CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -21,23 +11,6 @@ CREATE TABLE user ( open_id TEXT NOT NULL UNIQUE ); -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; - -- memo CREATE TABLE memo ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -46,43 +19,18 @@ CREATE TABLE memo ( updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', content TEXT NOT NULL DEFAULT '', - visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE', - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE' ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('memo', 1000); - -CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time` -AFTER -UPDATE - ON `memo` FOR EACH ROW BEGIN -UPDATE - `memo` -SET - updated_ts = (strftime('%s', 'now')) -WHERE - rowid = old.rowid; -END; - -- memo_organizer CREATE TABLE memo_organizer ( id INTEGER PRIMARY KEY AUTOINCREMENT, memo_id INTEGER NOT NULL, user_id INTEGER NOT NULL, pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, - FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, - FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE, UNIQUE(memo_id, user_id) ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('memo_organizer', 1000); - -- shortcut CREATE TABLE shortcut ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -91,27 +39,9 @@ CREATE TABLE shortcut ( updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', title TEXT NOT NULL DEFAULT '', - payload TEXT NOT NULL DEFAULT '{}', - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + payload TEXT NOT NULL DEFAULT '{}' ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('shortcut', 10000); - -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; - -- resource CREATE TABLE resource ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -122,33 +52,14 @@ CREATE TABLE resource ( blob BLOB DEFAULT NULL, external_link TEXT NOT NULL DEFAULT '', type TEXT NOT NULL DEFAULT '', - size INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE + size INTEGER NOT NULL DEFAULT 0 ); -INSERT INTO - sqlite_sequence (name, seq) -VALUES - ('resource', 10000); - -CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time` -AFTER -UPDATE - ON `resource` FOR EACH ROW BEGIN -UPDATE - `resource` -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, UNIQUE(user_id, key) ); @@ -158,8 +69,6 @@ CREATE TABLE memo_resource ( resource_id INTEGER NOT NULL, created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), - FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, - FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE, UNIQUE(memo_id, resource_id) ); diff --git a/store/shortcut.go b/store/shortcut.go index 9f3d1dda..43012bb2 100644 --- a/store/shortcut.go +++ b/store/shortcut.go @@ -198,6 +198,9 @@ func createShortcut(ctx context.Context, tx *sql.Tx, create *api.ShortcutCreate) func patchShortcut(ctx context.Context, tx *sql.Tx, patch *api.ShortcutPatch) (*shortcutRaw, error) { set, args := []string{}, []interface{}{} + if v := patch.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = ?"), append(args, *v) + } if v := patch.Title; v != nil { set, args = append(set, "title = ?"), append(args, *v) } diff --git a/store/user.go b/store/user.go index 217b5412..585d9868 100644 --- a/store/user.go +++ b/store/user.go @@ -230,6 +230,9 @@ func createUser(ctx context.Context, tx *sql.Tx, create *api.UserCreate) (*userR func patchUser(ctx context.Context, tx *sql.Tx, patch *api.UserPatch) (*userRaw, error) { set, args := []string{}, []interface{}{} + if v := patch.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = ?"), append(args, *v) + } if v := patch.RowStatus; v != nil { set, args = append(set, "row_status = ?"), append(args, *v) }