3 Commits

Author SHA1 Message Date
4f89f59232 fix(cluster-3): http hardening (M15/M16/M14/C2)
- pagination: clamp per_page to [1,100] and page to >=1 in the parser,
  guard the TotalPages division against per_page=0 (panic), and clamp the
  current page to [1,totalPages]; preserves cursor (next-token) mode
- middleware: add panic-recovery as the outermost middleware so handler
  panics return a 500 instead of crashing the process; re-panics
  http.ErrAbortHandler to keep file serving intact
- index: bound the index page query (Limit:200) so it no longer drains
  the entire courses table in 1000-row batches
2026-06-28 04:31:21 +00:00
40e5621eb9 fix(cluster-2): error contract unification (C1/M10/M11/M12)
- http: handleError now recognizes domain.ErrNotFound in addition to the
  common errors.ErrNotFound sentinel, so repo-not-found maps to 404
  instead of 500 (the two packages use distinct error types)
- sqlite_course_repository: propagate listCount errors instead of logging
  and swallowing them, which left callers with a silent Count=0
- synchandler: collect course/organization insert failures into a
  function-scoped error via errors.Join and return it; previously the
  loop-local err was overwritten and the handler always returned nil,
  hiding all insert failures
2026-06-28 04:28:48 +00:00
23c29aba1d fix(cluster-1): data corruption fixes (C4/C5/C8)
- synchandler: combine course date with clock time via time.Date instead
  of adding two absolute Unix epochs, which produced corrupt start times
- tracing: make DeploymentEnvironment configurable via config.Trace
  (defaults to development instead of hardcoded production)
- http: align course handler tracer name to 'kuriweb.http' to match the
  request middleware instrument so spans share the same tracer
2026-06-27 23:54:07 +00:00
16 changed files with 87 additions and 315 deletions

250
AGENTS.md
View File

