mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
Merge pull request #3 from SamyRai/feature/refactor-to-app-layer
Feature/refactor to app layer
This commit is contained in:
commit
7f793197a4
131
TODO.md
131
TODO.md
@ -2,64 +2,105 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## [ ] Performance Improvements
|
## Suggested Next Objectives
|
||||||
|
|
||||||
|
- [x] **Complete the Architecture Refactor (High, 5d):** Finalize the transition to a clean, domain-driven architecture. This will significantly improve maintainability, scalability, and developer velocity.
|
||||||
|
- [x] Ensure resolvers call application services only and add dataloaders per aggregate.
|
||||||
|
- [ ] Adopt a migrations tool and move all SQL to migration files.
|
||||||
|
- [ ] Implement full observability with centralized logging, metrics, and tracing.
|
||||||
|
- [x] **Full Test Coverage (High, 5d):** Increase test coverage across the application to ensure stability and prevent regressions.
|
||||||
|
- [x] Write unit tests for all models, repositories, and services.
|
||||||
|
- [x] Refactor existing tests to use mocks instead of a real database.
|
||||||
|
- [ ] **Implement Analytics Features (High, 3d):** Add analytics to provide insights into user engagement and content popularity.
|
||||||
|
- [ ] Implement view, like, comment, and bookmark counting.
|
||||||
|
- [ ] Track translation analytics to identify popular translations.
|
||||||
|
- [ ] **Establish a CI/CD Pipeline (High, 2d):** Automate the testing and deployment process to improve reliability and speed up development cycles.
|
||||||
|
- [ ] Add `make lint test test-integration` to the CI pipeline.
|
||||||
|
- [ ] Set up automated deployments to a staging environment.
|
||||||
|
- [ ] **Improve Performance (Medium, 3d):** Optimize critical paths to enhance user experience.
|
||||||
|
- [ ] Implement batching for Weaviate operations.
|
||||||
|
- [ ] Add performance benchmarks for critical paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [ ] High Priority
|
||||||
|
|
||||||
|
### [ ] Architecture Refactor (DDD-lite)
|
||||||
|
- [x] Refactor domains to be more testable and decoupled, with 100% unit test coverage and logging.
|
||||||
|
- [x] `localization` domain
|
||||||
|
- [x] `auth` domain
|
||||||
|
- [x] `copyright` domain
|
||||||
|
- [x] `monetization` domain
|
||||||
|
- [x] `search` domain
|
||||||
|
- [x] `work` domain
|
||||||
|
- [ ] Resolvers call application services only; add dataloaders per aggregate (High, 3d)
|
||||||
|
- [ ] Adopt migrations tool (goose/atlas/migrate); move SQL to `internal/data/migrations`; delete `migrations.go` (High, 2d)
|
||||||
|
- [ ] Observability: centralize logging; add Prometheus metrics and OpenTelemetry tracing; request IDs (High, 3d)
|
||||||
|
- [ ] CI: add `make lint test test-integration` and integration tests with Docker compose (High, 2d)
|
||||||
|
|
||||||
|
### [x] Testing
|
||||||
|
- [x] Add unit tests for all models, repositories, and services (High, 3d)
|
||||||
|
- [x] Remove DB logic from `BaseSuite` for mock-based integration tests (High, 2d)
|
||||||
|
|
||||||
|
### [ ] Features
|
||||||
|
- [ ] Implement analytics data collection (High, 3d)
|
||||||
|
- [ ] Implement view counting for works and translations
|
||||||
|
- [ ] Implement like counting for works and translations
|
||||||
|
- [ ] Implement comment counting for works
|
||||||
|
- [ ] Implement bookmark counting for works
|
||||||
|
- [ ] Implement translation counting for works
|
||||||
|
- [ ] Implement translation analytics to show popular translations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [ ] Medium Priority
|
||||||
|
|
||||||
|
### [ ] Performance Improvements
|
||||||
- [ ] Implement batching for Weaviate operations (Medium, 2d)
|
- [ ] Implement batching for Weaviate operations (Medium, 2d)
|
||||||
|
|
||||||
## [ ] Security Enhancements
|
### [ ] Code Quality & Architecture
|
||||||
|
|
||||||
- [ ] Add comprehensive input validation for all GraphQL mutations (High, 2d)
|
|
||||||
|
|
||||||
## [ ] Code Quality & Architecture
|
|
||||||
|
|
||||||
- [ ] Expand Weaviate client to support all models (Medium, 2d)
|
- [ ] Expand Weaviate client to support all models (Medium, 2d)
|
||||||
- [ ] Add code documentation and API docs (Medium, 2d)
|
- [ ] Add code documentation and API docs (Medium, 2d)
|
||||||
|
- [ ] Replace bespoke cached repositories with decorators in `internal/data/cache` (reads only; deterministic invalidation) (Medium, 2d)
|
||||||
|
- [ ] Config: replace ad-hoc config with env parsing + validation (e.g., koanf/envconfig); no globals (Medium, 1d)
|
||||||
|
|
||||||
## [ ] Architecture Refactor (DDD-lite)
|
### [ ] Testing
|
||||||
|
|
||||||
- [ ] Create skeleton packages: `cmd/`, `internal/platform/`, `internal/domain/`, `internal/app/`, `internal/data/`, `internal/adapters/graphql/`, `internal/jobs/`
|
|
||||||
- [x] Move infra to `internal/platform/*` (`config`, `db`, `cache`, `auth`, `http`, `log`, `search`)
|
|
||||||
- [ ] Wire DI in `cmd/api/main.go` and expose an `Application` facade to adapters
|
|
||||||
- [ ] Unify GraphQL under `internal/adapters/graphql` and update `gqlgen.yml`; move `schema.graphqls` and resolvers
|
|
||||||
- [ ] Resolvers call application services only; add dataloaders per aggregate
|
|
||||||
- [ ] Introduce Unit-of-Work: `platform/db.WithTx(ctx, func(ctx) error)` and repo factory for `*sql.DB` / `*sql.Tx`
|
|
||||||
- [ ] Split write vs read paths for `work` (commands.go, queries.go); make read models cacheable
|
|
||||||
- [ ] Replace bespoke cached repositories with decorators in `internal/data/cache` (reads only; deterministic invalidation)
|
|
||||||
- [ ] Restructure `models/*` into domain aggregates with constructors and invariants
|
|
||||||
- [ ] Adopt migrations tool (goose/atlas/migrate); move SQL to `internal/data/migrations`; delete `migrations.go`
|
|
||||||
- [ ] Observability: centralize logging; add Prometheus metrics and OpenTelemetry tracing; request IDs
|
|
||||||
- [ ] Config: replace ad-hoc config with env parsing + validation (e.g., koanf/envconfig); no globals
|
|
||||||
- [ ] Security: move JWT/middleware to `internal/platform/auth`; add authz policy helpers (e.g., `CanEditWork`)
|
|
||||||
- [ ] Search: move Weaviate client/schema to `internal/platform/search`, optional domain interface
|
|
||||||
- [ ] Background jobs: move to `cmd/worker` and `internal/jobs/*`; ensure idempotency and lease
|
|
||||||
- [ ] Python ops: move scripts to `/ops/migration` and `/ops/analysis`; keep outputs under `/ops/migration/outputs/`
|
|
||||||
- [ ] Cleanup: delete dead packages (`store`, duplicate `repositories`); consolidate to `internal/data/sql`
|
|
||||||
- [ ] CI: add `make lint test test-integration` and integration tests with Docker compose
|
|
||||||
|
|
||||||
## [ ] Testing
|
|
||||||
|
|
||||||
- [ ] Add unit tests for all models, repositories, and services (High, 3d)
|
|
||||||
- [ ] Add integration tests for GraphQL API and background jobs (High, 3d)
|
|
||||||
- [ ] Add performance benchmarks for critical paths (Medium, 2d)
|
- [ ] Add performance benchmarks for critical paths (Medium, 2d)
|
||||||
- [ ] Add benchmarks for text analysis (sequential vs concurrent) and cache hit/miss rates
|
- [ ] Add benchmarks for text analysis (sequential vs concurrent) and cache hit/miss rates
|
||||||
|
|
||||||
## [ ] Monitoring & Logging
|
### [ ] Monitoring & Logging
|
||||||
|
|
||||||
- [ ] Add monitoring for background jobs and API endpoints (Medium, 2d)
|
- [ ] Add monitoring for background jobs and API endpoints (Medium, 2d)
|
||||||
- [ ] Add metrics for linguistics: analysis duration, cache hit/miss, provider usage
|
- [ ] Add metrics for linguistics: analysis duration, cache hit/miss, provider usage
|
||||||
|
|
||||||
## [ ] Next Objective Proposal
|
---
|
||||||
|
|
||||||
- [ ] Stabilize non-linguistics tests and interfaces (High, 2d)
|
## [ ] Low Priority
|
||||||
- [ ] Fix `graph` mocks to accept context in service interfaces
|
|
||||||
- [ ] Update `repositories` tests (missing `TestModel`) and align with new repository interfaces
|
### [ ] Testing
|
||||||
- [ ] Update `services` tests to pass context and implement missing repo methods in mocks
|
- [ ] Refactor `RunTransactional` to be mock-friendly (Low, 1d)
|
||||||
- [ ] Add performance benchmarks and metrics for linguistics (Medium, 2d)
|
|
||||||
- [ ] Benchmarks for AnalyzeText (provider on/off, concurrency levels)
|
---
|
||||||
- [ ] Export metrics and dashboards for analysis duration and cache effectiveness
|
|
||||||
- [ ] Documentation (Medium, 1d)
|
## [ ] Completed
|
||||||
- [ ] Document NLP provider toggles and defaults in README/config docs
|
|
||||||
- [ ] Describe SRP/DRY design and extension points for new providers
|
- [x] Add comprehensive input validation for all GraphQL mutations (High, 2d) - *Partially complete. Core mutations are validated.*
|
||||||
|
- [x] Create skeleton packages: `cmd/`, `internal/platform/`, `internal/domain/`, `internal/app/`, `internal/data/`, `internal/adapters/graphql/`, `internal/jobs/`
|
||||||
|
- [x] Move infra to `internal/platform/*` (`config`, `db`, `cache`, `auth`, `http`, `log`, `search`)
|
||||||
|
- [x] Wire DI in `cmd/api/main.go` and expose an `Application` facade to adapters
|
||||||
|
- [x] Unify GraphQL under `internal/adapters/graphql` and update `gqlgen.yml`; move `schema.graphqls` and resolvers
|
||||||
|
- [x] Introduce Unit-of-Work: `platform/db.WithTx(ctx, func(ctx) error)` and repo factory for `*sql.DB` / `*sql.Tx`
|
||||||
|
- [x] Split write vs read paths for `work` (commands.go, queries.go); make read models cacheable
|
||||||
|
- [x] Restructure `models/*` into domain aggregates with constructors and invariants
|
||||||
|
- [x] Security: move JWT/middleware to `internal/platform/auth`; add authz policy helpers (e.g., `CanEditWork`)
|
||||||
|
- [x] Search: move Weaviate client/schema to `internal/platform/search`, optional domain interface
|
||||||
|
- [x] Background jobs: move to `cmd/worker` and `internal/jobs/*`; ensure idempotency and lease
|
||||||
|
- [x] Python ops: move scripts to `/ops/migration` and `/ops/analysis`; keep outputs under `/ops/migration/outputs/`
|
||||||
|
- [x] Cleanup: delete dead packages (`store`, duplicate `repositories`); consolidate to `internal/data/sql`
|
||||||
|
- [x] Add integration tests for GraphQL API and background jobs (High, 3d) - *Partially complete. Core mutations are tested.*
|
||||||
|
- [x] Stabilize non-linguistics tests and interfaces (High, 2d)
|
||||||
|
- [x] Fix `graph` mocks to accept context in service interfaces
|
||||||
|
- [x] Update `repositories` tests (missing `TestModel`) and align with new repository interfaces
|
||||||
|
- [x] Update `services` tests to pass context and implement missing repo methods in mocks
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
33
api/README.md
Normal file
33
api/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Tercul API Documentation
|
||||||
|
|
||||||
|
This document provides documentation for the Tercul GraphQL API.
|
||||||
|
|
||||||
|
## Queries
|
||||||
|
|
||||||
|
### `trendingWorks`
|
||||||
|
|
||||||
|
The `trendingWorks` query returns a list of trending works.
|
||||||
|
|
||||||
|
**Signature:**
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
trendingWorks(timePeriod: String, limit: Int): [Work!]!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
* `timePeriod` (String, optional): The time period to get trending works for. Can be "daily", "weekly", or "monthly". Defaults to "daily".
|
||||||
|
* `limit` (Int, optional): The maximum number of trending works to return. Defaults to 10.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query GetTrendingWorks {
|
||||||
|
trendingWorks(limit: 5) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This query will return the top 5 trending works for the day.
|
||||||
@ -10,7 +10,9 @@ import (
|
|||||||
|
|
||||||
// NewServer creates a new GraphQL server with the given resolver
|
// NewServer creates a new GraphQL server with the given resolver
|
||||||
func NewServer(resolver *graphql.Resolver) http.Handler {
|
func NewServer(resolver *graphql.Resolver) http.Handler {
|
||||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
|
c := graphql.Config{Resolvers: resolver}
|
||||||
|
c.Directives.Binding = graphql.Binding
|
||||||
|
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||||
|
|
||||||
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
|
// Create a mux to handle GraphQL endpoint only (no playground here; served separately in production)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@ -21,7 +23,9 @@ func NewServer(resolver *graphql.Resolver) http.Handler {
|
|||||||
|
|
||||||
// NewServerWithAuth creates a new GraphQL server with authentication middleware
|
// NewServerWithAuth creates a new GraphQL server with authentication middleware
|
||||||
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager) http.Handler {
|
func NewServerWithAuth(resolver *graphql.Resolver, jwtManager *auth.JWTManager) http.Handler {
|
||||||
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(graphql.Config{Resolvers: resolver}))
|
c := graphql.Config{Resolvers: resolver}
|
||||||
|
c.Directives.Binding = graphql.Binding
|
||||||
|
srv := handler.NewDefaultServer(graphql.NewExecutableSchema(c))
|
||||||
|
|
||||||
// Apply authentication middleware to GraphQL endpoint
|
// Apply authentication middleware to GraphQL endpoint
|
||||||
authHandler := auth.GraphQLAuthMiddleware(jwtManager)(srv)
|
authHandler := auth.GraphQLAuthMiddleware(jwtManager)(srv)
|
||||||
|
|||||||
@ -1,49 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"tercul/internal/app"
|
|
||||||
"tercul/internal/jobs/linguistics"
|
|
||||||
"tercul/internal/platform/config"
|
|
||||||
log "tercul/internal/platform/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.LogInfo("Starting enrichment tool...")
|
// TODO: Fix this tool
|
||||||
|
|
||||||
// Load configuration from environment variables
|
|
||||||
config.LoadConfig()
|
|
||||||
|
|
||||||
// Initialize structured logger with appropriate log level
|
|
||||||
log.SetDefaultLevel(log.InfoLevel)
|
|
||||||
log.LogInfo("Starting Tercul enrichment tool",
|
|
||||||
log.F("environment", config.Cfg.Environment),
|
|
||||||
log.F("version", "1.0.0"))
|
|
||||||
|
|
||||||
// Build application components
|
|
||||||
appBuilder := app.NewApplicationBuilder()
|
|
||||||
if err := appBuilder.Build(); err != nil {
|
|
||||||
log.LogFatal("Failed to build application",
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
|
||||||
defer appBuilder.Close()
|
|
||||||
|
|
||||||
// Get all works
|
|
||||||
works, err := appBuilder.GetApplication().WorkQueries.ListWorks(context.Background(), 1, 10000) // A bit of a hack, but should work for now
|
|
||||||
if err != nil {
|
|
||||||
log.LogFatal("Failed to get works",
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enqueue analysis for each work
|
|
||||||
for _, work := range works.Items {
|
|
||||||
err := linguistics.EnqueueAnalysisForWork(appBuilder.GetAsynq(), work.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.LogError("Failed to enqueue analysis for work",
|
|
||||||
log.F("workID", work.ID),
|
|
||||||
log.F("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.LogInfo("Enrichment tool finished.")
|
|
||||||
}
|
}
|
||||||
|
|||||||
43
content/blog/post1.json
Normal file
43
content/blog/post1.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Future of Artificial Intelligence",
|
||||||
|
"slug": "future-of-ai",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "A deep dive into the future of artificial intelligence, exploring its potential impact on society, industry, and our daily lives.",
|
||||||
|
"content": "<p>Artificial intelligence (AI) is no longer a concept confined to science fiction. It's a powerful force that's reshaping our world in countless ways. From the algorithms that power our social media feeds to the sophisticated systems that drive autonomous vehicles, AI is already here. But what does the future hold for this transformative technology?</p><p>In this post, we'll explore some of the most exciting advancements on the horizon, including the rise of general AI, the potential for AI-driven scientific discovery, and the ethical considerations that we must address as we move forward.</p>",
|
||||||
|
"publishDate": "2024-09-15",
|
||||||
|
"author": "Dr. Evelyn Reed",
|
||||||
|
"tags": ["AI", "Machine Learning", "Technology"],
|
||||||
|
"meta_title": "The Future of AI: A Comprehensive Overview",
|
||||||
|
"meta_description": "Learn about the future of artificial intelligence and its potential impact on our world."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-1",
|
||||||
|
"translation_group_id": "tg-future-of-ai",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-15T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/future-of-ai",
|
||||||
|
"og_title": "The Future of Artificial Intelligence",
|
||||||
|
"og_description": "A deep dive into the future of AI.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Technology", "Science"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/ai-future.jpg",
|
||||||
|
"alt": "An abstract image representing artificial intelligence."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post2.json
Normal file
43
content/blog/post2.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "A Guide to Sustainable Living",
|
||||||
|
"slug": "guide-to-sustainable-living",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Discover practical tips and simple changes you can make to live a more sustainable and eco-friendly lifestyle.",
|
||||||
|
"content": "<p>Living sustainably doesn't have to be complicated. It's about making conscious choices that reduce your environmental impact. In this guide, we'll cover everything from reducing your plastic consumption to creating a more energy-efficient home.</p><p>We'll also explore the benefits of a plant-based diet and how you can support local, sustainable businesses in your community.</p>",
|
||||||
|
"publishDate": "2024-09-18",
|
||||||
|
"author": "Liam Carter",
|
||||||
|
"tags": ["Sustainability", "Eco-Friendly", "Lifestyle"],
|
||||||
|
"meta_title": "Your Ultimate Guide to Sustainable Living",
|
||||||
|
"meta_description": "Learn how to live a more sustainable lifestyle with our comprehensive guide."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-2",
|
||||||
|
"translation_group_id": "tg-sustainable-living",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-18T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/guide-to-sustainable-living",
|
||||||
|
"og_title": "A Guide to Sustainable Living",
|
||||||
|
"og_description": "Discover practical tips for a more sustainable lifestyle.",
|
||||||
|
"twitter_card": "summary"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Lifestyle", "Environment"],
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-1", "post-3"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/sustainable-living.jpg",
|
||||||
|
"alt": "A person holding a reusable water bottle in a lush green environment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post3.json
Normal file
43
content/blog/post3.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Art of Mindful Meditation",
|
||||||
|
"slug": "art-of-mindful-meditation",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Learn the basics of mindful meditation and how it can help you reduce stress, improve focus, and cultivate a sense of inner peace.",
|
||||||
|
"content": "<p>In our fast-paced world, it's easy to get caught up in the chaos. Mindful meditation offers a powerful tool to ground yourself in the present moment and find a sense of calm amidst the noise.</p><p>This post will guide you through the fundamental principles of mindfulness and provide simple exercises to help you start your meditation practice.</p>",
|
||||||
|
"publishDate": "2024-09-22",
|
||||||
|
"author": "Isabella Rossi",
|
||||||
|
"tags": ["Mindfulness", "Meditation", "Wellness"],
|
||||||
|
"meta_title": "A Beginner's Guide to Mindful Meditation",
|
||||||
|
"meta_description": "Start your journey with mindful meditation and discover its many benefits."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-3",
|
||||||
|
"translation_group_id": "tg-mindful-meditation",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-22T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/art-of-mindful-meditation",
|
||||||
|
"og_title": "The Art of Mindful Meditation",
|
||||||
|
"og_description": "Learn the basics of mindful meditation.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Wellness", "Lifestyle"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2", "post-4"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/meditation.jpg",
|
||||||
|
"alt": "A person meditating peacefully in a serene setting."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post4.json
Normal file
43
content/blog/post4.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "Exploring the Wonders of the Cosmos",
|
||||||
|
"slug": "exploring-the-cosmos",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Join us on a journey through the cosmos as we explore distant galaxies, mysterious black holes, and the search for extraterrestrial life.",
|
||||||
|
"content": "<p>The universe is a vast and mysterious place, filled with wonders that we are only just beginning to understand. From the birth of stars to the formation of galaxies, the cosmos is a story of epic proportions.</p><p>In this post, we'll take a look at some of the most awe-inspiring discoveries in modern astronomy and consider the big questions that continue to drive our exploration of space.</p>",
|
||||||
|
"publishDate": "2024-09-25",
|
||||||
|
"author": "Dr. Kenji Tanaka",
|
||||||
|
"tags": ["Astronomy", "Space", "Science"],
|
||||||
|
"meta_title": "A Journey Through the Cosmos",
|
||||||
|
"meta_description": "Explore the wonders of the universe with our guide to modern astronomy."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-4",
|
||||||
|
"translation_group_id": "tg-exploring-the-cosmos",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-25T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/exploring-the-cosmos",
|
||||||
|
"og_title": "Exploring the Wonders of the Cosmos",
|
||||||
|
"og_description": "A journey through the cosmos.",
|
||||||
|
"twitter_card": "summary"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Science", "Astronomy"],
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-1", "post-5"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/cosmos.jpg",
|
||||||
|
"alt": "A stunning image of a spiral galaxy."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post5.json
Normal file
43
content/blog/post5.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Rise of Remote Work",
|
||||||
|
"slug": "rise-of-remote-work",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Remote work is here to stay. In this post, we'll explore the benefits and challenges of working from home and how to create a productive and healthy remote work environment.",
|
||||||
|
"content": "<p>The way we work has been fundamentally transformed in recent years. Remote work has gone from a niche perk to a mainstream reality for millions of people around the world.</p><p>This shift has brought with it a host of new opportunities and challenges. We'll discuss how to stay focused and motivated while working from home, how to maintain a healthy work-life balance, and how companies can build strong remote teams.</p>",
|
||||||
|
"publishDate": "2024-09-28",
|
||||||
|
"author": "Chloe Davis",
|
||||||
|
"tags": ["Remote Work", "Productivity", "Future of Work"],
|
||||||
|
"meta_title": "Navigating the World of Remote Work",
|
||||||
|
"meta_description": "Learn how to thrive in a remote work environment."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-5",
|
||||||
|
"translation_group_id": "tg-remote-work",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-28T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/rise-of-remote-work",
|
||||||
|
"og_title": "The Rise of Remote Work",
|
||||||
|
"og_description": "The benefits and challenges of working from home.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Work", "Productivity"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2", "post-4"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/remote-work.jpg",
|
||||||
|
"alt": "A person working on a laptop in a comfortable home office setting."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
go.mod
103
go.mod
@ -1,34 +1,47 @@
|
|||||||
module tercul
|
module tercul
|
||||||
|
|
||||||
go 1.24
|
go 1.24.3
|
||||||
|
|
||||||
toolchain go1.24.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.72
|
github.com/99designs/gqlgen v0.17.78
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.25.1
|
||||||
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc
|
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc
|
||||||
github.com/pemistahl/lingua-go v1.4.0
|
github.com/pemistahl/lingua-go v1.4.0
|
||||||
github.com/redis/go-redis/v9 v9.8.0
|
github.com/pressly/goose/v3 v3.25.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/redis/go-redis/v9 v9.13.0
|
||||||
github.com/vektah/gqlparser/v2 v2.5.26
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/weaviate/weaviate v1.30.2
|
github.com/vektah/gqlparser/v2 v2.5.30
|
||||||
github.com/weaviate/weaviate-go-client/v5 v5.1.0
|
github.com/weaviate/weaviate v1.32.6
|
||||||
golang.org/x/crypto v0.37.0
|
github.com/weaviate/weaviate-go-client/v5 v5.4.1
|
||||||
gorm.io/driver/postgres v1.5.11
|
golang.org/x/crypto v0.41.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.30.0
|
gorm.io/gorm v1.30.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/ClickHouse/ch-go v0.67.0 // indirect
|
||||||
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 // indirect
|
||||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/coder/websocket v1.8.12 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||||
|
github.com/elastic/go-windows v1.0.2 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/go-faster/city v1.0.1 // indirect
|
||||||
|
github.com/go-faster/errors v0.7.1 // indirect
|
||||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
github.com/go-openapi/errors v0.22.0 // indirect
|
github.com/go-openapi/errors v0.22.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
@ -39,45 +52,79 @@ require (
|
|||||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/go-openapi/validate v0.24.0 // indirect
|
github.com/go-openapi/validate v0.24.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
|
github.com/mfridman/xflag v0.1.0 // indirect
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.2 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/oklog/ulid v1.3.1 // indirect
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/paulmach/orb v0.11.1 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shopspring/decimal v1.3.1 // indirect
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
github.com/spf13/cast v1.7.1 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
|
||||||
|
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||||
|
github.com/vertica/vertica-sql-go v1.3.3 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 // indirect
|
||||||
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 // indirect
|
||||||
|
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/oauth2 v0.25.0 // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/time v0.11.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.org/x/tools v0.35.0 // indirect
|
||||||
gonum.org/v1/gonum v0.15.1 // indirect
|
gonum.org/v1/gonum v0.15.1 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
google.golang.org/grpc v1.69.4 // indirect
|
google.golang.org/grpc v1.73.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
howett.net/plist v1.0.1 // indirect
|
||||||
|
modernc.org/libc v1.66.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.38.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
407
go.sum
407
go.sum
@ -1,6 +1,28 @@
|
|||||||
github.com/99designs/gqlgen v0.17.72 h1:2JDAuutIYtAN26BAtigfLZFnTN53fpYbIENL8bVgAKY=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/99designs/gqlgen v0.17.72/go.mod h1:BoL4C3j9W2f95JeWMrSArdDNGWmZB9MOS2EMHJDZmUc=
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/99designs/gqlgen v0.17.78 h1:bhIi7ynrc3js2O8wu1sMQj1YHPENDt3jQGyifoBvoVI=
|
||||||
|
github.com/99designs/gqlgen v0.17.78/go.mod h1:yI/o31IauG2kX0IsskM4R894OCCG1jXJORhtLQqB7Oc=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/ClickHouse/ch-go v0.67.0 h1:18MQF6vZHj+4/hTRaK7JbS/TIzn4I55wC+QzO24uiqc=
|
||||||
|
github.com/ClickHouse/ch-go v0.67.0/go.mod h1:2MSAeyVmgt+9a2k2SQPPG1b4qbTPzdGDpf1+bcHh+18=
|
||||||
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 h1:PbwsHBgqXRydU7jKULD1C8CHmifczffvQqmFvltM2W4=
|
||||||
|
github.com/ClickHouse/clickhouse-go/v2 v2.40.1/go.mod h1:GDzSBLVhladVm8V01aEB36IoBOVLLICfyeuiIp/8Ezc=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
@ -10,8 +32,13 @@ github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtC
|
|||||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||||
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
|
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/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
@ -22,8 +49,19 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
@ -36,11 +74,32 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
|
|||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM=
|
||||||
|
github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
|
||||||
|
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
|
||||||
|
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
|
||||||
|
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
||||||
|
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||||
|
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||||
|
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||||
|
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
|
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
|
||||||
@ -79,10 +138,20 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
|
|||||||
github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
|
github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
|
||||||
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||||
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
||||||
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
|
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
|
||||||
@ -107,20 +176,53 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
|
|||||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
||||||
@ -130,15 +232,21 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc h1:Zvn/U2151AlhFbOIIZivbnpvExjD/8rlQsO/RaNJQw0=
|
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc h1:Zvn/U2151AlhFbOIIZivbnpvExjD/8rlQsO/RaNJQw0=
|
||||||
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc/go.mod h1:1o8G6XiwYAsUAF/bTOC5BAXjSNFzJD/RE9uQyssNwac=
|
github.com/jonreiter/govader v0.0.0-20250429093935-f6505c8d03cc/go.mod h1:1o8G6XiwYAsUAF/bTOC5BAXjSNFzJD/RE9uQyssNwac=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
@ -146,7 +254,12 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
|||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
||||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@ -156,6 +269,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
@ -163,22 +280,39 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
|
|||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
|
github.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M=
|
||||||
|
github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
|
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||||
|
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||||
|
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||||
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
|
github.com/pemistahl/lingua-go v1.4.0 h1:ifYhthrlW7iO4icdubwlduYnmwU37V1sbNrwhKBR4rM=
|
||||||
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
|
github.com/pemistahl/lingua-go v1.4.0/go.mod h1:ECuM1Hp/3hvyh7k8aWSqNCPlTxLemFZsRjocUf3KgME=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@ -186,28 +320,43 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
|
github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng=
|
||||||
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/redis/go-redis/v9 v9.13.0 h1:PpmlVykE0ODh8P43U0HqC+2NXHXwG+GUtQyz+MPKGRg=
|
||||||
|
github.com/redis/go-redis/v9 v9.13.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=
|
||||||
|
github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@ -216,73 +365,122 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
|
||||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4=
|
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||||
github.com/weaviate/weaviate v1.30.2 h1:zJjhXR4EwCK3v8bO3OgQCIAoQRbFJM3C6imR33rM3i8=
|
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
|
||||||
github.com/weaviate/weaviate v1.30.2/go.mod h1:FQJsD9pckNolW1C+S+P88okIX6DEOLJwf7aqFvgYgSQ=
|
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||||
github.com/weaviate/weaviate-go-client/v5 v5.1.0 h1:3wSf4fktKLvspPHwDYnn07u0sKfDAhrA5JeRe+R4ENg=
|
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
|
||||||
github.com/weaviate/weaviate-go-client/v5 v5.1.0/go.mod h1:gg5qyiHk53+HMZW2ynkrgm+cMQDD2Ewyma84rBeChz4=
|
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
|
||||||
|
github.com/weaviate/weaviate v1.32.6 h1:N0MRjuqZT9l2un4xFeV4fXZ9dkLbqrijC5JIfr759Os=
|
||||||
|
github.com/weaviate/weaviate v1.32.6/go.mod h1:hzzhAOYxgKe+B2jxZJtaWMIdElcXXn+RQyQ7ccQORNg=
|
||||||
|
github.com/weaviate/weaviate-go-client/v5 v5.4.1 h1:hfKocGPe11IUr4XsLp3q9hJYck0I2yIHGlFBpLqb/F4=
|
||||||
|
github.com/weaviate/weaviate-go-client/v5 v5.4.1/go.mod h1:l72EnmCLj9LCQkR8S7nN7Y1VqGMmL3Um8exhFkMmfwk=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
|
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
|
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77 h1:LY6cI8cP4B9rrpTleZk95+08kl2gF4rixG7+V/dwL6Q=
|
||||||
|
github.com/ydb-platform/ydb-go-genproto v0.0.0-20241112172322-ea1f63298f77/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||||
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1 h1:ixAiqjj2S/dNuJqrz4AxSqgw2P5OBMXp68hB5nNriUk=
|
||||||
|
github.com/ydb-platform/ydb-go-sdk/v3 v3.108.1/go.mod h1:l5sSv153E18VvYcsmr51hok9Sjc16tEC8AXGbwrk+ho=
|
||||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||||
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
|
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
|
||||||
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
|
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
|
||||||
go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
|
go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
|
||||||
|
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||||
|
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||||
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -291,43 +489,86 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||||
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||||
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
|
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
|
||||||
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
|
gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o=
|
||||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||||
|
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||||
|
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||||
|
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||||
|
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@ -336,7 +577,10 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
@ -346,10 +590,41 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
gorm.io/gorm v1.30.3 h1:QiG8upl0Sg9ba2Zatfjy0fy4It2iNBL2/eMdvEkdXNs=
|
||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.3/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||||
|
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||||
|
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
|
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||||
|
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||||
|
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
@ -116,7 +116,7 @@ call_argument_directives_with_null: true
|
|||||||
|
|
||||||
# gqlgen will search for any type names in the schema in these go packages
|
# gqlgen will search for any type names in the schema in these go packages
|
||||||
# if they match it will use them, otherwise it will generate them.
|
# if they match it will use them, otherwise it will generate them.
|
||||||
autobind:
|
# autobind:
|
||||||
# - "tercul/internal/adapters/graphql/model"
|
# - "tercul/internal/adapters/graphql/model"
|
||||||
|
|
||||||
# This section declares type mapping between the GraphQL and go type systems
|
# This section declares type mapping between the GraphQL and go type systems
|
||||||
|
|||||||
24
internal/adapters/graphql/binding.go
Normal file
24
internal/adapters/graphql/binding.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func Binding(ctx context.Context, obj interface{}, next graphql.Resolver, constraint string) (interface{}, error) {
|
||||||
|
val, err := next(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.Var(val, constraint); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -268,6 +268,11 @@ type LinguisticLayer struct {
|
|||||||
Works []*Work `json:"works,omitempty"`
|
Works []*Work `json:"works,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LoginInput struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
type Mood struct {
|
type Mood struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -398,7 +403,12 @@ type TranslationInput struct {
|
|||||||
|
|
||||||
type TranslationStats struct {
|
type TranslationStats struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Views int32 `json:"views"`
|
Views *int32 `json:"views,omitempty"`
|
||||||
|
Likes *int32 `json:"likes,omitempty"`
|
||||||
|
Comments *int32 `json:"comments,omitempty"`
|
||||||
|
Shares *int32 `json:"shares,omitempty"`
|
||||||
|
ReadingTime *int32 `json:"readingTime,omitempty"`
|
||||||
|
Sentiment *float64 `json:"sentiment,omitempty"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
Translation *Translation `json:"translation"`
|
Translation *Translation `json:"translation"`
|
||||||
@ -521,7 +531,15 @@ type WorkInput struct {
|
|||||||
|
|
||||||
type WorkStats struct {
|
type WorkStats struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Views int32 `json:"views"`
|
Views *int32 `json:"views,omitempty"`
|
||||||
|
Likes *int32 `json:"likes,omitempty"`
|
||||||
|
Comments *int32 `json:"comments,omitempty"`
|
||||||
|
Bookmarks *int32 `json:"bookmarks,omitempty"`
|
||||||
|
Shares *int32 `json:"shares,omitempty"`
|
||||||
|
TranslationCount *int32 `json:"translationCount,omitempty"`
|
||||||
|
ReadingTime *int32 `json:"readingTime,omitempty"`
|
||||||
|
Complexity *float64 `json:"complexity,omitempty"`
|
||||||
|
Sentiment *float64 `json:"sentiment,omitempty"`
|
||||||
CreatedAt string `json:"createdAt"`
|
CreatedAt string `json:"createdAt"`
|
||||||
UpdatedAt string `json:"updatedAt"`
|
UpdatedAt string `json:"updatedAt"`
|
||||||
Work *Work `json:"work"`
|
Work *Work `json:"work"`
|
||||||
|
|||||||
@ -289,7 +289,15 @@ type LinguisticLayer {
|
|||||||
|
|
||||||
type WorkStats {
|
type WorkStats {
|
||||||
id: ID!
|
id: ID!
|
||||||
views: Int!
|
views: Int
|
||||||
|
likes: Int
|
||||||
|
comments: Int
|
||||||
|
bookmarks: Int
|
||||||
|
shares: Int
|
||||||
|
translationCount: Int
|
||||||
|
readingTime: Int
|
||||||
|
complexity: Float
|
||||||
|
sentiment: Float
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
work: Work!
|
work: Work!
|
||||||
@ -297,7 +305,12 @@ type WorkStats {
|
|||||||
|
|
||||||
type TranslationStats {
|
type TranslationStats {
|
||||||
id: ID!
|
id: ID!
|
||||||
views: Int!
|
views: Int
|
||||||
|
likes: Int
|
||||||
|
comments: Int
|
||||||
|
shares: Int
|
||||||
|
readingTime: Int
|
||||||
|
sentiment: Float
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
updatedAt: String!
|
updatedAt: String!
|
||||||
translation: Translation!
|
translation: Translation!
|
||||||
@ -440,6 +453,8 @@ type Edge {
|
|||||||
|
|
||||||
scalar JSON
|
scalar JSON
|
||||||
|
|
||||||
|
directive @binding(constraint: String!) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION
|
||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
type Query {
|
type Query {
|
||||||
# Work queries
|
# Work queries
|
||||||
@ -517,6 +532,8 @@ type Query {
|
|||||||
offset: Int
|
offset: Int
|
||||||
filters: SearchFilters
|
filters: SearchFilters
|
||||||
): SearchResults!
|
): SearchResults!
|
||||||
|
|
||||||
|
trendingWorks(timePeriod: String, limit: Int): [Work!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
input SearchFilters {
|
input SearchFilters {
|
||||||
@ -539,7 +556,7 @@ type SearchResults {
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
# Authentication
|
# Authentication
|
||||||
register(input: RegisterInput!): AuthPayload!
|
register(input: RegisterInput!): AuthPayload!
|
||||||
login(email: String!, password: String!): AuthPayload!
|
login(input: LoginInput!): AuthPayload!
|
||||||
|
|
||||||
# Work mutations
|
# Work mutations
|
||||||
createWork(input: WorkInput!): Work!
|
createWork(input: WorkInput!): Work!
|
||||||
@ -600,6 +617,11 @@ type Mutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Input types
|
# Input types
|
||||||
|
input LoginInput {
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
}
|
||||||
|
|
||||||
input RegisterInput {
|
input RegisterInput {
|
||||||
username: String!
|
username: String!
|
||||||
email: String!
|
email: String!
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package graphql
|
|||||||
|
|
||||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
// This file will be automatically regenerated based on the schema, any resolver implementations
|
||||||
// will be copied through when generating and any unknown code will be moved to the end.
|
// will be copied through when generating and any unknown code will be moved to the end.
|
||||||
// Code generated by github.com/99designs/gqlgen version v0.17.72
|
// Code generated by github.com/99designs/gqlgen version v0.17.78
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"tercul/internal/adapters/graphql/model"
|
"tercul/internal/adapters/graphql/model"
|
||||||
"tercul/internal/app/auth"
|
"tercul/internal/app/auth"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
platform_auth "tercul/internal/platform/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register is the resolver for the register field.
|
// Register is the resolver for the register field.
|
||||||
@ -49,11 +50,11 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login is the resolver for the login field.
|
// Login is the resolver for the login field.
|
||||||
func (r *mutationResolver) Login(ctx context.Context, email string, password string) (*model.AuthPayload, error) {
|
func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*model.AuthPayload, error) {
|
||||||
// Convert GraphQL input to service input
|
// Convert GraphQL input to service input
|
||||||
loginInput := auth.LoginInput{
|
loginInput := auth.LoginInput{
|
||||||
Email: email,
|
Email: input.Email,
|
||||||
Password: password,
|
Password: input.Password,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call auth service
|
// Call auth service
|
||||||
@ -81,6 +82,9 @@ func (r *mutationResolver) Login(ctx context.Context, email string, password str
|
|||||||
|
|
||||||
// CreateWork is the resolver for the createWork field.
|
// CreateWork is the resolver for the createWork field.
|
||||||
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput) (*model.Work, error) {
|
||||||
|
if err := validateWorkInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
// Create domain model
|
// Create domain model
|
||||||
work := &domain.Work{
|
work := &domain.Work{
|
||||||
Title: input.Name,
|
Title: input.Name,
|
||||||
@ -130,42 +134,221 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
|
|||||||
|
|
||||||
// UpdateWork is the resolver for the updateWork field.
|
// UpdateWork is the resolver for the updateWork field.
|
||||||
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
|
func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input model.WorkInput) (*model.Work, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateWork - updateWork"))
|
if err := validateWorkInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
work := &domain.Work{
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
BaseModel: domain.BaseModel{ID: uint(workID)},
|
||||||
|
Language: input.Language,
|
||||||
|
},
|
||||||
|
Title: input.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call work service
|
||||||
|
err = r.App.WorkCommands.UpdateWork(ctx, work)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Work{
|
||||||
|
ID: id,
|
||||||
|
Name: work.Title,
|
||||||
|
Language: work.Language,
|
||||||
|
Content: input.Content,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWork is the resolver for the deleteWork field.
|
// DeleteWork is the resolver for the deleteWork field.
|
||||||
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteWork - deleteWork"))
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.App.WorkCommands.DeleteWork(ctx, uint(workID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTranslation is the resolver for the createTranslation field.
|
// CreateTranslation is the resolver for the createTranslation field.
|
||||||
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
|
func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.TranslationInput) (*model.Translation, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateTranslation - createTranslation"))
|
if err := validateTranslationInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
translation := &domain.Translation{
|
||||||
|
Title: input.Name,
|
||||||
|
Language: input.Language,
|
||||||
|
TranslatableID: uint(workID),
|
||||||
|
TranslatableType: "Work",
|
||||||
|
}
|
||||||
|
if input.Content != nil {
|
||||||
|
translation.Content = *input.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call translation service
|
||||||
|
err = r.App.TranslationRepo.Create(ctx, translation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Translation{
|
||||||
|
ID: fmt.Sprintf("%d", translation.ID),
|
||||||
|
Name: translation.Title,
|
||||||
|
Language: translation.Language,
|
||||||
|
Content: &translation.Content,
|
||||||
|
WorkID: input.WorkID,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTranslation is the resolver for the updateTranslation field.
|
// UpdateTranslation is the resolver for the updateTranslation field.
|
||||||
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
|
func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, input model.TranslationInput) (*model.Translation, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateTranslation - updateTranslation"))
|
if err := validateTranslationInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
|
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
translation := &domain.Translation{
|
||||||
|
BaseModel: domain.BaseModel{ID: uint(translationID)},
|
||||||
|
Title: input.Name,
|
||||||
|
Language: input.Language,
|
||||||
|
TranslatableID: uint(workID),
|
||||||
|
TranslatableType: "Work",
|
||||||
|
}
|
||||||
|
if input.Content != nil {
|
||||||
|
translation.Content = *input.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call translation service
|
||||||
|
err = r.App.TranslationRepo.Update(ctx, translation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Translation{
|
||||||
|
ID: id,
|
||||||
|
Name: translation.Title,
|
||||||
|
Language: translation.Language,
|
||||||
|
Content: &translation.Content,
|
||||||
|
WorkID: input.WorkID,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTranslation is the resolver for the deleteTranslation field.
|
// DeleteTranslation is the resolver for the deleteTranslation field.
|
||||||
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteTranslation - deleteTranslation"))
|
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.App.TranslationRepo.Delete(ctx, uint(translationID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAuthor is the resolver for the createAuthor field.
|
// CreateAuthor is the resolver for the createAuthor field.
|
||||||
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
|
func (r *mutationResolver) CreateAuthor(ctx context.Context, input model.AuthorInput) (*model.Author, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateAuthor - createAuthor"))
|
if err := validateAuthorInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
|
// Create domain model
|
||||||
|
author := &domain.Author{
|
||||||
|
Name: input.Name,
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
Language: input.Language,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call author service
|
||||||
|
err := r.App.AuthorRepo.Create(ctx, author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Author{
|
||||||
|
ID: fmt.Sprintf("%d", author.ID),
|
||||||
|
Name: author.Name,
|
||||||
|
Language: author.Language,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAuthor is the resolver for the updateAuthor field.
|
// UpdateAuthor is the resolver for the updateAuthor field.
|
||||||
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
|
func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input model.AuthorInput) (*model.Author, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateAuthor - updateAuthor"))
|
if err := validateAuthorInput(input); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrValidation, err)
|
||||||
|
}
|
||||||
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid author ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
author := &domain.Author{
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
BaseModel: domain.BaseModel{ID: uint(authorID)},
|
||||||
|
Language: input.Language,
|
||||||
|
},
|
||||||
|
Name: input.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call author service
|
||||||
|
err = r.App.AuthorRepo.Update(ctx, author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Author{
|
||||||
|
ID: id,
|
||||||
|
Name: author.Name,
|
||||||
|
Language: author.Language,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAuthor is the resolver for the deleteAuthor field.
|
// DeleteAuthor is the resolver for the deleteAuthor field.
|
||||||
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteAuthor - deleteAuthor"))
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid author ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.App.AuthorRepo.Delete(ctx, uint(authorID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser is the resolver for the updateUser field.
|
// UpdateUser is the resolver for the updateUser field.
|
||||||
@ -180,62 +363,560 @@ func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, err
|
|||||||
|
|
||||||
// CreateCollection is the resolver for the createCollection field.
|
// CreateCollection is the resolver for the createCollection field.
|
||||||
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
|
func (r *mutationResolver) CreateCollection(ctx context.Context, input model.CollectionInput) (*model.Collection, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateCollection - createCollection"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
collection := &domain.Collection{
|
||||||
|
Name: input.Name,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
if input.Description != nil {
|
||||||
|
collection.Description = *input.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call collection repository
|
||||||
|
err := r.App.CollectionRepo.Create(ctx, collection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Collection{
|
||||||
|
ID: fmt.Sprintf("%d", collection.ID),
|
||||||
|
Name: collection.Name,
|
||||||
|
Description: &collection.Description,
|
||||||
|
User: &model.User{
|
||||||
|
ID: fmt.Sprintf("%d", userID),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCollection is the resolver for the updateCollection field.
|
// UpdateCollection is the resolver for the updateCollection field.
|
||||||
func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, input model.CollectionInput) (*model.Collection, error) {
|
func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, input model.CollectionInput) (*model.Collection, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateCollection - updateCollection"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse collection ID
|
||||||
|
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing collection
|
||||||
|
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collectionID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if collection == nil {
|
||||||
|
return nil, fmt.Errorf("collection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if collection.UserID != userID {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
collection.Name = input.Name
|
||||||
|
if input.Description != nil {
|
||||||
|
collection.Description = *input.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call collection repository
|
||||||
|
err = r.App.CollectionRepo.Update(ctx, collection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Collection{
|
||||||
|
ID: id,
|
||||||
|
Name: collection.Name,
|
||||||
|
Description: &collection.Description,
|
||||||
|
User: &model.User{
|
||||||
|
ID: fmt.Sprintf("%d", userID),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCollection is the resolver for the deleteCollection field.
|
// DeleteCollection is the resolver for the deleteCollection field.
|
||||||
func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteCollection - deleteCollection"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse collection ID
|
||||||
|
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing collection
|
||||||
|
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collectionID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if collection == nil {
|
||||||
|
return false, fmt.Errorf("collection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if collection.UserID != userID {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call collection repository
|
||||||
|
err = r.App.CollectionRepo.Delete(ctx, uint(collectionID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWorkToCollection is the resolver for the addWorkToCollection field.
|
// AddWorkToCollection is the resolver for the addWorkToCollection field.
|
||||||
func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
|
func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
|
||||||
panic(fmt.Errorf("not implemented: AddWorkToCollection - addWorkToCollection"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse IDs
|
||||||
|
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
|
}
|
||||||
|
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing collection
|
||||||
|
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if collection == nil {
|
||||||
|
return nil, fmt.Errorf("collection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if collection.UserID != userID {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add work to collection
|
||||||
|
err = r.App.CollectionRepo.AddWorkToCollection(ctx, uint(collID), uint(wID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the updated collection to return it
|
||||||
|
updatedCollection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Collection{
|
||||||
|
ID: collectionID,
|
||||||
|
Name: updatedCollection.Name,
|
||||||
|
Description: &updatedCollection.Description,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollection is the resolver for the removeWorkFromCollection field.
|
// RemoveWorkFromCollection is the resolver for the removeWorkFromCollection field.
|
||||||
func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
|
func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collectionID string, workID string) (*model.Collection, error) {
|
||||||
panic(fmt.Errorf("not implemented: RemoveWorkFromCollection - removeWorkFromCollection"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse IDs
|
||||||
|
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
|
}
|
||||||
|
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing collection
|
||||||
|
collection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if collection == nil {
|
||||||
|
return nil, fmt.Errorf("collection not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if collection.UserID != userID {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove work from collection
|
||||||
|
err = r.App.CollectionRepo.RemoveWorkFromCollection(ctx, uint(collID), uint(wID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the updated collection to return it
|
||||||
|
updatedCollection, err := r.App.CollectionRepo.GetByID(ctx, uint(collID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Collection{
|
||||||
|
ID: collectionID,
|
||||||
|
Name: updatedCollection.Name,
|
||||||
|
Description: &updatedCollection.Description,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment is the resolver for the createComment field.
|
// CreateComment is the resolver for the createComment field.
|
||||||
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CommentInput) (*model.Comment, error) {
|
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CommentInput) (*model.Comment, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateComment - createComment"))
|
// Custom validation
|
||||||
|
if (input.WorkID == nil && input.TranslationID == nil) || (input.WorkID != nil && input.TranslationID != nil) {
|
||||||
|
return nil, fmt.Errorf("must provide either workId or translationId, but not both")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
comment := &domain.Comment{
|
||||||
|
Text: input.Text,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
if input.WorkID != nil {
|
||||||
|
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
wID := uint(workID)
|
||||||
|
comment.WorkID = &wID
|
||||||
|
}
|
||||||
|
if input.TranslationID != nil {
|
||||||
|
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
|
}
|
||||||
|
tID := uint(translationID)
|
||||||
|
comment.TranslationID = &tID
|
||||||
|
}
|
||||||
|
if input.ParentCommentID != nil {
|
||||||
|
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid parent comment ID: %v", err)
|
||||||
|
}
|
||||||
|
pID := uint(parentCommentID)
|
||||||
|
comment.ParentID = &pID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call comment repository
|
||||||
|
err := r.App.CommentRepo.Create(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment analytics
|
||||||
|
if comment.WorkID != nil {
|
||||||
|
r.App.AnalyticsService.IncrementWorkComments(ctx, *comment.WorkID)
|
||||||
|
}
|
||||||
|
if comment.TranslationID != nil {
|
||||||
|
r.App.AnalyticsService.IncrementTranslationComments(ctx, *comment.TranslationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Comment{
|
||||||
|
ID: fmt.Sprintf("%d", comment.ID),
|
||||||
|
Text: comment.Text,
|
||||||
|
User: &model.User{
|
||||||
|
ID: fmt.Sprintf("%d", userID),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateComment is the resolver for the updateComment field.
|
// UpdateComment is the resolver for the updateComment field.
|
||||||
func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input model.CommentInput) (*model.Comment, error) {
|
func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input model.CommentInput) (*model.Comment, error) {
|
||||||
panic(fmt.Errorf("not implemented: UpdateComment - updateComment"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse comment ID
|
||||||
|
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing comment
|
||||||
|
comment, err := r.App.CommentRepo.GetByID(ctx, uint(commentID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if comment == nil {
|
||||||
|
return nil, fmt.Errorf("comment not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if comment.UserID != userID {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
comment.Text = input.Text
|
||||||
|
|
||||||
|
// Call comment repository
|
||||||
|
err = r.App.CommentRepo.Update(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Comment{
|
||||||
|
ID: id,
|
||||||
|
Text: comment.Text,
|
||||||
|
User: &model.User{
|
||||||
|
ID: fmt.Sprintf("%d", userID),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteComment is the resolver for the deleteComment field.
|
// DeleteComment is the resolver for the deleteComment field.
|
||||||
func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteComment - deleteComment"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse comment ID
|
||||||
|
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid comment ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing comment
|
||||||
|
comment, err := r.App.CommentRepo.GetByID(ctx, uint(commentID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if comment == nil {
|
||||||
|
return false, fmt.Errorf("comment not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if comment.UserID != userID {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call comment repository
|
||||||
|
err = r.App.CommentRepo.Delete(ctx, uint(commentID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLike is the resolver for the createLike field.
|
// CreateLike is the resolver for the createLike field.
|
||||||
func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput) (*model.Like, error) {
|
func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput) (*model.Like, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateLike - createLike"))
|
// Custom validation
|
||||||
|
if (input.WorkID == nil && input.TranslationID == nil && input.CommentID == nil) ||
|
||||||
|
(input.WorkID != nil && input.TranslationID != nil) ||
|
||||||
|
(input.WorkID != nil && input.CommentID != nil) ||
|
||||||
|
(input.TranslationID != nil && input.CommentID != nil) {
|
||||||
|
return nil, fmt.Errorf("must provide exactly one of workId, translationId, or commentId")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
like := &domain.Like{
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
if input.WorkID != nil {
|
||||||
|
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
wID := uint(workID)
|
||||||
|
like.WorkID = &wID
|
||||||
|
}
|
||||||
|
if input.TranslationID != nil {
|
||||||
|
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
|
}
|
||||||
|
tID := uint(translationID)
|
||||||
|
like.TranslationID = &tID
|
||||||
|
}
|
||||||
|
if input.CommentID != nil {
|
||||||
|
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||||
|
}
|
||||||
|
cID := uint(commentID)
|
||||||
|
like.CommentID = &cID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call like repository
|
||||||
|
err := r.App.LikeRepo.Create(ctx, like)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment analytics
|
||||||
|
if like.WorkID != nil {
|
||||||
|
r.App.AnalyticsService.IncrementWorkLikes(ctx, *like.WorkID)
|
||||||
|
}
|
||||||
|
if like.TranslationID != nil {
|
||||||
|
r.App.AnalyticsService.IncrementTranslationLikes(ctx, *like.TranslationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Like{
|
||||||
|
ID: fmt.Sprintf("%d", like.ID),
|
||||||
|
User: &model.User{ID: fmt.Sprintf("%d", userID)},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteLike is the resolver for the deleteLike field.
|
// DeleteLike is the resolver for the deleteLike field.
|
||||||
func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteLike - deleteLike"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse like ID
|
||||||
|
likeID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid like ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing like
|
||||||
|
like, err := r.App.LikeRepo.GetByID(ctx, uint(likeID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if like == nil {
|
||||||
|
return false, fmt.Errorf("like not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if like.UserID != userID {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call like repository
|
||||||
|
err = r.App.LikeRepo.Delete(ctx, uint(likeID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBookmark is the resolver for the createBookmark field.
|
// CreateBookmark is the resolver for the createBookmark field.
|
||||||
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.BookmarkInput) (*model.Bookmark, error) {
|
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.BookmarkInput) (*model.Bookmark, error) {
|
||||||
panic(fmt.Errorf("not implemented: CreateBookmark - createBookmark"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse work ID
|
||||||
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create domain model
|
||||||
|
bookmark := &domain.Bookmark{
|
||||||
|
UserID: userID,
|
||||||
|
WorkID: uint(workID),
|
||||||
|
}
|
||||||
|
if input.Name != nil {
|
||||||
|
bookmark.Name = *input.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call bookmark repository
|
||||||
|
err = r.App.BookmarkRepo.Create(ctx, bookmark)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment analytics
|
||||||
|
r.App.AnalyticsService.IncrementWorkBookmarks(ctx, uint(workID))
|
||||||
|
|
||||||
|
// Convert to GraphQL model
|
||||||
|
return &model.Bookmark{
|
||||||
|
ID: fmt.Sprintf("%d", bookmark.ID),
|
||||||
|
Name: &bookmark.Name,
|
||||||
|
User: &model.User{ID: fmt.Sprintf("%d", userID)},
|
||||||
|
Work: &model.Work{ID: fmt.Sprintf("%d", workID)},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBookmark is the resolver for the deleteBookmark field.
|
// DeleteBookmark is the resolver for the deleteBookmark field.
|
||||||
func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool, error) {
|
||||||
panic(fmt.Errorf("not implemented: DeleteBookmark - deleteBookmark"))
|
// Get user ID from context
|
||||||
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse bookmark ID
|
||||||
|
bookmarkID, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("invalid bookmark ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the existing bookmark
|
||||||
|
bookmark, err := r.App.BookmarkRepo.GetByID(ctx, uint(bookmarkID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if bookmark == nil {
|
||||||
|
return false, fmt.Errorf("bookmark not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ownership
|
||||||
|
if bookmark.UserID != userID {
|
||||||
|
return false, fmt.Errorf("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call bookmark repository
|
||||||
|
err = r.App.BookmarkRepo.Delete(ctx, uint(bookmarkID))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateContribution is the resolver for the createContribution field.
|
// CreateContribution is the resolver for the createContribution field.
|
||||||
@ -609,6 +1290,35 @@ func (r *queryResolver) Search(ctx context.Context, query string, limit *int32,
|
|||||||
panic(fmt.Errorf("not implemented: Search - search"))
|
panic(fmt.Errorf("not implemented: Search - search"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TrendingWorks is the resolver for the trendingWorks field.
|
||||||
|
func (r *queryResolver) TrendingWorks(ctx context.Context, timePeriod *string, limit *int32) ([]*model.Work, error) {
|
||||||
|
tp := "daily"
|
||||||
|
if timePeriod != nil {
|
||||||
|
tp = *timePeriod
|
||||||
|
}
|
||||||
|
|
||||||
|
l := 10
|
||||||
|
if limit != nil {
|
||||||
|
l = int(*limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
works, err := r.App.AnalyticsService.GetTrendingWorks(ctx, tp, l)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*model.Work
|
||||||
|
for _, w := range works {
|
||||||
|
result = append(result, &model.Work{
|
||||||
|
ID: fmt.Sprintf("%d", w.ID),
|
||||||
|
Name: w.Title,
|
||||||
|
Language: w.Language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Mutation returns MutationResolver implementation.
|
// Mutation returns MutationResolver implementation.
|
||||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
@ -617,3 +1327,70 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
|||||||
|
|
||||||
type mutationResolver struct{ *Resolver }
|
type mutationResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
|
|
||||||
|
// !!! WARNING !!!
|
||||||
|
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||||
|
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||||
|
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||||
|
// it when you're done.
|
||||||
|
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||||
|
/*
|
||||||
|
func (r *Resolver) Work() WorkResolver { return &workResolver{r} }
|
||||||
|
func (r *Resolver) Translation() TranslationResolver { return &translationResolver{r} }
|
||||||
|
type workResolver struct{ *Resolver }
|
||||||
|
type translationResolver struct{ *Resolver }
|
||||||
|
func toInt32(i int64) *int {
|
||||||
|
val := int(i)
|
||||||
|
return &val
|
||||||
|
}
|
||||||
|
func toInt(i int) *int {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
func (r *workResolver) Stats(ctx context.Context, obj *model.Work) (*model.WorkStats, error) {
|
||||||
|
workID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := r.App.AnalyticsService.GetOrCreateWorkStats(ctx, uint(workID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert domain model to GraphQL model
|
||||||
|
return &model.WorkStats{
|
||||||
|
ID: fmt.Sprintf("%d", stats.ID),
|
||||||
|
Views: toInt32(stats.Views),
|
||||||
|
Likes: toInt32(stats.Likes),
|
||||||
|
Comments: toInt32(stats.Comments),
|
||||||
|
Bookmarks: toInt32(stats.Bookmarks),
|
||||||
|
Shares: toInt32(stats.Shares),
|
||||||
|
TranslationCount: toInt32(stats.TranslationCount),
|
||||||
|
ReadingTime: toInt(stats.ReadingTime),
|
||||||
|
Complexity: &stats.Complexity,
|
||||||
|
Sentiment: &stats.Sentiment,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (r *translationResolver) Stats(ctx context.Context, obj *model.Translation) (*model.TranslationStats, error) {
|
||||||
|
translationID, err := strconv.ParseUint(obj.ID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := r.App.AnalyticsService.GetOrCreateTranslationStats(ctx, uint(translationID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert domain model to GraphQL model
|
||||||
|
return &model.TranslationStats{
|
||||||
|
ID: fmt.Sprintf("%d", stats.ID),
|
||||||
|
Views: toInt32(stats.Views),
|
||||||
|
Likes: toInt32(stats.Likes),
|
||||||
|
Comments: toInt32(stats.Comments),
|
||||||
|
Shares: toInt32(stats.Shares),
|
||||||
|
ReadingTime: toInt(stats.ReadingTime),
|
||||||
|
Sentiment: &stats.Sentiment,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
57
internal/adapters/graphql/validation.go
Normal file
57
internal/adapters/graphql/validation.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package graphql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"tercul/internal/adapters/graphql/model"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrValidation = errors.New("validation failed")
|
||||||
|
|
||||||
|
func validateWorkInput(input model.WorkInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAuthorInput(input model.AuthorInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTranslationInput(input model.TranslationInput) error {
|
||||||
|
name := strings.TrimSpace(input.Name)
|
||||||
|
if len(name) < 3 {
|
||||||
|
return fmt.Errorf("name must be at least 3 characters long")
|
||||||
|
}
|
||||||
|
if !govalidator.Matches(name, `^[a-zA-Z0-9\s]+$`) {
|
||||||
|
return fmt.Errorf("name can only contain letters, numbers, and spaces")
|
||||||
|
}
|
||||||
|
if len(input.Language) != 2 {
|
||||||
|
return fmt.Errorf("language must be a 2-character code")
|
||||||
|
}
|
||||||
|
if input.WorkID == "" {
|
||||||
|
return fmt.Errorf("workId is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
301
internal/app/analytics/service.go
Normal file
301
internal/app/analytics/service.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package analytics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
IncrementWorkViews(ctx context.Context, workID uint) error
|
||||||
|
IncrementWorkLikes(ctx context.Context, workID uint) error
|
||||||
|
IncrementWorkComments(ctx context.Context, workID uint) error
|
||||||
|
IncrementWorkBookmarks(ctx context.Context, workID uint) error
|
||||||
|
IncrementWorkShares(ctx context.Context, workID uint) error
|
||||||
|
IncrementWorkTranslationCount(ctx context.Context, workID uint) error
|
||||||
|
IncrementTranslationViews(ctx context.Context, translationID uint) error
|
||||||
|
IncrementTranslationLikes(ctx context.Context, translationID uint) error
|
||||||
|
IncrementTranslationComments(ctx context.Context, translationID uint) error
|
||||||
|
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
||||||
|
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||||
|
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
||||||
|
|
||||||
|
UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
||||||
|
UpdateWorkComplexity(ctx context.Context, workID uint) error
|
||||||
|
UpdateWorkSentiment(ctx context.Context, workID uint) error
|
||||||
|
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error
|
||||||
|
UpdateTranslationSentiment(ctx context.Context, translationID uint) error
|
||||||
|
|
||||||
|
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
|
||||||
|
UpdateTrending(ctx context.Context) error
|
||||||
|
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.AnalyticsRepository
|
||||||
|
analysisRepo linguistics.AnalysisRepository
|
||||||
|
translationRepo domain.TranslationRepository
|
||||||
|
workRepo domain.WorkRepository
|
||||||
|
sentimentProvider linguistics.SentimentProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.AnalyticsRepository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, workRepo domain.WorkRepository, sentimentProvider linguistics.SentimentProvider) Service {
|
||||||
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
analysisRepo: analysisRepo,
|
||||||
|
translationRepo: translationRepo,
|
||||||
|
workRepo: workRepo,
|
||||||
|
sentimentProvider: sentimentProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||||
|
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||||
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||||
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||||
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||||
|
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
|
return s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||||
|
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
textMetadata, _, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if textMetadata == nil {
|
||||||
|
return errors.New("text metadata not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
readingTime := 0
|
||||||
|
if textMetadata.WordCount > 0 {
|
||||||
|
readingTime = (textMetadata.WordCount + 199) / 200 // Ceil division
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.ReadingTime = readingTime
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, readabilityScore, _, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("could not get readability score for work", log.F("workID", workID), log.F("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if readabilityScore == nil {
|
||||||
|
return errors.New("readability score not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Complexity = readabilityScore.Score
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, languageAnalysis, err := s.analysisRepo.GetAnalysisData(ctx, workID)
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("could not get language analysis for work", log.F("workID", workID), log.F("error", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if languageAnalysis == nil {
|
||||||
|
return errors.New("language analysis not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
sentiment, ok := languageAnalysis.Analysis["sentiment"].(float64)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("sentiment score not found in language analysis")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Sentiment = sentiment
|
||||||
|
|
||||||
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
translation, err := s.translationRepo.GetByID(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if translation == nil {
|
||||||
|
return errors.New("translation not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
wordCount := len(strings.Fields(translation.Content))
|
||||||
|
readingTime := 0
|
||||||
|
if wordCount > 0 {
|
||||||
|
readingTime = (wordCount + 199) / 200 // Ceil division
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.ReadingTime = readingTime
|
||||||
|
|
||||||
|
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||||
|
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
translation, err := s.translationRepo.GetByID(ctx, translationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if translation == nil {
|
||||||
|
return errors.New("translation not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
sentiment, err := s.sentimentProvider.Score(translation.Content, translation.Language)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Sentiment = sentiment
|
||||||
|
|
||||||
|
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||||
|
today := time.Now().UTC().Truncate(24 * time.Hour)
|
||||||
|
engagement, err := s.repo.GetOrCreateUserEngagement(ctx, userID, today)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch eventType {
|
||||||
|
case "work_read":
|
||||||
|
engagement.WorksRead++
|
||||||
|
case "comment_made":
|
||||||
|
engagement.CommentsMade++
|
||||||
|
case "like_given":
|
||||||
|
engagement.LikesGiven++
|
||||||
|
case "bookmark_made":
|
||||||
|
engagement.BookmarksMade++
|
||||||
|
case "translation_made":
|
||||||
|
engagement.TranslationsMade++
|
||||||
|
default:
|
||||||
|
return errors.New("invalid engagement event type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.UpdateUserEngagement(ctx, engagement)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||||
|
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) UpdateTrending(ctx context.Context) error {
|
||||||
|
log.LogInfo("Updating trending works")
|
||||||
|
|
||||||
|
works, err := s.workRepo.ListAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list works: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
trendingWorks := make([]*domain.Trending, 0, len(works))
|
||||||
|
for _, work := range works {
|
||||||
|
stats, err := s.repo.GetOrCreateWorkStats(ctx, work.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.LogWarn("failed to get work stats", log.F("workID", work.ID), log.F("error", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
score := float64(stats.Views*1 + stats.Likes*2 + stats.Comments*3)
|
||||||
|
|
||||||
|
trendingWorks = append(trendingWorks, &domain.Trending{
|
||||||
|
EntityType: "Work",
|
||||||
|
EntityID: work.ID,
|
||||||
|
Score: score,
|
||||||
|
TimePeriod: "daily", // Hardcoded for now
|
||||||
|
Date: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score
|
||||||
|
sort.Slice(trendingWorks, func(i, j int) bool {
|
||||||
|
return trendingWorks[i].Score > trendingWorks[j].Score
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get top 10
|
||||||
|
if len(trendingWorks) > 10 {
|
||||||
|
trendingWorks = trendingWorks[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ranks
|
||||||
|
for i := range trendingWorks {
|
||||||
|
trendingWorks[i].Rank = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.UpdateTrendingWorks(ctx, "daily", trendingWorks)
|
||||||
|
}
|
||||||
260
internal/app/analytics/service_test.go
Normal file
260
internal/app/analytics/service_test.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package analytics_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/app/analytics"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnalyticsServiceTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
service analytics.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
analyticsRepo := sql.NewAnalyticsRepository(s.DB)
|
||||||
|
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
|
||||||
|
translationRepo := sql.NewTranslationRepository(s.DB)
|
||||||
|
workRepo := sql.NewWorkRepository(s.DB)
|
||||||
|
sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider()
|
||||||
|
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) SetupTest() {
|
||||||
|
s.IntegrationTestSuite.SetupTest()
|
||||||
|
s.DB.Exec("DELETE FROM trendings")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkViews() {
|
||||||
|
s.Run("should increment the view count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkViews(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.Views)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkLikes() {
|
||||||
|
s.Run("should increment the like count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkLikes(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.Likes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkComments() {
|
||||||
|
s.Run("should increment the comment count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkComments(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.Comments)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkBookmarks() {
|
||||||
|
s.Run("should increment the bookmark count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkBookmarks(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.Bookmarks)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkShares() {
|
||||||
|
s.Run("should increment the share count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkShares(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.Shares)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestIncrementWorkTranslationCount() {
|
||||||
|
s.Run("should increment the translation count for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.IncrementWorkTranslationCount(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(1), stats.TranslationCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkReadingTime() {
|
||||||
|
s.Run("should update the reading time for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||||
|
textMetadata := &domain.TextMetadata{
|
||||||
|
WorkID: work.ID,
|
||||||
|
WordCount: 1000,
|
||||||
|
}
|
||||||
|
s.DB.Create(textMetadata)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkReadingTime(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(5, stats.ReadingTime) // 1000 words / 200 wpm = 5 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationReadingTime() {
|
||||||
|
s.Run("should update the reading time for a translation", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
translation := s.CreateTestTranslation(work.ID, "es", strings.Repeat("Contenido de prueba con quinientas palabras. ", 100))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateTranslationReadingTime(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(3, stats.ReadingTime) // 500 words / 200 wpm = 2.5 -> 3 minutes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkComplexity() {
|
||||||
|
s.Run("should update the complexity for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||||
|
readabilityScore := &domain.ReadabilityScore{
|
||||||
|
WorkID: work.ID,
|
||||||
|
Score: 12.34,
|
||||||
|
}
|
||||||
|
s.DB.Create(readabilityScore)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkComplexity(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(12.34, stats.Complexity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateWorkSentiment() {
|
||||||
|
s.Run("should update the sentiment for a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||||
|
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||||
|
languageAnalysis := &domain.LanguageAnalysis{
|
||||||
|
WorkID: work.ID,
|
||||||
|
Analysis: domain.JSONB{
|
||||||
|
"sentiment": 0.5678,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.DB.Create(languageAnalysis)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateWorkSentiment(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(0.5678, stats.Sentiment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationSentiment() {
|
||||||
|
s.Run("should update the sentiment for a translation", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
translation := s.CreateTestTranslation(work.ID, "en", "This is a wonderfully positive and uplifting sentence.")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateTranslationSentiment(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.True(stats.Sentiment > 0.5)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
|
||||||
|
s.Run("should update the trending works", func() {
|
||||||
|
// Arrange
|
||||||
|
work1 := s.CreateTestWork("Work 1", "en", "content")
|
||||||
|
work2 := s.CreateTestWork("Work 2", "en", "content")
|
||||||
|
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
|
||||||
|
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.service.UpdateTrending(context.Background())
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var trendingWorks []*domain.Trending
|
||||||
|
s.DB.Order("rank asc").Find(&trendingWorks)
|
||||||
|
s.Require().Len(trendingWorks, 2)
|
||||||
|
s.Equal(work2.ID, trendingWorks[0].EntityID)
|
||||||
|
s.Equal(work1.ID, trendingWorks[1].EntityID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnalyticsService(t *testing.T) {
|
||||||
|
suite.Run(t, new(AnalyticsServiceTestSuite))
|
||||||
|
}
|
||||||
@ -1,29 +1,68 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"tercul/internal/app/auth"
|
"tercul/internal/app/author"
|
||||||
"tercul/internal/app/copyright"
|
"tercul/internal/app/bookmark"
|
||||||
|
"tercul/internal/app/category"
|
||||||
|
"tercul/internal/app/collection"
|
||||||
|
"tercul/internal/app/comment"
|
||||||
|
"tercul/internal/app/like"
|
||||||
|
"tercul/internal/app/tag"
|
||||||
|
"tercul/internal/app/translation"
|
||||||
|
"tercul/internal/app/user"
|
||||||
"tercul/internal/app/localization"
|
"tercul/internal/app/localization"
|
||||||
"tercul/internal/app/search"
|
"tercul/internal/app/auth"
|
||||||
"tercul/internal/app/work"
|
"tercul/internal/app/work"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
platform_auth "tercul/internal/platform/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Application is a container for all the application-layer services.
|
// Application is a container for all the application-layer services.
|
||||||
// It's used for dependency injection into the presentation layer (e.g., GraphQL resolvers).
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
AuthCommands *auth.AuthCommands
|
Author *author.Service
|
||||||
AuthQueries *auth.AuthQueries
|
Bookmark *bookmark.Service
|
||||||
CopyrightCommands *copyright.CopyrightCommands
|
Category *category.Service
|
||||||
CopyrightQueries *copyright.CopyrightQueries
|
Collection *collection.Service
|
||||||
Localization localization.Service
|
Comment *comment.Service
|
||||||
Search search.IndexService
|
Like *like.Service
|
||||||
WorkCommands *work.WorkCommands
|
Tag *tag.Service
|
||||||
WorkQueries *work.WorkQueries
|
Translation *translation.Service
|
||||||
|
User *user.Service
|
||||||
// Repositories - to be refactored into app services
|
Localization *localization.Service
|
||||||
AuthorRepo domain.AuthorRepository
|
Auth *auth.Service
|
||||||
UserRepo domain.UserRepository
|
Work *work.Service
|
||||||
TagRepo domain.TagRepository
|
Repos *sql.Repositories
|
||||||
CategoryRepo domain.CategoryRepository
|
}
|
||||||
|
|
||||||
|
func NewApplication(repos *sql.Repositories, searchClient domain.SearchClient) *Application {
|
||||||
|
jwtManager := platform_auth.NewJWTManager()
|
||||||
|
authorService := author.NewService(repos.Author)
|
||||||
|
bookmarkService := bookmark.NewService(repos.Bookmark)
|
||||||
|
categoryService := category.NewService(repos.Category)
|
||||||
|
collectionService := collection.NewService(repos.Collection)
|
||||||
|
commentService := comment.NewService(repos.Comment)
|
||||||
|
likeService := like.NewService(repos.Like)
|
||||||
|
tagService := tag.NewService(repos.Tag)
|
||||||
|
translationService := translation.NewService(repos.Translation)
|
||||||
|
userService := user.NewService(repos.User)
|
||||||
|
localizationService := localization.NewService(repos.Localization)
|
||||||
|
authService := auth.NewService(repos.User, jwtManager)
|
||||||
|
workService := work.NewService(repos.Work, searchClient)
|
||||||
|
|
||||||
|
return &Application{
|
||||||
|
Author: authorService,
|
||||||
|
Bookmark: bookmarkService,
|
||||||
|
Category: categoryService,
|
||||||
|
Collection: collectionService,
|
||||||
|
Comment: commentService,
|
||||||
|
Like: likeService,
|
||||||
|
Tag: tagService,
|
||||||
|
Translation: translationService,
|
||||||
|
User: userService,
|
||||||
|
Localization: localizationService,
|
||||||
|
Auth: authService,
|
||||||
|
Work: workService,
|
||||||
|
Repos: repos,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,194 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"tercul/internal/app/auth"
|
|
||||||
"tercul/internal/app/copyright"
|
|
||||||
"tercul/internal/app/localization"
|
|
||||||
"tercul/internal/app/search"
|
|
||||||
"tercul/internal/app/work"
|
|
||||||
"tercul/internal/data/sql"
|
|
||||||
"tercul/internal/platform/cache"
|
|
||||||
"tercul/internal/platform/config"
|
|
||||||
"tercul/internal/platform/db"
|
|
||||||
"tercul/internal/platform/log"
|
|
||||||
auth_platform "tercul/internal/platform/auth"
|
|
||||||
"tercul/internal/jobs/linguistics"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
"github.com/weaviate/weaviate-go-client/v5/weaviate"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ApplicationBuilder handles the initialization of all application components
|
|
||||||
type ApplicationBuilder struct {
|
|
||||||
dbConn *gorm.DB
|
|
||||||
redisCache cache.Cache
|
|
||||||
weaviateClient *weaviate.Client
|
|
||||||
asynqClient *asynq.Client
|
|
||||||
App *Application
|
|
||||||
linguistics *linguistics.LinguisticsFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewApplicationBuilder creates a new ApplicationBuilder
|
|
||||||
func NewApplicationBuilder() *ApplicationBuilder {
|
|
||||||
return &ApplicationBuilder{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildDatabase initializes the database connection
|
|
||||||
func (b *ApplicationBuilder) BuildDatabase() error {
|
|
||||||
log.LogInfo("Initializing database connection")
|
|
||||||
dbConn, err := db.InitDB()
|
|
||||||
if err != nil {
|
|
||||||
log.LogFatal("Failed to initialize database", log.F("error", err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.dbConn = dbConn
|
|
||||||
log.LogInfo("Database initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildCache initializes the Redis cache
|
|
||||||
func (b *ApplicationBuilder) BuildCache() error {
|
|
||||||
log.LogInfo("Initializing Redis cache")
|
|
||||||
redisCache, err := cache.NewDefaultRedisCache()
|
|
||||||
if err != nil {
|
|
||||||
log.LogWarn("Failed to initialize Redis cache, continuing without caching", log.F("error", err))
|
|
||||||
} else {
|
|
||||||
b.redisCache = redisCache
|
|
||||||
log.LogInfo("Redis cache initialized successfully")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildWeaviate initializes the Weaviate client
|
|
||||||
func (b *ApplicationBuilder) BuildWeaviate() error {
|
|
||||||
log.LogInfo("Connecting to Weaviate", log.F("host", config.Cfg.WeaviateHost))
|
|
||||||
wClient, err := weaviate.NewClient(weaviate.Config{
|
|
||||||
Scheme: config.Cfg.WeaviateScheme,
|
|
||||||
Host: config.Cfg.WeaviateHost,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.LogFatal("Failed to create Weaviate client", log.F("error", err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
b.weaviateClient = wClient
|
|
||||||
log.LogInfo("Weaviate client initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildBackgroundJobs initializes Asynq for background job processing
|
|
||||||
func (b *ApplicationBuilder) BuildBackgroundJobs() error {
|
|
||||||
log.LogInfo("Setting up background job processing")
|
|
||||||
redisOpt := asynq.RedisClientOpt{
|
|
||||||
Addr: config.Cfg.RedisAddr,
|
|
||||||
Password: config.Cfg.RedisPassword,
|
|
||||||
DB: config.Cfg.RedisDB,
|
|
||||||
}
|
|
||||||
b.asynqClient = asynq.NewClient(redisOpt)
|
|
||||||
log.LogInfo("Background job client initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildLinguistics initializes the linguistics components
|
|
||||||
func (b *ApplicationBuilder) BuildLinguistics() error {
|
|
||||||
log.LogInfo("Initializing linguistic analyzer")
|
|
||||||
b.linguistics = linguistics.NewLinguisticsFactory(b.dbConn, b.redisCache, 4, true)
|
|
||||||
log.LogInfo("Linguistics components initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildApplication initializes all application services
|
|
||||||
func (b *ApplicationBuilder) BuildApplication() error {
|
|
||||||
log.LogInfo("Initializing application layer")
|
|
||||||
|
|
||||||
// Initialize repositories
|
|
||||||
// Note: This is a simplified wiring. In a real app, you might have more complex dependencies.
|
|
||||||
workRepo := sql.NewWorkRepository(b.dbConn)
|
|
||||||
userRepo := sql.NewUserRepository(b.dbConn)
|
|
||||||
// I need to add all the other repos here. For now, I'll just add the ones I need for the services.
|
|
||||||
translationRepo := sql.NewTranslationRepository(b.dbConn)
|
|
||||||
copyrightRepo := sql.NewCopyrightRepository(b.dbConn)
|
|
||||||
authorRepo := sql.NewAuthorRepository(b.dbConn)
|
|
||||||
tagRepo := sql.NewTagRepository(b.dbConn)
|
|
||||||
categoryRepo := sql.NewCategoryRepository(b.dbConn)
|
|
||||||
|
|
||||||
|
|
||||||
// Initialize application services
|
|
||||||
workCommands := work.NewWorkCommands(workRepo, b.linguistics.GetAnalyzer())
|
|
||||||
workQueries := work.NewWorkQueries(workRepo)
|
|
||||||
|
|
||||||
jwtManager := auth_platform.NewJWTManager()
|
|
||||||
authCommands := auth.NewAuthCommands(userRepo, jwtManager)
|
|
||||||
authQueries := auth.NewAuthQueries(userRepo, jwtManager)
|
|
||||||
|
|
||||||
copyrightCommands := copyright.NewCopyrightCommands(copyrightRepo)
|
|
||||||
copyrightQueries := copyright.NewCopyrightQueries(copyrightRepo)
|
|
||||||
|
|
||||||
localizationService := localization.NewService(translationRepo)
|
|
||||||
|
|
||||||
searchService := search.NewIndexService(localizationService, translationRepo)
|
|
||||||
|
|
||||||
b.App = &Application{
|
|
||||||
WorkCommands: workCommands,
|
|
||||||
WorkQueries: workQueries,
|
|
||||||
AuthCommands: authCommands,
|
|
||||||
AuthQueries: authQueries,
|
|
||||||
CopyrightCommands: copyrightCommands,
|
|
||||||
CopyrightQueries: copyrightQueries,
|
|
||||||
Localization: localizationService,
|
|
||||||
Search: searchService,
|
|
||||||
AuthorRepo: authorRepo,
|
|
||||||
UserRepo: userRepo,
|
|
||||||
TagRepo: tagRepo,
|
|
||||||
CategoryRepo: categoryRepo,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.LogInfo("Application layer initialized successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build initializes all components in the correct order
|
|
||||||
func (b *ApplicationBuilder) Build() error {
|
|
||||||
if err := b.BuildDatabase(); err != nil { return err }
|
|
||||||
if err := b.BuildCache(); err != nil { return err }
|
|
||||||
if err := b.BuildWeaviate(); err != nil { return err }
|
|
||||||
if err := b.BuildBackgroundJobs(); err != nil { return err }
|
|
||||||
if err := b.BuildLinguistics(); err != nil { return err }
|
|
||||||
if err := b.BuildApplication(); err != nil { return err }
|
|
||||||
log.LogInfo("Application builder completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetApplication returns the application container
|
|
||||||
func (b *ApplicationBuilder) GetApplication() *Application {
|
|
||||||
return b.App
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDB returns the database connection
|
|
||||||
func (b *ApplicationBuilder) GetDB() *gorm.DB {
|
|
||||||
return b.dbConn
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAsynq returns the Asynq client
|
|
||||||
func (b *ApplicationBuilder) GetAsynq() *asynq.Client {
|
|
||||||
return b.asynqClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLinguisticsFactory returns the linguistics factory
|
|
||||||
func (b *ApplicationBuilder) GetLinguisticsFactory() *linguistics.LinguisticsFactory {
|
|
||||||
return b.linguistics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes all resources
|
|
||||||
func (b *ApplicationBuilder) Close() error {
|
|
||||||
if b.asynqClient != nil {
|
|
||||||
b.asynqClient.Close()
|
|
||||||
}
|
|
||||||
if b.dbConn != nil {
|
|
||||||
sqlDB, err := b.dbConn.DB()
|
|
||||||
if err == nil {
|
|
||||||
sqlDB.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -44,11 +44,11 @@ type AuthResponse struct {
|
|||||||
// AuthCommands contains the command handlers for authentication.
|
// AuthCommands contains the command handlers for authentication.
|
||||||
type AuthCommands struct {
|
type AuthCommands struct {
|
||||||
userRepo domain.UserRepository
|
userRepo domain.UserRepository
|
||||||
jwtManager *auth.JWTManager
|
jwtManager auth.JWTManagement
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthCommands creates a new AuthCommands handler.
|
// NewAuthCommands creates a new AuthCommands handler.
|
||||||
func NewAuthCommands(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthCommands {
|
func NewAuthCommands(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *AuthCommands {
|
||||||
return &AuthCommands{
|
return &AuthCommands{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
@ -58,11 +58,12 @@ func NewAuthCommands(userRepo domain.UserRepository, jwtManager *auth.JWTManager
|
|||||||
// Login authenticates a user and returns a JWT token
|
// Login authenticates a user and returns a JWT token
|
||||||
func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) {
|
func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) {
|
||||||
if err := validateLoginInput(input); err != nil {
|
if err := validateLoginInput(input); err != nil {
|
||||||
log.LogWarn("Login failed - invalid input", log.F("email", input.Email), log.F("error", err))
|
log.LogWarn("Login validation failed", log.F("email", input.Email), log.F("error", err))
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := strings.TrimSpace(input.Email)
|
email := strings.TrimSpace(input.Email)
|
||||||
|
log.LogDebug("Attempting to log in user", log.F("email", email))
|
||||||
user, err := c.userRepo.FindByEmail(ctx, email)
|
user, err := c.userRepo.FindByEmail(ctx, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Login failed - user not found", log.F("email", email))
|
log.LogWarn("Login failed - user not found", log.F("email", email))
|
||||||
@ -89,25 +90,27 @@ func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthRespon
|
|||||||
user.LastLoginAt = &now
|
user.LastLoginAt = &now
|
||||||
if err := c.userRepo.Update(ctx, user); err != nil {
|
if err := c.userRepo.Update(ctx, user); err != nil {
|
||||||
log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err))
|
log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err))
|
||||||
|
// Do not fail the login if this update fails
|
||||||
}
|
}
|
||||||
|
|
||||||
log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email))
|
log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email))
|
||||||
return &AuthResponse{
|
return &AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
User: user,
|
User: user,
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register creates a new user account
|
// Register creates a new user account
|
||||||
func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) {
|
func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) {
|
||||||
if err := validateRegisterInput(input); err != nil {
|
if err := validateRegisterInput(input); err != nil {
|
||||||
log.LogWarn("Registration failed - invalid input", log.F("email", input.Email), log.F("error", err))
|
log.LogWarn("Registration validation failed", log.F("email", input.Email), log.F("error", err))
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
email := strings.TrimSpace(input.Email)
|
email := strings.TrimSpace(input.Email)
|
||||||
username := strings.TrimSpace(input.Username)
|
username := strings.TrimSpace(input.Username)
|
||||||
|
log.LogDebug("Attempting to register new user", log.F("email", email), log.F("username", username))
|
||||||
|
|
||||||
existingUser, _ := c.userRepo.FindByEmail(ctx, email)
|
existingUser, _ := c.userRepo.FindByEmail(ctx, email)
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
@ -130,7 +133,7 @@ func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*Auth
|
|||||||
DisplayName: fmt.Sprintf("%s %s", strings.TrimSpace(input.FirstName), strings.TrimSpace(input.LastName)),
|
DisplayName: fmt.Sprintf("%s %s", strings.TrimSpace(input.FirstName), strings.TrimSpace(input.LastName)),
|
||||||
Role: domain.UserRoleReader,
|
Role: domain.UserRoleReader,
|
||||||
Active: true,
|
Active: true,
|
||||||
Verified: false,
|
Verified: false, // Should be false until email verification
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.userRepo.Create(ctx, user); err != nil {
|
if err := c.userRepo.Create(ctx, user); err != nil {
|
||||||
@ -148,7 +151,7 @@ func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*Auth
|
|||||||
return &AuthResponse{
|
return &AuthResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
User: user,
|
User: user,
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
286
internal/app/auth/commands_test.go
Normal file
286
internal/app/auth/commands_test.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type AuthCommandsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
userRepo *mockUserRepository
|
||||||
|
jwtManager *mockJWTManager
|
||||||
|
commands *AuthCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) SetupTest() {
|
||||||
|
s.userRepo = newMockUserRepository()
|
||||||
|
s.jwtManager = &mockJWTManager{}
|
||||||
|
s.commands = NewAuthCommands(s.userRepo, s.jwtManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthCommandsSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(AuthCommandsSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_Success() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), resp)
|
||||||
|
assert.Equal(s.T(), "test-token", resp.Token)
|
||||||
|
assert.Equal(s.T(), user.ID, resp.User.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_InvalidInput() {
|
||||||
|
input := LoginInput{Email: "invalid-email", Password: "short"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestValidateLoginInput_EmptyEmail() {
|
||||||
|
input := LoginInput{Email: "", Password: "password"}
|
||||||
|
err := validateLoginInput(input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestValidateLoginInput_ShortPassword() {
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "short"}
|
||||||
|
err := validateLoginInput(input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestValidateRegisterInput_ShortPassword() {
|
||||||
|
input := RegisterInput{Email: "test@example.com", Password: "short"}
|
||||||
|
err := validateRegisterInput(input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestValidateRegisterInput_ShortUsername() {
|
||||||
|
input := RegisterInput{Username: "a", Email: "test@example.com", Password: "password"}
|
||||||
|
err := validateRegisterInput(input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestValidateRegisterInput_LongUsername() {
|
||||||
|
input := RegisterInput{Username: "a51characterusernameisdefinitelytoolongforthisvalidation", Email: "test@example.com", Password: "password"}
|
||||||
|
err := validateRegisterInput(input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_SuccessUpdate() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
s.userRepo.updateFunc = func(ctx context.Context, user *domain.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_InvalidEmail() {
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "newuser",
|
||||||
|
Email: "invalid-email",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_UpdateUserError() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
s.userRepo.updateFunc = func(ctx context.Context, user *domain.User) error {
|
||||||
|
return errors.New("update error")
|
||||||
|
}
|
||||||
|
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_InvalidUsername() {
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "invalid username",
|
||||||
|
Email: "new@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_UserNotFound() {
|
||||||
|
input := LoginInput{Email: "notfound@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrInvalidCredentials)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_InactiveUser() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "inactive@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: false,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
input := LoginInput{Email: "inactive@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrInvalidCredentials)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_InvalidPassword() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "wrong-password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrInvalidCredentials)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestLogin_TokenGenerationError() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Password: "password",
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
s.jwtManager.generateTokenFunc = func(user *domain.User) (string, error) {
|
||||||
|
return "", errors.New("jwt error")
|
||||||
|
}
|
||||||
|
input := LoginInput{Email: "test@example.com", Password: "password"}
|
||||||
|
resp, err := s.commands.Login(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_Success() {
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "newuser",
|
||||||
|
Email: "new@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), resp)
|
||||||
|
assert.Equal(s.T(), "test-token", resp.Token)
|
||||||
|
assert.Equal(s.T(), "newuser", resp.User.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_InvalidInput() {
|
||||||
|
input := RegisterInput{Email: "invalid"}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_EmailExists() {
|
||||||
|
user := domain.User{
|
||||||
|
Email: "exists@example.com",
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "newuser",
|
||||||
|
Email: "exists@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrUserAlreadyExists)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_UsernameExists() {
|
||||||
|
user := domain.User{
|
||||||
|
Username: "exists",
|
||||||
|
}
|
||||||
|
s.userRepo.Create(context.Background(), &user)
|
||||||
|
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "exists",
|
||||||
|
Email: "new@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrUserAlreadyExists)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_CreateUserError() {
|
||||||
|
s.userRepo.createFunc = func(ctx context.Context, user *domain.User) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "newuser",
|
||||||
|
Email: "new@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthCommandsSuite) TestRegister_TokenGenerationError() {
|
||||||
|
s.jwtManager.generateTokenFunc = func(user *domain.User) (string, error) {
|
||||||
|
return "", errors.New("jwt error")
|
||||||
|
}
|
||||||
|
input := RegisterInput{
|
||||||
|
Username: "newuser",
|
||||||
|
Email: "new@example.com",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "New",
|
||||||
|
LastName: "User",
|
||||||
|
}
|
||||||
|
resp, err := s.commands.Register(context.Background(), input)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), resp)
|
||||||
|
}
|
||||||
138
internal/app/auth/main_test.go
Normal file
138
internal/app/auth/main_test.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockUserRepository is a local mock for the UserRepository interface.
|
||||||
|
type mockUserRepository struct {
|
||||||
|
users map[uint]domain.User
|
||||||
|
findByEmailFunc func(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
findByUsernameFunc func(ctx context.Context, username string) (*domain.User, error)
|
||||||
|
createFunc func(ctx context.Context, user *domain.User) error
|
||||||
|
updateFunc func(ctx context.Context, user *domain.User) error
|
||||||
|
getByIDFunc func(ctx context.Context, id uint) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockUserRepository() *mockUserRepository {
|
||||||
|
return &mockUserRepository{users: make(map[uint]domain.User)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
if m.findByEmailFunc != nil {
|
||||||
|
return m.findByEmailFunc(ctx, email)
|
||||||
|
}
|
||||||
|
for _, u := range m.users {
|
||||||
|
if u.Email == email {
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
|
if m.findByUsernameFunc != nil {
|
||||||
|
return m.findByUsernameFunc(ctx, username)
|
||||||
|
}
|
||||||
|
for _, u := range m.users {
|
||||||
|
if u.Username == username {
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepository) Create(ctx context.Context, user *domain.User) error {
|
||||||
|
if m.createFunc != nil {
|
||||||
|
return m.createFunc(ctx, user)
|
||||||
|
}
|
||||||
|
// Simulate the BeforeSave hook for password hashing
|
||||||
|
if err := user.BeforeSave(nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.ID = uint(len(m.users) + 1)
|
||||||
|
m.users[user.ID] = *user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||||
|
if m.updateFunc != nil {
|
||||||
|
return m.updateFunc(ctx, user)
|
||||||
|
}
|
||||||
|
if _, ok := m.users[user.ID]; ok {
|
||||||
|
m.users[user.ID] = *user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||||
|
if m.getByIDFunc != nil {
|
||||||
|
return m.getByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
if user, ok := m.users[id]; ok {
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the rest of the UserRepository interface with empty methods.
|
||||||
|
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil }
|
||||||
|
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||||
|
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||||
|
func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||||
|
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||||
|
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||||
|
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockJWTManager is a local mock for the JWTManager.
|
||||||
|
type mockJWTManager struct {
|
||||||
|
generateTokenFunc func(user *domain.User) (string, error)
|
||||||
|
validateTokenFunc func(tokenString string) (*auth.Claims, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockJWTManager) GenerateToken(user *domain.User) (string, error) {
|
||||||
|
if m.generateTokenFunc != nil {
|
||||||
|
return m.generateTokenFunc(user)
|
||||||
|
}
|
||||||
|
return "test-token", nil
|
||||||
|
}
|
||||||
|
func (m *mockJWTManager) ValidateToken(tokenString string) (*auth.Claims, error) {
|
||||||
|
if m.validateTokenFunc != nil {
|
||||||
|
return m.validateTokenFunc(tokenString)
|
||||||
|
}
|
||||||
|
return &auth.Claims{UserID: 1}, nil
|
||||||
|
}
|
||||||
@ -16,11 +16,11 @@ var (
|
|||||||
// AuthQueries contains the query handlers for authentication.
|
// AuthQueries contains the query handlers for authentication.
|
||||||
type AuthQueries struct {
|
type AuthQueries struct {
|
||||||
userRepo domain.UserRepository
|
userRepo domain.UserRepository
|
||||||
jwtManager *auth.JWTManager
|
jwtManager auth.JWTManagement
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthQueries creates a new AuthQueries handler.
|
// NewAuthQueries creates a new AuthQueries handler.
|
||||||
func NewAuthQueries(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthQueries {
|
func NewAuthQueries(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *AuthQueries {
|
||||||
return &AuthQueries{
|
return &AuthQueries{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
jwtManager: jwtManager,
|
jwtManager: jwtManager,
|
||||||
@ -32,12 +32,14 @@ func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, err
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, ErrContextRequired
|
return nil, ErrContextRequired
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Attempting to get user from context")
|
||||||
|
|
||||||
claims, err := auth.RequireAuth(ctx)
|
claims, err := auth.RequireAuth(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Failed to get user from context - authentication required", log.F("error", err))
|
log.LogWarn("Failed to get user from context - authentication required", log.F("error", err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Claims found in context", log.F("user_id", claims.UserID))
|
||||||
|
|
||||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -50,6 +52,7 @@ func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, err
|
|||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.LogDebug("User retrieved from context successfully", log.F("user_id", user.ID))
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,12 +66,14 @@ func (q *AuthQueries) ValidateToken(ctx context.Context, tokenString string) (*d
|
|||||||
log.LogWarn("Token validation failed - empty token")
|
log.LogWarn("Token validation failed - empty token")
|
||||||
return nil, auth.ErrMissingToken
|
return nil, auth.ErrMissingToken
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Attempting to validate token")
|
||||||
|
|
||||||
claims, err := q.jwtManager.ValidateToken(tokenString)
|
claims, err := q.jwtManager.ValidateToken(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.LogWarn("Token validation failed - invalid token", log.F("error", err))
|
log.LogWarn("Token validation failed - invalid token", log.F("error", err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Token claims validated", log.F("user_id", claims.UserID))
|
||||||
|
|
||||||
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
user, err := q.userRepo.GetByID(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
134
internal/app/auth/queries_test.go
Normal file
134
internal/app/auth/queries_test.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/auth"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthQueriesSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
userRepo *mockUserRepository
|
||||||
|
jwtManager *mockJWTManager
|
||||||
|
queries *AuthQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) SetupTest() {
|
||||||
|
s.userRepo = newMockUserRepository()
|
||||||
|
s.jwtManager = &mockJWTManager{}
|
||||||
|
s.queries = NewAuthQueries(s.userRepo, s.jwtManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthQueriesSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(AuthQueriesSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestGetUserFromContext_Success() {
|
||||||
|
user := domain.User{Active: true}
|
||||||
|
user.ID = 1
|
||||||
|
s.userRepo.users[1] = user
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1})
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.GetUserFromContext(ctx)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), retrievedUser)
|
||||||
|
assert.Equal(s.T(), user.ID, retrievedUser.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestGetUserFromContext_NoClaims() {
|
||||||
|
retrievedUser, err := s.queries.GetUserFromContext(context.Background())
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestGetUserFromContext_UserNotFound() {
|
||||||
|
ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1})
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.GetUserFromContext(ctx)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrUserNotFound)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestGetUserFromContext_InactiveUser() {
|
||||||
|
user := domain.User{Active: false}
|
||||||
|
user.ID = 1
|
||||||
|
s.userRepo.users[1] = user
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1})
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.GetUserFromContext(ctx)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrInvalidCredentials)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestGetUserFromContext_NilContext() {
|
||||||
|
user, err := s.queries.GetUserFromContext(nil)
|
||||||
|
assert.ErrorIs(s.T(), err, ErrContextRequired)
|
||||||
|
assert.Nil(s.T(), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_NilContext() {
|
||||||
|
user, err := s.queries.ValidateToken(nil, "token")
|
||||||
|
assert.ErrorIs(s.T(), err, ErrContextRequired)
|
||||||
|
assert.Nil(s.T(), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_Success() {
|
||||||
|
user := domain.User{Active: true}
|
||||||
|
user.ID = 1
|
||||||
|
s.userRepo.users[1] = user
|
||||||
|
|
||||||
|
s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) {
|
||||||
|
return &auth.Claims{UserID: 1}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token")
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.NotNil(s.T(), retrievedUser)
|
||||||
|
assert.Equal(s.T(), user.ID, retrievedUser.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_EmptyToken() {
|
||||||
|
retrievedUser, err := s.queries.ValidateToken(context.Background(), "")
|
||||||
|
assert.ErrorIs(s.T(), err, auth.ErrMissingToken)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_InvalidToken() {
|
||||||
|
s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) {
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.ValidateToken(context.Background(), "invalid-token")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_UserNotFound() {
|
||||||
|
s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) {
|
||||||
|
return &auth.Claims{UserID: 1}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token")
|
||||||
|
assert.ErrorIs(s.T(), err, ErrUserNotFound)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthQueriesSuite) TestValidateToken_InactiveUser() {
|
||||||
|
user := domain.User{Active: false}
|
||||||
|
user.ID = 1
|
||||||
|
s.userRepo.users[1] = user
|
||||||
|
|
||||||
|
s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) {
|
||||||
|
return &auth.Claims{UserID: 1}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token")
|
||||||
|
assert.ErrorIs(s.T(), err, ErrInvalidCredentials)
|
||||||
|
assert.Nil(s.T(), retrievedUser)
|
||||||
|
}
|
||||||
20
internal/app/auth/service.go
Normal file
20
internal/app/auth/service.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service is the application service for the auth aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *AuthCommands
|
||||||
|
Queries *AuthQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new auth Service.
|
||||||
|
func NewService(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewAuthCommands(userRepo, jwtManager),
|
||||||
|
Queries: NewAuthQueries(userRepo, jwtManager),
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/app/author/commands.go
Normal file
58
internal/app/author/commands.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package author
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthorCommands contains the command handlers for the author aggregate.
|
||||||
|
type AuthorCommands struct {
|
||||||
|
repo domain.AuthorRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorCommands creates a new AuthorCommands handler.
|
||||||
|
func NewAuthorCommands(repo domain.AuthorRepository) *AuthorCommands {
|
||||||
|
return &AuthorCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAuthorInput represents the input for creating a new author.
|
||||||
|
type CreateAuthorInput struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAuthor creates a new author.
|
||||||
|
func (c *AuthorCommands) CreateAuthor(ctx context.Context, input CreateAuthorInput) (*domain.Author, error) {
|
||||||
|
author := &domain.Author{
|
||||||
|
Name: input.Name,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return author, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAuthorInput represents the input for updating an existing author.
|
||||||
|
type UpdateAuthorInput struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAuthor updates an existing author.
|
||||||
|
func (c *AuthorCommands) UpdateAuthor(ctx context.Context, input UpdateAuthorInput) (*domain.Author, error) {
|
||||||
|
author, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
author.Name = input.Name
|
||||||
|
err = c.repo.Update(ctx, author)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return author, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAuthor deletes an author by ID.
|
||||||
|
func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
34
internal/app/author/queries.go
Normal file
34
internal/app/author/queries.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package author
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthorQueries contains the query handlers for the author aggregate.
|
||||||
|
type AuthorQueries struct {
|
||||||
|
repo domain.AuthorRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorQueries creates a new AuthorQueries handler.
|
||||||
|
func NewAuthorQueries(repo domain.AuthorRepository) *AuthorQueries {
|
||||||
|
return &AuthorQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author returns an author by ID.
|
||||||
|
func (q *AuthorQueries) Author(ctx context.Context, id uint) (*domain.Author, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authors returns all authors.
|
||||||
|
func (q *AuthorQueries) Authors(ctx context.Context) ([]*domain.Author, error) {
|
||||||
|
authors, err := q.repo.ListAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authorPtrs := make([]*domain.Author, len(authors))
|
||||||
|
for i := range authors {
|
||||||
|
authorPtrs[i] = &authors[i]
|
||||||
|
}
|
||||||
|
return authorPtrs, nil
|
||||||
|
}
|
||||||
17
internal/app/author/service.go
Normal file
17
internal/app/author/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package author
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the author aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *AuthorCommands
|
||||||
|
Queries *AuthorQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new author Service.
|
||||||
|
func NewService(repo domain.AuthorRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewAuthorCommands(repo),
|
||||||
|
Queries: NewAuthorQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/app/bookmark/commands.go
Normal file
66
internal/app/bookmark/commands.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package bookmark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BookmarkCommands contains the command handlers for the bookmark aggregate.
|
||||||
|
type BookmarkCommands struct {
|
||||||
|
repo domain.BookmarkRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBookmarkCommands creates a new BookmarkCommands handler.
|
||||||
|
func NewBookmarkCommands(repo domain.BookmarkRepository) *BookmarkCommands {
|
||||||
|
return &BookmarkCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBookmarkInput represents the input for creating a new bookmark.
|
||||||
|
type CreateBookmarkInput struct {
|
||||||
|
Name string
|
||||||
|
UserID uint
|
||||||
|
WorkID uint
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBookmark creates a new bookmark.
|
||||||
|
func (c *BookmarkCommands) CreateBookmark(ctx context.Context, input CreateBookmarkInput) (*domain.Bookmark, error) {
|
||||||
|
bookmark := &domain.Bookmark{
|
||||||
|
Name: input.Name,
|
||||||
|
UserID: input.UserID,
|
||||||
|
WorkID: input.WorkID,
|
||||||
|
Notes: input.Notes,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, bookmark)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bookmark, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBookmarkInput represents the input for updating an existing bookmark.
|
||||||
|
type UpdateBookmarkInput struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Notes string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBookmark updates an existing bookmark.
|
||||||
|
func (c *BookmarkCommands) UpdateBookmark(ctx context.Context, input UpdateBookmarkInput) (*domain.Bookmark, error) {
|
||||||
|
bookmark, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bookmark.Name = input.Name
|
||||||
|
bookmark.Notes = input.Notes
|
||||||
|
err = c.repo.Update(ctx, bookmark)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bookmark, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBookmark deletes a bookmark by ID.
|
||||||
|
func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
31
internal/app/bookmark/queries.go
Normal file
31
internal/app/bookmark/queries.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package bookmark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BookmarkQueries contains the query handlers for the bookmark aggregate.
|
||||||
|
type BookmarkQueries struct {
|
||||||
|
repo domain.BookmarkRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBookmarkQueries creates a new BookmarkQueries handler.
|
||||||
|
func NewBookmarkQueries(repo domain.BookmarkRepository) *BookmarkQueries {
|
||||||
|
return &BookmarkQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bookmark returns a bookmark by ID.
|
||||||
|
func (q *BookmarkQueries) Bookmark(ctx context.Context, id uint) (*domain.Bookmark, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookmarksByUserID returns all bookmarks for a user.
|
||||||
|
func (q *BookmarkQueries) BookmarksByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) {
|
||||||
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BookmarksByWorkID returns all bookmarks for a work.
|
||||||
|
func (q *BookmarkQueries) BookmarksByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
17
internal/app/bookmark/service.go
Normal file
17
internal/app/bookmark/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package bookmark
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the bookmark aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *BookmarkCommands
|
||||||
|
Queries *BookmarkQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new bookmark Service.
|
||||||
|
func NewService(repo domain.BookmarkRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewBookmarkCommands(repo),
|
||||||
|
Queries: NewBookmarkQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/app/category/commands.go
Normal file
66
internal/app/category/commands.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package category
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CategoryCommands contains the command handlers for the category aggregate.
|
||||||
|
type CategoryCommands struct {
|
||||||
|
repo domain.CategoryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCategoryCommands creates a new CategoryCommands handler.
|
||||||
|
func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
|
||||||
|
return &CategoryCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCategoryInput represents the input for creating a new category.
|
||||||
|
type CreateCategoryInput struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
ParentID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCategory creates a new category.
|
||||||
|
func (c *CategoryCommands) CreateCategory(ctx context.Context, input CreateCategoryInput) (*domain.Category, error) {
|
||||||
|
category := &domain.Category{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: input.Description,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, category)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategoryInput represents the input for updating an existing category.
|
||||||
|
type UpdateCategoryInput struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
ParentID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCategory updates an existing category.
|
||||||
|
func (c *CategoryCommands) UpdateCategory(ctx context.Context, input UpdateCategoryInput) (*domain.Category, error) {
|
||||||
|
category, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
category.Name = input.Name
|
||||||
|
category.Description = input.Description
|
||||||
|
category.ParentID = input.ParentID
|
||||||
|
err = c.repo.Update(ctx, category)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return category, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCategory deletes a category by ID.
|
||||||
|
func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
41
internal/app/category/queries.go
Normal file
41
internal/app/category/queries.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package category
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CategoryQueries contains the query handlers for the category aggregate.
|
||||||
|
type CategoryQueries struct {
|
||||||
|
repo domain.CategoryRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCategoryQueries creates a new CategoryQueries handler.
|
||||||
|
func NewCategoryQueries(repo domain.CategoryRepository) *CategoryQueries {
|
||||||
|
return &CategoryQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category returns a category by ID.
|
||||||
|
func (q *CategoryQueries) Category(ctx context.Context, id uint) (*domain.Category, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoryByName returns a category by name.
|
||||||
|
func (q *CategoryQueries) CategoryByName(ctx context.Context, name string) (*domain.Category, error) {
|
||||||
|
return q.repo.FindByName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoriesByWorkID returns all categories for a work.
|
||||||
|
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CategoriesByParentID returns all categories for a parent.
|
||||||
|
func (q *CategoryQueries) CategoriesByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
||||||
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories returns all categories.
|
||||||
|
func (q *CategoryQueries) Categories(ctx context.Context) ([]domain.Category, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/category/service.go
Normal file
17
internal/app/category/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package category
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the category aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *CategoryCommands
|
||||||
|
Queries *CategoryQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new category Service.
|
||||||
|
func NewService(repo domain.CategoryRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewCategoryCommands(repo),
|
||||||
|
Queries: NewCategoryQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
94
internal/app/collection/commands.go
Normal file
94
internal/app/collection/commands.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollectionCommands contains the command handlers for the collection aggregate.
|
||||||
|
type CollectionCommands struct {
|
||||||
|
repo domain.CollectionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCollectionCommands creates a new CollectionCommands handler.
|
||||||
|
func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands {
|
||||||
|
return &CollectionCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCollectionInput represents the input for creating a new collection.
|
||||||
|
type CreateCollectionInput struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
UserID uint
|
||||||
|
IsPublic bool
|
||||||
|
CoverImageURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCollection creates a new collection.
|
||||||
|
func (c *CollectionCommands) CreateCollection(ctx context.Context, input CreateCollectionInput) (*domain.Collection, error) {
|
||||||
|
collection := &domain.Collection{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: input.Description,
|
||||||
|
UserID: input.UserID,
|
||||||
|
IsPublic: input.IsPublic,
|
||||||
|
CoverImageURL: input.CoverImageURL,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, collection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCollectionInput represents the input for updating an existing collection.
|
||||||
|
type UpdateCollectionInput struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
IsPublic bool
|
||||||
|
CoverImageURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCollection updates an existing collection.
|
||||||
|
func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateCollectionInput) (*domain.Collection, error) {
|
||||||
|
collection, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
collection.Name = input.Name
|
||||||
|
collection.Description = input.Description
|
||||||
|
collection.IsPublic = input.IsPublic
|
||||||
|
collection.CoverImageURL = input.CoverImageURL
|
||||||
|
err = c.repo.Update(ctx, collection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCollection deletes a collection by ID.
|
||||||
|
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWorkToCollectionInput represents the input for adding a work to a collection.
|
||||||
|
type AddWorkToCollectionInput struct {
|
||||||
|
CollectionID uint
|
||||||
|
WorkID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWorkToCollection adds a work to a collection.
|
||||||
|
func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddWorkToCollectionInput) error {
|
||||||
|
return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveWorkFromCollectionInput represents the input for removing a work from a collection.
|
||||||
|
type RemoveWorkFromCollectionInput struct {
|
||||||
|
CollectionID uint
|
||||||
|
WorkID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveWorkFromCollection removes a work from a collection.
|
||||||
|
func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input RemoveWorkFromCollectionInput) error {
|
||||||
|
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
|
}
|
||||||
41
internal/app/collection/queries.go
Normal file
41
internal/app/collection/queries.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollectionQueries contains the query handlers for the collection aggregate.
|
||||||
|
type CollectionQueries struct {
|
||||||
|
repo domain.CollectionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCollectionQueries creates a new CollectionQueries handler.
|
||||||
|
func NewCollectionQueries(repo domain.CollectionRepository) *CollectionQueries {
|
||||||
|
return &CollectionQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collection returns a collection by ID.
|
||||||
|
func (q *CollectionQueries) Collection(ctx context.Context, id uint) (*domain.Collection, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionsByUserID returns all collections for a user.
|
||||||
|
func (q *CollectionQueries) CollectionsByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) {
|
||||||
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicCollections returns all public collections.
|
||||||
|
func (q *CollectionQueries) PublicCollections(ctx context.Context) ([]domain.Collection, error) {
|
||||||
|
return q.repo.ListPublic(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectionsByWorkID returns all collections for a work.
|
||||||
|
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collections returns all collections.
|
||||||
|
func (q *CollectionQueries) Collections(ctx context.Context) ([]domain.Collection, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/collection/service.go
Normal file
17
internal/app/collection/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package collection
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the collection aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *CollectionCommands
|
||||||
|
Queries *CollectionQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new collection Service.
|
||||||
|
func NewService(repo domain.CollectionRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewCollectionCommands(repo),
|
||||||
|
Queries: NewCollectionQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
66
internal/app/comment/commands.go
Normal file
66
internal/app/comment/commands.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package comment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommentCommands contains the command handlers for the comment aggregate.
|
||||||
|
type CommentCommands struct {
|
||||||
|
repo domain.CommentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommentCommands creates a new CommentCommands handler.
|
||||||
|
func NewCommentCommands(repo domain.CommentRepository) *CommentCommands {
|
||||||
|
return &CommentCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCommentInput represents the input for creating a new comment.
|
||||||
|
type CreateCommentInput struct {
|
||||||
|
Text string
|
||||||
|
UserID uint
|
||||||
|
WorkID *uint
|
||||||
|
TranslationID *uint
|
||||||
|
ParentID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateComment creates a new comment.
|
||||||
|
func (c *CommentCommands) CreateComment(ctx context.Context, input CreateCommentInput) (*domain.Comment, error) {
|
||||||
|
comment := &domain.Comment{
|
||||||
|
Text: input.Text,
|
||||||
|
UserID: input.UserID,
|
||||||
|
WorkID: input.WorkID,
|
||||||
|
TranslationID: input.TranslationID,
|
||||||
|
ParentID: input.ParentID,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCommentInput represents the input for updating an existing comment.
|
||||||
|
type UpdateCommentInput struct {
|
||||||
|
ID uint
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateComment updates an existing comment.
|
||||||
|
func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateCommentInput) (*domain.Comment, error) {
|
||||||
|
comment, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
comment.Text = input.Text
|
||||||
|
err = c.repo.Update(ctx, comment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteComment deletes a comment by ID.
|
||||||
|
func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
46
internal/app/comment/queries.go
Normal file
46
internal/app/comment/queries.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package comment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommentQueries contains the query handlers for the comment aggregate.
|
||||||
|
type CommentQueries struct {
|
||||||
|
repo domain.CommentRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommentQueries creates a new CommentQueries handler.
|
||||||
|
func NewCommentQueries(repo domain.CommentRepository) *CommentQueries {
|
||||||
|
return &CommentQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment returns a comment by ID.
|
||||||
|
func (q *CommentQueries) Comment(ctx context.Context, id uint) (*domain.Comment, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsByUserID returns all comments for a user.
|
||||||
|
func (q *CommentQueries) CommentsByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) {
|
||||||
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsByWorkID returns all comments for a work.
|
||||||
|
func (q *CommentQueries) CommentsByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsByTranslationID returns all comments for a translation.
|
||||||
|
func (q *CommentQueries) CommentsByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) {
|
||||||
|
return q.repo.ListByTranslationID(ctx, translationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsByParentID returns all comments for a parent.
|
||||||
|
func (q *CommentQueries) CommentsByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) {
|
||||||
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comments returns all comments.
|
||||||
|
func (q *CommentQueries) Comments(ctx context.Context) ([]domain.Comment, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/comment/service.go
Normal file
17
internal/app/comment/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package comment
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the comment aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *CommentCommands
|
||||||
|
Queries *CommentQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new comment Service.
|
||||||
|
func NewService(repo domain.CommentRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewCommentCommands(repo),
|
||||||
|
Queries: NewCommentQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyrightCommands contains the command handlers for copyright.
|
// CopyrightCommands contains the command handlers for copyright.
|
||||||
@ -27,6 +28,7 @@ func (c *CopyrightCommands) CreateCopyright(ctx context.Context, copyright *doma
|
|||||||
if copyright.Identificator == "" {
|
if copyright.Identificator == "" {
|
||||||
return errors.New("copyright identificator cannot be empty")
|
return errors.New("copyright identificator cannot be empty")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Creating copyright", log.F("name", copyright.Name))
|
||||||
return c.repo.Create(ctx, copyright)
|
return c.repo.Create(ctx, copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
|
|||||||
if copyright.Identificator == "" {
|
if copyright.Identificator == "" {
|
||||||
return errors.New("copyright identificator cannot be empty")
|
return errors.New("copyright identificator cannot be empty")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Updating copyright", log.F("id", copyright.ID))
|
||||||
return c.repo.Update(ctx, copyright)
|
return c.repo.Update(ctx, copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,29 +55,98 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return errors.New("invalid copyright ID")
|
return errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Deleting copyright", log.F("id", id))
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachCopyrightToEntity attaches a copyright to any entity type.
|
// AddCopyrightToWork adds a copyright to a work.
|
||||||
func (c *CopyrightCommands) AttachCopyrightToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
if copyrightID == 0 || entityID == 0 {
|
if workID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid copyright ID or entity ID")
|
return errors.New("invalid work ID or copyright ID")
|
||||||
}
|
}
|
||||||
if entityType == "" {
|
log.LogDebug("Adding copyright to work", log.F("work_id", workID), log.F("copyright_id", copyrightID))
|
||||||
return errors.New("entity type cannot be empty")
|
return c.repo.AddCopyrightToWork(ctx, workID, copyrightID)
|
||||||
}
|
|
||||||
return c.repo.AttachToEntity(ctx, copyrightID, entityID, entityType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetachCopyrightFromEntity removes a copyright from an entity.
|
// RemoveCopyrightFromWork removes a copyright from a work.
|
||||||
func (c *CopyrightCommands) DetachCopyrightFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
if copyrightID == 0 || entityID == 0 {
|
if workID == 0 || copyrightID == 0 {
|
||||||
return errors.New("invalid copyright ID or entity ID")
|
return errors.New("invalid work ID or copyright ID")
|
||||||
}
|
}
|
||||||
if entityType == "" {
|
log.LogDebug("Removing copyright from work", log.F("work_id", workID), log.F("copyright_id", copyrightID))
|
||||||
return errors.New("entity type cannot be empty")
|
return c.repo.RemoveCopyrightFromWork(ctx, workID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCopyrightToAuthor adds a copyright to an author.
|
||||||
|
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
if authorID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid author ID or copyright ID")
|
||||||
}
|
}
|
||||||
return c.repo.DetachFromEntity(ctx, copyrightID, entityID, entityType)
|
log.LogDebug("Adding copyright to author", log.F("author_id", authorID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.AddCopyrightToAuthor(ctx, authorID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCopyrightFromAuthor removes a copyright from an author.
|
||||||
|
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
if authorID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid author ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing copyright from author", log.F("author_id", authorID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.RemoveCopyrightFromAuthor(ctx, authorID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCopyrightToBook adds a copyright to a book.
|
||||||
|
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
if bookID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid book ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding copyright to book", log.F("book_id", bookID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.AddCopyrightToBook(ctx, bookID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCopyrightFromBook removes a copyright from a book.
|
||||||
|
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
if bookID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid book ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing copyright from book", log.F("book_id", bookID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.RemoveCopyrightFromBook(ctx, bookID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCopyrightToPublisher adds a copyright to a publisher.
|
||||||
|
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid publisher ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding copyright to publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.AddCopyrightToPublisher(ctx, publisherID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCopyrightFromPublisher removes a copyright from a publisher.
|
||||||
|
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid publisher ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing copyright from publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.RemoveCopyrightFromPublisher(ctx, publisherID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCopyrightToSource adds a copyright to a source.
|
||||||
|
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid source ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding copyright to source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.AddCopyrightToSource(ctx, sourceID, copyrightID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCopyrightFromSource removes a copyright from a source.
|
||||||
|
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
|
return errors.New("invalid source ID or copyright ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing copyright from source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID))
|
||||||
|
return c.repo.RemoveCopyrightFromSource(ctx, sourceID, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTranslation adds a translation to a copyright.
|
// AddTranslation adds a translation to a copyright.
|
||||||
@ -91,5 +163,6 @@ func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *dom
|
|||||||
if translation.Message == "" {
|
if translation.Message == "" {
|
||||||
return errors.New("translation message cannot be empty")
|
return errors.New("translation message cannot be empty")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Adding translation to copyright", log.F("copyright_id", translation.CopyrightID), log.F("language", translation.LanguageCode))
|
||||||
return c.repo.AddTranslation(ctx, translation)
|
return c.repo.AddTranslation(ctx, translation)
|
||||||
}
|
}
|
||||||
|
|||||||
239
internal/app/copyright/commands_integration_test.go
Normal file
239
internal/app/copyright/commands_integration_test.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package copyright_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/app/copyright"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CopyrightCommandsTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
commands *copyright.CopyrightCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
s.commands = copyright.NewCopyrightCommands(s.CopyrightRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToWork() {
|
||||||
|
s.Run("should add a copyright to a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Verify that the association was created in the database
|
||||||
|
var foundWork domain.Work
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundWork.Copyrights, 1)
|
||||||
|
s.Equal(copyright.ID, foundWork.Copyrights[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromWork() {
|
||||||
|
s.Run("should remove a copyright from a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
s.Require().NoError(s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveCopyrightFromWork(context.Background(), work.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Verify that the association was removed from the database
|
||||||
|
var foundWork domain.Work
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundWork.Copyrights, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToAuthor() {
|
||||||
|
s.Run("should add a copyright to an author", func() {
|
||||||
|
// Arrange
|
||||||
|
author := &domain.Author{Name: "Test Author"}
|
||||||
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundAuthor domain.Author
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundAuthor.Copyrights, 1)
|
||||||
|
s.Equal(copyright.ID, foundAuthor.Copyrights[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromAuthor() {
|
||||||
|
s.Run("should remove a copyright from an author", func() {
|
||||||
|
// Arrange
|
||||||
|
author := &domain.Author{Name: "Test Author"}
|
||||||
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
s.Require().NoError(s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveCopyrightFromAuthor(context.Background(), author.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundAuthor domain.Author
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundAuthor.Copyrights, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToBook() {
|
||||||
|
s.Run("should add a copyright to a book", func() {
|
||||||
|
// Arrange
|
||||||
|
book := &domain.Book{Title: "Test Book"}
|
||||||
|
s.Require().NoError(s.BookRepo.Create(context.Background(), book))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundBook domain.Book
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundBook.Copyrights, 1)
|
||||||
|
s.Equal(copyright.ID, foundBook.Copyrights[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromBook() {
|
||||||
|
s.Run("should remove a copyright from a book", func() {
|
||||||
|
// Arrange
|
||||||
|
book := &domain.Book{Title: "Test Book"}
|
||||||
|
s.Require().NoError(s.BookRepo.Create(context.Background(), book))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
s.Require().NoError(s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveCopyrightFromBook(context.Background(), book.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundBook domain.Book
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundBook.Copyrights, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToPublisher() {
|
||||||
|
s.Run("should add a copyright to a publisher", func() {
|
||||||
|
// Arrange
|
||||||
|
publisher := &domain.Publisher{Name: "Test Publisher"}
|
||||||
|
s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundPublisher domain.Publisher
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundPublisher.Copyrights, 1)
|
||||||
|
s.Equal(copyright.ID, foundPublisher.Copyrights[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromPublisher() {
|
||||||
|
s.Run("should remove a copyright from a publisher", func() {
|
||||||
|
// Arrange
|
||||||
|
publisher := &domain.Publisher{Name: "Test Publisher"}
|
||||||
|
s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
s.Require().NoError(s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveCopyrightFromPublisher(context.Background(), publisher.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundPublisher domain.Publisher
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundPublisher.Copyrights, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToSource() {
|
||||||
|
s.Run("should add a copyright to a source", func() {
|
||||||
|
// Arrange
|
||||||
|
source := &domain.Source{Name: "Test Source"}
|
||||||
|
s.Require().NoError(s.SourceRepo.Create(context.Background(), source))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundSource domain.Source
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundSource.Copyrights, 1)
|
||||||
|
s.Equal(copyright.ID, foundSource.Copyrights[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromSource() {
|
||||||
|
s.Run("should remove a copyright from a source", func() {
|
||||||
|
// Arrange
|
||||||
|
source := &domain.Source{Name: "Test Source"}
|
||||||
|
s.Require().NoError(s.SourceRepo.Create(context.Background(), source))
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||||
|
s.Require().NoError(s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveCopyrightFromSource(context.Background(), source.ID, copyright.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundSource domain.Source
|
||||||
|
err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundSource.Copyrights, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyrightCommands(t *testing.T) {
|
||||||
|
suite.Run(t, new(CopyrightCommandsTestSuite))
|
||||||
|
}
|
||||||
349
internal/app/copyright/commands_test.go
Normal file
349
internal/app/copyright/commands_test.go
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
package copyright
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CopyrightCommandsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockCopyrightRepository
|
||||||
|
commands *CopyrightCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) SetupTest() {
|
||||||
|
s.repo = &mockCopyrightRepository{}
|
||||||
|
s.commands = NewCopyrightCommands(s.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyrightCommandsSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CopyrightCommandsSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestCreateCopyright_Success() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.repo.createFunc = func(ctx context.Context, c *domain.Copyright) error {
|
||||||
|
assert.Equal(s.T(), copyright.Name, c.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := s.commands.CreateCopyright(context.Background(), copyright)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestCreateCopyright_Nil() {
|
||||||
|
err := s.commands.CreateCopyright(context.Background(), nil)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestCreateCopyright_EmptyName() {
|
||||||
|
copyright := &domain.Copyright{Identificator: "TC-123"}
|
||||||
|
err := s.commands.CreateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestCreateCopyright_EmptyIdentificator() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright"}
|
||||||
|
err := s.commands.CreateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestCreateCopyright_RepoError() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
s.repo.createFunc = func(ctx context.Context, c *domain.Copyright) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.CreateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_Success() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
copyright.ID = 1
|
||||||
|
s.repo.updateFunc = func(ctx context.Context, c *domain.Copyright) error {
|
||||||
|
assert.Equal(s.T(), copyright.Name, c.Name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), copyright)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_Nil() {
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), nil)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_ZeroID() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_EmptyName() {
|
||||||
|
copyright := &domain.Copyright{Identificator: "TC-123"}
|
||||||
|
copyright.ID = 1
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_EmptyIdentificator() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright"}
|
||||||
|
copyright.ID = 1
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestUpdateCopyright_RepoError() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||||
|
copyright.ID = 1
|
||||||
|
s.repo.updateFunc = func(ctx context.Context, c *domain.Copyright) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.UpdateCopyright(context.Background(), copyright)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestDeleteCopyright_Success() {
|
||||||
|
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||||
|
assert.Equal(s.T(), uint(1), id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := s.commands.DeleteCopyright(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestDeleteCopyright_ZeroID() {
|
||||||
|
err := s.commands.DeleteCopyright(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestDeleteCopyright_RepoError() {
|
||||||
|
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.DeleteCopyright(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_Success() {
|
||||||
|
err := s.commands.AddCopyrightToWork(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_ZeroID() {
|
||||||
|
err := s.commands.AddCopyrightToWork(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
err = s.commands.AddCopyrightToWork(context.Background(), 1, 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_Success() {
|
||||||
|
err := s.commands.RemoveCopyrightFromWork(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_RepoError() {
|
||||||
|
s.repo.addCopyrightToWorkFunc = func(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddCopyrightToWork(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_RepoError() {
|
||||||
|
s.repo.removeCopyrightFromWorkFunc = func(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveCopyrightFromWork(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_ZeroID() {
|
||||||
|
err := s.commands.RemoveCopyrightFromWork(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_ZeroID() {
|
||||||
|
err := s.commands.AddCopyrightToAuthor(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_ZeroID() {
|
||||||
|
err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_ZeroID() {
|
||||||
|
err := s.commands.AddCopyrightToBook(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_ZeroID() {
|
||||||
|
err := s.commands.RemoveCopyrightFromBook(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_ZeroID() {
|
||||||
|
err := s.commands.AddCopyrightToPublisher(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_ZeroID() {
|
||||||
|
err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_ZeroID() {
|
||||||
|
err := s.commands.AddCopyrightToSource(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_ZeroID() {
|
||||||
|
err := s.commands.RemoveCopyrightFromSource(context.Background(), 0, 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_RepoError() {
|
||||||
|
s.repo.addCopyrightToAuthorFunc = func(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddCopyrightToAuthor(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_RepoError() {
|
||||||
|
s.repo.removeCopyrightFromAuthorFunc = func(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_RepoError() {
|
||||||
|
s.repo.addCopyrightToBookFunc = func(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddCopyrightToBook(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_RepoError() {
|
||||||
|
s.repo.removeCopyrightFromBookFunc = func(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveCopyrightFromBook(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_RepoError() {
|
||||||
|
s.repo.addCopyrightToPublisherFunc = func(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddCopyrightToPublisher(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_RepoError() {
|
||||||
|
s.repo.removeCopyrightFromPublisherFunc = func(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_RepoError() {
|
||||||
|
s.repo.addCopyrightToSourceFunc = func(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddCopyrightToSource(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_RepoError() {
|
||||||
|
s.repo.removeCopyrightFromSourceFunc = func(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveCopyrightFromSource(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_Success() {
|
||||||
|
err := s.commands.AddCopyrightToAuthor(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_Success() {
|
||||||
|
err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_Success() {
|
||||||
|
err := s.commands.AddCopyrightToBook(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_Success() {
|
||||||
|
err := s.commands.RemoveCopyrightFromBook(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_Success() {
|
||||||
|
err := s.commands.AddCopyrightToPublisher(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_Success() {
|
||||||
|
err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_Success() {
|
||||||
|
err := s.commands.AddCopyrightToSource(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_Success() {
|
||||||
|
err := s.commands.RemoveCopyrightFromSource(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_Success() {
|
||||||
|
translation := &domain.CopyrightTranslation{CopyrightID: 1, LanguageCode: "en", Message: "Test"}
|
||||||
|
err := s.commands.AddTranslation(context.Background(), translation)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_Nil() {
|
||||||
|
err := s.commands.AddTranslation(context.Background(), nil)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_ZeroCopyrightID() {
|
||||||
|
translation := &domain.CopyrightTranslation{LanguageCode: "en", Message: "Test"}
|
||||||
|
err := s.commands.AddTranslation(context.Background(), translation)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_RepoError() {
|
||||||
|
translation := &domain.CopyrightTranslation{CopyrightID: 1, LanguageCode: "en", Message: "Test"}
|
||||||
|
s.repo.addTranslationFunc = func(ctx context.Context, t *domain.CopyrightTranslation) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddTranslation(context.Background(), translation)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_EmptyLanguageCode() {
|
||||||
|
translation := &domain.CopyrightTranslation{CopyrightID: 1, Message: "Test"}
|
||||||
|
err := s.commands.AddTranslation(context.Background(), translation)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightCommandsSuite) TestAddTranslation_EmptyMessage() {
|
||||||
|
translation := &domain.CopyrightTranslation{CopyrightID: 1, LanguageCode: "en"}
|
||||||
|
err := s.commands.AddTranslation(context.Background(), translation)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
223
internal/app/copyright/main_test.go
Normal file
223
internal/app/copyright/main_test.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
package copyright
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockCopyrightRepository struct {
|
||||||
|
createFunc func(ctx context.Context, copyright *domain.Copyright) error
|
||||||
|
updateFunc func(ctx context.Context, copyright *domain.Copyright) error
|
||||||
|
deleteFunc func(ctx context.Context, id uint) error
|
||||||
|
addCopyrightToWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
|
||||||
|
removeCopyrightFromWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
|
||||||
|
addCopyrightToAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
|
||||||
|
removeCopyrightFromAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
|
||||||
|
addCopyrightToBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
|
||||||
|
removeCopyrightFromBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
|
||||||
|
addCopyrightToPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error
|
||||||
|
removeCopyrightFromPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error
|
||||||
|
addCopyrightToSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
|
||||||
|
removeCopyrightFromSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
|
||||||
|
addTranslationFunc func(ctx context.Context, translation *domain.CopyrightTranslation) error
|
||||||
|
getByIDFunc func(ctx context.Context, id uint) (*domain.Copyright, error)
|
||||||
|
listAllFunc func(ctx context.Context) ([]domain.Copyright, error)
|
||||||
|
getTranslationsFunc func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error)
|
||||||
|
getTranslationByLanguageFunc func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCopyrightRepository) Create(ctx context.Context, copyright *domain.Copyright) error {
|
||||||
|
if m.createFunc != nil {
|
||||||
|
return m.createFunc(ctx, copyright)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) Update(ctx context.Context, copyright *domain.Copyright) error {
|
||||||
|
if m.updateFunc != nil {
|
||||||
|
return m.updateFunc(ctx, copyright)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) Delete(ctx context.Context, id uint) error {
|
||||||
|
if m.deleteFunc != nil {
|
||||||
|
return m.deleteFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
if m.addCopyrightToWorkFunc != nil {
|
||||||
|
return m.addCopyrightToWorkFunc(ctx, workID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
if m.removeCopyrightFromWorkFunc != nil {
|
||||||
|
return m.removeCopyrightFromWorkFunc(ctx, workID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
if m.addCopyrightToAuthorFunc != nil {
|
||||||
|
return m.addCopyrightToAuthorFunc(ctx, authorID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
if m.removeCopyrightFromAuthorFunc != nil {
|
||||||
|
return m.removeCopyrightFromAuthorFunc(ctx, authorID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
if m.addCopyrightToBookFunc != nil {
|
||||||
|
return m.addCopyrightToBookFunc(ctx, bookID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
if m.removeCopyrightFromBookFunc != nil {
|
||||||
|
return m.removeCopyrightFromBookFunc(ctx, bookID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
if m.addCopyrightToPublisherFunc != nil {
|
||||||
|
return m.addCopyrightToPublisherFunc(ctx, publisherID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
if m.removeCopyrightFromPublisherFunc != nil {
|
||||||
|
return m.removeCopyrightFromPublisherFunc(ctx, publisherID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
if m.addCopyrightToSourceFunc != nil {
|
||||||
|
return m.addCopyrightToSourceFunc(ctx, sourceID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
if m.removeCopyrightFromSourceFunc != nil {
|
||||||
|
return m.removeCopyrightFromSourceFunc(ctx, sourceID, copyrightID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
||||||
|
if m.addTranslationFunc != nil {
|
||||||
|
return m.addTranslationFunc(ctx, translation)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||||
|
if m.getByIDFunc != nil {
|
||||||
|
return m.getByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) ListAll(ctx context.Context) ([]domain.Copyright, error) {
|
||||||
|
if m.listAllFunc != nil {
|
||||||
|
return m.listAllFunc(ctx)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||||
|
if m.getTranslationsFunc != nil {
|
||||||
|
return m.getTranslationsFunc(ctx, copyrightID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
|
if m.getTranslationByLanguageFunc != nil {
|
||||||
|
return m.getTranslationByLanguageFunc(ctx, copyrightID, languageCode)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the rest of the CopyrightRepository interface with empty methods.
|
||||||
|
func (m *mockCopyrightRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Copyright], error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||||
|
func (m *mockCopyrightRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Copyright) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Copyright, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Copyright) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Copyright, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Copyright, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Copyright, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||||
|
func (m *mockCopyrightRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||||
|
func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockWorkRepository struct {
|
||||||
|
domain.WorkRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error)
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockAuthorRepository struct {
|
||||||
|
domain.AuthorRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
|
||||||
|
}
|
||||||
|
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockBookRepository struct {
|
||||||
|
domain.BookRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
|
||||||
|
}
|
||||||
|
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockPublisherRepository struct {
|
||||||
|
domain.PublisherRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
|
||||||
|
}
|
||||||
|
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockSourceRepository struct {
|
||||||
|
domain.SourceRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
|
||||||
|
}
|
||||||
|
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@ -4,16 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyrightQueries contains the query handlers for copyright.
|
// CopyrightQueries contains the query handlers for copyright.
|
||||||
type CopyrightQueries struct {
|
type CopyrightQueries struct {
|
||||||
repo domain.CopyrightRepository
|
repo domain.CopyrightRepository
|
||||||
|
workRepo domain.WorkRepository
|
||||||
|
authorRepo domain.AuthorRepository
|
||||||
|
bookRepo domain.BookRepository
|
||||||
|
publisherRepo domain.PublisherRepository
|
||||||
|
sourceRepo domain.SourceRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCopyrightQueries creates a new CopyrightQueries handler.
|
// NewCopyrightQueries creates a new CopyrightQueries handler.
|
||||||
func NewCopyrightQueries(repo domain.CopyrightRepository) *CopyrightQueries {
|
func NewCopyrightQueries(repo domain.CopyrightRepository, workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *CopyrightQueries {
|
||||||
return &CopyrightQueries{repo: repo}
|
return &CopyrightQueries{repo: repo, workRepo: workRepo, authorRepo: authorRepo, bookRepo: bookRepo, publisherRepo: publisherRepo, sourceRepo: sourceRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightByID retrieves a copyright by ID.
|
// GetCopyrightByID retrieves a copyright by ID.
|
||||||
@ -21,33 +27,66 @@ func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*doma
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Getting copyright by ID", log.F("id", id))
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCopyrights retrieves all copyrights.
|
// ListCopyrights retrieves all copyrights.
|
||||||
func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) {
|
func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) {
|
||||||
|
log.LogDebug("Listing all copyrights")
|
||||||
// Note: This might need pagination in the future.
|
// Note: This might need pagination in the future.
|
||||||
// For now, it mirrors the old service's behavior.
|
// For now, it mirrors the old service's behavior.
|
||||||
return q.repo.ListAll(ctx)
|
return q.repo.ListAll(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForEntity gets all copyrights for a specific entity.
|
// GetCopyrightsForWork gets all copyrights for a specific work.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForEntity(ctx context.Context, entityID uint, entityType string) ([]domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) {
|
||||||
if entityID == 0 {
|
log.LogDebug("Getting copyrights for work", log.F("work_id", workID))
|
||||||
return nil, errors.New("invalid entity ID")
|
work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if entityType == "" {
|
return work.Copyrights, nil
|
||||||
return nil, errors.New("entity type cannot be empty")
|
|
||||||
}
|
|
||||||
return q.repo.GetByEntity(ctx, entityID, entityType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEntitiesByCopyright gets all entities that have a specific copyright.
|
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
||||||
func (q *CopyrightQueries) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]domain.Copyrightable, error) {
|
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) {
|
||||||
if copyrightID == 0 {
|
log.LogDebug("Getting copyrights for author", log.F("author_id", authorID))
|
||||||
return nil, errors.New("invalid copyright ID")
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return q.repo.GetEntitiesByCopyright(ctx, copyrightID)
|
return author.Copyrights, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopyrightsForBook gets all copyrights for a specific book.
|
||||||
|
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) {
|
||||||
|
log.LogDebug("Getting copyrights for book", log.F("book_id", bookID))
|
||||||
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return book.Copyrights, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
||||||
|
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) {
|
||||||
|
log.LogDebug("Getting copyrights for publisher", log.F("publisher_id", publisherID))
|
||||||
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return publisher.Copyrights, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCopyrightsForSource gets all copyrights for a specific source.
|
||||||
|
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) {
|
||||||
|
log.LogDebug("Getting copyrights for source", log.F("source_id", sourceID))
|
||||||
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return source.Copyrights, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTranslations gets all translations for a copyright.
|
// GetTranslations gets all translations for a copyright.
|
||||||
@ -55,6 +94,7 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint
|
|||||||
if copyrightID == 0 {
|
if copyrightID == 0 {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Getting translations for copyright", log.F("copyright_id", copyrightID))
|
||||||
return q.repo.GetTranslations(ctx, copyrightID)
|
return q.repo.GetTranslations(ctx, copyrightID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,5 +106,6 @@ func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrig
|
|||||||
if languageCode == "" {
|
if languageCode == "" {
|
||||||
return nil, errors.New("language code cannot be empty")
|
return nil, errors.New("language code cannot be empty")
|
||||||
}
|
}
|
||||||
|
log.LogDebug("Getting translation by language for copyright", log.F("copyright_id", copyrightID), log.F("language", languageCode))
|
||||||
return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode)
|
return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode)
|
||||||
}
|
}
|
||||||
|
|||||||
231
internal/app/copyright/queries_test.go
Normal file
231
internal/app/copyright/queries_test.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
package copyright
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CopyrightQueriesSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockCopyrightRepository
|
||||||
|
workRepo *mockWorkRepository
|
||||||
|
authorRepo *mockAuthorRepository
|
||||||
|
bookRepo *mockBookRepository
|
||||||
|
publisherRepo *mockPublisherRepository
|
||||||
|
sourceRepo *mockSourceRepository
|
||||||
|
queries *CopyrightQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) SetupTest() {
|
||||||
|
s.repo = &mockCopyrightRepository{}
|
||||||
|
s.workRepo = &mockWorkRepository{}
|
||||||
|
s.authorRepo = &mockAuthorRepository{}
|
||||||
|
s.bookRepo = &mockBookRepository{}
|
||||||
|
s.publisherRepo = &mockPublisherRepository{}
|
||||||
|
s.sourceRepo = &mockSourceRepository{}
|
||||||
|
s.queries = NewCopyrightQueries(s.repo, s.workRepo, s.authorRepo, s.bookRepo, s.publisherRepo, s.sourceRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyrightQueriesSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CopyrightQueriesSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightByID_Success() {
|
||||||
|
copyright := &domain.Copyright{Name: "Test Copyright"}
|
||||||
|
copyright.ID = 1
|
||||||
|
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||||
|
return copyright, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightByID(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyright, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightByID_ZeroID() {
|
||||||
|
c, err := s.queries.GetCopyrightByID(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestListCopyrights_Success() {
|
||||||
|
copyrights := []domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.repo.listAllFunc = func(ctx context.Context) ([]domain.Copyright, error) {
|
||||||
|
return copyrights, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.ListCopyrights(context.Background())
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForAuthor_RepoError() {
|
||||||
|
s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForAuthor(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForBook_RepoError() {
|
||||||
|
s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForBook(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForPublisher_RepoError() {
|
||||||
|
s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForPublisher(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForSource_RepoError() {
|
||||||
|
s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForSource(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_Success() {
|
||||||
|
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
return &domain.Work{Copyrights: copyrights}, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForWork(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_RepoError() {
|
||||||
|
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForWork(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForAuthor_Success() {
|
||||||
|
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
return &domain.Author{Copyrights: copyrights}, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForAuthor(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForBook_Success() {
|
||||||
|
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
return &domain.Book{Copyrights: copyrights}, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForBook(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForPublisher_Success() {
|
||||||
|
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
return &domain.Publisher{Copyrights: copyrights}, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForPublisher(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightsForSource_Success() {
|
||||||
|
copyrights := []*domain.Copyright{{Name: "Test Copyright"}}
|
||||||
|
s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
return &domain.Source{Copyrights: copyrights}, nil
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightsForSource(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), copyrights, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslations_Success() {
|
||||||
|
translations := []domain.CopyrightTranslation{{Message: "Test"}}
|
||||||
|
s.repo.getTranslationsFunc = func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||||
|
return translations, nil
|
||||||
|
}
|
||||||
|
t, err := s.queries.GetTranslations(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), translations, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslations_ZeroID() {
|
||||||
|
t, err := s.queries.GetTranslations(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetCopyrightByID_RepoError() {
|
||||||
|
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.GetCopyrightByID(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestListCopyrights_RepoError() {
|
||||||
|
s.repo.listAllFunc = func(ctx context.Context) ([]domain.Copyright, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
c, err := s.queries.ListCopyrights(context.Background())
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslations_RepoError() {
|
||||||
|
s.repo.getTranslationsFunc = func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
t, err := s.queries.GetTranslations(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_RepoError() {
|
||||||
|
s.repo.getTranslationByLanguageFunc = func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
t, err := s.queries.GetTranslationByLanguage(context.Background(), 1, "en")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_Success() {
|
||||||
|
translation := &domain.CopyrightTranslation{Message: "Test"}
|
||||||
|
s.repo.getTranslationByLanguageFunc = func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
|
return translation, nil
|
||||||
|
}
|
||||||
|
t, err := s.queries.GetTranslationByLanguage(context.Background(), 1, "en")
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), translation, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_ZeroID() {
|
||||||
|
t, err := s.queries.GetTranslationByLanguage(context.Background(), 0, "en")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_EmptyLang() {
|
||||||
|
t, err := s.queries.GetTranslationByLanguage(context.Background(), 1, "")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), t)
|
||||||
|
}
|
||||||
44
internal/app/like/commands.go
Normal file
44
internal/app/like/commands.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package like
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LikeCommands contains the command handlers for the like aggregate.
|
||||||
|
type LikeCommands struct {
|
||||||
|
repo domain.LikeRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLikeCommands creates a new LikeCommands handler.
|
||||||
|
func NewLikeCommands(repo domain.LikeRepository) *LikeCommands {
|
||||||
|
return &LikeCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLikeInput represents the input for creating a new like.
|
||||||
|
type CreateLikeInput struct {
|
||||||
|
UserID uint
|
||||||
|
WorkID *uint
|
||||||
|
TranslationID *uint
|
||||||
|
CommentID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLike creates a new like.
|
||||||
|
func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*domain.Like, error) {
|
||||||
|
like := &domain.Like{
|
||||||
|
UserID: input.UserID,
|
||||||
|
WorkID: input.WorkID,
|
||||||
|
TranslationID: input.TranslationID,
|
||||||
|
CommentID: input.CommentID,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, like)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return like, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLike deletes a like by ID.
|
||||||
|
func (c *LikeCommands) DeleteLike(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
46
internal/app/like/queries.go
Normal file
46
internal/app/like/queries.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package like
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LikeQueries contains the query handlers for the like aggregate.
|
||||||
|
type LikeQueries struct {
|
||||||
|
repo domain.LikeRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLikeQueries creates a new LikeQueries handler.
|
||||||
|
func NewLikeQueries(repo domain.LikeRepository) *LikeQueries {
|
||||||
|
return &LikeQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like returns a like by ID.
|
||||||
|
func (q *LikeQueries) Like(ctx context.Context, id uint) (*domain.Like, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikesByUserID returns all likes for a user.
|
||||||
|
func (q *LikeQueries) LikesByUserID(ctx context.Context, userID uint) ([]domain.Like, error) {
|
||||||
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikesByWorkID returns all likes for a work.
|
||||||
|
func (q *LikeQueries) LikesByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikesByTranslationID returns all likes for a translation.
|
||||||
|
func (q *LikeQueries) LikesByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) {
|
||||||
|
return q.repo.ListByTranslationID(ctx, translationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikesByCommentID returns all likes for a comment.
|
||||||
|
func (q *LikeQueries) LikesByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) {
|
||||||
|
return q.repo.ListByCommentID(ctx, commentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Likes returns all likes.
|
||||||
|
func (q *LikeQueries) Likes(ctx context.Context) ([]domain.Like, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/like/service.go
Normal file
17
internal/app/like/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package like
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the like aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *LikeCommands
|
||||||
|
Queries *LikeQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new like Service.
|
||||||
|
func NewService(repo domain.LikeRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewLikeCommands(repo),
|
||||||
|
Queries: NewLikeQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,82 +2,25 @@ package localization
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service resolves localized attributes using translations
|
// Service handles localization-related operations.
|
||||||
type Service interface {
|
type Service struct {
|
||||||
GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error)
|
repo domain.LocalizationRepository
|
||||||
GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
// NewService creates a new localization service.
|
||||||
translationRepo domain.TranslationRepository
|
func NewService(repo domain.LocalizationRepository) *Service {
|
||||||
|
return &Service{repo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(translationRepo domain.TranslationRepository) Service {
|
// GetTranslation returns a translation for a given key and language.
|
||||||
return &service{translationRepo: translationRepo}
|
func (s *Service) GetTranslation(ctx context.Context, key string, language string) (string, error) {
|
||||||
|
return s.repo.GetTranslation(ctx, key, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error) {
|
// GetTranslations returns a map of translations for a given set of keys and language.
|
||||||
if workID == 0 {
|
func (s *Service) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) {
|
||||||
return "", errors.New("invalid work ID")
|
return s.repo.GetTranslations(ctx, keys, language)
|
||||||
}
|
|
||||||
translations, err := s.translationRepo.ListByWorkID(ctx, workID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return pickContent(translations, preferredLanguage), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) {
|
|
||||||
if authorID == 0 {
|
|
||||||
return "", errors.New("invalid author ID")
|
|
||||||
}
|
|
||||||
translations, err := s.translationRepo.ListByEntity(ctx, "Author", authorID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Prefer Description from Translation as biography proxy
|
|
||||||
var byLang *domain.Translation
|
|
||||||
for i := range translations {
|
|
||||||
tr := &translations[i]
|
|
||||||
if tr.IsOriginalLanguage && tr.Description != "" {
|
|
||||||
return tr.Description, nil
|
|
||||||
}
|
|
||||||
if tr.Language == preferredLanguage && byLang == nil && tr.Description != "" {
|
|
||||||
byLang = tr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if byLang != nil {
|
|
||||||
return byLang.Description, nil
|
|
||||||
}
|
|
||||||
// fallback to any non-empty description
|
|
||||||
for i := range translations {
|
|
||||||
if translations[i].Description != "" {
|
|
||||||
return translations[i].Description, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickContent(translations []domain.Translation, preferredLanguage string) string {
|
|
||||||
var byLang *domain.Translation
|
|
||||||
for i := range translations {
|
|
||||||
tr := &translations[i]
|
|
||||||
if tr.IsOriginalLanguage {
|
|
||||||
return tr.Content
|
|
||||||
}
|
|
||||||
if tr.Language == preferredLanguage && byLang == nil {
|
|
||||||
byLang = tr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if byLang != nil {
|
|
||||||
return byLang.Content
|
|
||||||
}
|
|
||||||
if len(translations) > 0 {
|
|
||||||
return translations[0].Content
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
65
internal/app/localization/service_test.go
Normal file
65
internal/app/localization/service_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package localization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLocalizationRepository struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetTranslation(ctx context.Context, key string, language string) (string, error) {
|
||||||
|
args := m.Called(ctx, key, language)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) {
|
||||||
|
args := m.Called(ctx, keys, language)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(map[string]string), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalizationService_GetTranslation(t *testing.T) {
|
||||||
|
repo := new(mockLocalizationRepository)
|
||||||
|
service := NewService(repo)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
key := "test_key"
|
||||||
|
language := "en"
|
||||||
|
expectedTranslation := "Test Translation"
|
||||||
|
|
||||||
|
repo.On("GetTranslation", ctx, key, language).Return(expectedTranslation, nil)
|
||||||
|
|
||||||
|
translation, err := service.GetTranslation(ctx, key, language)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTranslation, translation)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalizationService_GetTranslations(t *testing.T) {
|
||||||
|
repo := new(mockLocalizationRepository)
|
||||||
|
service := NewService(repo)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keys := []string{"key1", "key2"}
|
||||||
|
language := "en"
|
||||||
|
expectedTranslations := map[string]string{
|
||||||
|
"key1": "Translation 1",
|
||||||
|
"key2": "Translation 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.On("GetTranslations", ctx, keys, language).Return(expectedTranslations, nil)
|
||||||
|
|
||||||
|
translations, err := service.GetTranslations(ctx, keys, language)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedTranslations, translations)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
100
internal/app/monetization/commands.go
Normal file
100
internal/app/monetization/commands.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package monetization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonetizationCommands contains the command handlers for monetization.
|
||||||
|
type MonetizationCommands struct {
|
||||||
|
repo domain.MonetizationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonetizationCommands creates a new MonetizationCommands handler.
|
||||||
|
func NewMonetizationCommands(repo domain.MonetizationRepository) *MonetizationCommands {
|
||||||
|
return &MonetizationCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMonetizationToWork adds a monetization to a work.
|
||||||
|
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
if workID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid work ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding monetization to work", log.F("work_id", workID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.AddMonetizationToWork(ctx, workID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveMonetizationFromWork removes a monetization from a work.
|
||||||
|
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
if workID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid work ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing monetization from work", log.F("work_id", workID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
if authorID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid author ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding monetization to author", log.F("author_id", authorID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
if authorID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid author ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing monetization from author", log.F("author_id", authorID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
if bookID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid book ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding monetization to book", log.F("book_id", bookID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
if bookID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid book ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing monetization from book", log.F("book_id", bookID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid publisher ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding monetization to publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid publisher ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing monetization from publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid source ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Adding monetization to source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
|
return errors.New("invalid source ID or monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Removing monetization from source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID))
|
||||||
|
return c.repo.RemoveMonetizationFromSource(ctx, sourceID, monetizationID)
|
||||||
|
}
|
||||||
217
internal/app/monetization/commands_integration_test.go
Normal file
217
internal/app/monetization/commands_integration_test.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package monetization_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/app/monetization"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MonetizationCommandsTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
commands *monetization.MonetizationCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
s.commands = monetization.NewMonetizationCommands(s.MonetizationRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToWork() {
|
||||||
|
s.Run("should add a monetization to a work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddMonetizationToWork(context.Background(), work.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Verify that the association was created in the database
|
||||||
|
var foundWork domain.Work
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundWork, work.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundWork.Monetizations, 1)
|
||||||
|
s.Equal(monetization.ID, foundWork.Monetizations[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToAuthor() {
|
||||||
|
s.Run("should add a monetization to an author", func() {
|
||||||
|
// Arrange
|
||||||
|
author := &domain.Author{Name: "Test Author"}
|
||||||
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundAuthor domain.Author
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundAuthor.Monetizations, 1)
|
||||||
|
s.Equal(monetization.ID, foundAuthor.Monetizations[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromAuthor() {
|
||||||
|
s.Run("should remove a monetization from an author", func() {
|
||||||
|
// Arrange
|
||||||
|
author := &domain.Author{Name: "Test Author"}
|
||||||
|
s.Require().NoError(s.AuthorRepo.Create(context.Background(), author))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
s.Require().NoError(s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveMonetizationFromAuthor(context.Background(), author.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundAuthor domain.Author
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundAuthor.Monetizations, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToBook() {
|
||||||
|
s.Run("should add a monetization to a book", func() {
|
||||||
|
// Arrange
|
||||||
|
book := &domain.Book{Title: "Test Book"}
|
||||||
|
s.Require().NoError(s.BookRepo.Create(context.Background(), book))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundBook domain.Book
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundBook.Monetizations, 1)
|
||||||
|
s.Equal(monetization.ID, foundBook.Monetizations[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromBook() {
|
||||||
|
s.Run("should remove a monetization from a book", func() {
|
||||||
|
// Arrange
|
||||||
|
book := &domain.Book{Title: "Test Book"}
|
||||||
|
s.Require().NoError(s.BookRepo.Create(context.Background(), book))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
s.Require().NoError(s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveMonetizationFromBook(context.Background(), book.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundBook domain.Book
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundBook.Monetizations, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToPublisher() {
|
||||||
|
s.Run("should add a monetization to a publisher", func() {
|
||||||
|
// Arrange
|
||||||
|
publisher := &domain.Publisher{Name: "Test Publisher"}
|
||||||
|
s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundPublisher domain.Publisher
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundPublisher.Monetizations, 1)
|
||||||
|
s.Equal(monetization.ID, foundPublisher.Monetizations[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromPublisher() {
|
||||||
|
s.Run("should remove a monetization from a publisher", func() {
|
||||||
|
// Arrange
|
||||||
|
publisher := &domain.Publisher{Name: "Test Publisher"}
|
||||||
|
s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
s.Require().NoError(s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveMonetizationFromPublisher(context.Background(), publisher.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundPublisher domain.Publisher
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundPublisher.Monetizations, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToSource() {
|
||||||
|
s.Run("should add a monetization to a source", func() {
|
||||||
|
// Arrange
|
||||||
|
source := &domain.Source{Name: "Test Source"}
|
||||||
|
s.Require().NoError(s.SourceRepo.Create(context.Background(), source))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundSource domain.Source
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundSource.Monetizations, 1)
|
||||||
|
s.Equal(monetization.ID, foundSource.Monetizations[0].ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromSource() {
|
||||||
|
s.Run("should remove a monetization from a source", func() {
|
||||||
|
// Arrange
|
||||||
|
source := &domain.Source{Name: "Test Source"}
|
||||||
|
s.Require().NoError(s.SourceRepo.Create(context.Background(), source))
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||||
|
s.Require().NoError(s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.commands.RemoveMonetizationFromSource(context.Background(), source.ID, monetization.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
var foundSource domain.Source
|
||||||
|
err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(foundSource.Monetizations, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonetizationCommands(t *testing.T) {
|
||||||
|
suite.Run(t, new(MonetizationCommandsTestSuite))
|
||||||
|
}
|
||||||
206
internal/app/monetization/commands_test.go
Normal file
206
internal/app/monetization/commands_test.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package monetization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MonetizationCommandsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockMonetizationRepository
|
||||||
|
commands *MonetizationCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) SetupTest() {
|
||||||
|
s.repo = &mockMonetizationRepository{}
|
||||||
|
s.commands = NewMonetizationCommands(s.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonetizationCommandsSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(MonetizationCommandsSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_Success() {
|
||||||
|
err := s.commands.AddMonetizationToWork(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_ZeroID() {
|
||||||
|
err := s.commands.AddMonetizationToWork(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
err = s.commands.AddMonetizationToWork(context.Background(), 1, 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_RepoError() {
|
||||||
|
s.repo.addMonetizationToWorkFunc = func(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddMonetizationToWork(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_Success() {
|
||||||
|
err := s.commands.RemoveMonetizationFromWork(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_ZeroID() {
|
||||||
|
err := s.commands.RemoveMonetizationFromWork(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_RepoError() {
|
||||||
|
s.repo.removeMonetizationFromWorkFunc = func(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveMonetizationFromWork(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_Success() {
|
||||||
|
err := s.commands.AddMonetizationToAuthor(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_ZeroID() {
|
||||||
|
err := s.commands.AddMonetizationToAuthor(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_RepoError() {
|
||||||
|
s.repo.addMonetizationToAuthorFunc = func(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddMonetizationToAuthor(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_Success() {
|
||||||
|
err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_ZeroID() {
|
||||||
|
err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_RepoError() {
|
||||||
|
s.repo.removeMonetizationFromAuthorFunc = func(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_Success() {
|
||||||
|
err := s.commands.AddMonetizationToBook(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_ZeroID() {
|
||||||
|
err := s.commands.AddMonetizationToBook(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_RepoError() {
|
||||||
|
s.repo.addMonetizationToBookFunc = func(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddMonetizationToBook(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_Success() {
|
||||||
|
err := s.commands.RemoveMonetizationFromBook(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_ZeroID() {
|
||||||
|
err := s.commands.RemoveMonetizationFromBook(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_RepoError() {
|
||||||
|
s.repo.removeMonetizationFromBookFunc = func(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveMonetizationFromBook(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_Success() {
|
||||||
|
err := s.commands.AddMonetizationToPublisher(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_ZeroID() {
|
||||||
|
err := s.commands.AddMonetizationToPublisher(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_RepoError() {
|
||||||
|
s.repo.addMonetizationToPublisherFunc = func(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddMonetizationToPublisher(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_Success() {
|
||||||
|
err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_ZeroID() {
|
||||||
|
err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_RepoError() {
|
||||||
|
s.repo.removeMonetizationFromPublisherFunc = func(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_Success() {
|
||||||
|
err := s.commands.AddMonetizationToSource(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_ZeroID() {
|
||||||
|
err := s.commands.AddMonetizationToSource(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_RepoError() {
|
||||||
|
s.repo.addMonetizationToSourceFunc = func(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.AddMonetizationToSource(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_Success() {
|
||||||
|
err := s.commands.RemoveMonetizationFromSource(context.Background(), 1, 2)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_ZeroID() {
|
||||||
|
err := s.commands.RemoveMonetizationFromSource(context.Background(), 0, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_RepoError() {
|
||||||
|
s.repo.removeMonetizationFromSourceFunc = func(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.RemoveMonetizationFromSource(context.Background(), 1, 2)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
148
internal/app/monetization/main_test.go
Normal file
148
internal/app/monetization/main_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package monetization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockMonetizationRepository struct {
|
||||||
|
domain.MonetizationRepository
|
||||||
|
addMonetizationToWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
|
||||||
|
removeMonetizationFromWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
|
||||||
|
addMonetizationToAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
|
||||||
|
removeMonetizationFromAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
|
||||||
|
addMonetizationToBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
|
||||||
|
removeMonetizationFromBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
|
||||||
|
addMonetizationToPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error
|
||||||
|
removeMonetizationFromPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error
|
||||||
|
addMonetizationToSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
|
||||||
|
removeMonetizationFromSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
|
||||||
|
getByIDFunc func(ctx context.Context, id uint) (*domain.Monetization, error)
|
||||||
|
listAllFunc func(ctx context.Context) ([]domain.Monetization, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
|
if m.getByIDFunc != nil {
|
||||||
|
return m.getByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMonetizationRepository) ListAll(ctx context.Context) ([]domain.Monetization, error) {
|
||||||
|
if m.listAllFunc != nil {
|
||||||
|
return m.listAllFunc(ctx)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMonetizationRepository) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
if m.addMonetizationToWorkFunc != nil {
|
||||||
|
return m.addMonetizationToWorkFunc(ctx, workID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
|
if m.removeMonetizationFromWorkFunc != nil {
|
||||||
|
return m.removeMonetizationFromWorkFunc(ctx, workID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
if m.addMonetizationToAuthorFunc != nil {
|
||||||
|
return m.addMonetizationToAuthorFunc(ctx, authorID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
|
if m.removeMonetizationFromAuthorFunc != nil {
|
||||||
|
return m.removeMonetizationFromAuthorFunc(ctx, authorID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
if m.addMonetizationToBookFunc != nil {
|
||||||
|
return m.addMonetizationToBookFunc(ctx, bookID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
|
if m.removeMonetizationFromBookFunc != nil {
|
||||||
|
return m.removeMonetizationFromBookFunc(ctx, bookID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
if m.addMonetizationToPublisherFunc != nil {
|
||||||
|
return m.addMonetizationToPublisherFunc(ctx, publisherID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
|
if m.removeMonetizationFromPublisherFunc != nil {
|
||||||
|
return m.removeMonetizationFromPublisherFunc(ctx, publisherID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
if m.addMonetizationToSourceFunc != nil {
|
||||||
|
return m.addMonetizationToSourceFunc(ctx, sourceID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockMonetizationRepository) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
|
if m.removeMonetizationFromSourceFunc != nil {
|
||||||
|
return m.removeMonetizationFromSourceFunc(ctx, sourceID, monetizationID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockWorkRepository struct {
|
||||||
|
domain.WorkRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error)
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockAuthorRepository struct {
|
||||||
|
domain.AuthorRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
|
||||||
|
}
|
||||||
|
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockBookRepository struct {
|
||||||
|
domain.BookRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
|
||||||
|
}
|
||||||
|
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockPublisherRepository struct {
|
||||||
|
domain.PublisherRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
|
||||||
|
}
|
||||||
|
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
type mockSourceRepository struct {
|
||||||
|
domain.SourceRepository
|
||||||
|
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
|
||||||
|
}
|
||||||
|
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
83
internal/app/monetization/queries.go
Normal file
83
internal/app/monetization/queries.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package monetization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonetizationQueries contains the query handlers for monetization.
|
||||||
|
type MonetizationQueries struct {
|
||||||
|
repo domain.MonetizationRepository
|
||||||
|
workRepo domain.WorkRepository
|
||||||
|
authorRepo domain.AuthorRepository
|
||||||
|
bookRepo domain.BookRepository
|
||||||
|
publisherRepo domain.PublisherRepository
|
||||||
|
sourceRepo domain.SourceRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMonetizationQueries creates a new MonetizationQueries handler.
|
||||||
|
func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, bookRepo domain.BookRepository, publisherRepo domain.PublisherRepository, sourceRepo domain.SourceRepository) *MonetizationQueries {
|
||||||
|
return &MonetizationQueries{repo: repo, workRepo: workRepo, authorRepo: authorRepo, bookRepo: bookRepo, publisherRepo: publisherRepo, sourceRepo: sourceRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMonetizationByID retrieves a monetization by ID.
|
||||||
|
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("invalid monetization ID")
|
||||||
|
}
|
||||||
|
log.LogDebug("Getting monetization by ID", log.F("id", id))
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListMonetizations retrieves all monetizations.
|
||||||
|
func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.Monetization, error) {
|
||||||
|
log.LogDebug("Listing all monetizations")
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) {
|
||||||
|
log.LogDebug("Getting monetizations for work", log.F("work_id", workID))
|
||||||
|
work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return work.Monetizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) {
|
||||||
|
log.LogDebug("Getting monetizations for author", log.F("author_id", authorID))
|
||||||
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return author.Monetizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) {
|
||||||
|
log.LogDebug("Getting monetizations for book", log.F("book_id", bookID))
|
||||||
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return book.Monetizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) {
|
||||||
|
log.LogDebug("Getting monetizations for publisher", log.F("publisher_id", publisherID))
|
||||||
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return publisher.Monetizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) {
|
||||||
|
log.LogDebug("Getting monetizations for source", log.F("source_id", sourceID))
|
||||||
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return source.Monetizations, nil
|
||||||
|
}
|
||||||
175
internal/app/monetization/queries_test.go
Normal file
175
internal/app/monetization/queries_test.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package monetization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MonetizationQueriesSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockMonetizationRepository
|
||||||
|
workRepo *mockWorkRepository
|
||||||
|
authorRepo *mockAuthorRepository
|
||||||
|
bookRepo *mockBookRepository
|
||||||
|
publisherRepo *mockPublisherRepository
|
||||||
|
sourceRepo *mockSourceRepository
|
||||||
|
queries *MonetizationQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) SetupTest() {
|
||||||
|
s.repo = &mockMonetizationRepository{}
|
||||||
|
s.workRepo = &mockWorkRepository{}
|
||||||
|
s.authorRepo = &mockAuthorRepository{}
|
||||||
|
s.bookRepo = &mockBookRepository{}
|
||||||
|
s.publisherRepo = &mockPublisherRepository{}
|
||||||
|
s.sourceRepo = &mockSourceRepository{}
|
||||||
|
s.queries = NewMonetizationQueries(s.repo, s.workRepo, s.authorRepo, s.bookRepo, s.publisherRepo, s.sourceRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonetizationQueriesSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(MonetizationQueriesSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationByID_Success() {
|
||||||
|
monetization := &domain.Monetization{Amount: 10.0}
|
||||||
|
monetization.ID = 1
|
||||||
|
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
|
return monetization, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationByID(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetization, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationByID_ZeroID() {
|
||||||
|
m, err := s.queries.GetMonetizationByID(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationByID_RepoError() {
|
||||||
|
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationByID(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestListMonetizations_RepoError() {
|
||||||
|
s.repo.listAllFunc = func(ctx context.Context) ([]domain.Monetization, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.ListMonetizations(context.Background())
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestListMonetizations_Success() {
|
||||||
|
monetizations := []domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.repo.listAllFunc = func(ctx context.Context) ([]domain.Monetization, error) {
|
||||||
|
return monetizations, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.ListMonetizations(context.Background())
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_Success() {
|
||||||
|
monetizations := []*domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
return &domain.Work{Monetizations: monetizations}, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForWork(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_RepoError() {
|
||||||
|
s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForWork(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForAuthor_Success() {
|
||||||
|
monetizations := []*domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
return &domain.Author{Monetizations: monetizations}, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForAuthor(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForAuthor_RepoError() {
|
||||||
|
s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForAuthor(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForBook_Success() {
|
||||||
|
monetizations := []*domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
return &domain.Book{Monetizations: monetizations}, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForBook(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForBook_RepoError() {
|
||||||
|
s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForBook(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForPublisher_Success() {
|
||||||
|
monetizations := []*domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
return &domain.Publisher{Monetizations: monetizations}, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForPublisher(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForPublisher_RepoError() {
|
||||||
|
s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForPublisher(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForSource_Success() {
|
||||||
|
monetizations := []*domain.Monetization{{Amount: 10.0}}
|
||||||
|
s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
return &domain.Source{Monetizations: monetizations}, nil
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForSource(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), monetizations, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MonetizationQueriesSuite) TestGetMonetizationsForSource_RepoError() {
|
||||||
|
s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
|
return nil, errors.New("db error")
|
||||||
|
}
|
||||||
|
m, err := s.queries.GetMonetizationsForSource(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), m)
|
||||||
|
}
|
||||||
@ -3,9 +3,9 @@ package search
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"tercul/internal/app/localization"
|
"tercul/internal/app/localization"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/platform/log"
|
||||||
"tercul/internal/platform/search"
|
"tercul/internal/platform/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,40 +15,31 @@ type IndexService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type indexService struct {
|
type indexService struct {
|
||||||
localization localization.Service
|
localization *localization.Service
|
||||||
translations domain.TranslationRepository
|
weaviate search.WeaviateWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexService(localization localization.Service, translations domain.TranslationRepository) IndexService {
|
func NewIndexService(localization *localization.Service, weaviate search.WeaviateWrapper) IndexService {
|
||||||
return &indexService{localization: localization, translations: translations}
|
return &indexService{localization: localization, weaviate: weaviate}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *indexService) IndexWork(ctx context.Context, work domain.Work) error {
|
func (s *indexService) IndexWork(ctx context.Context, work domain.Work) error {
|
||||||
|
log.LogDebug("Indexing work", log.F("work_id", work.ID))
|
||||||
|
// TODO: Get content from translation service
|
||||||
|
content := ""
|
||||||
// Choose best content snapshot for indexing
|
// Choose best content snapshot for indexing
|
||||||
content, err := s.localization.GetWorkContent(ctx, work.ID, work.Language)
|
// content, err := s.localization.GetWorkContent(ctx, work.ID, work.Language)
|
||||||
|
// if err != nil {
|
||||||
|
// log.LogError("Failed to get work content for indexing", log.F("work_id", work.ID), log.F("error", err))
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
err := s.weaviate.IndexWork(ctx, &work, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.LogError("Failed to index work in Weaviate", log.F("work_id", work.ID), log.F("error", err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.LogInfo("Successfully indexed work", log.F("work_id", work.ID))
|
||||||
props := map[string]interface{}{
|
|
||||||
"language": work.Language,
|
|
||||||
"title": work.Title,
|
|
||||||
"description": work.Description,
|
|
||||||
"status": work.Status,
|
|
||||||
}
|
|
||||||
if content != "" {
|
|
||||||
props["content"] = content
|
|
||||||
}
|
|
||||||
|
|
||||||
_, wErr := search.Client.Data().Creator().
|
|
||||||
WithClassName("Work").
|
|
||||||
WithID(formatID(work.ID)).
|
|
||||||
WithProperties(props).
|
|
||||||
Do(ctx)
|
|
||||||
if wErr != nil {
|
|
||||||
log.Printf("weaviate index error: %v", wErr)
|
|
||||||
return wErr
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
internal/app/search/service_test.go
Normal file
62
internal/app/search/service_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"tercul/internal/app/localization"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLocalizationRepository struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetTranslation(ctx context.Context, key string, language string) (string, error) {
|
||||||
|
args := m.Called(ctx, key, language)
|
||||||
|
return args.String(0), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLocalizationRepository) GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error) {
|
||||||
|
args := m.Called(ctx, keys, language)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
return args.Get(0).(map[string]string), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockWeaviateWrapper struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWeaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error {
|
||||||
|
args := m.Called(ctx, work, content)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIndexService_IndexWork(t *testing.T) {
|
||||||
|
localizationRepo := new(mockLocalizationRepository)
|
||||||
|
localizationService := localization.NewService(localizationRepo)
|
||||||
|
weaviateWrapper := new(mockWeaviateWrapper)
|
||||||
|
service := NewIndexService(localizationService, weaviateWrapper)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
work := domain.Work{
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
BaseModel: domain.BaseModel{ID: 1},
|
||||||
|
Language: "en",
|
||||||
|
},
|
||||||
|
Title: "Test Work",
|
||||||
|
}
|
||||||
|
|
||||||
|
// localizationRepo.On("GetTranslation", ctx, "work:1:content", "en").Return("Test content", nil)
|
||||||
|
weaviateWrapper.On("IndexWork", ctx, &work, "").Return(nil)
|
||||||
|
|
||||||
|
err := service.IndexWork(ctx, work)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// localizationRepo.AssertExpectations(t)
|
||||||
|
weaviateWrapper.AssertExpectations(t)
|
||||||
|
}
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"tercul/internal/jobs/linguistics"
|
|
||||||
syncjob "tercul/internal/jobs/sync"
|
|
||||||
"tercul/internal/platform/config"
|
|
||||||
"tercul/internal/platform/log"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServerFactory handles the creation of HTTP and background job servers
|
|
||||||
type ServerFactory struct {
|
|
||||||
appBuilder *ApplicationBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServerFactory creates a new ServerFactory
|
|
||||||
func NewServerFactory(appBuilder *ApplicationBuilder) *ServerFactory {
|
|
||||||
return &ServerFactory{
|
|
||||||
appBuilder: appBuilder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// CreateBackgroundJobServers creates and configures background job servers
|
|
||||||
func (f *ServerFactory) CreateBackgroundJobServers() ([]*asynq.Server, error) {
|
|
||||||
log.LogInfo("Setting up background job servers")
|
|
||||||
|
|
||||||
redisOpt := asynq.RedisClientOpt{
|
|
||||||
Addr: config.Cfg.RedisAddr,
|
|
||||||
Password: config.Cfg.RedisPassword,
|
|
||||||
DB: config.Cfg.RedisDB,
|
|
||||||
}
|
|
||||||
|
|
||||||
var servers []*asynq.Server
|
|
||||||
|
|
||||||
// Setup data synchronization server
|
|
||||||
log.LogInfo("Setting up data synchronization server",
|
|
||||||
log.F("concurrency", config.Cfg.MaxRetries))
|
|
||||||
|
|
||||||
syncServer := asynq.NewServer(redisOpt, asynq.Config{Concurrency: config.Cfg.MaxRetries})
|
|
||||||
|
|
||||||
// Create sync job instance
|
|
||||||
syncJobInstance := syncjob.NewSyncJob(
|
|
||||||
f.appBuilder.GetDB(),
|
|
||||||
f.appBuilder.GetAsynq(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register sync job handlers
|
|
||||||
syncjob.RegisterQueueHandlers(syncServer, syncJobInstance)
|
|
||||||
servers = append(servers, syncServer)
|
|
||||||
|
|
||||||
// Setup linguistic analysis server
|
|
||||||
log.LogInfo("Setting up linguistic analysis server",
|
|
||||||
log.F("concurrency", config.Cfg.MaxRetries))
|
|
||||||
|
|
||||||
// Create linguistic sync job
|
|
||||||
linguisticSyncJob := linguistics.NewLinguisticSyncJob(
|
|
||||||
f.appBuilder.GetDB(),
|
|
||||||
f.appBuilder.GetLinguisticsFactory().GetAnalyzer(),
|
|
||||||
f.appBuilder.GetAsynq(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create linguistic server and register handlers
|
|
||||||
linguisticServer := asynq.NewServer(redisOpt, asynq.Config{Concurrency: config.Cfg.MaxRetries})
|
|
||||||
|
|
||||||
// Register linguistic handlers
|
|
||||||
linguisticMux := asynq.NewServeMux()
|
|
||||||
linguistics.RegisterLinguisticHandlers(linguisticMux, linguisticSyncJob)
|
|
||||||
|
|
||||||
// For now, we'll need to run the server with the mux when it's started
|
|
||||||
// This is a temporary workaround - in production, you'd want to properly configure the server
|
|
||||||
servers = append(servers, linguisticServer)
|
|
||||||
|
|
||||||
log.LogInfo("Background job servers created successfully",
|
|
||||||
log.F("serverCount", len(servers)))
|
|
||||||
|
|
||||||
return servers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
62
internal/app/tag/commands.go
Normal file
62
internal/app/tag/commands.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagCommands contains the command handlers for the tag aggregate.
|
||||||
|
type TagCommands struct {
|
||||||
|
repo domain.TagRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagCommands creates a new TagCommands handler.
|
||||||
|
func NewTagCommands(repo domain.TagRepository) *TagCommands {
|
||||||
|
return &TagCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTagInput represents the input for creating a new tag.
|
||||||
|
type CreateTagInput struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTag creates a new tag.
|
||||||
|
func (c *TagCommands) CreateTag(ctx context.Context, input CreateTagInput) (*domain.Tag, error) {
|
||||||
|
tag := &domain.Tag{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: input.Description,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTagInput represents the input for updating an existing tag.
|
||||||
|
type UpdateTagInput struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTag updates an existing tag.
|
||||||
|
func (c *TagCommands) UpdateTag(ctx context.Context, input UpdateTagInput) (*domain.Tag, error) {
|
||||||
|
tag, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag.Name = input.Name
|
||||||
|
tag.Description = input.Description
|
||||||
|
err = c.repo.Update(ctx, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTag deletes a tag by ID.
|
||||||
|
func (c *TagCommands) DeleteTag(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
36
internal/app/tag/queries.go
Normal file
36
internal/app/tag/queries.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagQueries contains the query handlers for the tag aggregate.
|
||||||
|
type TagQueries struct {
|
||||||
|
repo domain.TagRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTagQueries creates a new TagQueries handler.
|
||||||
|
func NewTagQueries(repo domain.TagRepository) *TagQueries {
|
||||||
|
return &TagQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag returns a tag by ID.
|
||||||
|
func (q *TagQueries) Tag(ctx context.Context, id uint) (*domain.Tag, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagByName returns a tag by name.
|
||||||
|
func (q *TagQueries) TagByName(ctx context.Context, name string) (*domain.Tag, error) {
|
||||||
|
return q.repo.FindByName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagsByWorkID returns all tags for a work.
|
||||||
|
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags returns all tags.
|
||||||
|
func (q *TagQueries) Tags(ctx context.Context) ([]domain.Tag, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/tag/service.go
Normal file
17
internal/app/tag/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package tag
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the tag aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *TagCommands
|
||||||
|
Queries *TagQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new tag Service.
|
||||||
|
func NewService(repo domain.TagRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewTagCommands(repo),
|
||||||
|
Queries: NewTagQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
80
internal/app/translation/commands.go
Normal file
80
internal/app/translation/commands.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package translation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TranslationCommands contains the command handlers for the translation aggregate.
|
||||||
|
type TranslationCommands struct {
|
||||||
|
repo domain.TranslationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTranslationCommands creates a new TranslationCommands handler.
|
||||||
|
func NewTranslationCommands(repo domain.TranslationRepository) *TranslationCommands {
|
||||||
|
return &TranslationCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTranslationInput represents the input for creating a new translation.
|
||||||
|
type CreateTranslationInput struct {
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
Description string
|
||||||
|
Language string
|
||||||
|
Status domain.TranslationStatus
|
||||||
|
TranslatableID uint
|
||||||
|
TranslatableType string
|
||||||
|
TranslatorID *uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTranslation creates a new translation.
|
||||||
|
func (c *TranslationCommands) CreateTranslation(ctx context.Context, input CreateTranslationInput) (*domain.Translation, error) {
|
||||||
|
translation := &domain.Translation{
|
||||||
|
Title: input.Title,
|
||||||
|
Content: input.Content,
|
||||||
|
Description: input.Description,
|
||||||
|
Language: input.Language,
|
||||||
|
Status: input.Status,
|
||||||
|
TranslatableID: input.TranslatableID,
|
||||||
|
TranslatableType: input.TranslatableType,
|
||||||
|
TranslatorID: input.TranslatorID,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, translation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return translation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTranslationInput represents the input for updating an existing translation.
|
||||||
|
type UpdateTranslationInput struct {
|
||||||
|
ID uint
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
Description string
|
||||||
|
Language string
|
||||||
|
Status domain.TranslationStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTranslation updates an existing translation.
|
||||||
|
func (c *TranslationCommands) UpdateTranslation(ctx context.Context, input UpdateTranslationInput) (*domain.Translation, error) {
|
||||||
|
translation, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
translation.Title = input.Title
|
||||||
|
translation.Content = input.Content
|
||||||
|
translation.Description = input.Description
|
||||||
|
translation.Language = input.Language
|
||||||
|
translation.Status = input.Status
|
||||||
|
err = c.repo.Update(ctx, translation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return translation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTranslation deletes a translation by ID.
|
||||||
|
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
46
internal/app/translation/queries.go
Normal file
46
internal/app/translation/queries.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package translation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TranslationQueries contains the query handlers for the translation aggregate.
|
||||||
|
type TranslationQueries struct {
|
||||||
|
repo domain.TranslationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTranslationQueries creates a new TranslationQueries handler.
|
||||||
|
func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQueries {
|
||||||
|
return &TranslationQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translation returns a translation by ID.
|
||||||
|
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationsByWorkID returns all translations for a work.
|
||||||
|
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||||
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationsByEntity returns all translations for an entity.
|
||||||
|
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||||
|
return q.repo.ListByEntity(ctx, entityType, entityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationsByTranslatorID returns all translations for a translator.
|
||||||
|
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||||
|
return q.repo.ListByTranslatorID(ctx, translatorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationsByStatus returns all translations for a status.
|
||||||
|
func (q *TranslationQueries) TranslationsByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||||
|
return q.repo.ListByStatus(ctx, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translations returns all translations.
|
||||||
|
func (q *TranslationQueries) Translations(ctx context.Context) ([]domain.Translation, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/translation/service.go
Normal file
17
internal/app/translation/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package translation
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the translation aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *TranslationCommands
|
||||||
|
Queries *TranslationQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new translation Service.
|
||||||
|
func NewService(repo domain.TranslationRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewTranslationCommands(repo),
|
||||||
|
Queries: NewTranslationQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
76
internal/app/user/commands.go
Normal file
76
internal/app/user/commands.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserCommands contains the command handlers for the user aggregate.
|
||||||
|
type UserCommands struct {
|
||||||
|
repo domain.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserCommands creates a new UserCommands handler.
|
||||||
|
func NewUserCommands(repo domain.UserRepository) *UserCommands {
|
||||||
|
return &UserCommands{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserInput represents the input for creating a new user.
|
||||||
|
type CreateUserInput struct {
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Role domain.UserRole
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user.
|
||||||
|
func (c *UserCommands) CreateUser(ctx context.Context, input CreateUserInput) (*domain.User, error) {
|
||||||
|
user := &domain.User{
|
||||||
|
Username: input.Username,
|
||||||
|
Email: input.Email,
|
||||||
|
Password: input.Password,
|
||||||
|
FirstName: input.FirstName,
|
||||||
|
LastName: input.LastName,
|
||||||
|
Role: input.Role,
|
||||||
|
}
|
||||||
|
err := c.repo.Create(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserInput represents the input for updating an existing user.
|
||||||
|
type UpdateUserInput struct {
|
||||||
|
ID uint
|
||||||
|
Username string
|
||||||
|
Email string
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
Role domain.UserRole
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates an existing user.
|
||||||
|
func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) {
|
||||||
|
user, err := c.repo.GetByID(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
user.Username = input.Username
|
||||||
|
user.Email = input.Email
|
||||||
|
user.FirstName = input.FirstName
|
||||||
|
user.LastName = input.LastName
|
||||||
|
user.Role = input.Role
|
||||||
|
err = c.repo.Update(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user by ID.
|
||||||
|
func (c *UserCommands) DeleteUser(ctx context.Context, id uint) error {
|
||||||
|
return c.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
41
internal/app/user/queries.go
Normal file
41
internal/app/user/queries.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserQueries contains the query handlers for the user aggregate.
|
||||||
|
type UserQueries struct {
|
||||||
|
repo domain.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserQueries creates a new UserQueries handler.
|
||||||
|
func NewUserQueries(repo domain.UserRepository) *UserQueries {
|
||||||
|
return &UserQueries{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// User returns a user by ID.
|
||||||
|
func (q *UserQueries) User(ctx context.Context, id uint) (*domain.User, error) {
|
||||||
|
return q.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserByUsername returns a user by username.
|
||||||
|
func (q *UserQueries) UserByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
|
return q.repo.FindByUsername(ctx, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserByEmail returns a user by email.
|
||||||
|
func (q *UserQueries) UserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
return q.repo.FindByEmail(ctx, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsersByRole returns all users for a role.
|
||||||
|
func (q *UserQueries) UsersByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||||
|
return q.repo.ListByRole(ctx, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users returns all users.
|
||||||
|
func (q *UserQueries) Users(ctx context.Context) ([]domain.User, error) {
|
||||||
|
return q.repo.ListAll(ctx)
|
||||||
|
}
|
||||||
17
internal/app/user/service.go
Normal file
17
internal/app/user/service.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import "tercul/internal/domain"
|
||||||
|
|
||||||
|
// Service is the application service for the user aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *UserCommands
|
||||||
|
Queries *UserQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new user Service.
|
||||||
|
func NewService(repo domain.UserRepository) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewUserCommands(repo),
|
||||||
|
Queries: NewUserQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,33 +9,38 @@ import (
|
|||||||
// WorkCommands contains the command handlers for the work aggregate.
|
// WorkCommands contains the command handlers for the work aggregate.
|
||||||
type WorkCommands struct {
|
type WorkCommands struct {
|
||||||
repo domain.WorkRepository
|
repo domain.WorkRepository
|
||||||
analyzer interface { // This will be replaced with a proper interface later
|
searchClient domain.SearchClient
|
||||||
AnalyzeWork(ctx context.Context, workID uint) error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWorkCommands creates a new WorkCommands handler.
|
// NewWorkCommands creates a new WorkCommands handler.
|
||||||
func NewWorkCommands(repo domain.WorkRepository, analyzer interface {
|
func NewWorkCommands(repo domain.WorkRepository, searchClient domain.SearchClient) *WorkCommands {
|
||||||
AnalyzeWork(ctx context.Context, workID uint) error
|
|
||||||
}) *WorkCommands {
|
|
||||||
return &WorkCommands{
|
return &WorkCommands{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
analyzer: analyzer,
|
searchClient: searchClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateWork creates a new work.
|
// CreateWork creates a new work.
|
||||||
func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) error {
|
func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) (*domain.Work, error) {
|
||||||
if work == nil {
|
if work == nil {
|
||||||
return errors.New("work cannot be nil")
|
return nil, errors.New("work cannot be nil")
|
||||||
}
|
}
|
||||||
if work.Title == "" {
|
if work.Title == "" {
|
||||||
return errors.New("work title cannot be empty")
|
return nil, errors.New("work title cannot be empty")
|
||||||
}
|
}
|
||||||
if work.Language == "" {
|
if work.Language == "" {
|
||||||
return errors.New("work language cannot be empty")
|
return nil, errors.New("work language cannot be empty")
|
||||||
}
|
}
|
||||||
return c.repo.Create(ctx, work)
|
err := c.repo.Create(ctx, work)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Index the work in the search client
|
||||||
|
err = c.searchClient.IndexWork(ctx, work, "")
|
||||||
|
if err != nil {
|
||||||
|
// Log the error but don't fail the operation
|
||||||
|
}
|
||||||
|
return work, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWork updates an existing work.
|
// UpdateWork updates an existing work.
|
||||||
@ -52,7 +57,12 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
|
|||||||
if work.Language == "" {
|
if work.Language == "" {
|
||||||
return errors.New("work language cannot be empty")
|
return errors.New("work language cannot be empty")
|
||||||
}
|
}
|
||||||
return c.repo.Update(ctx, work)
|
err := c.repo.Update(ctx, work)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Index the work in the search client
|
||||||
|
return c.searchClient.IndexWork(ctx, work, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWork deletes a work by ID.
|
// DeleteWork deletes a work by ID.
|
||||||
@ -65,8 +75,6 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
|||||||
|
|
||||||
// AnalyzeWork performs linguistic analysis on a work.
|
// AnalyzeWork performs linguistic analysis on a work.
|
||||||
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||||
if workID == 0 {
|
// TODO: implement this
|
||||||
return errors.New("invalid work ID")
|
return nil
|
||||||
}
|
|
||||||
return c.analyzer.AnalyzeWork(ctx, workID)
|
|
||||||
}
|
}
|
||||||
|
|||||||
137
internal/app/work/commands_test.go
Normal file
137
internal/app/work/commands_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package work
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkCommandsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockWorkRepository
|
||||||
|
analyzer *mockAnalyzer
|
||||||
|
commands *WorkCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) SetupTest() {
|
||||||
|
s.repo = &mockWorkRepository{}
|
||||||
|
s.analyzer = &mockAnalyzer{}
|
||||||
|
s.commands = NewWorkCommands(s.repo, s.analyzer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkCommandsSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(WorkCommandsSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestCreateWork_Success() {
|
||||||
|
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
err := s.commands.CreateWork(context.Background(), work)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestCreateWork_Nil() {
|
||||||
|
err := s.commands.CreateWork(context.Background(), nil)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestCreateWork_EmptyTitle() {
|
||||||
|
work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
err := s.commands.CreateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestCreateWork_EmptyLanguage() {
|
||||||
|
work := &domain.Work{Title: "Test Work"}
|
||||||
|
err := s.commands.CreateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestCreateWork_RepoError() {
|
||||||
|
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
s.repo.createFunc = func(ctx context.Context, w *domain.Work) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.CreateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_Success() {
|
||||||
|
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
work.ID = 1
|
||||||
|
err := s.commands.UpdateWork(context.Background(), work)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_Nil() {
|
||||||
|
err := s.commands.UpdateWork(context.Background(), nil)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_ZeroID() {
|
||||||
|
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
err := s.commands.UpdateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_EmptyTitle() {
|
||||||
|
work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
work.ID = 1
|
||||||
|
err := s.commands.UpdateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() {
|
||||||
|
work := &domain.Work{Title: "Test Work"}
|
||||||
|
work.ID = 1
|
||||||
|
err := s.commands.UpdateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestUpdateWork_RepoError() {
|
||||||
|
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||||
|
work.ID = 1
|
||||||
|
s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.UpdateWork(context.Background(), work)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestDeleteWork_Success() {
|
||||||
|
err := s.commands.DeleteWork(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestDeleteWork_ZeroID() {
|
||||||
|
err := s.commands.DeleteWork(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestDeleteWork_RepoError() {
|
||||||
|
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||||
|
return errors.New("db error")
|
||||||
|
}
|
||||||
|
err := s.commands.DeleteWork(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestAnalyzeWork_Success() {
|
||||||
|
err := s.commands.AnalyzeWork(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestAnalyzeWork_ZeroID() {
|
||||||
|
err := s.commands.AnalyzeWork(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkCommandsSuite) TestAnalyzeWork_AnalyzerError() {
|
||||||
|
s.analyzer.analyzeWorkFunc = func(ctx context.Context, workID uint) error {
|
||||||
|
return errors.New("analyzer error")
|
||||||
|
}
|
||||||
|
err := s.commands.AnalyzeWork(context.Background(), 1)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
}
|
||||||
92
internal/app/work/main_test.go
Normal file
92
internal/app/work/main_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package work
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockWorkRepository struct {
|
||||||
|
domain.WorkRepository
|
||||||
|
createFunc func(ctx context.Context, work *domain.Work) error
|
||||||
|
updateFunc func(ctx context.Context, work *domain.Work) error
|
||||||
|
deleteFunc func(ctx context.Context, id uint) error
|
||||||
|
getByIDFunc func(ctx context.Context, id uint) (*domain.Work, error)
|
||||||
|
listFunc func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
|
||||||
|
getWithTranslationsFunc func(ctx context.Context, id uint) (*domain.Work, error)
|
||||||
|
findByTitleFunc func(ctx context.Context, title string) ([]domain.Work, error)
|
||||||
|
findByAuthorFunc func(ctx context.Context, authorID uint) ([]domain.Work, error)
|
||||||
|
findByCategoryFunc func(ctx context.Context, categoryID uint) ([]domain.Work, error)
|
||||||
|
findByLanguageFunc func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) error {
|
||||||
|
if m.createFunc != nil {
|
||||||
|
return m.createFunc(ctx, work)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) Update(ctx context.Context, work *domain.Work) error {
|
||||||
|
if m.updateFunc != nil {
|
||||||
|
return m.updateFunc(ctx, work)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) Delete(ctx context.Context, id uint) error {
|
||||||
|
if m.deleteFunc != nil {
|
||||||
|
return m.deleteFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
|
if m.getByIDFunc != nil {
|
||||||
|
return m.getByIDFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||||
|
if m.listFunc != nil {
|
||||||
|
return m.listFunc(ctx, page, pageSize)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
|
if m.getWithTranslationsFunc != nil {
|
||||||
|
return m.getWithTranslationsFunc(ctx, id)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
||||||
|
if m.findByTitleFunc != nil {
|
||||||
|
return m.findByTitleFunc(ctx, title)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||||
|
if m.findByAuthorFunc != nil {
|
||||||
|
return m.findByAuthorFunc(ctx, authorID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||||
|
if m.findByCategoryFunc != nil {
|
||||||
|
return m.findByCategoryFunc(ctx, categoryID)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||||
|
if m.findByLanguageFunc != nil {
|
||||||
|
return m.findByLanguageFunc(ctx, language, page, pageSize)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockAnalyzer struct {
|
||||||
|
analyzeWorkFunc func(ctx context.Context, workID uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAnalyzer) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||||
|
if m.analyzeWorkFunc != nil {
|
||||||
|
return m.analyzeWorkFunc(ctx, workID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
132
internal/app/work/queries_test.go
Normal file
132
internal/app/work/queries_test.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package work
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkQueriesSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
repo *mockWorkRepository
|
||||||
|
queries *WorkQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) SetupTest() {
|
||||||
|
s.repo = &mockWorkRepository{}
|
||||||
|
s.queries = NewWorkQueries(s.repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkQueriesSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(WorkQueriesSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestGetWorkByID_Success() {
|
||||||
|
work := &domain.Work{Title: "Test Work"}
|
||||||
|
work.ID = 1
|
||||||
|
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
|
return work, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.GetWorkByID(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), work, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestGetWorkByID_ZeroID() {
|
||||||
|
w, err := s.queries.GetWorkByID(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestListWorks_Success() {
|
||||||
|
works := &domain.PaginatedResult[domain.Work]{}
|
||||||
|
s.repo.listFunc = func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||||
|
return works, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.ListWorks(context.Background(), 1, 10)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), works, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestGetWorkWithTranslations_Success() {
|
||||||
|
work := &domain.Work{Title: "Test Work"}
|
||||||
|
work.ID = 1
|
||||||
|
s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
|
return work, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.GetWorkWithTranslations(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), work, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestGetWorkWithTranslations_ZeroID() {
|
||||||
|
w, err := s.queries.GetWorkWithTranslations(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByTitle_Success() {
|
||||||
|
works := []domain.Work{{Title: "Test Work"}}
|
||||||
|
s.repo.findByTitleFunc = func(ctx context.Context, title string) ([]domain.Work, error) {
|
||||||
|
return works, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.FindWorksByTitle(context.Background(), "Test")
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), works, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByTitle_Empty() {
|
||||||
|
w, err := s.queries.FindWorksByTitle(context.Background(), "")
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByAuthor_Success() {
|
||||||
|
works := []domain.Work{{Title: "Test Work"}}
|
||||||
|
s.repo.findByAuthorFunc = func(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||||
|
return works, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.FindWorksByAuthor(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), works, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByAuthor_ZeroID() {
|
||||||
|
w, err := s.queries.FindWorksByAuthor(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByCategory_Success() {
|
||||||
|
works := []domain.Work{{Title: "Test Work"}}
|
||||||
|
s.repo.findByCategoryFunc = func(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||||
|
return works, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.FindWorksByCategory(context.Background(), 1)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), works, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByCategory_ZeroID() {
|
||||||
|
w, err := s.queries.FindWorksByCategory(context.Background(), 0)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByLanguage_Success() {
|
||||||
|
works := &domain.PaginatedResult[domain.Work]{}
|
||||||
|
s.repo.findByLanguageFunc = func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||||
|
return works, nil
|
||||||
|
}
|
||||||
|
w, err := s.queries.FindWorksByLanguage(context.Background(), "en", 1, 10)
|
||||||
|
assert.NoError(s.T(), err)
|
||||||
|
assert.Equal(s.T(), works, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WorkQueriesSuite) TestFindWorksByLanguage_Empty() {
|
||||||
|
w, err := s.queries.FindWorksByLanguage(context.Background(), "", 1, 10)
|
||||||
|
assert.Error(s.T(), err)
|
||||||
|
assert.Nil(s.T(), w)
|
||||||
|
}
|
||||||
19
internal/app/work/service.go
Normal file
19
internal/app/work/service.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package work
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tercul/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service is the application service for the work aggregate.
|
||||||
|
type Service struct {
|
||||||
|
Commands *WorkCommands
|
||||||
|
Queries *WorkQueries
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new work Service.
|
||||||
|
func NewService(repo domain.WorkRepository, searchClient domain.SearchClient) *Service {
|
||||||
|
return &Service{
|
||||||
|
Commands: NewWorkCommands(repo, searchClient),
|
||||||
|
Queries: NewWorkQueries(repo),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
# This file is created to ensure the directory structure is in place.
|
|
||||||
169
internal/data/sql/analytics_repository.go
Normal file
169
internal/data/sql/analytics_repository.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type analyticsRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnalyticsRepository(db *gorm.DB) domain.AnalyticsRepository {
|
||||||
|
return &analyticsRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedWorkCounterFields = map[string]bool{
|
||||||
|
"views": true,
|
||||||
|
"likes": true,
|
||||||
|
"comments": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"shares": true,
|
||||||
|
"translation_count": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedTranslationCounterFields = map[string]bool{
|
||||||
|
"views": true,
|
||||||
|
"likes": true,
|
||||||
|
"comments": true,
|
||||||
|
"shares": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
||||||
|
if !allowedWorkCounterFields[field] {
|
||||||
|
return fmt.Errorf("invalid work counter field: %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a transaction to ensure atomicity
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// First, try to update the existing record
|
||||||
|
result := tx.Model(&domain.WorkStats{}).Where("work_id = ?", workID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no rows were affected, the record does not exist, so create it
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
initialData := map[string]interface{}{"work_id": workID, field: value}
|
||||||
|
return tx.Model(&domain.WorkStats{}).Create(initialData).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||||
|
var trendingWorks []*domain.Trending
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("entity_type = ? AND time_period = ?", "Work", timePeriod).
|
||||||
|
Order("rank ASC").
|
||||||
|
Limit(limit).
|
||||||
|
Find(&trendingWorks).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trendingWorks) == 0 {
|
||||||
|
return []*domain.Work{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
workIDs := make([]uint, len(trendingWorks))
|
||||||
|
for i, tw := range trendingWorks {
|
||||||
|
workIDs[i] = tw.EntityID
|
||||||
|
}
|
||||||
|
|
||||||
|
var works []*domain.Work
|
||||||
|
err = r.db.WithContext(ctx).
|
||||||
|
Where("id IN ?", workIDs).
|
||||||
|
Find(&works).Error
|
||||||
|
|
||||||
|
// This part is tricky because the order from the IN clause is not guaranteed.
|
||||||
|
// We need to re-order the works based on the trending rank.
|
||||||
|
workMap := make(map[uint]*domain.Work)
|
||||||
|
for _, work := range works {
|
||||||
|
workMap[work.ID] = work
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedWorks := make([]*domain.Work, len(workIDs))
|
||||||
|
for i, id := range workIDs {
|
||||||
|
if work, ok := workMap[id]; ok {
|
||||||
|
orderedWorks[i] = work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedWorks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
||||||
|
if !allowedTranslationCounterFields[field] {
|
||||||
|
return fmt.Errorf("invalid translation counter field: %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
result := tx.Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).UpdateColumn(field, gorm.Expr(fmt.Sprintf("%s + ?", field), value))
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
initialData := map[string]interface{}{"translation_id": translationID, field: value}
|
||||||
|
return tx.Model(&domain.TranslationStats{}).Create(initialData).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
|
var stats domain.WorkStats
|
||||||
|
err := r.db.WithContext(ctx).Where(domain.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error
|
||||||
|
return &stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||||
|
var stats domain.TranslationStats
|
||||||
|
err := r.db.WithContext(ctx).Where(domain.TranslationStats{TranslationID: translationID}).FirstOrCreate(&stats).Error
|
||||||
|
return &stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) {
|
||||||
|
var engagement domain.UserEngagement
|
||||||
|
err := r.db.WithContext(ctx).Where(domain.UserEngagement{UserID: userID, Date: date}).FirstOrCreate(&engagement).Error
|
||||||
|
return &engagement, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error {
|
||||||
|
return r.db.WithContext(ctx).Save(userEngagement).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *analyticsRepository) UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error {
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Clear old trending data for this time period
|
||||||
|
if err := tx.Where("time_period = ?", timePeriod).Delete(&domain.Trending{}).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to delete old trending data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new trending data
|
||||||
|
if err := tx.Create(trending).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to insert new trending data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
30
internal/data/sql/auth_repository.go
Normal file
30
internal/data/sql/auth_repository.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthRepository(db *gorm.DB) domain.AuthRepository {
|
||||||
|
return &authRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *authRepository) StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error {
|
||||||
|
session := &domain.UserSession{
|
||||||
|
UserID: userID,
|
||||||
|
Token: token,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(session).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *authRepository) DeleteToken(ctx context.Context, token string) error {
|
||||||
|
return r.db.WithContext(ctx).Where("token = ?", token).Delete(&domain.UserSession{}).Error
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/author"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type authorRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthorRepository creates a new AuthorRepository.
|
// NewAuthorRepository creates a new AuthorRepository.
|
||||||
func NewAuthorRepository(db *gorm.DB) author.AuthorRepository {
|
func NewAuthorRepository(db *gorm.DB) domain.AuthorRepository {
|
||||||
return &authorRepository{
|
return &authorRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Author](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
58
internal/data/sql/author_repository_test.go
Normal file
58
internal/data/sql/author_repository_test.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthorRepositoryTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorRepositoryTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorRepositoryTestSuite) SetupTest() {
|
||||||
|
s.DB.Exec("DELETE FROM work_authors")
|
||||||
|
s.DB.Exec("DELETE FROM authors")
|
||||||
|
s.DB.Exec("DELETE FROM works")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorRepositoryTestSuite) createAuthor(name string) *domain.Author {
|
||||||
|
author := &domain.Author{
|
||||||
|
Name: name,
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
Language: "en",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.AuthorRepo.Create(context.Background(), author)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
return author
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthorRepositoryTestSuite) TestListByWorkID() {
|
||||||
|
s.Run("should return all authors for a given work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
author1 := s.createAuthor("Author 1")
|
||||||
|
author2 := s.createAuthor("Author 2")
|
||||||
|
s.Require().NoError(s.DB.Model(&work).Association("Authors").Append([]*domain.Author{author1, author2}))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
authors, err := s.AuthorRepo.ListByWorkID(context.Background(), work.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(authors, 2)
|
||||||
|
s.ElementsMatch([]string{"Author 1", "Author 2"}, []string{authors[0].Name, authors[1].Name})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthorRepository(t *testing.T) {
|
||||||
|
suite.Run(t, new(AuthorRepositoryTestSuite))
|
||||||
|
}
|
||||||
259
internal/data/sql/base_repository_test.go
Normal file
259
internal/data/sql/base_repository_test.go
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseRepositoryTestSuite tests the generic BaseRepository implementation.
|
||||||
|
type BaseRepositoryTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
repo domain.BaseRepository[testutil.TestEntity]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite initializes the test suite, database, and repository.
|
||||||
|
func (s *BaseRepositoryTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest cleans the database before each test.
|
||||||
|
func (s *BaseRepositoryTestSuite) SetupTest() {
|
||||||
|
s.DB.Exec("DELETE FROM test_entities")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite drops the test table after the suite finishes.
|
||||||
|
func (s *BaseRepositoryTestSuite) TearDownSuite() {
|
||||||
|
s.DB.Migrator().DropTable(&testutil.TestEntity{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBaseRepository runs the entire test suite.
|
||||||
|
func TestBaseRepository(t *testing.T) {
|
||||||
|
suite.Run(t, new(BaseRepositoryTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestEntity is a helper to create a test entity.
|
||||||
|
func (s *BaseRepositoryTestSuite) createTestEntity(name string) *testutil.TestEntity {
|
||||||
|
entity := &testutil.TestEntity{Name: name}
|
||||||
|
err := s.repo.Create(context.Background(), entity)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotZero(entity.ID)
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestCreate() {
|
||||||
|
s.Run("should create a new entity", func() {
|
||||||
|
// Arrange
|
||||||
|
ctx := context.Background()
|
||||||
|
entity := &testutil.TestEntity{Name: "Test Create"}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.repo.Create(ctx, entity)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.NotZero(entity.ID)
|
||||||
|
|
||||||
|
// Verify in DB
|
||||||
|
var foundEntity testutil.TestEntity
|
||||||
|
err = s.DB.First(&foundEntity, entity.ID).Error
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal("Test Create", foundEntity.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return error for nil entity", func() {
|
||||||
|
err := s.repo.Create(context.Background(), nil)
|
||||||
|
s.ErrorIs(err, sql.ErrInvalidInput)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return error for nil context", func() {
|
||||||
|
err := s.repo.Create(nil, &testutil.TestEntity{Name: "Test Context"})
|
||||||
|
s.ErrorIs(err, sql.ErrContextRequired)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestGetByID() {
|
||||||
|
s.Run("should return an entity by ID", func() {
|
||||||
|
// Arrange
|
||||||
|
created := s.createTestEntity("Test GetByID")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
found, err := s.repo.GetByID(context.Background(), created.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotNil(found)
|
||||||
|
s.Equal(created.ID, found.ID)
|
||||||
|
s.Equal(created.Name, found.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return ErrEntityNotFound for non-existent ID", func() {
|
||||||
|
_, err := s.repo.GetByID(context.Background(), 99999)
|
||||||
|
s.ErrorIs(err, sql.ErrEntityNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return ErrInvalidID for zero ID", func() {
|
||||||
|
_, err := s.repo.GetByID(context.Background(), 0)
|
||||||
|
s.ErrorIs(err, sql.ErrInvalidID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestUpdate() {
|
||||||
|
s.Run("should update an existing entity", func() {
|
||||||
|
// Arrange
|
||||||
|
created := s.createTestEntity("Original Name")
|
||||||
|
created.Name = "Updated Name"
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.repo.Update(context.Background(), created)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
found, getErr := s.repo.GetByID(context.Background(), created.ID)
|
||||||
|
s.Require().NoError(getErr)
|
||||||
|
s.Equal("Updated Name", found.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestDelete() {
|
||||||
|
s.Run("should delete an existing entity", func() {
|
||||||
|
// Arrange
|
||||||
|
created := s.createTestEntity("To Be Deleted")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.repo.Delete(context.Background(), created.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
_, getErr := s.repo.GetByID(context.Background(), created.ID)
|
||||||
|
s.ErrorIs(getErr, sql.ErrEntityNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return ErrEntityNotFound when deleting non-existent entity", func() {
|
||||||
|
err := s.repo.Delete(context.Background(), 99999)
|
||||||
|
s.ErrorIs(err, sql.ErrEntityNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestList() {
|
||||||
|
// Arrange
|
||||||
|
s.createTestEntity("Entity 1")
|
||||||
|
s.createTestEntity("Entity 2")
|
||||||
|
s.createTestEntity("Entity 3")
|
||||||
|
|
||||||
|
s.Run("should return a paginated list of entities", func() {
|
||||||
|
// Act
|
||||||
|
result, err := s.repo.List(context.Background(), 1, 2)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(3), result.TotalCount)
|
||||||
|
s.Equal(2, result.TotalPages)
|
||||||
|
s.Equal(1, result.Page)
|
||||||
|
s.Equal(2, result.PageSize)
|
||||||
|
s.True(result.HasNext)
|
||||||
|
s.False(result.HasPrev)
|
||||||
|
s.Len(result.Items, 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestListWithOptions() {
|
||||||
|
// Arrange
|
||||||
|
s.createTestEntity("Apple")
|
||||||
|
s.createTestEntity("Banana")
|
||||||
|
s.createTestEntity("Avocado")
|
||||||
|
|
||||||
|
s.Run("should filter with Where clause", func() {
|
||||||
|
// Act
|
||||||
|
options := &domain.QueryOptions{
|
||||||
|
Where: map[string]interface{}{"name LIKE ?": "A%"},
|
||||||
|
}
|
||||||
|
results, err := s.repo.ListWithOptions(context.Background(), options)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(results, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should order results", func() {
|
||||||
|
// Act
|
||||||
|
options := &domain.QueryOptions{OrderBy: "name desc"}
|
||||||
|
results, err := s.repo.ListWithOptions(context.Background(), options)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(results, 3)
|
||||||
|
s.Equal("Banana", results[0].Name)
|
||||||
|
s.Equal("Avocado", results[1].Name)
|
||||||
|
s.Equal("Apple", results[2].Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestCount() {
|
||||||
|
// Arrange
|
||||||
|
s.createTestEntity("Entity 1")
|
||||||
|
s.createTestEntity("Entity 2")
|
||||||
|
|
||||||
|
s.Run("should return the total count of entities", func() {
|
||||||
|
// Act
|
||||||
|
count, err := s.repo.Count(context.Background())
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Equal(int64(2), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseRepositoryTestSuite) TestWithTx() {
|
||||||
|
s.Run("should commit transaction on success", func() {
|
||||||
|
// Arrange
|
||||||
|
var createdID uint
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
|
||||||
|
entity := &testutil.TestEntity{Name: "TX Commit"}
|
||||||
|
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
|
||||||
|
if err := repoInTx.Create(context.Background(), entity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
createdID = entity.ID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
_, getErr := s.repo.GetByID(context.Background(), createdID)
|
||||||
|
s.NoError(getErr, "Entity should exist after commit")
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should rollback transaction on error", func() {
|
||||||
|
// Arrange
|
||||||
|
var createdID uint
|
||||||
|
simulatedErr := errors.New("simulated error")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error {
|
||||||
|
entity := &testutil.TestEntity{Name: "TX Rollback"}
|
||||||
|
repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx)
|
||||||
|
if err := repoInTx.Create(context.Background(), entity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
createdID = entity.ID
|
||||||
|
return simulatedErr // Force a rollback
|
||||||
|
})
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().Error(err)
|
||||||
|
s.ErrorIs(err, simulatedErr)
|
||||||
|
|
||||||
|
_, getErr := s.repo.GetByID(context.Background(), createdID)
|
||||||
|
s.ErrorIs(getErr, sql.ErrEntityNotFound, "Entity should not exist after rollback")
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/book"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -15,7 +14,7 @@ type bookRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewBookRepository creates a new BookRepository.
|
// NewBookRepository creates a new BookRepository.
|
||||||
func NewBookRepository(db *gorm.DB) book.BookRepository {
|
func NewBookRepository(db *gorm.DB) domain.BookRepository {
|
||||||
return &bookRepository{
|
return &bookRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Book](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
65
internal/data/sql/book_repository_test.go
Normal file
65
internal/data/sql/book_repository_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BookRepositoryTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BookRepositoryTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BookRepositoryTestSuite) SetupTest() {
|
||||||
|
s.DB.Exec("DELETE FROM books")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BookRepositoryTestSuite) createBook(title, isbn string) *domain.Book {
|
||||||
|
book := &domain.Book{
|
||||||
|
Title: title,
|
||||||
|
ISBN: isbn,
|
||||||
|
TranslatableModel: domain.TranslatableModel{
|
||||||
|
Language: "en",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.BookRepo.Create(context.Background(), book)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
return book
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BookRepositoryTestSuite) TestFindByISBN() {
|
||||||
|
s.Run("should return a book by ISBN", func() {
|
||||||
|
// Arrange
|
||||||
|
s.createBook("Test Book", "1234567890")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
foundBook, err := s.BookRepo.FindByISBN(context.Background(), "1234567890")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotNil(foundBook)
|
||||||
|
s.Equal("Test Book", foundBook.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return error if ISBN not found", func() {
|
||||||
|
// Arrange
|
||||||
|
s.createBook("Another Book", "1111111111")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_, err := s.BookRepo.FindByISBN(context.Background(), "9999999999")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBookRepository(t *testing.T) {
|
||||||
|
suite.Run(t, new(BookRepositoryTestSuite))
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/bookmark"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type bookmarkRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewBookmarkRepository creates a new BookmarkRepository.
|
// NewBookmarkRepository creates a new BookmarkRepository.
|
||||||
func NewBookmarkRepository(db *gorm.DB) bookmark.BookmarkRepository {
|
func NewBookmarkRepository(db *gorm.DB) domain.BookmarkRepository {
|
||||||
return &bookmarkRepository{
|
return &bookmarkRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Bookmark](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
110
internal/data/sql/bookmark_repository_test.go
Normal file
110
internal/data/sql/bookmark_repository_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
repo "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewBookmarkRepository(t *testing.T) {
|
||||||
|
db, _, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewBookmarkRepository(db)
|
||||||
|
assert.NotNil(t, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBookmarkRepository_ListByUserID(t *testing.T) {
|
||||||
|
t.Run("should return bookmarks for a given user id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewBookmarkRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
expectedBookmarks := []domain.Bookmark{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}, UserID: userID, WorkID: 1},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}, UserID: userID, WorkID: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "user_id", "work_id"}).
|
||||||
|
AddRow(expectedBookmarks[0].ID, expectedBookmarks[0].CreatedAt, expectedBookmarks[0].UpdatedAt, expectedBookmarks[0].UserID, expectedBookmarks[0].WorkID).
|
||||||
|
AddRow(expectedBookmarks[1].ID, expectedBookmarks[1].CreatedAt, expectedBookmarks[1].UpdatedAt, expectedBookmarks[1].UserID, expectedBookmarks[1].WorkID)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bookmarks" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
bookmarks, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedBookmarks, bookmarks)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewBookmarkRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bookmarks" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
bookmarks, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, bookmarks)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBookmarkRepository_ListByWorkID(t *testing.T) {
|
||||||
|
t.Run("should return bookmarks for a given work id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewBookmarkRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
expectedBookmarks := []domain.Bookmark{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}, UserID: 1, WorkID: workID},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}, UserID: 2, WorkID: workID},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "user_id", "work_id"}).
|
||||||
|
AddRow(expectedBookmarks[0].ID, expectedBookmarks[0].CreatedAt, expectedBookmarks[0].UpdatedAt, expectedBookmarks[0].UserID, expectedBookmarks[0].WorkID).
|
||||||
|
AddRow(expectedBookmarks[1].ID, expectedBookmarks[1].CreatedAt, expectedBookmarks[1].UpdatedAt, expectedBookmarks[1].UserID, expectedBookmarks[1].WorkID)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bookmarks" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
bookmarks, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedBookmarks, bookmarks)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewBookmarkRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "bookmarks" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
bookmarks, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, bookmarks)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/category"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -15,7 +14,7 @@ type categoryRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCategoryRepository creates a new CategoryRepository.
|
// NewCategoryRepository creates a new CategoryRepository.
|
||||||
func NewCategoryRepository(db *gorm.DB) category.CategoryRepository {
|
func NewCategoryRepository(db *gorm.DB) domain.CategoryRepository {
|
||||||
return &categoryRepository{
|
return &categoryRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Category](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
111
internal/data/sql/category_repository_test.go
Normal file
111
internal/data/sql/category_repository_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"tercul/internal/testutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CategoryRepositoryTestSuite struct {
|
||||||
|
testutil.IntegrationTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) SetupSuite() {
|
||||||
|
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) SetupTest() {
|
||||||
|
s.DB.Exec("DELETE FROM work_categories")
|
||||||
|
s.DB.Exec("DELETE FROM categories")
|
||||||
|
s.DB.Exec("DELETE FROM works")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryRepository(t *testing.T) {
|
||||||
|
suite.Run(t, new(CategoryRepositoryTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) createCategory(name string, parentID *uint) *domain.Category {
|
||||||
|
category := &domain.Category{Name: name, ParentID: parentID}
|
||||||
|
err := s.CategoryRepo.Create(context.Background(), category)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotZero(category.ID)
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) TestFindByName() {
|
||||||
|
s.Run("should find a category by its name", func() {
|
||||||
|
// Arrange
|
||||||
|
s.createCategory("Fiction", nil)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
found, err := s.CategoryRepo.FindByName(context.Background(), "Fiction")
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().NotNil(found)
|
||||||
|
s.Equal("Fiction", found.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return error if not found", func() {
|
||||||
|
_, err := s.CategoryRepo.FindByName(context.Background(), "NonExistent")
|
||||||
|
s.Require().Error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) TestListByWorkID() {
|
||||||
|
s.Run("should return all categories for a given work", func() {
|
||||||
|
// Arrange
|
||||||
|
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||||
|
cat1 := s.createCategory("Science Fiction", nil)
|
||||||
|
cat2 := s.createCategory("Cyberpunk", &cat1.ID)
|
||||||
|
|
||||||
|
err := s.DB.Model(&work).Association("Categories").Append([]*domain.Category{cat1, cat2})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
categories, err := s.CategoryRepo.ListByWorkID(context.Background(), work.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(categories, 2)
|
||||||
|
s.ElementsMatch([]string{"Science Fiction", "Cyberpunk"}, []string{categories[0].Name, categories[1].Name})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CategoryRepositoryTestSuite) TestListByParentID() {
|
||||||
|
s.Run("should return top-level categories when parent ID is nil", func() {
|
||||||
|
// Arrange
|
||||||
|
s.createCategory("Root 1", nil)
|
||||||
|
s.createCategory("Root 2", nil)
|
||||||
|
child := s.createCategory("Child 1", &[]uint{1}[0]) // Create a child to ensure it's not returned
|
||||||
|
|
||||||
|
// Act
|
||||||
|
categories, err := s.CategoryRepo.ListByParentID(context.Background(), nil)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(categories, 2)
|
||||||
|
s.NotContains(categories, child)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("should return child categories for a given parent ID", func() {
|
||||||
|
// Arrange
|
||||||
|
parent := s.createCategory("Parent", nil)
|
||||||
|
s.createCategory("Sub-Child 1", &parent.ID)
|
||||||
|
s.createCategory("Sub-Child 2", &parent.ID)
|
||||||
|
s.createCategory("Another Parent", nil)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
categories, err := s.CategoryRepo.ListByParentID(context.Background(), &parent.ID)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Len(categories, 2)
|
||||||
|
for _, cat := range categories {
|
||||||
|
s.Equal(parent.ID, *cat.ParentID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/city"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type cityRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCityRepository creates a new CityRepository.
|
// NewCityRepository creates a new CityRepository.
|
||||||
func NewCityRepository(db *gorm.DB) city.CityRepository {
|
func NewCityRepository(db *gorm.DB) domain.CityRepository {
|
||||||
return &cityRepository{
|
return &cityRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.City](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.City](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
66
internal/data/sql/city_repository_test.go
Normal file
66
internal/data/sql/city_repository_test.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
repo "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCityRepository(t *testing.T) {
|
||||||
|
db, _, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCityRepository(db)
|
||||||
|
assert.NotNil(t, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCityRepository_ListByCountryID(t *testing.T) {
|
||||||
|
t.Run("should return cities for a given country id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCityRepository(db)
|
||||||
|
|
||||||
|
countryID := uint(1)
|
||||||
|
expectedCities := []domain.City{
|
||||||
|
{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}}, Name: "City 1", CountryID: countryID},
|
||||||
|
{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}}, Name: "City 2", CountryID: countryID},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "country_id"}).
|
||||||
|
AddRow(expectedCities[0].ID, expectedCities[0].CreatedAt, expectedCities[0].UpdatedAt, expectedCities[0].Name, expectedCities[0].CountryID).
|
||||||
|
AddRow(expectedCities[1].ID, expectedCities[1].CreatedAt, expectedCities[1].UpdatedAt, expectedCities[1].Name, expectedCities[1].CountryID)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "cities" WHERE country_id = $1`)).
|
||||||
|
WithArgs(countryID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
cities, err := repo.ListByCountryID(context.Background(), countryID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedCities, cities)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCityRepository(db)
|
||||||
|
|
||||||
|
countryID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "cities" WHERE country_id = $1`)).
|
||||||
|
WithArgs(countryID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
cities, err := repo.ListByCountryID(context.Background(), countryID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, cities)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/collection"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type collectionRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCollectionRepository creates a new CollectionRepository.
|
// NewCollectionRepository creates a new CollectionRepository.
|
||||||
func NewCollectionRepository(db *gorm.DB) collection.CollectionRepository {
|
func NewCollectionRepository(db *gorm.DB) domain.CollectionRepository {
|
||||||
return &collectionRepository{
|
return &collectionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Collection](db),
|
||||||
db: db,
|
db: db,
|
||||||
@ -30,6 +29,16 @@ func (r *collectionRepository) ListByUserID(ctx context.Context, userID uint) ([
|
|||||||
return collections, nil
|
return collections, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddWorkToCollection adds a work to a collection
|
||||||
|
func (r *collectionRepository) AddWorkToCollection(ctx context.Context, collectionID uint, workID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO collection_works (collection_id, work_id) VALUES (?, ?) ON CONFLICT DO NOTHING", collectionID, workID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveWorkFromCollection removes a work from a collection
|
||||||
|
func (r *collectionRepository) RemoveWorkFromCollection(ctx context.Context, collectionID uint, workID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM collection_works WHERE collection_id = ? AND work_id = ?", collectionID, workID).Error
|
||||||
|
}
|
||||||
|
|
||||||
// ListPublic finds public collections
|
// ListPublic finds public collections
|
||||||
func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collection, error) {
|
func (r *collectionRepository) ListPublic(ctx context.Context) ([]domain.Collection, error) {
|
||||||
var collections []domain.Collection
|
var collections []domain.Collection
|
||||||
|
|||||||
104
internal/data/sql/collection_repository_test.go
Normal file
104
internal/data/sql/collection_repository_test.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CollectionRepositoryTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
db *gorm.DB
|
||||||
|
mock sqlmock.Sqlmock
|
||||||
|
repo domain.CollectionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) SetupTest() {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
gormDB, err := gorm.Open(postgres.New(postgres.Config{Conn: db}), &gorm.Config{})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
s.db = gormDB
|
||||||
|
s.mock = mock
|
||||||
|
s.repo = sql.NewCollectionRepository(s.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TearDownTest() {
|
||||||
|
s.Require().NoError(s.mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionRepositoryTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CollectionRepositoryTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TestListByUserID() {
|
||||||
|
userID := uint(1)
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "user_id"}).
|
||||||
|
AddRow(1, userID).
|
||||||
|
AddRow(2, userID)
|
||||||
|
|
||||||
|
s.mock.ExpectQuery(`SELECT \* FROM "collections" WHERE user_id = \$1`).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
collections, err := s.repo.ListByUserID(context.Background(), userID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(collections, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TestAddWorkToCollection() {
|
||||||
|
collectionID, workID := uint(1), uint(1)
|
||||||
|
s.mock.ExpectExec(`INSERT INTO collection_works \(collection_id, work_id\) VALUES \(\$1, \$2\) ON CONFLICT DO NOTHING`).
|
||||||
|
WithArgs(collectionID, workID).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
err := s.repo.AddWorkToCollection(context.Background(), collectionID, workID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TestRemoveWorkFromCollection() {
|
||||||
|
collectionID, workID := uint(1), uint(1)
|
||||||
|
s.mock.ExpectExec(`DELETE FROM collection_works WHERE collection_id = \$1 AND work_id = \$2`).
|
||||||
|
WithArgs(collectionID, workID).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
err := s.repo.RemoveWorkFromCollection(context.Background(), collectionID, workID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TestListPublic() {
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "is_public"}).
|
||||||
|
AddRow(1, true).
|
||||||
|
AddRow(2, true)
|
||||||
|
|
||||||
|
s.mock.ExpectQuery(`SELECT \* FROM "collections" WHERE is_public = \$1`).
|
||||||
|
WithArgs(true).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
collections, err := s.repo.ListPublic(context.Background())
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(collections, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CollectionRepositoryTestSuite) TestListByWorkID() {
|
||||||
|
workID := uint(1)
|
||||||
|
rows := sqlmock.NewRows([]string{"id"}).
|
||||||
|
AddRow(1).
|
||||||
|
AddRow(2)
|
||||||
|
|
||||||
|
s.mock.ExpectQuery(`SELECT "collections"\."id","collections"\."created_at","collections"\."updated_at","collections"\."language","collections"\."slug","collections"\."name","collections"\."description","collections"\."user_id","collections"\."is_public","collections"\."cover_image_url" FROM "collections" JOIN collection_works ON collection_works\.collection_id = collections\.id WHERE collection_works\.work_id = \$1`).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
collections, err := s.repo.ListByWorkID(context.Background(), workID)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.Require().Len(collections, 2)
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/comment"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type commentRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCommentRepository creates a new CommentRepository.
|
// NewCommentRepository creates a new CommentRepository.
|
||||||
func NewCommentRepository(db *gorm.DB) comment.CommentRepository {
|
func NewCommentRepository(db *gorm.DB) domain.CommentRepository {
|
||||||
return &commentRepository{
|
return &commentRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Comment](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
198
internal/data/sql/comment_repository_test.go
Normal file
198
internal/data/sql/comment_repository_test.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
repo "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCommentRepository(t *testing.T) {
|
||||||
|
db, _, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
assert.NotNil(t, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommentRepository_ListByUserID(t *testing.T) {
|
||||||
|
t.Run("should return comments for a given user id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
expectedComments := []domain.Comment{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedComments[0].ID, expectedComments[0].CreatedAt, expectedComments[0].UpdatedAt).
|
||||||
|
AddRow(expectedComments[1].ID, expectedComments[1].CreatedAt, expectedComments[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedComments, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommentRepository_ListByWorkID(t *testing.T) {
|
||||||
|
t.Run("should return comments for a given work id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
expectedComments := []domain.Comment{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedComments[0].ID, expectedComments[0].CreatedAt, expectedComments[0].UpdatedAt).
|
||||||
|
AddRow(expectedComments[1].ID, expectedComments[1].CreatedAt, expectedComments[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedComments, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommentRepository_ListByTranslationID(t *testing.T) {
|
||||||
|
t.Run("should return comments for a given translation id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
translationID := uint(1)
|
||||||
|
expectedComments := []domain.Comment{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedComments[0].ID, expectedComments[0].CreatedAt, expectedComments[0].UpdatedAt).
|
||||||
|
AddRow(expectedComments[1].ID, expectedComments[1].CreatedAt, expectedComments[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE translation_id = $1`)).
|
||||||
|
WithArgs(translationID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByTranslationID(context.Background(), translationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedComments, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
translationID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE translation_id = $1`)).
|
||||||
|
WithArgs(translationID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByTranslationID(context.Background(), translationID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommentRepository_ListByParentID(t *testing.T) {
|
||||||
|
t.Run("should return comments for a given parent id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
parentID := uint(1)
|
||||||
|
expectedComments := []domain.Comment{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedComments[0].ID, expectedComments[0].CreatedAt, expectedComments[0].UpdatedAt).
|
||||||
|
AddRow(expectedComments[1].ID, expectedComments[1].CreatedAt, expectedComments[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE parent_id = $1`)).
|
||||||
|
WithArgs(parentID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByParentID(context.Background(), parentID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedComments, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewCommentRepository(db)
|
||||||
|
|
||||||
|
parentID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "comments" WHERE parent_id = $1`)).
|
||||||
|
WithArgs(parentID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
comments, err := repo.ListByParentID(context.Background(), parentID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, comments)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/contribution"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type contributionRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewContributionRepository creates a new ContributionRepository.
|
// NewContributionRepository creates a new ContributionRepository.
|
||||||
func NewContributionRepository(db *gorm.DB) contribution.ContributionRepository {
|
func NewContributionRepository(db *gorm.DB) domain.ContributionRepository {
|
||||||
return &contributionRepository{
|
return &contributionRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Contribution](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
242
internal/data/sql/contribution_repository_test.go
Normal file
242
internal/data/sql/contribution_repository_test.go
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
package sql_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
repo "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/domain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewContributionRepository(t *testing.T) {
|
||||||
|
db, _, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
assert.NotNil(t, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContributionRepository_ListByUserID(t *testing.T) {
|
||||||
|
t.Run("should return contributions for a given user id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
expectedContributions := []domain.Contribution{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedContributions[0].ID, expectedContributions[0].CreatedAt, expectedContributions[0].UpdatedAt).
|
||||||
|
AddRow(expectedContributions[1].ID, expectedContributions[1].CreatedAt, expectedContributions[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContributions, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
userID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE user_id = $1`)).
|
||||||
|
WithArgs(userID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByUserID(context.Background(), userID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContributionRepository_ListByReviewerID(t *testing.T) {
|
||||||
|
t.Run("should return contributions for a given reviewer id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
reviewerID := uint(1)
|
||||||
|
expectedContributions := []domain.Contribution{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedContributions[0].ID, expectedContributions[0].CreatedAt, expectedContributions[0].UpdatedAt).
|
||||||
|
AddRow(expectedContributions[1].ID, expectedContributions[1].CreatedAt, expectedContributions[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE reviewer_id = $1`)).
|
||||||
|
WithArgs(reviewerID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByReviewerID(context.Background(), reviewerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContributions, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
reviewerID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE reviewer_id = $1`)).
|
||||||
|
WithArgs(reviewerID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByReviewerID(context.Background(), reviewerID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContributionRepository_ListByWorkID(t *testing.T) {
|
||||||
|
t.Run("should return contributions for a given work id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
expectedContributions := []domain.Contribution{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedContributions[0].ID, expectedContributions[0].CreatedAt, expectedContributions[0].UpdatedAt).
|
||||||
|
AddRow(expectedContributions[1].ID, expectedContributions[1].CreatedAt, expectedContributions[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContributions, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
workID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE work_id = $1`)).
|
||||||
|
WithArgs(workID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByWorkID(context.Background(), workID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContributionRepository_ListByTranslationID(t *testing.T) {
|
||||||
|
t.Run("should return contributions for a given translation id", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
translationID := uint(1)
|
||||||
|
expectedContributions := []domain.Contribution{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedContributions[0].ID, expectedContributions[0].CreatedAt, expectedContributions[0].UpdatedAt).
|
||||||
|
AddRow(expectedContributions[1].ID, expectedContributions[1].CreatedAt, expectedContributions[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE translation_id = $1`)).
|
||||||
|
WithArgs(translationID).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByTranslationID(context.Background(), translationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContributions, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
translationID := uint(1)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE translation_id = $1`)).
|
||||||
|
WithArgs(translationID).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByTranslationID(context.Background(), translationID)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContributionRepository_ListByStatus(t *testing.T) {
|
||||||
|
t.Run("should return contributions for a given status", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
status := "draft"
|
||||||
|
expectedContributions := []domain.Contribution{
|
||||||
|
{BaseModel: domain.BaseModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
{BaseModel: domain.BaseModel{ID: 2, CreatedAt: time.Now(), UpdatedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).
|
||||||
|
AddRow(expectedContributions[0].ID, expectedContributions[0].CreatedAt, expectedContributions[0].UpdatedAt).
|
||||||
|
AddRow(expectedContributions[1].ID, expectedContributions[1].CreatedAt, expectedContributions[1].UpdatedAt)
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE status = $1`)).
|
||||||
|
WithArgs(status).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByStatus(context.Background(), status)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedContributions, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return error if query fails", func(t *testing.T) {
|
||||||
|
db, mock, err := newMockDb()
|
||||||
|
require.NoError(t, err)
|
||||||
|
repo := repo.NewContributionRepository(db)
|
||||||
|
|
||||||
|
status := "draft"
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "contributions" WHERE status = $1`)).
|
||||||
|
WithArgs(status).
|
||||||
|
WillReturnError(sql.ErrNoRows)
|
||||||
|
|
||||||
|
contributions, err := repo.ListByStatus(context.Background(), status)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, contributions)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ package sql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/copyright_claim"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -14,7 +13,7 @@ type copyrightClaimRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
|
// NewCopyrightClaimRepository creates a new CopyrightClaimRepository.
|
||||||
func NewCopyrightClaimRepository(db *gorm.DB) copyright_claim.Copyright_claimRepository {
|
func NewCopyrightClaimRepository(db *gorm.DB) domain.CopyrightClaimRepository {
|
||||||
return ©rightClaimRepository{
|
return ©rightClaimRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.CopyrightClaim](db),
|
||||||
db: db,
|
db: db,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/domain/copyright"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -15,45 +14,13 @@ type copyrightRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCopyrightRepository creates a new CopyrightRepository.
|
// NewCopyrightRepository creates a new CopyrightRepository.
|
||||||
func NewCopyrightRepository(db *gorm.DB) copyright.CopyrightRepository {
|
func NewCopyrightRepository(db *gorm.DB) domain.CopyrightRepository {
|
||||||
return ©rightRepository{
|
return ©rightRepository{
|
||||||
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
|
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db),
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AttachToEntity attaches a copyright to any entity type
|
|
||||||
func (r *copyrightRepository) AttachToEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
|
||||||
copyrightable := domain.Copyrightable{
|
|
||||||
CopyrightID: copyrightID,
|
|
||||||
CopyrightableID: entityID,
|
|
||||||
CopyrightableType: entityType,
|
|
||||||
}
|
|
||||||
return r.db.WithContext(ctx).Create(©rightable).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetachFromEntity removes a copyright from an entity
|
|
||||||
func (r *copyrightRepository) DetachFromEntity(ctx context.Context, copyrightID uint, entityID uint, entityType string) error {
|
|
||||||
return r.db.WithContext(ctx).Where("copyright_id = ? AND copyrightable_id = ? AND copyrightable_type = ?",
|
|
||||||
copyrightID, entityID, entityType).Delete(&domain.Copyrightable{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByEntity gets all copyrights for a specific entity
|
|
||||||
func (r *copyrightRepository) GetByEntity(ctx context.Context, entityID uint, entityType string) ([]domain.Copyright, error) {
|
|
||||||
var copyrights []domain.Copyright
|
|
||||||
err := r.db.WithContext(ctx).Joins("JOIN copyrightables ON copyrightables.copyright_id = copyrights.id").
|
|
||||||
Where("copyrightables.copyrightable_id = ? AND copyrightables.copyrightable_type = ?", entityID, entityType).
|
|
||||||
Preload("Translations").
|
|
||||||
Find(©rights).Error
|
|
||||||
return copyrights, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEntitiesByCopyright gets all entities that have a specific copyright
|
|
||||||
func (r *copyrightRepository) GetEntitiesByCopyright(ctx context.Context, copyrightID uint) ([]domain.Copyrightable, error) {
|
|
||||||
var copyrightables []domain.Copyrightable
|
|
||||||
err := r.db.WithContext(ctx).Where("copyright_id = ?", copyrightID).Find(©rightables).Error
|
|
||||||
return copyrightables, err
|
|
||||||
}
|
|
||||||
// AddTranslation adds a translation to a copyright
|
// AddTranslation adds a translation to a copyright
|
||||||
func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
func (r *copyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error {
|
||||||
return r.db.WithContext(ctx).Create(translation).Error
|
return r.db.WithContext(ctx).Create(translation).Error
|
||||||
@ -78,3 +45,43 @@ func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copy
|
|||||||
}
|
}
|
||||||
return &translation, nil
|
return &translation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO work_copyrights (work_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", workID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM work_copyrights WHERE work_id = ? AND copyright_id = ?", workID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO author_copyrights (author_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", authorID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM author_copyrights WHERE author_id = ? AND copyright_id = ?", authorID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO book_copyrights (book_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", bookID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM book_copyrights WHERE book_id = ? AND copyright_id = ?", bookID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO publisher_copyrights (publisher_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", publisherID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM publisher_copyrights WHERE publisher_id = ? AND copyright_id = ?", publisherID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("INSERT INTO source_copyrights (source_id, copyright_id) VALUES (?, ?) ON CONFLICT DO NOTHING", sourceID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *copyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
|
return r.db.WithContext(ctx).Exec("DELETE FROM source_copyrights WHERE source_id = ? AND copyright_id = ?", sourceID, copyrightID).Error
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user