package tokens import ( "context" "database/sql" "encoding/json" "fmt" "time" "atlas9.dev/c/core" ) type PostgresStore[T any] struct { db *sql.DB opts Options } func NewPostgresStore[T any](db *sql.DB, opts Options) *PostgresStore[T] { return &PostgresStore[T]{db: db, opts: opts} } func (s *PostgresStore[T]) Put(ctx context.Context, key string, data T) (*Token[T], error) { dataJSON, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("marshaling token data: %w", err) } expiresAt := time.Now().UTC().Add(s.opts.Expiration) _, err = s.db.ExecContext(ctx, "INSERT INTO "+s.opts.Table+" (key, data, expires_at) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET data = $2, expires_at = $3", key, dataJSON, expiresAt) if err != nil { return nil, fmt.Errorf("storing token: %w", err) } return &Token[T]{Key: key, ExpiresAt: expiresAt, Data: data}, nil } func (s *PostgresStore[T]) Get(ctx context.Context, key string) (*Token[T], error) { var dataJSON []byte var expiresAt time.Time err := s.db.QueryRowContext(ctx, "SELECT data, expires_at FROM "+s.opts.Table+" WHERE key = $1 AND expires_at > now()", key).Scan(&dataJSON, &expiresAt) if err == sql.ErrNoRows { return nil, core.ErrNotFound } if err != nil { return nil, fmt.Errorf("getting token: %w", err) } var data T if err := json.Unmarshal(dataJSON, &data); err != nil { return nil, fmt.Errorf("unmarshaling token data: %w", err) } return &Token[T]{Key: key, ExpiresAt: expiresAt, Data: data}, nil } func (s *PostgresStore[T]) Delete(ctx context.Context, key string) error { _, err := s.db.ExecContext(ctx, "DELETE FROM "+s.opts.Table+" WHERE key = $1", key) if err != nil { return fmt.Errorf("deleting token: %w", err) } return nil }