@ -1,250 +0,0 @@
# AGENTS.md
Guidance for AI agents (and humans) working in this repository.
## Project Overview
**kurious** (`git.loyso.art/frx/kurious`) is a course/education platform that
aggregates and serves educational course listings. It scrapes/syncs course data
from an external source (sravni.ru) via a rate-limited HTTP client, stores it
locally, and exposes it through a server-rendered web UI with filtering,
pagination, and statistics.
The project is written in Go and follows a hexagonal (ports & adapters)
architecture with a CQRS-flavored application layer.
## Tech Stack
- **Language:** Go 1.26 (toolchain `go1.26.4`; see `go.mod`)
- **HTTP routing:** `github.com/gorilla/mux`
- **HTTP client:** `github.com/go-resty/resty/v2` (sravni.ru scraper)
- **Database:** SQLite via `modernc.org/sqlite` (pure-Go, CGO disabled).
YDB was historically supported but is **no longer supported**
(`service.NewApplication` returns an error for the YDB engine).
- **DB access:** `github.com/jmoiron/sqlx` (named queries)
- **Templating:** `github.com/a-h/templ` (`.templ` files compile to Go)
- **Observability:** OpenTelemetry (`go.opentelemetry.io/otel`) — traces and
metrics with stdout / OTLP (HTTP & gRPC) exporters
- **Background jobs:** `github.com/robfig/cron/v3`
- **Logging:** standard `log/slog` (text or JSON, configurable)
- **Rate limiting:** `golang.org/x/time/rate`
- **Testing:** `github.com/stretchr/testify` (assert, require, mock)
- **Mocking:** `github.com/vektra/mockery/v2` (config in `.mockery.yaml`)
- **Linting:** `golangci-lint` v1.55.2
- **Build/task runner:** [Taskfile](https://taskfile.dev) (`Taskfile.yml`)
- **Build flags:** ldflags inject `version`, `commit`, `buildTime`
(see `kurious.go`)
## Architecture
The codebase implements a **hexagonal architecture** (ports & adapters) with a
CQRS-style separation between commands (writes) and queries (reads).
```
┌─────────────────────────────────────────────┐
delivery │ ports/ (HTTP server, cron jobs) │
mechanisms └──────────────────────┬──────────────────────┘
│ depends on
┌──────────────────────▼──────────────────────┐
application │ app/ (command/, query/) │
layer │ app.Application { Commands, Queries } │
│ decorator/ (logging decorators) │
└──────────────────────┬──────────────────────┘
│ depends on
┌──────────────────────▼──────────────────────┐
domain │ domain/ (entities, repository ports) │
└──────────────────────┬──────────────────────┘
│ implemented by
┌──────────────────────▼──────────────────────┐
adapters │ adapters/ (sqlite_*, memory_mapper, │
│ ydb_* legacy stub) │
└─────────────────────────────────────────────┘
```
- **`domain/`** — pure business entities (`Course`, `Organization`,
`LearningCategory`) and the **repository interfaces** (ports) that the
application depends on: `CourseRepository`, `OrganizationRepository`,
`LearningCategoryRepository`. No I/O or framework code lives here.
- **`app/`** — the application layer. `app.Application` aggregates a
`Commands` struct and a `Queries` struct. Handlers in `app/command/` perform
writes; handlers in `app/query/` perform reads. Each handler implements a
generic `decorator.CommandHandler[T]` or `decorator.QueryHandler[Q, U]`
interface and is wrapped with logging decorators at construction time.
- **`adapters/`** — concrete implementations of the domain repository ports.
`sqlite_course_repository.go`, `sqlite_organization_repository.go`, etc.
translate between domain types and SQL rows. `memory_mapper.go` is an
in-memory mapper translating external dictionary IDs to human-readable names.
- **`ports/`** — delivery mechanisms. `ports/http/` is the HTTP server (gorilla/mux
+ templ templates); `ports/background/` runs scheduled cron jobs (e.g.
`SyncSravniHandler`).
- **`service/`** — the **composition root**. `service.NewApplication` selects
the DB engine, constructs the SQLite connection, builds the adapters, and
wires everything into an `app.Application`. This is what binaries call.
Data flows: an HTTP request enters `ports/http`, calls a handler on
`service.Application` (which delegates to `app/command` or `app/query`), which
calls a `domain` repository interface implemented by an `adapters/*` repository.
## Directory Structure
```
.
├── kurious.go # Root package: version/commit/buildTime getters
├── Taskfile.yml # Build, test, lint, generate, run task definitions
├── .mockery.yaml # mockery config (with-expecter, keeptree)
├── go.mod / go.sum
├── cmd/ # Entry points (one package per binary)
│ ├── kuriweb/ # Main HTTP web server (config.go, main.go, http.go, trace.go)
│ ├── background/ # Background sync process (cron-driven sravni sync)
│ └── dev/sravnicli/ # Developer CLI for inspecting the sravni source
├── internal/
│ ├── kurious/ # Application core (the hexagon)
│ │ ├── domain/ # Entities + repository interface ports
│ │ ├── app/
│ │ │ ├── app.go # Application{Commands, Queries} aggregate
│ │ │ ├── command/ # Write-side handlers (Create, Delete, Update...)
│ │ │ └── query/ # Read-side handlers (List, Get, Stats...)
│ │ ├── adapters/ # Repository implementations + mocks/
│ │ ├── ports/
│ │ │ ├── http/ # HTTP server, course routes
│ │ │ │ └── bootstrap/ # templ templates (.templ) + generated _templ.go
│ │ │ ├── background/ # Cron job handlers
│ │ │ ├── background.go # BackgroundProcess (cron scheduler)
│ │ │ └── services.go # Services{HTTP, Background} aggregate
│ │ └── service/ # Composition root: NewApplication wiring
│ └── common/ # Shared, reusable utilities
│ ├── client/sravni/ # sravni.ru HTTP client + mocks/
│ ├── config/ # Config structs (HTTP, Log, Sqlite, Trace, YDB, Duration)
│ ├── decorator/ # Command/Query handler interfaces + logging decorators
│ ├── errors/ # SimpleError, ValidationError, sentinel errors
│ ├── generator/ # ID generators
│ ├── nullable/ # Generic nullable Value[T]
│ ├── xcontext/ # context helpers (request id, log fields)
│ ├── xlog/ # slog + cron logger adapters
│ └── xslices/ # slice helpers (Map, Filter, ForEach, LRU)
├── pkg/
│ └── xdefault/ # WithFallback helper (public-ish pkg)
├── migrations/
│ └── sqlite/ # SQL migrations (embed.FS) + migrator.go
├── assets/kurious/static/ # Static web assets (embedded via go:embed)
└── htmlexamples/ # Standalone HTML/templ prototyping examples
```
## Build, Test, and Lint Commands
All operations are driven by **Taskfile** (`task <name>`). The toolchain is
installed into a local `bin/` (`GOBIN={{.USER_WORKING_DIR}}/bin`) and
`CGO_ENABLED=0` is enforced.
| Command | What it does |
|--------------------------|--------------------------------------------------------------------|
| `task install_tools` | Install `golangci-lint`, `templ`, `mockery` into `bin/` |
| `task generate` | Run `templ generate` (compiles `.templ``_templ.go`) |
| `task mocks` | Run `go generate ./internal/...` (regenerate mockery mocks) |
| `task check` | Run `golangci-lint run ./...` (depends on `generate`) |
| `task test` | Run `go test ./internal/...` (depends on `generate`) |
| `task build_web` | Build `bin/kuriousweb` (depends on `check` + `test`) |
| `task build_background` | Build `bin/kuriousbg` (depends on `check` + `test`) |
| `task build_dev_cli` | Build `bin/sravnicli` (depends on `check` + `test`) |
| `task build` | Build all three binaries |
| `task run` | Build then run `bin/kuriousweb` |
Typical workflow before committing: `task check && task test`.
### Running the binaries
Binaries read a JSON config file path as their first argument, defaulting to
`config.json` (note: `*.json` is gitignored, so configs are local). Example
fields: `log`, `sqlite`, `http.listen_addr`, `db_engine`, `tracing`.
```bash
./bin/kuriousweb path/to/config.json
```
## Code Conventions
### Decorators / handler pattern
Command and query handlers implement generic interfaces and are wrapped with
logging decorators at construction time. When adding a new use case:
1. Define the command/query struct and a `*Handler` type alias of
`decorator.CommandHandler[T]` / `decorator.QueryHandler[Q, U]` in
`app/command/` or `app/query/`.
2. Implement a private `*Handler` struct holding its repository dependencies.
3. Expose a `New*Handler(deps..., log *slog.Logger)` constructor that returns
the decorated handler (`decorator.ApplyCommandDecorators` /
`decorator.AddQueryDecorators`).
4. Register the new handler on `app.Commands` / `app.Queries` in
`app/app.go` and wire it in `service/service.go`.
### Mock generation (mockery)
- Interfaces marked with `//go:generate mockery ...` directives (see
`domain/repository.go`, `common/client/sravni/client.go`) are mocked into
sibling `mocks/` packages (`keeptree: True`, `with-expecter: true`).
- Regenerate with `task mocks` (runs `go generate ./internal/...`).
- Mocks are committed to the repo.
### Templating (templ)
- HTML is authored as `.templ` files under
`internal/kurious/ports/http/bootstrap/`.
- `task generate` compiles them to `*_templ.go` (committed). Run it whenever a
`.templ` file changes; `task check` and `task test` both depend on it.
### Database / migrations
- SQLite migrations live in `migrations/sqlite/*.sql` and are embedded via
`go:embed`. The migrator (`migrations/sqlite/migrator.go`) applies them in
order inside a transaction.
- Repository adapters use `sqlx` named queries; domain ↔ row translation is
centralized via `AsDomain()` methods.
### Logging & observability
- Use `log/slog` with structured attributes. Request-scoped fields (e.g.
`request_id`) are propagated through `context` via `internal/common/xcontext`.
- HTTP handlers wrap requests in trace spans and record metrics
(request duration histogram). Database adapters emit spans tagged with
`db.*` attributes.
### Error handling
- Domain/repository errors use sentinels from `internal/common/errors`
(`ErrNotFound`, `ErrNotImplemented`) and `*ValidationError` (mapped to HTTP
400/404 in `ports/http/server.go`).
- Wrap errors with `fmt.Errorf("doing X: %w", err)` to add context while
preserving the underlying error for `errors.Is` / `errors.As`.
## Testing Patterns
- **Scope:** unit tests live next to the code they test
(`*_test.go`). `task test` runs `go test ./internal/...`.
- **Framework:** `github.com/stretchr/testify``require` for fatal
assertions, `assert` for non-fatal, `mock` for mock interactions.
- **Mocks:** generated mockery mocks with the **expecter** API:
```go
repo.EXPECT().Create(mock.Anything, expectedParams).
Return(domain.Course{ID: "c1"}, nil).Once()
```
Note: when the generated mock does not yet implement every interface method,
tests embed the mock and add the missing methods (see the `fullRepo` pattern
in `internal/kurious/app/command/command_test.go`).
- **Logger:** tests use a `quietLogger()` helper that discards output
(`slog.New(slog.NewTextHandler(io.Discard, nil))`).
- **Adapter tests:** `internal/kurious/adapters/sqlite_*_test.go` exercise the
real SQLite repositories against an in-memory/temp database.
- **Conventions:** table-driven where appropriate; use `context.Background()`
in tests; assert on wrapped errors with `assert.ErrorIs` and message
fragments with `assert.Contains`.
## Notes for Agents
- Run `task generate` before linting/testing if you touched any `.templ` file.
- Do not re-introduce YDB as a working engine without updating
`service.NewApplication` and the migration story (currently SQLite-only).
- Generated files (`*_templ.go`, `mocks/*.go`) are committed — regenerate and
commit them alongside source changes.
- `*.json`, `*.sqlite`, `bin/`, and `*.log` are gitignored; do not commit
local configs or databases.

View File

@ -85,6 +85,19 @@ type Filters struct {
IsCourseProfeccion Flagged `json:"isCourseProfession"` IsCourseProfeccion Flagged `json:"isCourseProfession"`
} }
type DataSourceType string
const (
DataSourceTypeDictionaryConfig DataSourceType = "dictionaryConfig"
DataSourceTypeDictionary DataSourceType = "dictionary"
DataSourceTypeOrganization DataSourceType = "organization"
)
type ReduxDataSource struct {
Type DataSourceType `json:"type"`
Source string `json:"source"`
}
type ReduxConfigDictionaryBaseUnit struct { type ReduxConfigDictionaryBaseUnit struct {
Label string `json:"label"` Label string `json:"label"`
Value string `json:"value"` Value string `json:"value"`
@ -111,16 +124,23 @@ type ReduxConfigFilterUnit struct {
} }
type ReduxConfigSorting struct { type ReduxConfigSorting struct {
Name string `json:"name"`
Property string `json:"property"` Property string `json:"property"`
Direction string `json:"direction"` // 3 more fields
} }
type ReduxConfig struct { type ReduxConfig struct {
Default struct { Default struct {
SortingParameters []ReduxConfigSorting `json:"sortingParameters"` SortingParameters []ReduxConfigSorting `json:"sortingParameters"`
} `json:"default"` } `json:"default"`
DataSources map[string]string `json:"dataSources"` DataSources map[string]ReduxDataSource `json:"dataSources"`
Dictionaries struct {
PriceFilter []ReduxConfigDictionaryRangeUnit `json:"dictionaryPriceFilter"`
GraphicFilter []ReduxConfigDictionaryBaseUnit `json:"dictionaryGraphicFilterNew"`
FormatFilter []ReduxConfigDictionaryBaseUnit `json:"dictionaryFormatFilterNew"`
LevelFilter []ReduxConfigDictionaryBaseUnit `json:"dictionaryLevelFilterNew"`
TimeFilter []ReduxConfigDictionaryRangeUnit `json:"dictionaryTimeFilter"`
InstallmentFilter []ReduxConfigDictionaryRangeUnit `json:"dictionaryInstallmentFilter"`
}
Filters []ReduxConfigFilterUnit `json:"filters"` Filters []ReduxConfigFilterUnit `json:"filters"`
Sorting []ReduxConfigSorting `json:"sorting"` Sorting []ReduxConfigSorting `json:"sorting"`
} }

View File

@ -5,7 +5,7 @@ import (
"testing" "testing"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"

View File

@ -1,42 +0,0 @@
package adapters
import (
"context"
cerrors "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/kurious/domain"
)
type NotImplementedOrganizationRepository struct{}
func (NotImplementedOrganizationRepository) ListStats(
context.Context,
domain.ListOrganizationsParams,
) ([]domain.OrganizationStat, error) {
return nil, cerrors.ErrNotImplemented
}
func (NotImplementedOrganizationRepository) List(context.Context, domain.ListOrganizationsParams) ([]domain.Organization, error) {
return nil, cerrors.ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Get(context.Context, domain.GetOrganizationParams) (domain.Organization, error) {
return domain.Organization{}, cerrors.ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Create(context.Context, domain.CreateOrganizationParams) (domain.Organization, error) {
return domain.Organization{}, cerrors.ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error {
return cerrors.ErrNotImplemented
}
type NotImplementedLearningCategory struct{}
func (NotImplementedLearningCategory) Upsert(context.Context, domain.LearningCategory) error {
return cerrors.ErrNotImplemented
}
func (NotImplementedLearningCategory) List(context.Context) ([]domain.LearningCategory, error) {
return nil, cerrors.ErrNotImplemented
}
func (NotImplementedLearningCategory) Get(context.Context, string) (domain.LearningCategory, error) {
return domain.LearningCategory{}, cerrors.ErrNotImplemented
}

View File

@ -8,7 +8,6 @@ import (
"log/slog" "log/slog"
"strings" "strings"
cerrors "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
@ -157,7 +156,7 @@ func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (c
err = r.db.GetContext(ctx, &cdb, query, id) err = r.db.GetContext(ctx, &cdb, query, id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return domain.LearningCategory{}, cerrors.ErrNotFound return domain.LearningCategory{}, domain.ErrNotFound
} }
return domain.LearningCategory{}, fmt.Errorf("executing query: %w", err) return domain.LearningCategory{}, fmt.Errorf("executing query: %w", err)
} }

View File

@ -3,7 +3,6 @@ package adapters
import ( import (
"testing" "testing"
"git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
@ -76,7 +75,7 @@ func (s *sqliteLearningCategoriesRepositorySuite) TestUpsert() {
const categoryID = "test-id-1" const categoryID = "test-id-1"
repo := s.connection.LearningCategory() repo := s.connection.LearningCategory()
gotCategory, err := repo.Get(s.ctx, categoryID) gotCategory, err := repo.Get(s.ctx, categoryID)
s.ErrorIs(err, errors.ErrNotFound) s.ErrorIs(err, domain.ErrNotFound)
s.Empty(gotCategory) s.Empty(gotCategory)
createdCategory := domain.LearningCategory{ createdCategory := domain.LearningCategory{

View File

@ -9,7 +9,6 @@ import (
"strings" "strings"
"time" "time"
cerrors "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xslices" "git.loyso.art/frx/kurious/internal/common/xslices"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
@ -229,7 +228,7 @@ func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.Ge
err = r.db.GetContext(ctx, &orgdb, query, args...) err = r.db.GetContext(ctx, &orgdb, query, args...)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return out, cerrors.ErrNotFound return out, domain.ErrNotFound
} }
return out, fmt.Errorf("executing query: %w", err) return out, fmt.Errorf("executing query: %w", err)
} }
@ -307,14 +306,14 @@ func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) (e
result, err := r.db.ExecContext(ctx, query, id) result, err := r.db.ExecContext(ctx, query, id)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return cerrors.ErrNotFound return domain.ErrNotFound
} }
return fmt.Errorf("executing query: %w", err) return fmt.Errorf("executing query: %w", err)
} }
affected, _ := result.RowsAffected() affected, _ := result.RowsAffected()
if affected == 0 { if affected == 0 {
return cerrors.ErrNotFound return domain.ErrNotFound
} }
return nil return nil

View File

@ -110,11 +110,11 @@ func (conn *YDBConnection) Close() error {
} }
func (conn *YDBConnection) Organization() domain.OrganizationRepository { func (conn *YDBConnection) Organization() domain.OrganizationRepository {
return NotImplementedOrganizationRepository{} return domain.NotImplementedOrganizationRepository{}
} }
func (conn *YDBConnection) LearningCategory() domain.LearningCategoryRepository { func (conn *YDBConnection) LearningCategory() domain.LearningCategoryRepository {
return NotImplementedLearningCategory{} return domain.NotImplementedLearningCategory{}
} }
func (conn *YDBConnection) CourseRepository() *ydbCourseRepository { func (conn *YDBConnection) CourseRepository() *ydbCourseRepository {

View File

@ -9,7 +9,7 @@ import (
"git.loyso.art/frx/kurious/internal/common/nullable" "git.loyso.art/frx/kurious/internal/common/nullable"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"

View File

@ -8,7 +8,7 @@ import (
"testing" "testing"
"git.loyso.art/frx/kurious/internal/kurious/domain" "git.loyso.art/frx/kurious/internal/kurious/domain"
mockrepo "git.loyso.art/frx/kurious/internal/kurious/adapters/mocks" mockrepo "git.loyso.art/frx/kurious/internal/kurious/domain/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"

View File

@ -0,0 +1,12 @@
package domain
const (
ErrNotFound PlainError = "not found"
ErrNotImplemented PlainError = "not implemented"
)
type PlainError string
func (err PlainError) Error() string {
return string(err)
}

View File

@ -80,7 +80,7 @@ type ListStatisticsResult struct {
LearningTypeStatistics []StatisticUnit LearningTypeStatistics []StatisticUnit
} }
//go:generate mockery --name CourseRepository --output ../adapters/mocks //go:generate mockery --name CourseRepository
type CourseRepository interface { type CourseRepository interface {
// List courses by specifid parameters. // List courses by specifid parameters.
List(context.Context, ListCoursesParams) (ListCoursesResult, error) List(context.Context, ListCoursesParams) (ListCoursesResult, error)
@ -126,7 +126,7 @@ type ListOrganizationsParams struct {
IDs []string IDs []string
} }
//go:generate mockery --name OrganizationRepository --output ../adapters/mocks //go:generate mockery --name OrganizationRepository
type OrganizationRepository interface { type OrganizationRepository interface {
ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error) ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error)
List(context.Context, ListOrganizationsParams) ([]Organization, error) List(context.Context, ListOrganizationsParams) ([]Organization, error)
@ -135,10 +135,44 @@ type OrganizationRepository interface {
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
} }
//go:generate mockery --name LearningCategoryRepository --output ../adapters/mocks type NotImplementedOrganizationRepository struct{}
func (NotImplementedOrganizationRepository) ListStats(
context.Context,
ListOrganizationsParams,
) ([]OrganizationStat, error) {
return nil, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) {
return nil, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) {
return Organization{}, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Create(context.Context, CreateOrganizationParams) (Organization, error) {
return Organization{}, ErrNotImplemented
}
func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error {
return ErrNotImplemented
}
//go:generate mockery --name LearningCategoryRepository
type LearningCategoryRepository interface { type LearningCategoryRepository interface {
Upsert(context.Context, LearningCategory) error Upsert(context.Context, LearningCategory) error
List(context.Context) ([]LearningCategory, error) List(context.Context) ([]LearningCategory, error)
Get(context.Context, string) (LearningCategory, error) Get(context.Context, string) (LearningCategory, error)
} }
type NotImplementedLearningCategory struct{}
func (NotImplementedLearningCategory) Upsert(context.Context, LearningCategory) error {
return ErrNotImplemented
}
func (NotImplementedLearningCategory) List(context.Context) ([]LearningCategory, error) {
return nil, ErrNotImplemented
}
func (NotImplementedLearningCategory) Get(context.Context, string) (LearningCategory, error) {
return LearningCategory{}, ErrNotImplemented
}

View File

@ -9,6 +9,7 @@ import (
"git.loyso.art/frx/kurious/internal/common/errors" "git.loyso.art/frx/kurious/internal/common/errors"
"git.loyso.art/frx/kurious/internal/common/xcontext" "git.loyso.art/frx/kurious/internal/common/xcontext"
"git.loyso.art/frx/kurious/internal/kurious/domain"
"git.loyso.art/frx/kurious/internal/kurious/service" "git.loyso.art/frx/kurious/internal/kurious/service"
"git.loyso.art/frx/kurious/pkg/xdefault" "git.loyso.art/frx/kurious/pkg/xdefault"
@ -49,7 +50,7 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
case stderrors.As(err, &valErr): case stderrors.As(err, &valErr):
errorString = valErr.Error() errorString = valErr.Error()
code = http.StatusBadRequest code = http.StatusBadRequest
case stderrors.Is(err, errors.ErrNotFound): case stderrors.Is(err, errors.ErrNotFound), stderrors.Is(err, domain.ErrNotFound):
errorString = err.Error() errorString = err.Error()
code = http.StatusNotFound code = http.StatusNotFound
default: default: