diff --git a/store/activity.go b/store/activity.go new file mode 100644 index 0000000..80487a2 --- /dev/null +++ b/store/activity.go @@ -0,0 +1,185 @@ +package store + +import ( + "context" + "database/sql" + "strings" +) + +type ActivityType string + +const ( + // ActivityShortcutView is the activity type of shortcut create. + ActivityShortcutCreate ActivityType = "shortcut.create" + // ActivityShortcutView is the activity type of shortcut view. + ActivityShortcutView ActivityType = "shortcut.view" +) + +func (t ActivityType) String() string { + switch t { + case ActivityShortcutCreate: + return "shortcut.create" + case ActivityShortcutView: + return "shortcut.view" + } + return "" +} + +type ActivityLevel string + +const ( + // ActivityInfo is the activity level of info. + ActivityInfo ActivityLevel = "INFO" + // ActivityWarn is the activity level of warn. + ActivityWarn ActivityLevel = "WARN" + // ActivityError is the activity level of error. + ActivityError ActivityLevel = "ERROR" +) + +func (l ActivityLevel) String() string { + switch l { + case ActivityInfo: + return "INFO" + case ActivityWarn: + return "WARN" + case ActivityError: + return "ERROR" + } + return "" +} + +type Activity struct { + ID int + CreatorID int + CreatedTs int64 + Type ActivityType + Level ActivityLevel + Payload string +} + +type FindActivity struct { + Type ActivityType + Level ActivityLevel +} + +func (s *Store) CreateActivity(ctx context.Context, create *Activity) (*Activity, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + query := ` + INSERT INTO activity ( + creator_id, + type, + level, + payload + ) + VALUES (?, ?, ?, ?) + RETURNING id, created_ts + ` + if err := tx.QueryRowContext(ctx, query, + create.CreatorID, + create.Type.String(), + create.Level.String(), + create.Payload, + ).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, err + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + activity := create + return activity, nil +} + +func (s *Store) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + list, err := listActivities(ctx, tx, find) + if err != nil { + return nil, err + } + + return list, nil +} + +func (s *Store) GetActivity(ctx context.Context, find *FindActivity) (*Activity, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + defer tx.Rollback() + + list, err := listActivities(ctx, tx, find) + if err != nil { + return nil, err + } + + if len(list) == 0 { + return nil, nil + } + + activity := list[0] + return activity, nil +} + +func listActivities(ctx context.Context, tx *sql.Tx, find *FindActivity) ([]*Activity, error) { + where, args := []string{"1 = 1"}, []any{} + if find.Type != "" { + where, args = append(where, "type = ?"), append(args, find.Type.String()) + } + if find.Level != "" { + where, args = append(where, "level = ?"), append(args, find.Level.String()) + } + + query := ` + SELECT + id, + creator_id, + created_ts, + type, + level, + payload + FROM activity + WHERE ` + strings.Join(where, " AND ") + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + + defer rows.Close() + + list := []*Activity{} + for rows.Next() { + activity := &Activity{} + if err := rows.Scan( + &activity.ID, + &activity.CreatorID, + &activity.CreatedTs, + &activity.Type, + &activity.Level, + &activity.Payload, + ); err != nil { + return nil, err + } + + list = append(list, activity) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} diff --git a/store/db/migration/dev/LATEST__SCHEMA.sql b/store/db/migration/dev/LATEST__SCHEMA.sql index 8d006f0..aa4cc78 100644 --- a/store/db/migration/dev/LATEST__SCHEMA.sql +++ b/store/db/migration/dev/LATEST__SCHEMA.sql @@ -43,3 +43,13 @@ CREATE TABLE shortcut ( visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', tag TEXT NOT NULL DEFAULT '' ); + +-- 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 DEFAULT '', + level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' +); diff --git a/store/db/migration/prod/LATEST__SCHEMA.sql b/store/db/migration/prod/LATEST__SCHEMA.sql index 8d006f0..aa4cc78 100644 --- a/store/db/migration/prod/LATEST__SCHEMA.sql +++ b/store/db/migration/prod/LATEST__SCHEMA.sql @@ -43,3 +43,13 @@ CREATE TABLE shortcut ( visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE', tag TEXT NOT NULL DEFAULT '' ); + +-- 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 DEFAULT '', + level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' +); diff --git a/test/store/activity_test.go b/test/store/activity_test.go new file mode 100644 index 0000000..0f1cf17 --- /dev/null +++ b/test/store/activity_test.go @@ -0,0 +1,28 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/boojack/shortify/store" + "github.com/stretchr/testify/require" +) + +func TestActivityStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + list, err := ts.ListActivities(ctx, &store.FindActivity{}) + require.NoError(t, err) + require.Equal(t, 0, len(list)) + activity, err := ts.CreateActivity(ctx, &store.Activity{ + CreatorID: -1, + Type: store.ActivityShortcutCreate, + Level: store.ActivityInfo, + Payload: "", + }) + require.NoError(t, err) + list, err = ts.ListActivities(ctx, &store.FindActivity{}) + require.NoError(t, err) + require.Equal(t, 1, len(list)) + require.Equal(t, activity, list[0]) +}