filter by learning_types and course_thematics

This commit is contained in:
Aleksandr Trushkin
2024-01-10 00:02:40 +03:00
parent 728c8fa59e
commit 5fd0861e2d
10 changed files with 346 additions and 22 deletions

View File

@ -3,6 +3,7 @@ package main
import ( import (
"log/slog" "log/slog"
"net/http" "net/http"
"strings"
"time" "time"
"git.loyso.art/frx/kurious/assets/kurious" "git.loyso.art/frx/kurious/assets/kurious"
@ -13,6 +14,23 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
const (
pathParamLearningType = "learning_type"
pathParamThematicType = "thematic_type"
)
func makePathTemplate(params ...string) string {
var sb strings.Builder
for _, param := range params {
sb.WriteRune('/')
sb.WriteRune('{')
sb.WriteString(param)
sb.WriteRune('}')
}
return sb.String()
}
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server { func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
router := mux.NewRouter() router := mux.NewRouter()
@ -23,8 +41,12 @@ func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server
router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost) router.HandleFunc("/updatedesc", coursesAPI.UdpateDescription).Methods(http.MethodPost)
coursesRouter := router.PathPrefix("/courses").Subrouter() coursesRouter := router.PathPrefix("/courses").Subrouter()
coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet) coursesRouter.HandleFunc("/", coursesAPI.List).Methods(http.MethodGet)
coursesListLearningOnlyPath := makePathTemplate(pathParamLearningType)
coursesRouter.HandleFunc(coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
coursesListFullPath := makePathTemplate(pathParamLearningType, pathParamThematicType)
coursesRouter.HandleFunc(coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
courseRouter := coursesRouter.PathPrefix("/{course_id}").Subrouter() courseRouter := router.PathPrefix("/course").PathPrefix("/{course_id}").Subrouter()
courseRouter.HandleFunc("/", coursesAPI.Get).Methods(http.MethodGet) courseRouter.HandleFunc("/", coursesAPI.Get).Methods(http.MethodGet)
courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet) courseRouter.HandleFunc("/short", coursesAPI.GetShort).Methods(http.MethodGet)
courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet) courseRouter.HandleFunc("/editdesc", coursesAPI.RenderEditDescription).Methods(http.MethodGet)

View File

@ -16,6 +16,7 @@ import (
"git.loyso.art/frx/kurious/internal/kurious/adapters" "git.loyso.art/frx/kurious/internal/kurious/adapters"
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http" xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
"git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/internal/kurious/service"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -88,26 +89,30 @@ func app(ctx context.Context) error {
slog.String("addr", httpServer.Addr), slog.String("addr", httpServer.Addr),
) )
err := httpServer.ListenAndServe() if err := httpServer.ListenAndServe(); err != nil {
if err != nil {
if !errors.Is(err, http.ErrServerClosed) { if !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("listening http: %w", err) return fmt.Errorf("listening http: %w", err)
} }
} }
return nil return nil
}) })
eg.Go(func() error { eg.Go(func() error {
<-egctx.Done() <-egctx.Done()
xcontext.LogInfo(ctx, log, "trying to shutdown http") xcontext.LogInfo(ctx, log, "trying to shutdown http")
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*10) sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*10)
defer sdcancel() defer sdcancel()
err := httpServer.Shutdown(sdctx) err := httpServer.Shutdown(sdctx)
if err != nil { if err != nil {
return fmt.Errorf("shutting down the server: %w", err) return fmt.Errorf("shutting down the server: %w", err)
} }
xcontext.LogInfo(ctx, log, "server closed successfuly") xcontext.LogInfo(ctx, log, "server closed successfuly")
return nil return nil
}) })

View File

