diff --git a/go.mod b/go.mod index 0a47dd9..5d33b41 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,11 @@ toolchain go1.23.10 require ( github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 + github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect ) diff --git a/go.sum b/go.sum index a5e4eeb..d5a5504 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,12 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= diff --git a/internals/db/progress.go b/internals/db/progress.go new file mode 100644 index 0000000..1db59c2 --- /dev/null +++ b/internals/db/progress.go @@ -0,0 +1,94 @@ +package db + +import ( + "context" + "database/sql" + "errors" + "time" + + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +type Progress struct { + ID string + UserID string + LevelID string + Status string + CompletedAt sql.NullTime + AttemptData sql.NullString + Stars sql.NullInt64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type ProgressClient struct { + db *sql.DB +} + +func NewProgressClient(db *sql.DB) *ProgressClient { + return &ProgressClient{db: db} +} + +func (c *ProgressClient) UpsertProgress(ctx context.Context, p *Progress) error { + _, err := c.db.ExecContext(ctx, ` + INSERT INTO progress (id, user_id, level_id, status, completed_at, attempt_data, stars) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id, level_id) DO UPDATE SET + status = excluded.status, + completed_at = excluded.completed_at, + attempt_data = excluded.attempt_data, + stars = excluded.stars, + updated_at = CURRENT_TIMESTAMP + `, p.ID, p.UserID, p.LevelID, p.Status, p.CompletedAt, p.AttemptData, p.Stars) + return err +} + +func (c *ProgressClient) GetProgressByUser(ctx context.Context, userID string) ([]*Progress, error) { + rows, err := c.db.QueryContext(ctx, ` + SELECT id, user_id, level_id, status, completed_at, attempt_data, stars, created_at, updated_at + FROM progress + WHERE user_id = ? + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var progressList []*Progress + for rows.Next() { + var p Progress + err := rows.Scan(&p.ID, &p.UserID, &p.LevelID, &p.Status, &p.CompletedAt, &p.AttemptData, &p.Stars, &p.CreatedAt, &p.UpdatedAt) + if err != nil { + return nil, err + } + progressList = append(progressList, &p) + } + + return progressList, nil +} + +func (c *ProgressClient) GetProgress(ctx context.Context, userID, levelID string) (*Progress, error) { + row := c.db.QueryRowContext(ctx, ` + SELECT id, user_id, level_id, status, completed_at, attempt_data, stars, created_at, updated_at + FROM progress + WHERE user_id = ? AND level_id = ? + `, userID, levelID) + + var p Progress + err := row.Scan(&p.ID, &p.UserID, &p.LevelID, &p.Status, &p.CompletedAt, &p.AttemptData, &p.Stars, &p.CreatedAt, &p.UpdatedAt) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + return &p, nil +} + +func (c *ProgressClient) DeleteProgress(ctx context.Context, userID, levelID string) error { + _, err := c.db.ExecContext(ctx, ` + DELETE FROM progress + WHERE user_id = ? AND level_id = ? + `, userID, levelID) + return err +} diff --git a/internals/db/subscriptions.go b/internals/db/subscriptions.go new file mode 100644 index 0000000..65ce182 --- /dev/null +++ b/internals/db/subscriptions.go @@ -0,0 +1,64 @@ +package db + +import ( + "context" + "database/sql" + "time" + + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +type Subscription struct { + ID string + UserID string + StripeID string + Status string + StartedAt time.Time + EndsAt sql.NullTime +} + +type SubscriptionClient struct { + db *sql.DB +} + +func NewSubscriptionClient(db *sql.DB) *SubscriptionClient { + return &SubscriptionClient{db: db} +} + +// Insert or update subscription +func (c *SubscriptionClient) Upsert(ctx context.Context, s *Subscription) error { + _, err := c.db.ExecContext(ctx, ` + INSERT INTO subscriptions (id, user_id, stripe_id, status, started_at, ends_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + status = excluded.status, + started_at = excluded.started_at, + ends_at = excluded.ends_at + `, s.ID, s.UserID, s.StripeID, s.Status, s.StartedAt, s.EndsAt) + return err +} + +// Get by user ID +func (c *SubscriptionClient) GetByUserID(ctx context.Context, userID string) (*Subscription, error) { + row := c.db.QueryRowContext(ctx, ` + SELECT id, user_id, stripe_id, status, started_at, ends_at + FROM subscriptions + WHERE user_id = ? + `, userID) + + var s Subscription + err := row.Scan(&s.ID, &s.UserID, &s.StripeID, &s.Status, &s.StartedAt, &s.EndsAt) + if err != nil { + return nil, err + } + return &s, nil +} + +// Delete by user ID +func (c *SubscriptionClient) DeleteByUserID(ctx context.Context, userID string) error { + _, err := c.db.ExecContext(ctx, ` + DELETE FROM subscriptions + WHERE user_id = ? + `, userID) + return err +} diff --git a/internals/db/users.go b/internals/db/users.go new file mode 100644 index 0000000..f68de77 --- /dev/null +++ b/internals/db/users.go @@ -0,0 +1,57 @@ +package db + +import ( + "context" + "database/sql" + "time" + + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +type User struct { + ID string + GitHubID int64 + GitHubLogin string + AvatarURL string + Email string + CreatedAt time.Time + UpdatedAt time.Time +} + +type UserClient struct { + db *sql.DB +} + +func NewUserClient(db *sql.DB) *UserClient { + return &UserClient{db: db} +} + +func (uc *UserClient) UpsertUser(ctx context.Context, user User) error { + _, err := uc.db.ExecContext(ctx, ` + INSERT INTO users (id, github_id, github_login, avatar_url, email) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(github_id) DO UPDATE SET + github_login = excluded.github_login, + avatar_url = excluded.avatar_url, + email = excluded.email, + updated_at = CURRENT_TIMESTAMP + `, user.ID, user.GitHubID, user.GitHubLogin, user.AvatarURL, user.Email) + return err +} + +func (uc *UserClient) GetUserByGitHubID(ctx context.Context, githubID int64) (*User, error) { + row := uc.db.QueryRowContext(ctx, ` + SELECT id, github_id, github_login, avatar_url, email, created_at, updated_at + FROM users WHERE github_id = ? + `, githubID) + + var user User + err := row.Scan( + &user.ID, &user.GitHubID, &user.GitHubLogin, + &user.AvatarURL, &user.Email, &user.CreatedAt, &user.UpdatedAt, + ) + if err == sql.ErrNoRows { + return nil, nil + } + return &user, err +}