@ -179,6 +179,8 @@ func (r *ydbCourseRepository) List(
return result, fmt.Errorf("rendering query params: %w", err) return result, fmt.Errorf("rendering query params: %w", err)
} }
xcontext.LogInfo(ctx, r.log, "query prepared", slog.String("query", query), slog.String("args", tableParamOptsToString(opts...)))
courses := make([]domain.Course, 0, 1_000) courses := make([]domain.Course, 0, 1_000)
readTx := table.TxControl( readTx := table.TxControl(
table.BeginTx( table.BeginTx(
@ -192,7 +194,7 @@ func (r *ydbCourseRepository) List(
func(ctx context.Context, s table.Session) error { func(ctx context.Context, s table.Session) error {
start := time.Now() start := time.Now()
defer func() { defer func() {
since := time.Since(start) since := time.Since(start).Truncate(time.Millisecond)
xcontext.LogInfo( xcontext.LogInfo(
ctx, r.log, ctx, r.log,
"executed query", "executed query",
@ -242,6 +244,156 @@ func (r *ydbCourseRepository) List(
return result, err return result, err
} }
func (r *ydbCourseRepository) ListLearningTypes(
ctx context.Context,
) (result domain.ListLearningTypeResult, err error) {
const queryName = "list_learning_type"
const querySelect = `SELECT DISTINCT learning_type FROM courses;`
readTx := table.TxControl(
table.BeginTx(
table.WithOnlineReadOnly(),
),
table.CommitTx(),
)
err = r.db.Table().Do(
ctx,
func(ctx context.Context, s table.Session) error {
start := time.Now()
defer func() {
since := time.Since(start).Truncate(time.Millisecond)
xcontext.LogInfo(
ctx, r.log,
"executed query",
slog.String("name", queryName),
slog.Duration("elapsed", since),
)
}()
_, res, err := s.Execute(
ctx, readTx, querySelect, table.NewQueryParameters(),
options.WithCollectStatsModeNone(),
)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
if !res.NextResultSet(ctx) || !res.HasNextRow() {
return nil
}
for res.NextRow() {
var learningTypeID string
if err = res.Scan(&learningTypeID); err != nil {
return fmt.Errorf("scanning row: %w", err)
}
result.LearningTypeIDs = append(result.LearningTypeIDs, learningTypeID)
}
if err = res.Err(); err != nil {
return err
}
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.LearningTypeIDs)))
return nil
},
table.WithIdempotent(),
)
if err != nil {
return result, err
}
return result, nil
}
func (r *ydbCourseRepository) ListCourseThematics(
ctx context.Context,
params domain.ListCourseThematicsParams,
) (result domain.ListCourseThematicsResult, err error) {
const queryName = "list_course_thematics"
qtParams := queryTemplateParams{
Fields: "DISTINCT course_thematic",
Table: "courses",
Declares: []queryTemplateDeclaration{},
Conditions: []string{},
}
learningTypeValue := types.TextValue(params.LearningTypeID)
d := queryTemplateDeclaration{
Name: "course_thematic",
Type: learningTypeValue.Type().String(),
}
qtParams.Declares = append(qtParams.Declares, d)
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
opts := []table.ParameterOption{
table.ValueParam(d.Arg(), learningTypeValue),
}
query, err := qtParams.render()
if err != nil {
return result, fmt.Errorf("rendering query params: %w", err)
}
readTx := table.TxControl(
table.BeginTx(
table.WithOnlineReadOnly(),
),
table.CommitTx(),
)
err = r.db.Table().Do(
ctx,
func(ctx context.Context, s table.Session) error {
start := time.Now()
defer func() {
since := time.Since(start).Truncate(time.Millisecond)
xcontext.LogInfo(
ctx, r.log,
"executed query",
slog.String("name", queryName),
slog.Duration("elapsed", since),
)
}()
_, res, err := s.Execute(
ctx, readTx, query, table.NewQueryParameters(opts...),
options.WithCollectStatsModeNone(),
)
if err != nil {
return fmt.Errorf("executing query: %w", err)
}
if !res.NextResultSet(ctx) || !res.HasNextRow() {
return nil
}
for res.NextRow() {
var courseThematicID string
if err = res.Scan(&courseThematicID); err != nil {
return fmt.Errorf("scanning row: %w", err)
}
result.CourseThematicIDs = append(result.CourseThematicIDs, courseThematicID)
}
if err = res.Err(); err != nil {
return err
}
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.CourseThematicIDs)))
return nil
},
table.WithIdempotent(),
)
if err != nil {
return result, err
}
return result, nil
}
func (r *ydbCourseRepository) Get( func (r *ydbCourseRepository) Get(
ctx context.Context, ctx context.Context,
id string, id string,
@ -735,15 +887,16 @@ func (p queryTemplateParams) render() (string, error) {
const queryTemplateSelect = `{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}};{{end}} const queryTemplateSelect = `{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}};{{end}}
SELECT {{.Fields}} SELECT {{.Fields}}
FROM {{.Table}} FROM {{.Table}}
WHERE {{ range .Conditions }}{{.}}{{end}} WHERE 1=1 {{ range .Conditions }} AND {{.}} {{ end }}
{{.Suffix}}` {{.Suffix}}`
var querySelect = template.Must(template.New("").Parse(queryTemplateSelect)) var querySelect = template.Must(template.New("").Parse(queryTemplateSelect))
// func tableParamOptsToString(in ...table.ParameterOption) string { func tableParamOptsToString(in ...table.ParameterOption) string {
// var sb strings.Builder var sb strings.Builder
// for _, opt := range in { for _, opt := range in {
// sb.WriteString(opt.Name() + "(" + opt.Value().Type().String() + ");") sb.WriteString(opt.Name() + ":" + opt.Value().Yql() + ";")
// } // sb.WriteString(opt.Name() + " (" + opt.Value().Type().String() + "); ")
// return sb.String() }
// } return sb.String()
}

View File

@ -13,8 +13,10 @@ type Commands struct {
} }
type Queries struct { type Queries struct {
GetCourse query.GetCourseHandler GetCourse query.GetCourseHandler
ListCourses query.ListCourseHandler ListCourses query.ListCourseHandler
ListLearningTypes query.ListLearningTypesHandler
ListCourseThematics query.ListCourseThematicsHandler
} }
type Application struct { type Application struct {

View File

@ -16,15 +16,18 @@ type GetCourse struct {
type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course] type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course]
type getCourseHandler struct { type getCourseHandler struct {
repo domain.CourseRepository repo domain.CourseRepository
mapper domain.CourseMapper
} }
func NewGetCourseHandler( func NewGetCourseHandler(
repo domain.CourseRepository, repo domain.CourseRepository,
mapper domain.CourseMapper,
log *slog.Logger, log *slog.Logger,
) GetCourseHandler { ) GetCourseHandler {
h := getCourseHandler{ h := getCourseHandler{
repo: repo, repo: repo,
mapper: mapper,
} }
return decorator.AddQueryDecorators(h, log) return decorator.AddQueryDecorators(h, log)
} }
@ -35,5 +38,8 @@ func (h getCourseHandler) Handle(ctx context.Context, query GetCourse) (domain.C
return domain.Course{}, fmt.Errorf("getting course: %w", err) return domain.Course{}, fmt.Errorf("getting course: %w", err)
} }
course.LearningType = h.mapper.LearningTypeNameByID(course.LearningTypeID)
course.Thematic = h.mapper.CourseThematicNameByID(course.ThematicID)
return course, nil return course, nil
} }

View File

@ -0,0 +1,62 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type ListCourseThematics struct {
LearningTypeID string
}
type CourseThematic struct {
ID string
Name string
}
type ListCourseThematicsResult struct {
CourseThematics []CourseThematic
}
type ListCourseThematicsHandler decorator.QueryHandler[ListCourseThematics, ListCourseThematicsResult]
type listCourseThematicsHandler struct {
repo domain.CourseRepository
mapper domain.CourseMapper
}
func NewListCourseThematicsHandler(
repo domain.CourseRepository,
mapper domain.CourseMapper,
log *slog.Logger,
) ListCourseThematicsHandler {
h := listCourseThematicsHandler{
repo: repo,
mapper: mapper,
}
return decorator.AddQueryDecorators(h, log)
}
func (h listCourseThematicsHandler) Handle(ctx context.Context, query ListCourseThematics) (out ListCourseThematicsResult, err error) {
result, err := h.repo.ListCourseThematics(ctx, domain.ListCourseThematicsParams{
LearningTypeID: query.LearningTypeID,
})
if err != nil {
return out, fmt.Errorf("listing course thematics from repo: %w", err)
}
out.CourseThematics = make([]CourseThematic, 0, len(result.CourseThematicIDs))
for _, ct := range result.CourseThematicIDs {
var item CourseThematic
item.ID = ct
item.Name = h.mapper.CourseThematicNameByID(ct)
out.CourseThematics = append(out.CourseThematics, item)
}
return out, nil
}

View File

@ -0,0 +1,58 @@
package query
import (
"context"
"fmt"
"log/slog"
"git.loyso.art/frx/kurious/internal/common/decorator"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type ListLearningTypes struct{}
type LearningType struct {
ID string
Name string
}
type ListLearningTypesResult struct {
LearningTypes []LearningType
}
type ListLearningTypesHandler decorator.QueryHandler[ListLearningTypes, ListLearningTypesResult]
type listLearningTypesHandler struct {
repo domain.CourseRepository
mapper domain.CourseMapper
}
func NewListLearningTypesHandler(
repo domain.CourseRepository,
mapper domain.CourseMapper,
log *slog.Logger,
) ListLearningTypesHandler {
h := listLearningTypesHandler{
repo: repo,
mapper: mapper,
}
return decorator.AddQueryDecorators(h, log)
}
func (h listLearningTypesHandler) Handle(ctx context.Context, query ListLearningTypes) (out ListLearningTypesResult, err error) {
result, err := h.repo.ListLearningTypes(ctx)
if err != nil {
return out, fmt.Errorf("listing learning types from repo: %w", err)
}
out.LearningTypes = make([]LearningType, 0, len(result.LearningTypeIDs))
for _, lt := range result.LearningTypeIDs {
var item LearningType
item.ID = lt
item.Name = h.mapper.LearningTypeNameByID(lt)
out.LearningTypes = append(out.LearningTypes, item)
}
return out, nil
}

View File

@ -39,10 +39,24 @@ type ListCoursesResult struct {
NextPageToken string NextPageToken string
} }
type ListLearningTypeResult struct {
LearningTypeIDs []string
}
type ListCourseThematicsParams struct {
LearningTypeID string
}
type ListCourseThematicsResult struct {
CourseThematicIDs []string
}
//go:generate mockery --name CourseRepository //go:generate mockery --name CourseRepository
type CourseRepository interface { type CourseRepository interface {
// List courses by specifid parameters. // List courses by specifid parameters.
List(ctx context.Context, params ListCoursesParams) (ListCoursesResult, error) List(context.Context, ListCoursesParams) (ListCoursesResult, error)
ListLearningTypes(context.Context) (ListLearningTypeResult, error)
ListCourseThematics(context.Context, ListCourseThematicsParams) (ListCourseThematicsResult, error)
// Get course by id. // Get course by id.
// Should return ErrNotFound in case course not found. // Should return ErrNotFound in case course not found.
Get(ctx context.Context, id string) (Course, error) Get(ctx context.Context, id string) (Course, error)

View File

@ -50,9 +50,9 @@ func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error)
return out, err return out, err
} }
query := r.URL.Query() vars := mux.Vars(r)
out.learningType = query.Get("category") out.learningType = vars["learning_type"]
out.courseThematic = query.Get("type") out.courseThematic = vars["thematic_type"]
return out, nil return out, nil
} }

View File

@ -43,8 +43,10 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
UpdateCourseDescription: command.NewUpdateCourseDescriptionHandler(courseadapter, log), UpdateCourseDescription: command.NewUpdateCourseDescriptionHandler(courseadapter, log),
}, },
Queries: app.Queries{ Queries: app.Queries{
GetCourse: query.NewGetCourseHandler(courseadapter, log), ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log), ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
}, },
} }