mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 04:01:34 +00:00
Refactor ID handling to use UUIDs across the application
Some checks failed
Some checks failed
- Updated database models and repositories to replace uint IDs with UUIDs. - Modified test fixtures to generate and use UUIDs for authors, translations, users, and works. - Adjusted mock implementations to align with the new UUID structure. - Ensured all relevant functions and methods are updated to handle UUIDs correctly. - Added necessary imports for UUID handling in various files.
This commit is contained in:
parent
6fdf0a97fd
commit
d50722dad5
@ -6,7 +6,7 @@ tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
# Just plain old shell command. You could use `make` as well.
|
||||
cmd = "go build -o ./tmp/tercul ."
|
||||
cmd = "go build -o ./tmp/tercul ./cmd/api"
|
||||
# Binary file yields from `cmd`.
|
||||
bin = "tmp/tercul"
|
||||
# Customize binary.
|
||||
@ -43,4 +43,4 @@ runner = "green"
|
||||
|
||||
[misc]
|
||||
# Delete tmp directory on exit
|
||||
clean_on_exit = true
|
||||
clean_on_exit = true
|
||||
|
||||
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@ -1,16 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "deps"
|
||||
include: "scope"
|
||||
769
.github/workflows/README.md
vendored
769
.github/workflows/README.md
vendored
@ -1,769 +0,0 @@
|
||||
# GitHub Actions CI/CD Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the GitHub Actions CI/CD pipeline for the Tercul backend
|
||||
project, updated to 2025 best practices. The pipeline ensures code quality,
|
||||
security, and reliable deployments through automated testing, linting, security
|
||||
scanning, and containerized deployments.
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Workflow | File | Purpose | Triggers |
|
||||
|----------|------|---------|----------|
|
||||
| **Lint** | `lint.yml` | Code quality & style | Push/PR to main, develop |
|
||||
| **Test** | `test.yml` | Unit tests & compatibility | Push/PR to main, develop |
|
||||
| **Build** | `build.yml` | Binary compilation | Push/PR to main, develop |
|
||||
| **Security** | `security.yml` | CodeQL scanning | Push/PR to main + Weekly |
|
||||
| **Docker Build** | `docker-build.yml` | Container images | Push to main, Tags, PRs |
|
||||
| **Deploy** | `deploy.yml` | Production deployment | Tags (v*), Manual |
|
||||
|
||||
## Architecture
|
||||
|
||||
The CI/CD pipeline follows the **Single Responsibility Principle** with focused workflows:
|
||||
|
||||
1. **Lint** (`lint.yml`) - Code quality and style enforcement
|
||||
2. **Test** (`test.yml`) - Unit tests and compatibility matrix
|
||||
3. **Build** (`build.yml`) - Binary compilation and verification
|
||||
4. **Security** (`security.yml`) - CodeQL security scanning
|
||||
5. **Docker Build** (`docker-build.yml`) - Container image building and publishing
|
||||
6. **Deploy** (`deploy.yml`) - Production deployment orchestration
|
||||
|
||||
## Workflows
|
||||
|
||||
### Lint Workflow (`lint.yml`)
|
||||
|
||||
**Purpose**: Ensures code quality and consistent style across the codebase.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Push to `main` and `develop` branches
|
||||
- Pull requests targeting `main` and `develop` branches
|
||||
|
||||
**Jobs**:
|
||||
|
||||
- `golangci-lint`: Go linting with golangci-lint
|
||||
- Checkout code
|
||||
- Setup Go 1.25 with caching
|
||||
- Install dependencies
|
||||
- Tidy modules (ensures go.mod/go.sum are clean)
|
||||
- Run linter with 5-minute timeout
|
||||
|
||||
**Configuration**:
|
||||
|
||||
- **Timeout**: 5 minutes
|
||||
- **Target**: All Go files (`./...`)
|
||||
- **Cache**: Enabled for faster runs
|
||||
|
||||
### Test Workflow (`test.yml`)
|
||||
|
||||
**Purpose**: Validates code functionality through comprehensive testing.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Push to `main` and `develop` branches
|
||||
- Pull requests targeting `main` and `develop` branches
|
||||
|
||||
**Jobs**:
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
- **Environment**: Ubuntu with PostgreSQL 15 and Redis 7
|
||||
- **Features**:
|
||||
|
||||
- Race detection enabled
|
||||
- Code coverage reporting (atomic mode)
|
||||
- HTML coverage report generation
|
||||
- Test result summaries in GitHub UI
|
||||
- 30-day artifact retention
|
||||
|
||||
**Services**:
|
||||
|
||||
- PostgreSQL 15 with health checks
|
||||
- Redis 7-alpine with health checks
|
||||
|
||||
#### Compatibility Matrix
|
||||
|
||||
- **Trigger**: Push to `main` branch only
|
||||
- **Strategy**: Tests across Go versions 1.22, 1.23, 1.24, 1.25
|
||||
- **Purpose**: Ensures compatibility with multiple Go versions
|
||||
- **Fail-fast**: Disabled (all versions tested even if one fails)
|
||||
|
||||
### Build Workflow (`build.yml`)
|
||||
|
||||
**Purpose**: Compiles the application binary and validates the build.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Push to `main` and `develop` branches
|
||||
- Pull requests targeting `main` and `develop` branches
|
||||
|
||||
**Jobs**:
|
||||
|
||||
- `build-binary`: Binary compilation and verification
|
||||
- Dependency verification with `go mod verify`
|
||||
- Build to `bin/tercul-backend`
|
||||
- Binary validation test
|
||||
- Artifact upload (30-day retention)
|
||||
|
||||
**Permissions**:
|
||||
|
||||
- `contents: read` - Read repository code
|
||||
- `attestations: write` - Future SLSA attestation support
|
||||
- `id-token: write` - OIDC token for attestations
|
||||
|
||||
### Security Workflow (`security.yml`)
|
||||
|
||||
**Purpose**: Automated security vulnerability detection with CodeQL.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Push to `main` branch
|
||||
- Pull requests targeting `main` branch
|
||||
- Scheduled: Every Monday at 14:20 UTC
|
||||
|
||||
**Jobs**:
|
||||
|
||||
- `codeql-analysis`: CodeQL security scanning for Go
|
||||
- Setup Go 1.25 (must run before CodeQL init)
|
||||
- Initialize CodeQL with Go language support
|
||||
- Build code for analysis
|
||||
- Perform security scan
|
||||
- Category: "backend-security" for tracking
|
||||
- Continues on error (warns if code scanning not enabled)
|
||||
|
||||
**Important Notes**:
|
||||
|
||||
- **Go Setup Order**: Go must be set up BEFORE CodeQL initialization to ensure version compatibility
|
||||
- **Code Scanning**: Must be enabled in repository settings (Settings > Security > Code scanning)
|
||||
- **Error Handling**: Workflow continues on CodeQL errors to allow scanning even if upload fails
|
||||
|
||||
**CodeQL Configuration**:
|
||||
|
||||
The workflow can be customized with additional query suites:
|
||||
|
||||
```yaml
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
# Run security-extended suite for more comprehensive scanning
|
||||
queries: security-extended
|
||||
# Or use security-and-quality for maintainability checks
|
||||
# queries: security-and-quality
|
||||
```
|
||||
|
||||
**Available Query Suites**:
|
||||
|
||||
- `security-extended`: Default queries plus lower severity/precision queries
|
||||
- `security-and-quality`: Security queries plus maintainability and reliability
|
||||
|
||||
**Custom Query Packs**:
|
||||
|
||||
Add custom CodeQL query packs for specialized analysis:
|
||||
|
||||
```yaml
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
packs: my-org/go-security-queries@1.0.0
|
||||
```
|
||||
|
||||
### Docker Build Workflow (`docker-build.yml`)
|
||||
|
||||
**Purpose**: Builds and publishes multi-architecture Docker images.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Push to `main` branch
|
||||
- Tag pushes with `v*` pattern
|
||||
- Pull requests targeting `main` branch
|
||||
|
||||
**Jobs**:
|
||||
|
||||
- `build-image`: Multi-platform Docker image building
|
||||
|
||||
- Docker Buildx setup for multi-arch builds
|
||||
- Login to GitHub Container Registry
|
||||
- Metadata extraction for tags and labels
|
||||
- Build for AMD64 and ARM64 architectures
|
||||
- Push to registry (except for PRs)
|
||||
- Generate build provenance attestation
|
||||
|
||||
**Image Tagging Strategy**:
|
||||
|
||||
- `main` branch pushes → `main` + `sha-<hash>` tags
|
||||
- Tag pushes (`v1.2.3`) → `v1.2.3`, `1.2.3`, `1.2`, `sha-<hash>` tags
|
||||
- Pull requests → `pr-<number>` tag (build only, not pushed)
|
||||
|
||||
**Push Behavior**:
|
||||
|
||||
- **Pushes to main/tags**: Build and push to registry
|
||||
- **Pull requests**: Build only (validation, no push)
|
||||
|
||||
**Platforms**: linux/amd64, linux/arm64
|
||||
|
||||
### Deploy Workflow (`deploy.yml`)
|
||||
|
||||
**Purpose**: Production deployment orchestration to Docker Swarm.
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Tag pushes with `v*` pattern
|
||||
- Manual dispatch with version input
|
||||
|
||||
**Jobs**:
|
||||
|
||||
- `deploy-production`: Deployment to production environment
|
||||
|
||||
- Version extraction from tag or manual input
|
||||
- Docker Swarm service update (SSH-based deployment template)
|
||||
- Deployment summary with timestamp
|
||||
- Environment protection and tracking
|
||||
|
||||
**Environment**:
|
||||
|
||||
- Name: `production`
|
||||
- URL: Configurable production URL
|
||||
- Protection: Supports required reviewers and wait timers
|
||||
|
||||
**Manual Deployment**:
|
||||
|
||||
Deployments can be triggered manually from the Actions tab with a specific version.
|
||||
|
||||
**Required Secrets**:
|
||||
|
||||
- `SWARM_HOST`: Docker Swarm manager hostname/IP
|
||||
- `SWARM_SSH_KEY`: SSH private key for Swarm access
|
||||
|
||||
## Workflow Execution Order
|
||||
|
||||
**On Pull Request**:
|
||||
|
||||
1. Lint → Validates code style
|
||||
2. Test → Runs unit tests with coverage
|
||||
3. Build → Compiles binary
|
||||
4. Security → CodeQL analysis (main branch PRs only)
|
||||
5. Docker Build → Builds image (no push)
|
||||
|
||||
**On Push to main**:
|
||||
|
||||
1. Lint → Code quality check
|
||||
2. Test → Unit tests + compatibility matrix
|
||||
3. Build → Binary compilation
|
||||
4. Security → CodeQL scan
|
||||
5. Docker Build → Build and push image
|
||||
|
||||
**On Tag Push (v\*)**:
|
||||
|
||||
1. Docker Build → Build and push versioned image
|
||||
2. Deploy → Deploy to production
|
||||
|
||||
## Security Features
|
||||
|
||||
### Permissions Management
|
||||
|
||||
- **Principle of Least Privilege**: Each workflow has minimal required permissions
|
||||
- **GITHUB_TOKEN Restrictions**: Read-only by default, elevated only when necessary
|
||||
- **Workflow Separation**: Each workflow operates independently
|
||||
- **Attestation Permissions**: For build provenance and SLSA compliance
|
||||
|
||||
### Code Security
|
||||
|
||||
- **CodeQL Integration**: Automated security scanning for Go code
|
||||
- **Dependency Verification**: `go mod verify` ensures integrity
|
||||
- **Module Tidying**: `go mod tidy` prevents dependency drift
|
||||
|
||||
### Container Security
|
||||
|
||||
- **Multi-platform Builds**: Ensures compatibility and security across architectures
|
||||
- **Provenance Attestation**: Cryptographic proof of build integrity
|
||||
- **Registry Security**: GitHub Container Registry with token-based authentication
|
||||
|
||||
### Secrets Management
|
||||
|
||||
- **No Hardcoded Secrets**: All sensitive data uses GitHub secrets
|
||||
- **Environment Variables**: Proper isolation of configuration
|
||||
- **GITHUB_TOKEN**: Automatic authentication for package registries
|
||||
- **Granular Permissions**: Package-level access control
|
||||
|
||||
### Package Registry Security
|
||||
|
||||
- **GITHUB_TOKEN Authentication**: No personal access tokens required
|
||||
- **Automatic Permissions**: Packages inherit repository visibility
|
||||
- **Repository Scoped**: Packages linked to source repository
|
||||
- **Granular Access**: Fine-grained permissions per package
|
||||
- **Artifact Attestation**: Cryptographic proof of build provenance
|
||||
- **OIDC Support**: Token-based authentication without long-lived credentials
|
||||
|
||||
## Best Practices Implemented
|
||||
|
||||
### 2025 Updates
|
||||
|
||||
- **Semantic Versioning**: Actions pinned to major versions (e.g., `@v5`) instead of SHA
|
||||
- **Caching Optimization**: Go module and Docker layer caching
|
||||
- **Matrix Testing**: Cross-version compatibility validation
|
||||
- **Service Health Checks**: Database and Redis readiness verification
|
||||
- **Artifact Management**: Proper retention policies and naming
|
||||
|
||||
### Performance Optimizations
|
||||
|
||||
- **Dependency Caching**: Reduces setup time significantly
|
||||
- **Parallel Jobs**: Independent jobs run concurrently
|
||||
- **Conditional Execution**: Security scans only on main branch
|
||||
- **Artifact Upload**: Efficient storage and retrieval
|
||||
|
||||
### Reliability Features
|
||||
|
||||
- **Timeout Configuration**: Prevents hanging jobs
|
||||
- **Error Handling**: Proper exit codes and logging
|
||||
- **Health Checks**: Service readiness validation
|
||||
- **Retention Policies**: Balanced storage management
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Go Version
|
||||
|
||||
- Primary: Go 1.25
|
||||
- Matrix: Go 1.22, 1.23, 1.24, 1.25
|
||||
|
||||
### Services
|
||||
|
||||
- **PostgreSQL**: Version 15 with health checks
|
||||
- **Redis**: Version 7-alpine with ping health checks
|
||||
|
||||
### Tools
|
||||
|
||||
- **Linter**: golangci-lint latest with 5-minute timeout
|
||||
- **Testing**: `go test` with race detection and coverage
|
||||
- **Building**: `go build` with verbose output
|
||||
- **Security**: CodeQL for Go analysis
|
||||
|
||||
### Caching
|
||||
|
||||
- **Go Modules**: Automatic caching via setup-go action
|
||||
- **Docker Layers**: GitHub Actions cache with GHA type
|
||||
- **CodeQL Databases**: Stored in `${{ github.runner_temp }}/codeql_databases`
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Dependabot Configuration
|
||||
|
||||
- **GitHub Actions**: Weekly updates with "ci" prefix
|
||||
- **Go Modules**: Weekly updates with "deps" prefix
|
||||
- **Automated PRs**: Keeps dependencies current and secure
|
||||
|
||||
### Monitoring
|
||||
|
||||
- **Workflow Runs**: GitHub Actions tab for execution monitoring
|
||||
- **Security Alerts**: Code scanning results and dependency alerts
|
||||
- **Coverage Reports**: Artifact downloads for test coverage analysis
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Common Issues
|
||||
|
||||
1. **Cache Misses**: Clear caches if corruption suspected
|
||||
2. **Service Failures**: Check health check configurations
|
||||
3. **Permission Errors**: Verify GITHUB_TOKEN scopes
|
||||
4. **Timeout Issues**: Adjust timeout values in workflow configurations
|
||||
5. **Docker Push Failures**: Check package write permissions
|
||||
6. **Registry Authentication**: Ensure `packages: write` permission is set
|
||||
|
||||
#### Package Registry Issues
|
||||
|
||||
**Problem**: Cannot push to GitHub Container Registry
|
||||
|
||||
```yaml
|
||||
# Solution: Ensure proper permissions
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
```
|
||||
|
||||
**Problem**: Package not visible after push
|
||||
|
||||
- Check package visibility settings (public/private/internal)
|
||||
- Verify repository is linked to package
|
||||
- Ensure workflow completed successfully
|
||||
|
||||
**Problem**: Cannot pull package in workflow
|
||||
|
||||
```yaml
|
||||
# Solution: Login to registry first
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
#### Debugging
|
||||
|
||||
- **Manual Dispatch**: Use `workflow_dispatch` for testing
|
||||
- **Log Analysis**: Review step outputs for error details
|
||||
- **Artifact Inspection**: Download build artifacts for verification
|
||||
- **Package Logs**: Check package activity in GitHub UI
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
- **SLSA Integration**: Enhanced build attestation
|
||||
- **Dependency Review**: Automated dependency vulnerability checking
|
||||
- **Performance Testing**: Load testing integration
|
||||
- **Multi-environment Deployment**: Staging and production separation
|
||||
|
||||
### Scalability Considerations
|
||||
|
||||
- **Self-hosted Runners**: For resource-intensive jobs
|
||||
- **Job Parallelization**: Further optimization of concurrent execution
|
||||
- **Cache Optimization**: Advanced caching strategies
|
||||
|
||||
## Docker Image Usage
|
||||
|
||||
### Pulling Images
|
||||
|
||||
Pull the latest image:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/<owner>/<repository>:latest
|
||||
```
|
||||
|
||||
Pull a specific version:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/<owner>/<repository>:1.2.3
|
||||
```
|
||||
|
||||
### Running Locally
|
||||
|
||||
Run the container:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name tercul-backend \
|
||||
-p 8080:8080 \
|
||||
-e DATABASE_URL="postgres://..." \
|
||||
ghcr.io/<owner>/<repository>:latest
|
||||
```
|
||||
|
||||
### Docker Swarm Deployment
|
||||
|
||||
Deploy as a stack:
|
||||
|
||||
```bash
|
||||
docker stack deploy -c docker-compose.yml tercul
|
||||
```
|
||||
|
||||
Update running service:
|
||||
|
||||
```bash
|
||||
docker service update \
|
||||
--image ghcr.io/<owner>/<repository>:1.2.3 \
|
||||
tercul_backend
|
||||
```
|
||||
|
||||
### Verifying Attestations
|
||||
|
||||
Verify build provenance:
|
||||
|
||||
```bash
|
||||
gh attestation verify \
|
||||
oci://ghcr.io/<owner>/<repository>:latest \
|
||||
--owner <owner>
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When modifying workflows:
|
||||
|
||||
1. Test changes using `workflow_dispatch`
|
||||
2. Ensure backward compatibility
|
||||
3. Update this documentation
|
||||
4. Follow security best practices
|
||||
5. Use semantic versioning for action references
|
||||
6. Test Docker builds locally before pushing
|
||||
7. Verify package permissions after changes
|
||||
8. Review CodeQL alerts before merging PRs
|
||||
9. Update query packs regularly for latest security rules
|
||||
10. Test configuration changes with `workflow_dispatch`
|
||||
|
||||
## CodeQL Advanced Configuration
|
||||
|
||||
### Custom Configuration File
|
||||
|
||||
For complex CodeQL setups, use a configuration file (`.github/codeql/codeql-config.yml`):
|
||||
|
||||
```yaml
|
||||
name: "CodeQL Config"
|
||||
|
||||
# Disable default queries to run only custom queries
|
||||
disable-default-queries: false
|
||||
|
||||
# Specify query packs
|
||||
packs:
|
||||
- scope/go-security-pack
|
||||
- scope/go-compliance-pack@1.2.3
|
||||
|
||||
# Add custom queries
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
- uses: ./custom-queries
|
||||
|
||||
# Filter queries by severity
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
- exclude:
|
||||
id: go/redundant-assignment
|
||||
|
||||
# Scan specific directories
|
||||
paths:
|
||||
- internal
|
||||
- cmd
|
||||
- pkg
|
||||
|
||||
paths-ignore:
|
||||
- "**/*_test.go"
|
||||
- vendor
|
||||
- "**/testdata/**"
|
||||
|
||||
# Extend threat model (preview)
|
||||
threat-models: local
|
||||
```
|
||||
|
||||
Reference the config in your workflow:
|
||||
|
||||
```yaml
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
```
|
||||
|
||||
### Inline Configuration
|
||||
|
||||
Alternatively, specify configuration inline:
|
||||
|
||||
```yaml
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
config: |
|
||||
disable-default-queries: false
|
||||
queries:
|
||||
- uses: security-extended
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
```
|
||||
|
||||
### Scheduling CodeQL Scans
|
||||
|
||||
Run CodeQL on a schedule for regular security audits:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
# Run at 14:20 UTC every Monday
|
||||
- cron: '20 14 * * 1'
|
||||
```
|
||||
|
||||
### Avoiding Unnecessary Scans
|
||||
|
||||
Skip CodeQL for specific file changes:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.txt'
|
||||
- 'docs/**'
|
||||
```
|
||||
|
||||
### Analysis Categories
|
||||
|
||||
Categorize multiple analyses in monorepos:
|
||||
|
||||
```yaml
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "backend-api"
|
||||
```
|
||||
|
||||
### External Query Packs
|
||||
|
||||
Use query packs from GitHub Enterprise Server:
|
||||
|
||||
```yaml
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
registries: |
|
||||
- url: https://containers.GHEHOSTNAME/v2/
|
||||
packages:
|
||||
- my-company/*
|
||||
token: ${{ secrets.GHES_TOKEN }}
|
||||
packs: my-company/go-queries
|
||||
```
|
||||
|
||||
### Query Suite Examples
|
||||
|
||||
**Security-focused**:
|
||||
|
||||
```yaml
|
||||
queries:
|
||||
- uses: security-extended
|
||||
```
|
||||
|
||||
**Quality and security**:
|
||||
|
||||
```yaml
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
```
|
||||
|
||||
**Custom suite**:
|
||||
|
||||
```yaml
|
||||
queries:
|
||||
- uses: ./custom-queries/critical-security.qls
|
||||
```
|
||||
|
||||
## Advanced Workflow Techniques
|
||||
|
||||
### Workflow Commands
|
||||
|
||||
GitHub Actions supports workflow commands for advanced functionality:
|
||||
|
||||
#### Debug Logging
|
||||
|
||||
Enable detailed debugging with the `ACTIONS_STEP_DEBUG` secret:
|
||||
|
||||
```yaml
|
||||
- name: Debug step
|
||||
run: echo "::debug::Detailed debugging information"
|
||||
```
|
||||
|
||||
#### Annotations
|
||||
|
||||
Create annotations for notices, warnings, and errors:
|
||||
|
||||
```yaml
|
||||
# Notice annotation
|
||||
- run: echo "::notice file=app.go,line=10::Consider refactoring"
|
||||
|
||||
# Warning annotation
|
||||
- run: echo "::warning file=main.go,line=5,col=10::Deprecated function"
|
||||
|
||||
# Error annotation
|
||||
- run: echo "::error file=handler.go,line=20,title=Build Error::Missing import"
|
||||
```
|
||||
|
||||
#### Grouping Log Lines
|
||||
|
||||
Organize logs with collapsible groups:
|
||||
|
||||
```yaml
|
||||
- name: Build application
|
||||
run: |
|
||||
echo "::group::Compiling Go code"
|
||||
go build -v ./...
|
||||
echo "::endgroup::"
|
||||
```
|
||||
|
||||
#### Masking Secrets
|
||||
|
||||
Prevent sensitive values from appearing in logs:
|
||||
|
||||
```yaml
|
||||
- name: Generate token
|
||||
run: |
|
||||
TOKEN=$(generate_token)
|
||||
echo "::add-mask::$TOKEN"
|
||||
echo "TOKEN=$TOKEN" >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
#### Job Summaries
|
||||
|
||||
Add Markdown summaries to workflow runs:
|
||||
|
||||
```yaml
|
||||
- name: Test summary
|
||||
run: |
|
||||
echo "### Test Results :white_check_mark:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Total: 150 tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Passed: 148" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Failed: 2" >> $GITHUB_STEP_SUMMARY
|
||||
```
|
||||
|
||||
### Environment Files
|
||||
|
||||
Use environment files for dynamic configuration:
|
||||
|
||||
```yaml
|
||||
# Set environment variable for subsequent steps
|
||||
- name: Set build info
|
||||
run: |
|
||||
echo "BUILD_TIME=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_ENV
|
||||
echo "COMMIT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_ENV
|
||||
|
||||
# Use in later steps
|
||||
- name: Deploy
|
||||
run: echo "Deploying $COMMIT_SHA built at $BUILD_TIME"
|
||||
```
|
||||
|
||||
### Output Parameters
|
||||
|
||||
Share data between steps:
|
||||
|
||||
```yaml
|
||||
- name: Calculate version
|
||||
id: version
|
||||
run: echo "VERSION=1.2.3" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Use version
|
||||
run: echo "Building version ${{ steps.version.outputs.VERSION }}"
|
||||
```
|
||||
|
||||
### Multiline Values
|
||||
|
||||
Handle multiline strings safely:
|
||||
|
||||
```yaml
|
||||
- name: Store API response
|
||||
run: |
|
||||
{
|
||||
echo 'API_RESPONSE<<EOF'
|
||||
curl https://api.example.com/data
|
||||
echo EOF
|
||||
} >> $GITHUB_ENV
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
||||
- [Go CI/CD Best Practices](https://github.com/golang/go/wiki/Go-Release-Cycle)
|
||||
- [Security Hardening Guide](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
|
||||
- [Dependency Caching](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows)
|
||||
- [Workflow Commands Reference](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions)
|
||||
- [Publishing Packages with Actions](https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions)
|
||||
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
|
||||
- [Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
|
||||
- [CodeQL Configuration Reference](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning)
|
||||
- [CodeQL Query Suites](https://docs.github.com/en/code-security/code-scanning/managing-your-code-scanning-configuration/codeql-query-suites)
|
||||
- [CodeQL CLI Reference](https://docs.github.com/en/code-security/codeql-cli)
|
||||
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@ -1,46 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
build-binary:
|
||||
name: Build Binary
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Verify dependencies
|
||||
run: go mod verify
|
||||
|
||||
- name: Build application
|
||||
run: |
|
||||
go build -v -o bin/tercul-backend ./cmd/api
|
||||
ls -la bin/
|
||||
|
||||
- name: Test binary
|
||||
run: ./bin/tercul-backend --help || echo "Binary built successfully"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: tercul-backend-binary
|
||||
path: bin/
|
||||
retention-days: 30
|
||||
60
.github/workflows/deploy.yml
vendored
60
.github/workflows/deploy.yml
vendored
@ -1,60 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to deploy (e.g., v1.2.3)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: production
|
||||
url: https://tercul.example.com
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "VERSION=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Deploy to Docker Swarm
|
||||
env:
|
||||
SWARM_HOST: ${{ secrets.SWARM_HOST }}
|
||||
SWARM_SSH_KEY: ${{ secrets.SWARM_SSH_KEY }}
|
||||
IMAGE_TAG: ${{ steps.version.outputs.VERSION }}
|
||||
run: |
|
||||
# Uncomment and configure for actual Docker Swarm deployment
|
||||
# echo "$SWARM_SSH_KEY" > swarm_key
|
||||
# chmod 600 swarm_key
|
||||
# ssh -i swarm_key -o StrictHostKeyChecking=no \
|
||||
# deploy@$SWARM_HOST \
|
||||
# "docker service update \
|
||||
# --image ghcr.io/${{ github.repository }}:${IMAGE_TAG} \
|
||||
# tercul-backend"
|
||||
# rm swarm_key
|
||||
|
||||
echo "Deploying version ${{ steps.version.outputs.VERSION }} to production"
|
||||
echo "Image: ghcr.io/${{ github.repository }}:${IMAGE_TAG}"
|
||||
|
||||
- name: Deployment summary
|
||||
run: |
|
||||
echo "### Deployment Complete :rocket:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version**: ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image**: ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Deployed at**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
|
||||
65
.github/workflows/docker-build.yml
vendored
65
.github/workflows/docker-build.yml
vendored
@ -1,65 +0,0 @@
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,format=long
|
||||
|
||||
- name: Build and push
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-name: ghcr.io/${{ github.repository}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
33
.github/workflows/lint.yml
vendored
33
.github/workflows/lint.yml
vendored
@ -1,33 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
golangci-lint:
|
||||
name: Go Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Tidy modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout=5m ./...
|
||||
70
.github/workflows/security.yml
vendored
70
.github/workflows/security.yml
vendored
@ -1,70 +0,0 @@
|
||||
name: Security
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
# Run CodeQL scan every Monday at 14:20 UTC
|
||||
- cron: "20 14 * * 1"
|
||||
|
||||
jobs:
|
||||
codeql-analysis:
|
||||
name: CodeQL Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache: true
|
||||
|
||||
- name: Verify Go installation
|
||||
run: |
|
||||
echo "Go version: $(go version)"
|
||||
echo "Go path: $(which go)"
|
||||
echo "GOROOT: $GOROOT"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
# CodeQL will use the Go version installed by setup-go above
|
||||
# Optionally use security-extended for more comprehensive scanning
|
||||
# queries: security-extended
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build for analysis
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
id: codeql-analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "backend-security"
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check CodeQL Results
|
||||
if: steps.codeql-analysis.outcome == 'failure'
|
||||
run: |
|
||||
echo "⚠️ CodeQL analysis completed with warnings/errors"
|
||||
echo "This may be due to:"
|
||||
echo " 1. Code scanning not enabled in repository settings"
|
||||
echo " 2. Security alerts that need review"
|
||||
echo ""
|
||||
echo "To enable code scanning:"
|
||||
echo " Go to Settings > Security > Code security and analysis"
|
||||
echo " Click 'Set up' under Code scanning"
|
||||
echo ""
|
||||
echo "Analysis results are still available in the workflow artifacts."
|
||||
116
.github/workflows/test.yml
vendored
116
.github/workflows/test.yml
vendored
@ -1,116 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: testdb
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
- name: Generate test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### Test Results :test_tube:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Coverage**: See artifact for detailed report" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Race Detection**: Enabled" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Go Version**: 1.25" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: coverage-report
|
||||
path: |
|
||||
coverage.out
|
||||
coverage.html
|
||||
retention-days: 30
|
||||
|
||||
compatibility-matrix:
|
||||
name: Go ${{ matrix.go-version }} Compatibility
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: ["1.22", "1.23", "1.24", "1.25"]
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: testdb
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v -race ./...
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
# Install git and required dependencies
|
||||
RUN apk add --no-cache git build-base
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
FROM golang:1.24 AS development
|
||||
FROM golang:1.25 AS development
|
||||
|
||||
# Install Air for hot reloading (using the updated repository)
|
||||
RUN go install github.com/air-verse/air@latest
|
||||
RUN go install github.com/air-verse/air@v1.52.3
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
@ -19,4 +19,4 @@ COPY . .
|
||||
EXPOSE 8080
|
||||
|
||||
# Command to run the application with Air for hot reloading
|
||||
CMD ["air"]
|
||||
CMD ["air"]
|
||||
|
||||
15
Makefile
15
Makefile
@ -1,7 +1,18 @@
|
||||
.PHONY: lint-test
|
||||
.PHONY: lint-test dev dev-deps dev-down
|
||||
|
||||
DOCKER_COMPOSE := $(shell command -v docker-compose >/dev/null 2>&1 && echo docker-compose || echo "docker compose")
|
||||
|
||||
dev-deps:
|
||||
$(DOCKER_COMPOSE) up -d
|
||||
|
||||
dev-down:
|
||||
$(DOCKER_COMPOSE) down
|
||||
|
||||
dev: dev-deps
|
||||
go run ./cmd/cli serve
|
||||
|
||||
lint-test:
|
||||
@echo "Running linter..."
|
||||
golangci-lint run --timeout=5m
|
||||
@echo "Running tests..."
|
||||
go test ./...
|
||||
go test ./...
|
||||
|
||||
@ -126,13 +126,36 @@ func main() {
|
||||
// Create repositories
|
||||
repos := dbsql.NewRepositories(database, cfg)
|
||||
|
||||
// Initialize Redis cache once (shared by APQ, optional repo caching, and linguistics)
|
||||
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
|
||||
|
||||
// Create linguistics dependencies
|
||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
||||
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
|
||||
if sErr != nil {
|
||||
app_log.Fatal(sErr, "Failed to create sentiment provider")
|
||||
}
|
||||
|
||||
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
|
||||
StatsRepo: repos.Analytics,
|
||||
LikeCounter: repos.Like,
|
||||
CommentCounter: repos.Comment,
|
||||
BookmarkCounter: repos.Bookmark,
|
||||
TranslationCount: repos.Translation,
|
||||
TranslationList: repos.Translation,
|
||||
}
|
||||
|
||||
linguisticsFactory := linguistics.NewLinguisticsFactory(
|
||||
cfg,
|
||||
database,
|
||||
redisCache,
|
||||
2,
|
||||
true,
|
||||
sentimentProvider,
|
||||
workAnalyticsDeps,
|
||||
)
|
||||
|
||||
analysisRepo := linguisticsFactory.GetAnalysisRepository()
|
||||
|
||||
// Create platform components
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
|
||||
@ -182,8 +205,6 @@ func main() {
|
||||
App: application,
|
||||
}
|
||||
|
||||
// Initialize Redis Cache for APQ
|
||||
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
|
||||
var queryCache gql.Cache[string]
|
||||
if cacheErr != nil {
|
||||
app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error())
|
||||
|
||||
@ -1 +1 @@
|
||||
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T12:33:05.005477+01:00"}
|
||||
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T15:37:36.561092+01:00"}
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tercul/internal/data/sql"
|
||||
@ -30,7 +29,7 @@ const (
|
||||
)
|
||||
|
||||
type checkpoint struct {
|
||||
LastProcessedID uint `json:"last_processed_id"`
|
||||
LastProcessedID string `json:"last_processed_id"`
|
||||
TotalProcessed int `json:"total_processed"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
@ -104,7 +103,7 @@ Example:
|
||||
if resume {
|
||||
cp = loadCheckpoint()
|
||||
if cp != nil {
|
||||
logger.Info(fmt.Sprintf("Resuming from checkpoint: last_id=%d, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed))
|
||||
logger.Info(fmt.Sprintf("Resuming from checkpoint: last_id=%s, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed))
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,10 +267,10 @@ func migrateTranslations(
|
||||
logger.Info(fmt.Sprintf("Found %d translations", totalTranslations))
|
||||
|
||||
// Filter translations if resuming from checkpoint
|
||||
if cp != nil && cp.LastProcessedID > 0 {
|
||||
if cp != nil && cp.LastProcessedID != "" {
|
||||
filtered := make([]domain.Translation, 0, len(translations))
|
||||
for _, t := range translations {
|
||||
if t.ID > cp.LastProcessedID {
|
||||
if t.ID.String() > cp.LastProcessedID {
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
}
|
||||
@ -282,11 +281,11 @@ func migrateTranslations(
|
||||
|
||||
// Process translations in batches
|
||||
batch := make([]domain.Translation, 0, batchSize)
|
||||
lastProcessedID := uint(0)
|
||||
lastProcessedID := ""
|
||||
|
||||
for i, translation := range translations {
|
||||
batch = append(batch, translation)
|
||||
lastProcessedID = translation.ID
|
||||
lastProcessedID = translation.ID.String()
|
||||
|
||||
// Process batch when it reaches the batch size or at the end
|
||||
if len(batch) >= batchSize || i == len(translations)-1 {
|
||||
@ -326,7 +325,7 @@ func indexBatch(index bleve.Index, translations []domain.Translation) error {
|
||||
batch := index.NewBatch()
|
||||
for _, t := range translations {
|
||||
doc := map[string]interface{}{
|
||||
"id": strconv.FormatUint(uint64(t.ID), 10),
|
||||
"id": t.ID.String(),
|
||||
"title": t.Title,
|
||||
"content": t.Content,
|
||||
"description": t.Description,
|
||||
|
||||
@ -38,11 +38,11 @@ func TestMigrateTranslations_LargeBatch(t *testing.T) {
|
||||
translations := make([]domain.Translation, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
translations[i] = domain.Translation{
|
||||
BaseModel: domain.BaseModel{ID: uint(i + 1)},
|
||||
Title: "Test Translation",
|
||||
Content: "Content",
|
||||
Language: "en",
|
||||
Status: domain.TranslationStatusPublished,
|
||||
BaseModel: domain.BaseModel{ID: uint(i + 1)},
|
||||
Title: "Test Translation",
|
||||
Content: "Content",
|
||||
Language: "en",
|
||||
Status: domain.TranslationStatusPublished,
|
||||
TranslatableID: uint(i + 1),
|
||||
TranslatableType: "works",
|
||||
}
|
||||
@ -104,11 +104,11 @@ func TestIndexBatch_WithTranslatorID(t *testing.T) {
|
||||
translatorID := uint(123)
|
||||
translations := []domain.Translation{
|
||||
{
|
||||
BaseModel: domain.BaseModel{ID: 1},
|
||||
Title: "Test",
|
||||
Content: "Content",
|
||||
Language: "en",
|
||||
Status: domain.TranslationStatusPublished,
|
||||
BaseModel: domain.BaseModel{ID: 1},
|
||||
Title: "Test",
|
||||
Content: "Content",
|
||||
Language: "en",
|
||||
Status: domain.TranslationStatusPublished,
|
||||
TranslatableID: 100,
|
||||
TranslatableType: "works",
|
||||
TranslatorID: &translatorID,
|
||||
@ -129,4 +129,3 @@ func TestCheckpoint_InvalidJSON(t *testing.T) {
|
||||
// This would require mocking file system, but for now we test the happy path
|
||||
// Invalid JSON handling is tested implicitly through file operations
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Trans
|
||||
}
|
||||
|
||||
// Implement other required methods with minimal implementations
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
|
||||
|
||||
@ -114,4 +114,3 @@ func TestRootCommand(t *testing.T) {
|
||||
assert.NotEmpty(t, cmd.Short)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ package commands
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"tercul/cmd/cli/internal/bootstrap"
|
||||
"tercul/internal/enrichment"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"tercul/internal/platform/db"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -33,7 +33,7 @@ Example:
|
||||
return fmt.Errorf("both --type and --id are required")
|
||||
}
|
||||
|
||||
entityIDUint, err := strconv.ParseUint(entityID, 10, 64)
|
||||
entityIDUUID, err := uuid.Parse(entityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid entity ID: %w", err)
|
||||
}
|
||||
@ -71,11 +71,11 @@ Example:
|
||||
|
||||
// Fetch, enrich, and save the entity
|
||||
ctx := context.Background()
|
||||
log.Info(fmt.Sprintf("Enriching %s with ID %d", entityType, entityIDUint))
|
||||
log.Info(fmt.Sprintf("Enriching %s with ID %s", entityType, entityIDUUID.String()))
|
||||
|
||||
switch entityType {
|
||||
case "author":
|
||||
author, err := deps.Repos.Author.GetByID(ctx, uint(entityIDUint))
|
||||
author, err := deps.Repos.Author.GetByID(ctx, entityIDUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get author: %w", err)
|
||||
}
|
||||
|
||||
@ -6,7 +6,10 @@ import (
|
||||
"syscall"
|
||||
|
||||
"tercul/cmd/cli/internal/bootstrap"
|
||||
dbsql "tercul/internal/data/sql"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/jobs/sync"
|
||||
"tercul/internal/platform/cache"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/db"
|
||||
app_log "tercul/internal/platform/log"
|
||||
@ -73,16 +76,47 @@ func NewWorkerCommand() *cobra.Command {
|
||||
},
|
||||
)
|
||||
|
||||
repos := dbsql.NewRepositories(database, cfg)
|
||||
|
||||
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
|
||||
if cacheErr != nil {
|
||||
app_log.Warn("Redis cache initialization failed for linguistics: " + cacheErr.Error())
|
||||
}
|
||||
|
||||
sentimentProvider, spErr := linguistics.NewGoVADERSentimentProvider()
|
||||
if spErr != nil {
|
||||
return spErr
|
||||
}
|
||||
|
||||
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
|
||||
StatsRepo: repos.Analytics,
|
||||
LikeCounter: repos.Like,
|
||||
CommentCounter: repos.Comment,
|
||||
BookmarkCounter: repos.Bookmark,
|
||||
TranslationCount: repos.Translation,
|
||||
TranslationList: repos.Translation,
|
||||
}
|
||||
|
||||
linguisticsFactory := linguistics.NewLinguisticsFactory(
|
||||
cfg,
|
||||
database,
|
||||
redisCache,
|
||||
2,
|
||||
true,
|
||||
sentimentProvider,
|
||||
workAnalyticsDeps,
|
||||
)
|
||||
|
||||
// Create SyncJob with all dependencies
|
||||
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
|
||||
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
|
||||
|
||||
// Create a new ServeMux for routing jobs
|
||||
mux := asynq.NewServeMux()
|
||||
|
||||
// Register all job handlers
|
||||
sync.RegisterQueueHandlers(mux, syncJob)
|
||||
// Placeholder for other job handlers that might be added in the future
|
||||
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
||||
linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
||||
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
||||
|
||||
// Start the server in a goroutine
|
||||
|
||||
@ -48,7 +48,7 @@ type Dependencies struct {
|
||||
Repos *dbsql.Repositories
|
||||
Application *app.Application
|
||||
JWTManager *platform_auth.JWTManager
|
||||
AnalysisRepo *linguistics.GORMAnalysisRepository
|
||||
AnalysisRepo linguistics.AnalysisRepository
|
||||
SentimentProvider *linguistics.GoVADERSentimentProvider
|
||||
}
|
||||
|
||||
@ -61,12 +61,32 @@ func Bootstrap(cfg *config.Config, database *gorm.DB, weaviateClient *weaviate.C
|
||||
repos := dbsql.NewRepositories(database, cfg)
|
||||
|
||||
// Create linguistics dependencies
|
||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
||||
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
|
||||
StatsRepo: repos.Analytics,
|
||||
LikeCounter: repos.Like,
|
||||
CommentCounter: repos.Comment,
|
||||
BookmarkCounter: repos.Bookmark,
|
||||
TranslationCount: repos.Translation,
|
||||
TranslationList: repos.Translation,
|
||||
}
|
||||
|
||||
linguisticsFactory := linguistics.NewLinguisticsFactory(
|
||||
cfg,
|
||||
database,
|
||||
nil, // optional cache
|
||||
2,
|
||||
false,
|
||||
sentimentProvider,
|
||||
workAnalyticsDeps,
|
||||
)
|
||||
|
||||
analysisRepo := linguisticsFactory.GetAnalysisRepository()
|
||||
|
||||
// Create platform components
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
|
||||
|
||||
@ -109,4 +109,3 @@ func TestBootstrapWithMetrics(t *testing.T) {
|
||||
assert.NotNil(t, deps)
|
||||
assert.NotNil(t, deps.Application)
|
||||
}
|
||||
|
||||
|
||||
@ -5,12 +5,13 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/enrichment"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/db"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -24,7 +25,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
entityID, err := strconv.ParseUint(*entityIDStr, 10, 64)
|
||||
entityID, err := uuid.Parse(*entityIDStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Invalid entity ID: %v\n", err)
|
||||
os.Exit(1)
|
||||
@ -55,7 +56,7 @@ func main() {
|
||||
|
||||
switch *entityType {
|
||||
case "author":
|
||||
author, err := repos.Author.GetByID(ctx, uint(entityID))
|
||||
author, err := repos.Author.GetByID(ctx, entityID)
|
||||
if err != nil {
|
||||
log.Fatal(err, "Failed to get author")
|
||||
}
|
||||
|
||||
@ -5,7 +5,11 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
dbsql "tercul/internal/data/sql"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/jobs/sync"
|
||||
"tercul/internal/platform/cache"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/platform/db"
|
||||
app_log "tercul/internal/platform/log"
|
||||
@ -70,13 +74,44 @@ func main() {
|
||||
// Create SyncJob with all dependencies
|
||||
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
|
||||
|
||||
repos := dbsql.NewRepositories(database, cfg)
|
||||
|
||||
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
|
||||
if cacheErr != nil {
|
||||
app_log.Warn("Redis cache initialization failed for linguistics: " + cacheErr.Error())
|
||||
}
|
||||
|
||||
sentimentProvider, spErr := linguistics.NewGoVADERSentimentProvider()
|
||||
if spErr != nil {
|
||||
app_log.Fatal(spErr, "Failed to create sentiment provider")
|
||||
}
|
||||
|
||||
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
|
||||
StatsRepo: repos.Analytics,
|
||||
LikeCounter: repos.Like,
|
||||
CommentCounter: repos.Comment,
|
||||
BookmarkCounter: repos.Bookmark,
|
||||
TranslationCount: repos.Translation,
|
||||
TranslationList: repos.Translation,
|
||||
}
|
||||
|
||||
linguisticsFactory := linguistics.NewLinguisticsFactory(
|
||||
cfg,
|
||||
database,
|
||||
redisCache,
|
||||
2,
|
||||
true,
|
||||
sentimentProvider,
|
||||
workAnalyticsDeps,
|
||||
)
|
||||
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
|
||||
|
||||
// Create a new ServeMux for routing jobs
|
||||
mux := asynq.NewServeMux()
|
||||
|
||||
// Register all job handlers
|
||||
sync.RegisterQueueHandlers(mux, syncJob)
|
||||
// Placeholder for other job handlers that might be added in the future
|
||||
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
||||
linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
||||
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
||||
|
||||
// Start the server in a goroutine
|
||||
|
||||
@ -14,7 +14,8 @@ services:
|
||||
- DB_PASSWORD=postgres
|
||||
- DB_NAME=tercul
|
||||
- REDIS_ADDR=redis:6379
|
||||
- WEAVIATE_HOST=http://weaviate:8080
|
||||
- WEAVIATE_HOST=weaviate:8080
|
||||
- WEAVIATE_SCHEME=http
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module tercul
|
||||
|
||||
go 1.24.10
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.72
|
||||
|
||||
@ -73,4 +73,4 @@ func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*domain.Work), args.Error(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,4 +46,4 @@ func NewErrorPresenter() graphql.ErrorPresenterFunc {
|
||||
|
||||
return gqlErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
package graphql
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// resolveWorkContent uses the Work service to fetch preferred content for a work.
|
||||
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, preferredLanguage string) *string {
|
||||
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uuid.UUID, preferredLanguage string) *string {
|
||||
if r.App.Work == nil || r.App.Work.Queries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ func (m *mockLikeRepository) Create(ctx context.Context, entity *domain.Like) er
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockLikeRepository) GetByID(ctx context.Context, id uint) (*domain.Like, error) {
|
||||
func (m *mockLikeRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Like, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -135,4 +135,4 @@ func (m *mockLikeRepository) Exists(ctx context.Context, id uint) (bool, error)
|
||||
func (m *mockLikeRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockLikeRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,4 +116,4 @@ func (s *LikeResolversUnitSuite) TestDeleteLike() {
|
||||
|
||||
// Verify that the repository's Delete method was called
|
||||
s.mockLikeRepo.AssertCalled(s.T(), "Delete", mock.Anything, uint(likeIDUint))
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
@ -26,6 +25,8 @@ import (
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/platform/log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func toModelUserRole(role domain.UserRole) model.UserRole {
|
||||
@ -144,7 +145,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
|
||||
|
||||
// Convert to GraphQL model
|
||||
return &model.Work{
|
||||
ID: fmt.Sprintf("%d", createdWork.ID),
|
||||
ID: createdWork.ID.String(),
|
||||
Name: createdWork.Title,
|
||||
Language: createdWork.Language,
|
||||
Content: input.Content,
|
||||
@ -157,7 +158,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(id, 10, 32)
|
||||
workID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
@ -165,7 +166,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
|
||||
// Create domain model
|
||||
workModel := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{
|
||||
BaseModel: domain.BaseModel{ID: uint(workID)},
|
||||
BaseModel: domain.BaseModel{ID: workID},
|
||||
Language: input.Language,
|
||||
},
|
||||
Title: input.Name,
|
||||
@ -188,12 +189,12 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
|
||||
|
||||
// DeleteWork is the resolver for the deleteWork field.
|
||||
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
|
||||
workID, err := strconv.ParseUint(id, 10, 32)
|
||||
workID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
err = r.App.Work.Commands.DeleteWork(ctx, uint(workID))
|
||||
err = r.App.Work.Commands.DeleteWork(ctx, workID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -207,7 +208,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
@ -221,7 +222,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
||||
Title: input.Name,
|
||||
Content: content,
|
||||
Language: input.Language,
|
||||
TranslatableID: uint(workID),
|
||||
TranslatableID: workID,
|
||||
TranslatableType: "works",
|
||||
}
|
||||
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
|
||||
@ -230,7 +231,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), uint(workID)); err != nil {
|
||||
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), workID); err != nil {
|
||||
log.Error(err, "failed to increment work translation count")
|
||||
}
|
||||
}()
|
||||
@ -250,7 +251,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
@ -264,7 +265,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
|
||||
Title: input.Name,
|
||||
Content: content,
|
||||
Language: input.Language,
|
||||
TranslatableID: uint(workID),
|
||||
TranslatableID: workID,
|
||||
TranslatableType: "works",
|
||||
}
|
||||
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
|
||||
@ -283,12 +284,12 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
|
||||
|
||||
// DeleteTranslation is the resolver for the deleteTranslation field.
|
||||
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
|
||||
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||
translationID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
|
||||
err = r.App.Translation.Commands.DeleteTranslation(ctx, uint(translationID))
|
||||
err = r.App.Translation.Commands.DeleteTranslation(ctx, translationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -329,13 +330,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||
bookID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
updateInput := book.UpdateBookInput{
|
||||
ID: uint(bookID),
|
||||
ID: bookID,
|
||||
Title: &input.Name,
|
||||
Description: input.Description,
|
||||
Language: &input.Language,
|
||||
@ -358,12 +359,12 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
|
||||
|
||||
// DeleteBook is the resolver for the deleteBook field.
|
||||
func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) {
|
||||
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||
bookID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
err = r.App.Book.Commands.DeleteBook(ctx, uint(bookID))
|
||||
err = r.App.Book.Commands.DeleteBook(ctx, bookID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -396,13 +397,13 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
|
||||
if err := Validate(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||
authorID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid author ID: %v", err)
|
||||
}
|
||||
|
||||
updateInput := author.UpdateAuthorInput{
|
||||
ID: uint(authorID),
|
||||
ID: authorID,
|
||||
Name: input.Name,
|
||||
}
|
||||
updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput)
|
||||
@ -419,12 +420,12 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
|
||||
|
||||
// DeleteAuthor is the resolver for the deleteAuthor field.
|
||||
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
|
||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||
authorID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid author ID: %v", err)
|
||||
}
|
||||
|
||||
err = r.App.Author.Commands.DeleteAuthor(ctx, uint(authorID))
|
||||
err = r.App.Author.Commands.DeleteAuthor(ctx, authorID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -438,13 +439,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID, err := strconv.ParseUint(id, 10, 32)
|
||||
userID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user ID: %v", err)
|
||||
}
|
||||
|
||||
updateInput := user.UpdateUserInput{
|
||||
ID: uint(userID),
|
||||
ID: userID,
|
||||
Username: input.Username,
|
||||
Email: input.Email,
|
||||
Password: input.Password,
|
||||
@ -463,28 +464,25 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
||||
}
|
||||
|
||||
if input.CountryID != nil {
|
||||
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
||||
countryID, err := uuid.Parse(*input.CountryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||
}
|
||||
uid := uint(countryID)
|
||||
updateInput.CountryID = &uid
|
||||
updateInput.CountryID = &countryID
|
||||
}
|
||||
if input.CityID != nil {
|
||||
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
||||
cityID, err := uuid.Parse(*input.CityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||
}
|
||||
uid := uint(cityID)
|
||||
updateInput.CityID = &uid
|
||||
updateInput.CityID = &cityID
|
||||
}
|
||||
if input.AddressID != nil {
|
||||
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
||||
addressID, err := uuid.Parse(*input.AddressID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||
}
|
||||
uid := uint(addressID)
|
||||
updateInput.AddressID = &uid
|
||||
updateInput.AddressID = &addressID
|
||||
}
|
||||
|
||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
||||
@ -509,12 +507,12 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
||||
|
||||
// DeleteUser is the resolver for the deleteUser field.
|
||||
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
|
||||
userID, err := strconv.ParseUint(id, 10, 32)
|
||||
userID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
|
||||
err = r.App.User.Commands.DeleteUser(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -558,13 +556,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||
collectionID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||
}
|
||||
|
||||
updateInput := collection.UpdateCollectionInput{
|
||||
ID: uint(collectionID),
|
||||
ID: collectionID,
|
||||
Name: input.Name,
|
||||
UserID: userID,
|
||||
}
|
||||
@ -593,12 +591,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||
collectionID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid collection ID: %v", err)
|
||||
}
|
||||
|
||||
err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID)
|
||||
err = r.App.Collection.Commands.DeleteCollection(ctx, collectionID, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -613,18 +611,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||
collID, err := uuid.Parse(collectionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||
}
|
||||
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||
wID, err := uuid.Parse(workID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
|
||||
addInput := collection.AddWorkToCollectionInput{
|
||||
CollectionID: uint(collID),
|
||||
WorkID: uint(wID),
|
||||
CollectionID: collID,
|
||||
WorkID: wID,
|
||||
UserID: userID,
|
||||
}
|
||||
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
||||
@ -632,7 +630,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -651,18 +649,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||
collID, err := uuid.Parse(collectionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||
}
|
||||
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||
wID, err := uuid.Parse(workID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
|
||||
removeInput := collection.RemoveWorkFromCollectionInput{
|
||||
CollectionID: uint(collID),
|
||||
WorkID: uint(wID),
|
||||
CollectionID: collID,
|
||||
WorkID: wID,
|
||||
UserID: userID,
|
||||
}
|
||||
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
||||
@ -670,7 +668,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -698,28 +696,25 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
|
||||
UserID: userID,
|
||||
}
|
||||
if input.WorkID != nil {
|
||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(*input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
wID := uint(workID)
|
||||
createInput.WorkID = &wID
|
||||
createInput.WorkID = &workID
|
||||
}
|
||||
if input.TranslationID != nil {
|
||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||
translationID, err := uuid.Parse(*input.TranslationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
tID := uint(translationID)
|
||||
createInput.TranslationID = &tID
|
||||
createInput.TranslationID = &translationID
|
||||
}
|
||||
if input.ParentCommentID != nil {
|
||||
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
|
||||
parentCommentID, err := uuid.Parse(*input.ParentCommentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid parent comment ID: %v", err)
|
||||
}
|
||||
pID := uint(parentCommentID)
|
||||
createInput.ParentID = &pID
|
||||
createInput.ParentID = &parentCommentID
|
||||
}
|
||||
|
||||
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
|
||||
@ -754,12 +749,12 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||
commentID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||
}
|
||||
|
||||
commentModel, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
|
||||
commentModel, err := r.App.Comment.Queries.Comment(ctx, commentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -772,7 +767,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
|
||||
}
|
||||
|
||||
updateInput := comment.UpdateCommentInput{
|
||||
ID: uint(commentID),
|
||||
ID: commentID,
|
||||
Text: input.Text,
|
||||
}
|
||||
updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput)
|
||||
@ -796,12 +791,12 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||
commentID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid comment ID: %v", err)
|
||||
}
|
||||
|
||||
comment, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
|
||||
comment, err := r.App.Comment.Queries.Comment(ctx, commentID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -813,7 +808,7 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
err = r.App.Comment.Commands.DeleteComment(ctx, uint(commentID))
|
||||
err = r.App.Comment.Commands.DeleteComment(ctx, commentID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -839,28 +834,25 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
|
||||
UserID: userID,
|
||||
}
|
||||
if input.WorkID != nil {
|
||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(*input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
wID := uint(workID)
|
||||
createInput.WorkID = &wID
|
||||
createInput.WorkID = &workID
|
||||
}
|
||||
if input.TranslationID != nil {
|
||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||
translationID, err := uuid.Parse(*input.TranslationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
tID := uint(translationID)
|
||||
createInput.TranslationID = &tID
|
||||
createInput.TranslationID = &translationID
|
||||
}
|
||||
if input.CommentID != nil {
|
||||
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
|
||||
commentID, err := uuid.Parse(*input.CommentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||
}
|
||||
cID := uint(commentID)
|
||||
createInput.CommentID = &cID
|
||||
createInput.CommentID = &commentID
|
||||
}
|
||||
|
||||
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
|
||||
@ -892,12 +884,12 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
likeID, err := strconv.ParseUint(id, 10, 32)
|
||||
likeID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid like ID: %v", err)
|
||||
}
|
||||
|
||||
like, err := r.App.Like.Queries.Like(ctx, uint(likeID))
|
||||
like, err := r.App.Like.Queries.Like(ctx, likeID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -909,7 +901,7 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
err = r.App.Like.Commands.DeleteLike(ctx, uint(likeID))
|
||||
err = r.App.Like.Commands.DeleteLike(ctx, likeID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -924,14 +916,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
|
||||
createInput := bookmark.CreateBookmarkInput{
|
||||
UserID: userID,
|
||||
WorkID: uint(workID),
|
||||
WorkID: workID,
|
||||
}
|
||||
if input.Name != nil {
|
||||
createInput.Name = *input.Name
|
||||
@ -942,7 +934,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, uint(workID)); err != nil {
|
||||
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, workID); err != nil {
|
||||
log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
|
||||
}
|
||||
|
||||
@ -961,12 +953,12 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
bookmarkID, err := strconv.ParseUint(id, 10, 32)
|
||||
bookmarkID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid bookmark ID: %v", err)
|
||||
}
|
||||
|
||||
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, uint(bookmarkID))
|
||||
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, bookmarkID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -978,7 +970,7 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
|
||||
return false, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
err = r.App.Bookmark.Commands.DeleteBookmark(ctx, uint(bookmarkID))
|
||||
err = r.App.Bookmark.Commands.DeleteBookmark(ctx, bookmarkID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -998,21 +990,19 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
|
||||
}
|
||||
|
||||
if input.WorkID != nil {
|
||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||
workID, err := uuid.Parse(*input.WorkID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
wID := uint(workID)
|
||||
createInput.WorkID = &wID
|
||||
createInput.WorkID = &workID
|
||||
}
|
||||
|
||||
if input.TranslationID != nil {
|
||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||
translationID, err := uuid.Parse(*input.TranslationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
tID := uint(translationID)
|
||||
createInput.TranslationID = &tID
|
||||
createInput.TranslationID = &translationID
|
||||
}
|
||||
|
||||
if input.Status != nil {
|
||||
@ -1043,13 +1033,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
|
||||
return nil, domain.ErrUnauthorized
|
||||
}
|
||||
|
||||
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||
contributionID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
updateInput := contribution.UpdateContributionInput{
|
||||
ID: uint(contributionID),
|
||||
ID: contributionID,
|
||||
UserID: userID,
|
||||
Name: &input.Name,
|
||||
}
|
||||
@ -1081,12 +1071,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
|
||||
return false, domain.ErrUnauthorized
|
||||
}
|
||||
|
||||
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||
contributionID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
err = r.App.Contribution.Commands.DeleteContribution(ctx, uint(contributionID), userID)
|
||||
err = r.App.Contribution.Commands.DeleteContribution(ctx, contributionID, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -1096,13 +1086,13 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
|
||||
|
||||
// ReviewContribution is the resolver for the reviewContribution field.
|
||||
func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) {
|
||||
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||
contributionID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
reviewInput := contribution.ReviewContributionInput{
|
||||
ID: uint(contributionID),
|
||||
ID: contributionID,
|
||||
Status: status.String(),
|
||||
Feedback: feedback,
|
||||
}
|
||||
@ -1211,28 +1201,25 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
|
||||
}
|
||||
|
||||
if input.CountryID != nil {
|
||||
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
||||
countryID, err := uuid.Parse(*input.CountryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||
}
|
||||
uid := uint(countryID)
|
||||
updateInput.CountryID = &uid
|
||||
updateInput.CountryID = &countryID
|
||||
}
|
||||
if input.CityID != nil {
|
||||
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
||||
cityID, err := uuid.Parse(*input.CityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||
}
|
||||
uid := uint(cityID)
|
||||
updateInput.CityID = &uid
|
||||
updateInput.CityID = &cityID
|
||||
}
|
||||
if input.AddressID != nil {
|
||||
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
||||
addressID, err := uuid.Parse(*input.AddressID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||
}
|
||||
uid := uint(addressID)
|
||||
updateInput.AddressID = &uid
|
||||
updateInput.AddressID = &addressID
|
||||
}
|
||||
|
||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
||||
@ -1278,12 +1265,12 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword s
|
||||
|
||||
// Work is the resolver for the work field.
|
||||
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
|
||||
workID, err := strconv.ParseUint(id, 10, 32)
|
||||
workID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||
}
|
||||
|
||||
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
|
||||
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, workID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
return nil, nil
|
||||
@ -1292,7 +1279,7 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil {
|
||||
if err := r.App.Analytics.IncrementWorkViews(context.Background(), workID); err != nil {
|
||||
log.Error(err, "failed to increment work views")
|
||||
}
|
||||
}()
|
||||
@ -1338,12 +1325,12 @@ func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32,
|
||||
|
||||
// Translation is the resolver for the translation field.
|
||||
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
|
||||
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||
translationID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||
}
|
||||
|
||||
translationDTO, err := r.App.Translation.Queries.Translation(ctx, uint(translationID))
|
||||
translationDTO, err := r.App.Translation.Queries.Translation(ctx, translationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1352,7 +1339,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), uint(translationID)); err != nil {
|
||||
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), translationID); err != nil {
|
||||
log.Error(err, "failed to increment translation views")
|
||||
}
|
||||
}()
|
||||
@ -1368,7 +1355,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
|
||||
|
||||
// Translations is the resolver for the translations field.
|
||||
func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) {
|
||||
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||
wID, err := uuid.Parse(workID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
@ -1382,7 +1369,7 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
|
||||
page = int(*offset)/pageSize + 1
|
||||
}
|
||||
|
||||
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, uint(wID), language, page, pageSize)
|
||||
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, wID, language, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1402,12 +1389,12 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
|
||||
|
||||
// Book is the resolver for the book field.
|
||||
func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) {
|
||||
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||
bookID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
bookRecord, err := r.App.Book.Queries.Book(ctx, uint(bookID))
|
||||
bookRecord, err := r.App.Book.Queries.Book(ctx, bookID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1447,12 +1434,12 @@ func (r *queryResolver) Books(ctx context.Context, limit *int32, offset *int32)
|
||||
|
||||
// Author is the resolver for the author field.
|
||||
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
|
||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||
authorID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
authorRecord, err := r.App.Author.Queries.Author(ctx, uint(authorID))
|
||||
authorRecord, err := r.App.Author.Queries.Author(ctx, authorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1481,18 +1468,17 @@ func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, e
|
||||
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
|
||||
var authors []*domain.Author
|
||||
var err error
|
||||
var countryIDUint *uint
|
||||
var countryIDUUID *uuid.UUID
|
||||
|
||||
if countryID != nil {
|
||||
parsedID, err := strconv.ParseUint(*countryID, 10, 32)
|
||||
parsedID, err := uuid.Parse(*countryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid := uint(parsedID)
|
||||
countryIDUint = &uid
|
||||
countryIDUUID = &parsedID
|
||||
}
|
||||
|
||||
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint)
|
||||
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1521,12 +1507,12 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32
|
||||
|
||||
// User is the resolver for the user field.
|
||||
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
|
||||
userID, err := strconv.ParseUint(id, 10, 32)
|
||||
userID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
userRecord, err := r.App.User.Queries.User(ctx, uint(userID))
|
||||
userRecord, err := r.App.User.Queries.User(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1690,12 +1676,12 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
|
||||
|
||||
// UserProfile is the resolver for the userProfile field.
|
||||
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
|
||||
uID, err := strconv.ParseUint(userID, 10, 32)
|
||||
uID, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID))
|
||||
profile, err := r.App.User.Queries.UserProfile(ctx, uID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1703,12 +1689,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := r.App.User.Queries.User(ctx, uint(uID))
|
||||
user, err := r.App.User.Queries.User(ctx, uID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not found for profile %d", profile.ID)
|
||||
return nil, fmt.Errorf("user not found for profile %s", profile.ID.String())
|
||||
}
|
||||
|
||||
return &model.UserProfile{
|
||||
@ -1738,12 +1724,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
|
||||
|
||||
// Collection is the resolver for the collection field.
|
||||
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
|
||||
collID, err := strconv.ParseUint(id, 10, 32)
|
||||
collID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, collID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1751,7 +1737,7 @@ func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Colle
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, uint(collID))
|
||||
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, collID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1784,11 +1770,11 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
|
||||
var err error
|
||||
|
||||
if userID != nil {
|
||||
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
||||
uID, idErr := uuid.Parse(*userID)
|
||||
if idErr != nil {
|
||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uint(uID))
|
||||
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uID)
|
||||
} else {
|
||||
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
|
||||
}
|
||||
@ -1829,18 +1815,18 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
|
||||
|
||||
// Tag is the resolver for the tag field.
|
||||
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
|
||||
tagID, err := strconv.ParseUint(id, 10, 32)
|
||||
tagID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag, err := r.App.Tag.Queries.Tag(ctx, uint(tagID))
|
||||
tag, err := r.App.Tag.Queries.Tag(ctx, tagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Tag{
|
||||
ID: fmt.Sprintf("%d", tag.ID),
|
||||
ID: tag.ID.String(),
|
||||
Name: tag.Name,
|
||||
}, nil
|
||||
}
|
||||
@ -1865,12 +1851,12 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) (
|
||||
|
||||
// Category is the resolver for the category field.
|
||||
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
|
||||
categoryID, err := strconv.ParseUint(id, 10, 32)
|
||||
categoryID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid category ID: %v", err)
|
||||
}
|
||||
|
||||
category, err := r.App.Category.Queries.Category(ctx, uint(categoryID))
|
||||
category, err := r.App.Category.Queries.Category(ctx, categoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1879,7 +1865,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
|
||||
}
|
||||
|
||||
return &model.Category{
|
||||
ID: fmt.Sprintf("%d", category.ID),
|
||||
ID: category.ID.String(),
|
||||
Name: category.Name,
|
||||
}, nil
|
||||
}
|
||||
@ -1904,12 +1890,12 @@ func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *in
|
||||
|
||||
// Comment is the resolver for the comment field.
|
||||
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
|
||||
cID, err := strconv.ParseUint(id, 10, 32)
|
||||
cID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
commentRecord, err := r.App.Comment.Queries.Comment(ctx, uint(cID))
|
||||
commentRecord, err := r.App.Comment.Queries.Comment(ctx, cID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1918,10 +1904,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
|
||||
}
|
||||
|
||||
return &model.Comment{
|
||||
ID: fmt.Sprintf("%d", commentRecord.ID),
|
||||
ID: commentRecord.ID.String(),
|
||||
Text: commentRecord.Text,
|
||||
User: &model.User{
|
||||
ID: fmt.Sprintf("%d", commentRecord.UserID),
|
||||
ID: commentRecord.UserID.String(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@ -1932,23 +1918,23 @@ func (r *queryResolver) Comments(ctx context.Context, workID *string, translatio
|
||||
var err error
|
||||
|
||||
if workID != nil {
|
||||
wID, idErr := strconv.ParseUint(*workID, 10, 32)
|
||||
wID, idErr := uuid.Parse(*workID)
|
||||
if idErr != nil {
|
||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, uint(wID))
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, wID)
|
||||
} else if translationID != nil {
|
||||
tID, idErr := strconv.ParseUint(*translationID, 10, 32)
|
||||
tID, idErr := uuid.Parse(*translationID)
|
||||
if idErr != nil {
|
||||
return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation)
|
||||
}
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, uint(tID))
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, tID)
|
||||
} else if userID != nil {
|
||||
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
||||
uID, idErr := uuid.Parse(*userID)
|
||||
if idErr != nil {
|
||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
||||
}
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uint(uID))
|
||||
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uID)
|
||||
} else {
|
||||
commentRecords, err = r.App.Comment.Queries.Comments(ctx)
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@ package graphql
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/app/user"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
@ -20,7 +20,7 @@ import (
|
||||
type mockUserRepositoryForUserResolver struct{ mock.Mock }
|
||||
|
||||
// Implement domain.UserRepository
|
||||
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -106,7 +106,7 @@ func (m *mockUserRepositoryForUserResolver) WithTx(ctx context.Context, fn func(
|
||||
type mockUserProfileRepository struct{ mock.Mock }
|
||||
|
||||
// Implement domain.UserProfileRepository
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -121,7 +121,7 @@ func (m *mockUserProfileRepository) Create(ctx context.Context, entity *domain.U
|
||||
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
|
||||
@ -634,4 +634,4 @@ func (s *UserResolversUnitSuite) TestDeleteUserMutation() {
|
||||
_, err := s.resolver.Mutation().DeleteUser(ctx, "invalid")
|
||||
s.Require().Error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,4 +30,4 @@ func Validate(s interface{}) error {
|
||||
|
||||
// For other unexpected errors, like invalid validation input.
|
||||
return fmt.Errorf("unexpected error during validation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ func (m *mockWorkRepository) Create(ctx context.Context, entity *domain.Work) er
|
||||
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
return m.Create(ctx, entity)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -153,4 +153,4 @@ func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionI
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package graphql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/authz"
|
||||
@ -11,6 +10,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
domainsearch "tercul/internal/domain/search"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
@ -26,7 +26,7 @@ func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) erro
|
||||
work.ID = 1
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -52,10 +52,18 @@ func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*dom
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
@ -63,24 +71,47 @@ func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
||||
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
||||
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockWorkRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||
func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||
|
||||
func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockAuthorRepository struct{ mock.Mock }
|
||||
|
||||
@ -96,57 +127,111 @@ func (m *mockAuthorRepository) Create(ctx context.Context, author *domain.Author
|
||||
author.ID = 1
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockUserRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) 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) 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) Update(ctx context.Context, entity *domain.User) error { return nil }
|
||||
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil }
|
||||
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) { return nil, nil }
|
||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]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) 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) Count(ctx context.Context) (int64, error) { return 0, 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 }
|
||||
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
|
||||
}
|
||||
|
||||
type mockSearchClient struct{ mock.Mock }
|
||||
|
||||
@ -162,7 +247,6 @@ func (m *mockSearchClient) Search(ctx context.Context, params domainsearch.Searc
|
||||
return args.Get(0).(*domainsearch.SearchResults), args.Error(1)
|
||||
}
|
||||
|
||||
|
||||
type mockAnalyticsService struct{ mock.Mock }
|
||||
|
||||
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
@ -174,26 +258,62 @@ func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID ui
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return nil }
|
||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil }
|
||||
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { return nil }
|
||||
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockTranslationRepository struct{ mock.Mock }
|
||||
|
||||
@ -201,41 +321,80 @@ func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *dom
|
||||
args := m.Called(ctx, translation)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||
func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *mockTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||
|
||||
func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkResolversUnitSuite is a unit test suite for the work resolvers.
|
||||
type WorkResolversUnitSuite struct {
|
||||
suite.Suite
|
||||
resolver *Resolver
|
||||
mockWorkRepo *mockWorkRepository
|
||||
mockAuthorRepo *mockAuthorRepository
|
||||
mockUserRepo *mockUserRepository
|
||||
mockTranslationRepo *mockTranslationRepository
|
||||
mockSearchClient *mockSearchClient
|
||||
mockAnalyticsSvc *mockAnalyticsService
|
||||
resolver *Resolver
|
||||
mockWorkRepo *mockWorkRepository
|
||||
mockAuthorRepo *mockAuthorRepository
|
||||
mockUserRepo *mockUserRepository
|
||||
mockTranslationRepo *mockTranslationRepository
|
||||
mockSearchClient *mockSearchClient
|
||||
mockAnalyticsSvc *mockAnalyticsService
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) SetupTest() {
|
||||
@ -453,4 +612,4 @@ func (s *WorkResolversUnitSuite) TestWorksQuery_Unit() {
|
||||
_, err := s.resolver.Query().Works(ctx, &limit, &offset, nil, nil, nil, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,18 +4,20 @@ import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AnalyticsRepository defines the data access layer for analytics.
|
||||
type Repository interface {
|
||||
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
|
||||
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
|
||||
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||
UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error
|
||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
||||
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error)
|
||||
IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error
|
||||
IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error
|
||||
UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
|
||||
UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error
|
||||
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
||||
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
|
||||
GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error)
|
||||
UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error
|
||||
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error
|
||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,34 +13,36 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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
|
||||
DecrementWorkLikes(ctx context.Context, workID uint) error
|
||||
DecrementTranslationLikes(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)
|
||||
IncrementWorkViews(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementWorkComments(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementWorkShares(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error
|
||||
IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error
|
||||
IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
||||
DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
||||
DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
||||
IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error
|
||||
IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error
|
||||
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
||||
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*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
|
||||
UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error
|
||||
UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error
|
||||
UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error
|
||||
UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error
|
||||
UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error
|
||||
|
||||
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
|
||||
UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error
|
||||
UpdateTrending(ctx context.Context) error
|
||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
|
||||
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||
UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
@ -63,91 +65,91 @@ func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, tr
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkViews(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkViews")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkLikes")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
||||
}
|
||||
|
||||
func (s *service) DecrementWorkLikes(ctx context.Context, workID uint) error {
|
||||
func (s *service) DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "DecrementWorkLikes")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", -1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkComments(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkComments")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkBookmarks")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkShares(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkShares")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementWorkTranslationCount")
|
||||
defer span.End()
|
||||
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementTranslationViews")
|
||||
defer span.End()
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementTranslationLikes")
|
||||
defer span.End()
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
||||
}
|
||||
|
||||
func (s *service) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
func (s *service) DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "DecrementTranslationLikes")
|
||||
defer span.End()
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", -1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementTranslationComments")
|
||||
defer span.End()
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
||||
}
|
||||
|
||||
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "IncrementTranslationShares")
|
||||
defer span.End()
|
||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
||||
}
|
||||
|
||||
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "GetOrCreateWorkStats")
|
||||
defer span.End()
|
||||
return s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||
}
|
||||
|
||||
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
||||
defer span.End()
|
||||
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||
}
|
||||
|
||||
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateWorkReadingTime")
|
||||
defer span.End()
|
||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
||||
@ -174,7 +176,7 @@ func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateWorkComplexity")
|
||||
defer span.End()
|
||||
logger := log.FromContext(ctx).With("workID", workID)
|
||||
@ -198,7 +200,7 @@ func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateWorkSentiment")
|
||||
defer span.End()
|
||||
logger := log.FromContext(ctx).With("workID", workID)
|
||||
@ -227,7 +229,7 @@ func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateTranslationReadingTime")
|
||||
defer span.End()
|
||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||
@ -255,7 +257,7 @@ func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationI
|
||||
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateTranslationSentiment")
|
||||
defer span.End()
|
||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
||||
@ -282,7 +284,7 @@ func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID
|
||||
return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
|
||||
}
|
||||
|
||||
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||
func (s *service) UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateUserEngagement")
|
||||
defer span.End()
|
||||
today := time.Now().UTC().Truncate(24 * time.Hour)
|
||||
@ -315,7 +317,7 @@ func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit
|
||||
return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
|
||||
}
|
||||
|
||||
func (s *service) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
func (s *service) UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error {
|
||||
ctx, span := s.tracer.Start(ctx, "UpdateWorkStats")
|
||||
defer span.End()
|
||||
return s.repo.UpdateWorkStats(ctx, workID, stats)
|
||||
|
||||
@ -3,13 +3,13 @@ package analytics_test
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/jobs/linguistics"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -271,4 +271,4 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
|
||||
// TestAnalyticsService runs the full test suite.
|
||||
func TestAnalyticsService(t *testing.T) {
|
||||
suite.Run(t, new(AnalyticsServiceTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,4 +80,4 @@ func NewApplication(
|
||||
Search: searchSvc,
|
||||
Analytics: analyticsSvc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@ -38,9 +39,9 @@ type RegisterInput struct {
|
||||
|
||||
// AuthResponse represents authentication response
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
Token string `json:"token"`
|
||||
User *domain.User `json:"user"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
// AuthCommands contains the command handlers for authentication.
|
||||
@ -178,7 +179,7 @@ func (c *AuthCommands) ResendVerificationEmail(ctx context.Context, email string
|
||||
|
||||
// ChangePasswordInput represents the input for changing a password.
|
||||
type ChangePasswordInput struct {
|
||||
UserID uint
|
||||
UserID uuid.UUID
|
||||
CurrentPassword string
|
||||
NewPassword string
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
type AuthCommandsSuite struct {
|
||||
suite.Suite
|
||||
userRepo *mockUserRepository
|
||||
|
||||
@ -10,12 +10,12 @@ import (
|
||||
|
||||
// mockUserRepository is a local mock for the UserRepository interface.
|
||||
type mockUserRepository struct {
|
||||
users map[uint]domain.User
|
||||
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)
|
||||
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 {
|
||||
@ -71,7 +71,7 @@ func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) erro
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
}
|
||||
@ -85,8 +85,8 @@ func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User
|
||||
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) 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
|
||||
}
|
||||
@ -114,7 +114,7 @@ func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offse
|
||||
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) 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
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package author
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AuthorCommands contains the command handlers for the author aggregate.
|
||||
@ -34,7 +36,7 @@ func (c *AuthorCommands) CreateAuthor(ctx context.Context, input CreateAuthorInp
|
||||
|
||||
// UpdateAuthorInput represents the input for updating an existing author.
|
||||
type UpdateAuthorInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
}
|
||||
|
||||
@ -53,6 +55,6 @@ func (c *AuthorCommands) UpdateAuthor(ctx context.Context, input UpdateAuthorInp
|
||||
}
|
||||
|
||||
// DeleteAuthor deletes an author by ID.
|
||||
func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uint) error {
|
||||
func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uuid.UUID) error {
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package author
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AuthorQueries contains the query handlers for the author aggregate.
|
||||
@ -16,12 +18,12 @@ func NewAuthorQueries(repo domain.AuthorRepository) *AuthorQueries {
|
||||
}
|
||||
|
||||
// Author returns an author by ID.
|
||||
func (q *AuthorQueries) Author(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
func (q *AuthorQueries) Author(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// Authors returns all authors, with optional filtering by country.
|
||||
func (q *AuthorQueries) Authors(ctx context.Context, countryID *uint) ([]*domain.Author, error) {
|
||||
func (q *AuthorQueries) Authors(ctx context.Context, countryID *uuid.UUID) ([]*domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
var err error
|
||||
|
||||
@ -43,6 +45,6 @@ func (q *AuthorQueries) Authors(ctx context.Context, countryID *uint) ([]*domain
|
||||
}
|
||||
|
||||
// AuthorWithTranslations returns an author by ID with its translations.
|
||||
func (q *AuthorQueries) AuthorWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
func (q *AuthorQueries) AuthorWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
return q.repo.GetWithTranslations(ctx, id)
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Service provides authorization checks for the application.
|
||||
@ -26,7 +28,7 @@ func NewService(workRepo domain.WorkRepository, authorRepo domain.AuthorReposito
|
||||
|
||||
// CanEditWork checks if a user has permission to edit a work.
|
||||
// For now, we'll implement a simple rule: only an admin or the work's author can edit it.
|
||||
func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) {
|
||||
func (s *Service) CanEditWork(ctx context.Context, userID uuid.UUID, work *domain.Work) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -61,7 +63,7 @@ func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Wor
|
||||
}
|
||||
|
||||
// CanDeleteWork checks if a user has permission to delete a work.
|
||||
func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) {
|
||||
func (s *Service) CanDeleteWork(ctx context.Context, userID uuid.UUID, work *domain.Work) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -96,7 +98,7 @@ func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.W
|
||||
}
|
||||
|
||||
// CanEditEntity checks if a user has permission to edit a specific translatable entity.
|
||||
func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableType string, translatableID uint) (bool, error) {
|
||||
func (s *Service) CanEditEntity(ctx context.Context, userID uuid.UUID, translatableType string, translatableID uuid.UUID) (bool, error) {
|
||||
switch translatableType {
|
||||
case "works":
|
||||
// For works, we can reuse the CanEditWork logic.
|
||||
@ -114,7 +116,7 @@ func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableTy
|
||||
}
|
||||
|
||||
// CanDeleteTranslation checks if a user can delete a translation.
|
||||
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) {
|
||||
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uuid.UUID, translationID uuid.UUID) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -157,7 +159,7 @@ func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Service) CanEditTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) {
|
||||
func (s *Service) CanEditTranslation(ctx context.Context, userID uuid.UUID, translationID uuid.UUID) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -211,7 +213,7 @@ func (s *Service) CanDeleteBook(ctx context.Context) (bool, error) {
|
||||
return false, domain.ErrForbidden
|
||||
}
|
||||
|
||||
func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint) (bool, error) {
|
||||
func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uuid.UUID) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -232,7 +234,7 @@ func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint)
|
||||
|
||||
// CanDeleteComment checks if a user has permission to delete a comment.
|
||||
// For now, we'll implement a simple rule: only an admin or the comment's author can delete it.
|
||||
func (s *Service) CanDeleteComment(ctx context.Context, userID uint, comment *domain.Comment) (bool, error) {
|
||||
func (s *Service) CanDeleteComment(ctx context.Context, userID uuid.UUID, comment *domain.Comment) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -249,4 +251,4 @@ func (s *Service) CanDeleteComment(ctx context.Context, userID uint, comment *do
|
||||
}
|
||||
|
||||
return false, domain.ErrForbidden
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BookCommands contains the command handlers for the book aggregate.
|
||||
@ -26,7 +28,7 @@ type CreateBookInput struct {
|
||||
Description string
|
||||
Language string
|
||||
ISBN *string
|
||||
AuthorIDs []uint
|
||||
AuthorIDs []uuid.UUID
|
||||
}
|
||||
|
||||
// CreateBook creates a new book.
|
||||
@ -62,12 +64,12 @@ func (c *BookCommands) CreateBook(ctx context.Context, input CreateBookInput) (*
|
||||
|
||||
// UpdateBookInput represents the input for updating an existing book.
|
||||
type UpdateBookInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Title *string
|
||||
Description *string
|
||||
Language *string
|
||||
ISBN *string
|
||||
AuthorIDs []uint
|
||||
AuthorIDs []uuid.UUID
|
||||
}
|
||||
|
||||
// UpdateBook updates an existing book.
|
||||
@ -106,7 +108,7 @@ func (c *BookCommands) UpdateBook(ctx context.Context, input UpdateBookInput) (*
|
||||
}
|
||||
|
||||
// DeleteBook deletes a book by ID.
|
||||
func (c *BookCommands) DeleteBook(ctx context.Context, id uint) error {
|
||||
func (c *BookCommands) DeleteBook(ctx context.Context, id uuid.UUID) error {
|
||||
can, err := c.authzSvc.CanDeleteBook(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -115,4 +117,4 @@ func (c *BookCommands) DeleteBook(ctx context.Context, id uint) error {
|
||||
return domain.ErrForbidden
|
||||
}
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package book
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BookQueries contains the query handlers for the book aggregate.
|
||||
@ -16,11 +18,11 @@ func NewBookQueries(repo domain.BookRepository) *BookQueries {
|
||||
}
|
||||
|
||||
// Book retrieves a book by its ID.
|
||||
func (q *BookQueries) Book(ctx context.Context, id uint) (*domain.Book, error) {
|
||||
func (q *BookQueries) Book(ctx context.Context, id uuid.UUID) (*domain.Book, error) {
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// Books retrieves a list of all books.
|
||||
func (q *BookQueries) Books(ctx context.Context) ([]domain.Book, error) {
|
||||
return q.repo.ListAll(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,4 +17,4 @@ func NewService(repo domain.BookRepository, authzSvc *authz.Service) *Service {
|
||||
Commands: NewBookCommands(repo, authzSvc),
|
||||
Queries: NewBookQueries(repo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BookmarkCommands contains the command handlers for the bookmark aggregate.
|
||||
@ -24,8 +26,8 @@ func NewBookmarkCommands(repo domain.BookmarkRepository, analyticsSvc analytics.
|
||||
// CreateBookmarkInput represents the input for creating a new bookmark.
|
||||
type CreateBookmarkInput struct {
|
||||
Name string
|
||||
UserID uint
|
||||
WorkID uint
|
||||
UserID uuid.UUID
|
||||
WorkID uuid.UUID
|
||||
Notes string
|
||||
}
|
||||
|
||||
@ -55,7 +57,7 @@ func (c *BookmarkCommands) CreateBookmark(ctx context.Context, input CreateBookm
|
||||
|
||||
// UpdateBookmarkInput represents the input for updating an existing bookmark.
|
||||
type UpdateBookmarkInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Notes string
|
||||
}
|
||||
@ -76,6 +78,6 @@ func (c *BookmarkCommands) UpdateBookmark(ctx context.Context, input UpdateBookm
|
||||
}
|
||||
|
||||
// DeleteBookmark deletes a bookmark by ID.
|
||||
func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uint) error {
|
||||
func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uuid.UUID) error {
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package bookmark
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BookmarkQueries contains the query handlers for the bookmark aggregate.
|
||||
@ -16,16 +18,16 @@ func NewBookmarkQueries(repo domain.BookmarkRepository) *BookmarkQueries {
|
||||
}
|
||||
|
||||
// Bookmark returns a bookmark by ID.
|
||||
func (q *BookmarkQueries) Bookmark(ctx context.Context, id uint) (*domain.Bookmark, error) {
|
||||
func (q *BookmarkQueries) Bookmark(ctx context.Context, id uuid.UUID) (*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) {
|
||||
func (q *BookmarkQueries) BookmarksByUserID(ctx context.Context, userID uuid.UUID) ([]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) {
|
||||
func (q *BookmarkQueries) BookmarksByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Bookmark, error) {
|
||||
return q.repo.ListByWorkID(ctx, workID)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package category
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CategoryCommands contains the command handlers for the category aggregate.
|
||||
@ -19,7 +21,7 @@ func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
|
||||
type CreateCategoryInput struct {
|
||||
Name string
|
||||
Description string
|
||||
ParentID *uint
|
||||
ParentID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateCategory creates a new category.
|
||||
@ -38,10 +40,10 @@ func (c *CategoryCommands) CreateCategory(ctx context.Context, input CreateCateg
|
||||
|
||||
// UpdateCategoryInput represents the input for updating an existing category.
|
||||
type UpdateCategoryInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Description string
|
||||
ParentID *uint
|
||||
ParentID *uuid.UUID
|
||||
}
|
||||
|
||||
// UpdateCategory updates an existing category.
|
||||
@ -61,6 +63,6 @@ func (c *CategoryCommands) UpdateCategory(ctx context.Context, input UpdateCateg
|
||||
}
|
||||
|
||||
// DeleteCategory deletes a category by ID.
|
||||
func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uint) error {
|
||||
func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uuid.UUID) error {
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package category
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CategoryQueries contains the query handlers for the category aggregate.
|
||||
@ -16,7 +18,7 @@ func NewCategoryQueries(repo domain.CategoryRepository) *CategoryQueries {
|
||||
}
|
||||
|
||||
// Category returns a category by ID.
|
||||
func (q *CategoryQueries) Category(ctx context.Context, id uint) (*domain.Category, error) {
|
||||
func (q *CategoryQueries) Category(ctx context.Context, id uuid.UUID) (*domain.Category, error) {
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
@ -26,12 +28,12 @@ func (q *CategoryQueries) CategoryByName(ctx context.Context, name string) (*dom
|
||||
}
|
||||
|
||||
// CategoriesByWorkID returns all categories for a work.
|
||||
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
||||
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uuid.UUID) ([]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) {
|
||||
func (q *CategoryQueries) CategoriesByParentID(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error) {
|
||||
return q.repo.ListByParentID(ctx, parentID)
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CollectionCommands contains the command handlers for the collection aggregate.
|
||||
@ -20,7 +22,7 @@ func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands
|
||||
type CreateCollectionInput struct {
|
||||
Name string
|
||||
Description string
|
||||
UserID uint
|
||||
UserID uuid.UUID
|
||||
IsPublic bool
|
||||
CoverImageURL string
|
||||
}
|
||||
@ -43,12 +45,12 @@ func (c *CollectionCommands) CreateCollection(ctx context.Context, input CreateC
|
||||
|
||||
// UpdateCollectionInput represents the input for updating an existing collection.
|
||||
type UpdateCollectionInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Description string
|
||||
IsPublic bool
|
||||
CoverImageURL string
|
||||
UserID uint
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
// UpdateCollection updates an existing collection.
|
||||
@ -58,7 +60,7 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
||||
return nil, err
|
||||
}
|
||||
if collection.UserID != input.UserID {
|
||||
return nil, fmt.Errorf("unauthorized: user %d cannot update collection %d", input.UserID, input.ID)
|
||||
return nil, fmt.Errorf("unauthorized: user %s cannot update collection %s", input.UserID, input.ID)
|
||||
}
|
||||
collection.Name = input.Name
|
||||
collection.Description = input.Description
|
||||
@ -72,22 +74,22 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
||||
}
|
||||
|
||||
// DeleteCollection deletes a collection by ID.
|
||||
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint, userID uint) error {
|
||||
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uuid.UUID, userID uuid.UUID) error {
|
||||
collection, err := c.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if collection.UserID != userID {
|
||||
return fmt.Errorf("unauthorized: user %d cannot delete collection %d", userID, id)
|
||||
return fmt.Errorf("unauthorized: user %s cannot delete collection %s", userID, id)
|
||||
}
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// AddWorkToCollectionInput represents the input for adding a work to a collection.
|
||||
type AddWorkToCollectionInput struct {
|
||||
CollectionID uint
|
||||
WorkID uint
|
||||
UserID uint
|
||||
CollectionID uuid.UUID
|
||||
WorkID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
// AddWorkToCollection adds a work to a collection.
|
||||
@ -97,16 +99,16 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW
|
||||
return err
|
||||
}
|
||||
if collection.UserID != input.UserID {
|
||||
return fmt.Errorf("unauthorized: user %d cannot add work to collection %d", input.UserID, input.CollectionID)
|
||||
return fmt.Errorf("unauthorized: user %s cannot add work to collection %s", input.UserID, input.CollectionID)
|
||||
}
|
||||
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
|
||||
UserID uint
|
||||
CollectionID uuid.UUID
|
||||
WorkID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
// RemoveWorkFromCollection removes a work from a collection.
|
||||
@ -116,7 +118,7 @@ func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input
|
||||
return err
|
||||
}
|
||||
if collection.UserID != input.UserID {
|
||||
return fmt.Errorf("unauthorized: user %d cannot remove work from collection %d", input.UserID, input.CollectionID)
|
||||
return fmt.Errorf("unauthorized: user %s cannot remove work from collection %s", input.UserID, input.CollectionID)
|
||||
}
|
||||
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package collection
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CollectionQueries contains the query handlers for the collection aggregate.
|
||||
@ -16,12 +18,12 @@ func NewCollectionQueries(repo domain.CollectionRepository) *CollectionQueries {
|
||||
}
|
||||
|
||||
// Collection returns a collection by ID.
|
||||
func (q *CollectionQueries) Collection(ctx context.Context, id uint) (*domain.Collection, error) {
|
||||
func (q *CollectionQueries) Collection(ctx context.Context, id uuid.UUID) (*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) {
|
||||
func (q *CollectionQueries) CollectionsByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Collection, error) {
|
||||
return q.repo.ListByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
@ -31,7 +33,7 @@ func (q *CollectionQueries) PublicCollections(ctx context.Context) ([]domain.Col
|
||||
}
|
||||
|
||||
// CollectionsByWorkID returns all collections for a work.
|
||||
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
||||
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Collection, error) {
|
||||
return q.repo.ListByWorkID(ctx, workID)
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import (
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CommentCommands contains the command handlers for the comment aggregate.
|
||||
@ -30,10 +32,10 @@ func NewCommentCommands(repo domain.CommentRepository, authzSvc *authz.Service,
|
||||
// CreateCommentInput represents the input for creating a new comment.
|
||||
type CreateCommentInput struct {
|
||||
Text string
|
||||
UserID uint
|
||||
WorkID *uint
|
||||
TranslationID *uint
|
||||
ParentID *uint
|
||||
UserID uuid.UUID
|
||||
WorkID *uuid.UUID
|
||||
TranslationID *uuid.UUID
|
||||
ParentID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateComment creates a new comment.
|
||||
@ -72,7 +74,7 @@ func (c *CommentCommands) CreateComment(ctx context.Context, input CreateComment
|
||||
|
||||
// UpdateCommentInput represents the input for updating an existing comment.
|
||||
type UpdateCommentInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Text string
|
||||
}
|
||||
|
||||
@ -86,7 +88,7 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
|
||||
comment, err := c.repo.GetByID(ctx, input.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
return nil, fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, input.ID)
|
||||
return nil, fmt.Errorf("%w: comment with id %s not found", domain.ErrEntityNotFound, input.ID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -108,7 +110,7 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
|
||||
}
|
||||
|
||||
// DeleteComment deletes a comment by ID after an authorization check.
|
||||
func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
||||
func (c *CommentCommands) DeleteComment(ctx context.Context, id uuid.UUID) error {
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
return domain.ErrUnauthorized
|
||||
@ -117,7 +119,7 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
||||
comment, err := c.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
return fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, id)
|
||||
return fmt.Errorf("%w: comment with id %s not found", domain.ErrEntityNotFound, id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -131,4 +133,4 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package comment
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CommentQueries contains the query handlers for the comment aggregate.
|
||||
@ -16,27 +18,27 @@ func NewCommentQueries(repo domain.CommentRepository) *CommentQueries {
|
||||
}
|
||||
|
||||
// Comment returns a comment by ID.
|
||||
func (q *CommentQueries) Comment(ctx context.Context, id uint) (*domain.Comment, error) {
|
||||
func (q *CommentQueries) Comment(ctx context.Context, id uuid.UUID) (*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) {
|
||||
func (q *CommentQueries) CommentsByUserID(ctx context.Context, userID uuid.UUID) ([]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) {
|
||||
func (q *CommentQueries) CommentsByWorkID(ctx context.Context, workID uuid.UUID) ([]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) {
|
||||
func (q *CommentQueries) CommentsByTranslationID(ctx context.Context, translationID uuid.UUID) ([]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) {
|
||||
func (q *CommentQueries) CommentsByParentID(ctx context.Context, parentID uuid.UUID) ([]domain.Comment, error) {
|
||||
return q.repo.ListByParentID(ctx, parentID)
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Commands contains the command handlers for the contribution aggregate.
|
||||
@ -25,8 +27,8 @@ func NewCommands(repo domain.ContributionRepository, authzSvc *authz.Service) *C
|
||||
type CreateContributionInput struct {
|
||||
Name string
|
||||
Status string
|
||||
WorkID *uint
|
||||
TranslationID *uint
|
||||
WorkID *uuid.UUID
|
||||
TranslationID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateContribution creates a new contribution.
|
||||
@ -56,8 +58,8 @@ func (c *Commands) CreateContribution(ctx context.Context, input CreateContribut
|
||||
|
||||
// UpdateContributionInput represents the input for updating a contribution.
|
||||
type UpdateContributionInput struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
ID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
Name *string
|
||||
Status *string
|
||||
}
|
||||
@ -89,7 +91,7 @@ func (c *Commands) UpdateContribution(ctx context.Context, input UpdateContribut
|
||||
}
|
||||
|
||||
// DeleteContribution deletes a contribution.
|
||||
func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint, userID uint) error {
|
||||
func (c *Commands) DeleteContribution(ctx context.Context, contributionID uuid.UUID, userID uuid.UUID) error {
|
||||
contribution, err := c.repo.GetByID(ctx, contributionID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -105,7 +107,7 @@ func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint,
|
||||
|
||||
// ReviewContributionInput represents the input for reviewing a contribution.
|
||||
type ReviewContributionInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Status string
|
||||
Feedback *string
|
||||
}
|
||||
@ -135,4 +137,4 @@ func (c *Commands) ReviewContribution(ctx context.Context, input ReviewContribut
|
||||
}
|
||||
|
||||
return contribution, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,4 +11,4 @@ func NewService(commands *Commands) *Service {
|
||||
return &Service{
|
||||
Commands: commands,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CopyrightCommands contains the command handlers for copyright.
|
||||
@ -37,8 +39,8 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
|
||||
if copyright == nil {
|
||||
return errors.New("copyright cannot be nil")
|
||||
}
|
||||
if copyright.ID == 0 {
|
||||
return errors.New("copyright ID cannot be zero")
|
||||
if copyright.ID == uuid.Nil {
|
||||
return errors.New("copyright ID cannot be nil")
|
||||
}
|
||||
if copyright.Name == "" {
|
||||
return errors.New("copyright name cannot be empty")
|
||||
@ -51,8 +53,8 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
|
||||
}
|
||||
|
||||
// DeleteCopyright deletes a copyright.
|
||||
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error {
|
||||
if id == 0 {
|
||||
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uuid.UUID) error {
|
||||
if id == uuid.Nil {
|
||||
return errors.New("invalid copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("id", id).Debug("Deleting copyright")
|
||||
@ -60,8 +62,8 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
// AddCopyrightToWork adds a copyright to a work.
|
||||
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||
if workID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid work ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Adding copyright to work")
|
||||
@ -69,8 +71,8 @@ func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint,
|
||||
}
|
||||
|
||||
// RemoveCopyrightFromWork removes a copyright from a work.
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||
if workID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid work ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Removing copyright from work")
|
||||
@ -78,8 +80,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID
|
||||
}
|
||||
|
||||
// AddCopyrightToAuthor adds a copyright to an author.
|
||||
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||
if authorID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid author ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Adding copyright to author")
|
||||
@ -87,8 +89,8 @@ func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID u
|
||||
}
|
||||
|
||||
// RemoveCopyrightFromAuthor removes a copyright from an author.
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||
if authorID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid author ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Removing copyright from author")
|
||||
@ -96,8 +98,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, autho
|
||||
}
|
||||
|
||||
// AddCopyrightToBook adds a copyright to a book.
|
||||
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||
if bookID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid book ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Adding copyright to book")
|
||||
@ -105,8 +107,8 @@ func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint,
|
||||
}
|
||||
|
||||
// RemoveCopyrightFromBook removes a copyright from a book.
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||
if bookID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid book ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Removing copyright from book")
|
||||
@ -114,8 +116,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID
|
||||
}
|
||||
|
||||
// AddCopyrightToPublisher adds a copyright to a publisher.
|
||||
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||
if publisherID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid publisher ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Adding copyright to publisher")
|
||||
@ -123,8 +125,8 @@ func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publish
|
||||
}
|
||||
|
||||
// RemoveCopyrightFromPublisher removes a copyright from a publisher.
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||
if publisherID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid publisher ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Removing copyright from publisher")
|
||||
@ -132,8 +134,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, pu
|
||||
}
|
||||
|
||||
// AddCopyrightToSource adds a copyright to a source.
|
||||
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||
if sourceID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid source ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Adding copyright to source")
|
||||
@ -141,8 +143,8 @@ func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID u
|
||||
}
|
||||
|
||||
// RemoveCopyrightFromSource removes a copyright from a source.
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||
if sourceID == 0 || copyrightID == 0 {
|
||||
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
|
||||
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
||||
return errors.New("invalid source ID or copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Removing copyright from source")
|
||||
@ -154,8 +156,8 @@ func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *dom
|
||||
if translation == nil {
|
||||
return errors.New("translation cannot be nil")
|
||||
}
|
||||
if translation.CopyrightID == 0 {
|
||||
return errors.New("copyright ID cannot be zero")
|
||||
if translation.CopyrightID == uuid.Nil {
|
||||
return errors.New("copyright ID cannot be nil")
|
||||
}
|
||||
if translation.LanguageCode == "" {
|
||||
return errors.New("language code cannot be empty")
|
||||
|
||||
@ -4,10 +4,10 @@ package copyright_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/app/copyright"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@ -7,24 +7,24 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
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)
|
||||
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 {
|
||||
@ -111,7 +111,7 @@ func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translatio
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
}
|
||||
@ -165,7 +165,9 @@ func (m *mockCopyrightRepository) FindWithPreload(ctx context.Context, preloads
|
||||
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) 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
|
||||
@ -182,40 +184,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
|
||||
}
|
||||
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)
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CopyrightQueries contains the query handlers for copyright.
|
||||
@ -23,8 +25,8 @@ func NewCopyrightQueries(repo domain.CopyrightRepository, workRepo domain.WorkRe
|
||||
}
|
||||
|
||||
// GetCopyrightByID retrieves a copyright by ID.
|
||||
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||
if id == 0 {
|
||||
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
|
||||
if id == uuid.Nil {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("id", id).Debug("Getting copyright by ID")
|
||||
@ -40,7 +42,7 @@ func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyrig
|
||||
}
|
||||
|
||||
// GetCopyrightsForWork gets all copyrights for a specific work.
|
||||
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) {
|
||||
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uuid.UUID) ([]*domain.Copyright, error) {
|
||||
log.FromContext(ctx).With("work_id", workID).Debug("Getting copyrights for work")
|
||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||
if err != nil {
|
||||
@ -50,7 +52,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint
|
||||
}
|
||||
|
||||
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
||||
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) {
|
||||
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uuid.UUID) ([]*domain.Copyright, error) {
|
||||
log.FromContext(ctx).With("author_id", authorID).Debug("Getting copyrights for author")
|
||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||
if err != nil {
|
||||
@ -60,7 +62,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID
|
||||
}
|
||||
|
||||
// GetCopyrightsForBook gets all copyrights for a specific book.
|
||||
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) {
|
||||
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uuid.UUID) ([]*domain.Copyright, error) {
|
||||
log.FromContext(ctx).With("book_id", bookID).Debug("Getting copyrights for book")
|
||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||
if err != nil {
|
||||
@ -70,7 +72,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint
|
||||
}
|
||||
|
||||
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
||||
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) {
|
||||
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uuid.UUID) ([]*domain.Copyright, error) {
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting copyrights for publisher")
|
||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||
if err != nil {
|
||||
@ -80,7 +82,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis
|
||||
}
|
||||
|
||||
// GetCopyrightsForSource gets all copyrights for a specific source.
|
||||
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) {
|
||||
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uuid.UUID) ([]*domain.Copyright, error) {
|
||||
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting copyrights for source")
|
||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||
if err != nil {
|
||||
@ -90,8 +92,8 @@ func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID
|
||||
}
|
||||
|
||||
// GetTranslations gets all translations for a copyright.
|
||||
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||
if copyrightID == 0 {
|
||||
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uuid.UUID) ([]domain.CopyrightTranslation, error) {
|
||||
if copyrightID == uuid.Nil {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
log.FromContext(ctx).With("copyright_id", copyrightID).Debug("Getting translations for copyright")
|
||||
@ -99,8 +101,8 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint
|
||||
}
|
||||
|
||||
// GetTranslationByLanguage gets a specific translation by language code.
|
||||
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||
if copyrightID == 0 {
|
||||
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uuid.UUID, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||
if copyrightID == uuid.Nil {
|
||||
return nil, errors.New("invalid copyright ID")
|
||||
}
|
||||
if languageCode == "" {
|
||||
|
||||
@ -6,6 +6,8 @@ import (
|
||||
"tercul/internal/app/analytics"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LikeCommands contains the command handlers for the like aggregate.
|
||||
@ -24,10 +26,10 @@ func NewLikeCommands(repo domain.LikeRepository, analyticsSvc analytics.Service)
|
||||
|
||||
// CreateLikeInput represents the input for creating a new like.
|
||||
type CreateLikeInput struct {
|
||||
UserID uint
|
||||
WorkID *uint
|
||||
TranslationID *uint
|
||||
CommentID *uint
|
||||
UserID uuid.UUID
|
||||
WorkID *uuid.UUID
|
||||
TranslationID *uuid.UUID
|
||||
CommentID *uuid.UUID
|
||||
}
|
||||
|
||||
// CreateLike creates a new like and increments the relevant counter.
|
||||
@ -69,7 +71,7 @@ func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*
|
||||
}
|
||||
|
||||
// DeleteLike deletes a like by ID and decrements the relevant counter.
|
||||
func (c *LikeCommands) DeleteLike(ctx context.Context, id uint) error {
|
||||
func (c *LikeCommands) DeleteLike(ctx context.Context, id uuid.UUID) error {
|
||||
// First, get the like to determine what it was attached to.
|
||||
like, err := c.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
@ -3,6 +3,8 @@ package like
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LikeQueries contains the query handlers for the like aggregate.
|
||||
@ -16,27 +18,27 @@ func NewLikeQueries(repo domain.LikeRepository) *LikeQueries {
|
||||
}
|
||||
|
||||
// Like returns a like by ID.
|
||||
func (q *LikeQueries) Like(ctx context.Context, id uint) (*domain.Like, error) {
|
||||
func (q *LikeQueries) Like(ctx context.Context, id uuid.UUID) (*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) {
|
||||
func (q *LikeQueries) LikesByUserID(ctx context.Context, userID uuid.UUID) ([]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) {
|
||||
func (q *LikeQueries) LikesByWorkID(ctx context.Context, workID uuid.UUID) ([]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) {
|
||||
func (q *LikeQueries) LikesByTranslationID(ctx context.Context, translationID uuid.UUID) ([]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) {
|
||||
func (q *LikeQueries) LikesByCommentID(ctx context.Context, commentID uuid.UUID) ([]domain.Like, error) {
|
||||
return q.repo.ListByCommentID(ctx, commentID)
|
||||
}
|
||||
|
||||
|
||||
@ -10,4 +10,4 @@ type LocalizationCommands struct {
|
||||
// NewLocalizationCommands creates a new LocalizationCommands handler.
|
||||
func NewLocalizationCommands(repo domain.LocalizationRepository) *LocalizationCommands {
|
||||
return &LocalizationCommands{repo: repo}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package localization
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// LocalizationQueries contains the query handlers for the localization aggregate.
|
||||
@ -26,11 +28,11 @@ func (q *LocalizationQueries) GetTranslations(ctx context.Context, keys []string
|
||||
}
|
||||
|
||||
// GetAuthorBiography returns the biography of an author in a specific language.
|
||||
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) {
|
||||
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uuid.UUID, language string) (string, error) {
|
||||
return q.repo.GetAuthorBiography(ctx, authorID, language)
|
||||
}
|
||||
|
||||
// GetWorkContent returns the content of a work in a specific language.
|
||||
func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||
func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uuid.UUID, language string) (string, error) {
|
||||
return q.repo.GetWorkContent(ctx, workID, language)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,4 +14,4 @@ func NewService(repo domain.LocalizationRepository) *Service {
|
||||
Commands: NewLocalizationCommands(repo),
|
||||
Queries: NewLocalizationQueries(repo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,4 +90,4 @@ func TestLocalizationService_GetAuthorBiography(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedBiography, biography)
|
||||
repo.AssertExpectations(t)
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MonetizationCommands contains the command handlers for monetization.
|
||||
@ -18,8 +20,8 @@ func NewMonetizationCommands(repo domain.MonetizationRepository) *MonetizationCo
|
||||
}
|
||||
|
||||
// AddMonetizationToWork adds a monetization to a work.
|
||||
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||
if workID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid work ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Adding monetization to work")
|
||||
@ -27,72 +29,72 @@ func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID
|
||||
}
|
||||
|
||||
// RemoveMonetizationFromWork removes a monetization from a work.
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||
if workID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid work ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Removing monetization from work")
|
||||
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||
if authorID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid author ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Adding monetization to author")
|
||||
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||
if authorID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid author ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Removing monetization from author")
|
||||
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||
if bookID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid book ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Adding monetization to book")
|
||||
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||
if bookID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid book ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Removing monetization from book")
|
||||
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||
if publisherID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid publisher ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Adding monetization to publisher")
|
||||
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||
if publisherID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid publisher ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Removing monetization from publisher")
|
||||
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||
if sourceID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid source ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Adding monetization to source")
|
||||
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
||||
}
|
||||
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||
if sourceID == 0 || monetizationID == 0 {
|
||||
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
|
||||
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
||||
return errors.New("invalid source ID or monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Removing monetization from source")
|
||||
|
||||
@ -4,10 +4,10 @@ package monetization_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/app/monetization"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/testutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@ -7,21 +7,21 @@ import (
|
||||
|
||||
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
|
||||
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)
|
||||
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) {
|
||||
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
}
|
||||
@ -107,40 +107,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
|
||||
}
|
||||
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)
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/log"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MonetizationQueries contains the query handlers for monetization.
|
||||
@ -23,8 +25,8 @@ func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo domain.
|
||||
}
|
||||
|
||||
// GetMonetizationByID retrieves a monetization by ID.
|
||||
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||
if id == 0 {
|
||||
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
|
||||
if id == uuid.Nil {
|
||||
return nil, errors.New("invalid monetization ID")
|
||||
}
|
||||
log.FromContext(ctx).With("id", id).Debug("Getting monetization by ID")
|
||||
@ -37,7 +39,7 @@ func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.M
|
||||
return q.repo.ListAll(ctx)
|
||||
}
|
||||
|
||||
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) {
|
||||
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uuid.UUID) ([]*domain.Monetization, error) {
|
||||
log.FromContext(ctx).With("work_id", workID).Debug("Getting monetizations for work")
|
||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||
if err != nil {
|
||||
@ -46,7 +48,7 @@ func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workI
|
||||
return workRecord.Monetizations, nil
|
||||
}
|
||||
|
||||
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) {
|
||||
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uuid.UUID) ([]*domain.Monetization, error) {
|
||||
log.FromContext(ctx).With("author_id", authorID).Debug("Getting monetizations for author")
|
||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||
if err != nil {
|
||||
@ -55,7 +57,7 @@ func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, aut
|
||||
return author.Monetizations, nil
|
||||
}
|
||||
|
||||
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) {
|
||||
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uuid.UUID) ([]*domain.Monetization, error) {
|
||||
log.FromContext(ctx).With("book_id", bookID).Debug("Getting monetizations for book")
|
||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||
if err != nil {
|
||||
@ -64,7 +66,7 @@ func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookI
|
||||
return book.Monetizations, nil
|
||||
}
|
||||
|
||||
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) {
|
||||
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uuid.UUID) ([]*domain.Monetization, error) {
|
||||
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting monetizations for publisher")
|
||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||
if err != nil {
|
||||
@ -73,7 +75,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context,
|
||||
return publisher.Monetizations, nil
|
||||
}
|
||||
|
||||
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) {
|
||||
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uuid.UUID) ([]*domain.Monetization, error) {
|
||||
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting monetizations for source")
|
||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||
if err != nil {
|
||||
|
||||
@ -52,4 +52,4 @@ func (s *service) IndexWork(ctx context.Context, work domain.Work) error {
|
||||
}
|
||||
logger.Info("Successfully indexed work")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ func TestSearchService_Search(t *testing.T) {
|
||||
Query: testQuery,
|
||||
Filters: testFilters,
|
||||
Limit: 10,
|
||||
Offset: 0,
|
||||
Offset: 0,
|
||||
}
|
||||
weaviateWrapper.On("Search", ctx, params).Return(expectedResults, nil)
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ package tag
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TagCommands contains the command handlers for the tag aggregate.
|
||||
@ -36,7 +38,7 @@ func (c *TagCommands) CreateTag(ctx context.Context, input CreateTagInput) (*dom
|
||||
|
||||
// UpdateTagInput represents the input for updating an existing tag.
|
||||
type UpdateTagInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
@ -57,6 +59,6 @@ func (c *TagCommands) UpdateTag(ctx context.Context, input UpdateTagInput) (*dom
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag by ID.
|
||||
func (c *TagCommands) DeleteTag(ctx context.Context, id uint) error {
|
||||
func (c *TagCommands) DeleteTag(ctx context.Context, id uuid.UUID) error {
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package tag
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TagQueries contains the query handlers for the tag aggregate.
|
||||
@ -16,7 +18,7 @@ func NewTagQueries(repo domain.TagRepository) *TagQueries {
|
||||
}
|
||||
|
||||
// Tag returns a tag by ID.
|
||||
func (q *TagQueries) Tag(ctx context.Context, id uint) (*domain.Tag, error) {
|
||||
func (q *TagQueries) Tag(ctx context.Context, id uuid.UUID) (*domain.Tag, error) {
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
@ -26,7 +28,7 @@ func (q *TagQueries) TagByName(ctx context.Context, name string) (*domain.Tag, e
|
||||
}
|
||||
|
||||
// TagsByWorkID returns all tags for a work.
|
||||
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
||||
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Tag, error) {
|
||||
return q.repo.ListByWorkID(ctx, workID)
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@ -34,9 +35,9 @@ type CreateOrUpdateTranslationInput struct {
|
||||
Description string
|
||||
Language string
|
||||
Status domain.TranslationStatus
|
||||
TranslatableID uint
|
||||
TranslatableID uuid.UUID
|
||||
TranslatableType string
|
||||
TranslatorID *uint
|
||||
TranslatorID *uuid.UUID
|
||||
IsOriginalLanguage bool
|
||||
}
|
||||
|
||||
@ -49,8 +50,8 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
||||
if input.Language == "" {
|
||||
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
|
||||
}
|
||||
if input.TranslatableID == 0 {
|
||||
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation)
|
||||
if input.TranslatableID == uuid.Nil {
|
||||
return nil, fmt.Errorf("%w: translatable ID cannot be nil", domain.ErrValidation)
|
||||
}
|
||||
if input.TranslatableType == "" {
|
||||
return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation)
|
||||
@ -70,7 +71,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
||||
return nil, domain.ErrForbidden
|
||||
}
|
||||
|
||||
var translatorID uint
|
||||
var translatorID uuid.UUID
|
||||
if input.TranslatorID != nil {
|
||||
translatorID = *input.TranslatorID
|
||||
} else {
|
||||
@ -97,7 +98,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
||||
}
|
||||
|
||||
// DeleteTranslation deletes a translation by ID.
|
||||
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
|
||||
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uuid.UUID) error {
|
||||
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
|
||||
defer span.End()
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -281,4 +281,4 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
|
||||
func TestTranslationCommands(t *testing.T) {
|
||||
suite.Run(t, new(TranslationCommandsTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package translation
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// TranslationDTO is a read model for a translation.
|
||||
type TranslationDTO struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Title string
|
||||
Language string
|
||||
Content string
|
||||
TranslatableID uint
|
||||
}
|
||||
TranslatableID uuid.UUID
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ package translation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
type mockTranslationRepository struct {
|
||||
@ -12,7 +12,7 @@ type mockTranslationRepository struct {
|
||||
listByWorkIDPaginatedFunc func(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error)
|
||||
}
|
||||
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
}
|
||||
@ -92,4 +92,4 @@ func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status dom
|
||||
}
|
||||
func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@ -23,7 +24,7 @@ func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQuerie
|
||||
}
|
||||
|
||||
// Translation returns a translation by ID.
|
||||
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*TranslationDTO, error) {
|
||||
func (q *TranslationQueries) Translation(ctx context.Context, id uuid.UUID) (*TranslationDTO, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "Translation")
|
||||
defer span.End()
|
||||
|
||||
@ -45,21 +46,21 @@ func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*Transla
|
||||
}
|
||||
|
||||
// TranslationsByWorkID returns all translations for a work.
|
||||
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "TranslationsByWorkID")
|
||||
defer span.End()
|
||||
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) {
|
||||
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "TranslationsByEntity")
|
||||
defer span.End()
|
||||
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) {
|
||||
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID")
|
||||
defer span.End()
|
||||
return q.repo.ListByTranslatorID(ctx, translatorID)
|
||||
@ -80,7 +81,7 @@ func (q *TranslationQueries) Translations(ctx context.Context) ([]domain.Transla
|
||||
}
|
||||
|
||||
// ListTranslations returns a paginated list of translations for a work, with optional language filtering.
|
||||
func (q *TranslationQueries) ListTranslations(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[TranslationDTO], error) {
|
||||
func (q *TranslationQueries) ListTranslations(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[TranslationDTO], error) {
|
||||
ctx, span := q.tracer.Start(ctx, "ListTranslations")
|
||||
defer span.End()
|
||||
|
||||
|
||||
@ -76,4 +76,4 @@ func (s *TranslationQueriesSuite) TestListTranslations_Success() {
|
||||
TotalPages: 1,
|
||||
}
|
||||
assert.Equal(s.T(), expectedDTOs, paginatedDTOs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserCommands contains the command handlers for the user aggregate.
|
||||
@ -52,7 +54,7 @@ func (c *UserCommands) CreateUser(ctx context.Context, input CreateUserInput) (*
|
||||
|
||||
// UpdateUserInput represents the input for updating an existing user.
|
||||
type UpdateUserInput struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Username *string
|
||||
Email *string
|
||||
Password *string
|
||||
@ -64,12 +66,13 @@ type UpdateUserInput struct {
|
||||
Role *domain.UserRole
|
||||
Verified *bool
|
||||
Active *bool
|
||||
CountryID *uint
|
||||
CityID *uint
|
||||
AddressID *uint
|
||||
CountryID *uuid.UUID
|
||||
CityID *uuid.UUID
|
||||
AddressID *uuid.UUID
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user.
|
||||
//
|
||||
//nolint:gocyclo // Complex update logic
|
||||
func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) {
|
||||
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
@ -147,7 +150,7 @@ func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user by ID.
|
||||
func (c *UserCommands) DeleteUser(ctx context.Context, id uint) error {
|
||||
func (c *UserCommands) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
return domain.ErrUnauthorized
|
||||
@ -162,4 +165,4 @@ func (c *UserCommands) DeleteUser(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,4 +311,4 @@ func (s *UserCommandsSuite) TestDeleteUser_NotFound() {
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -149,4 +149,4 @@ func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRol
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// UserQueries contains the query handlers for the user aggregate.
|
||||
@ -17,7 +19,7 @@ func NewUserQueries(repo domain.UserRepository, profileRepo domain.UserProfileRe
|
||||
}
|
||||
|
||||
// User returns a user by ID.
|
||||
func (q *UserQueries) User(ctx context.Context, id uint) (*domain.User, error) {
|
||||
func (q *UserQueries) User(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
||||
return q.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
@ -42,6 +44,6 @@ func (q *UserQueries) Users(ctx context.Context) ([]domain.User, error) {
|
||||
}
|
||||
|
||||
// UserProfile returns a user profile by user ID.
|
||||
func (q *UserQueries) UserProfile(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||
func (q *UserQueries) UserProfile(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
|
||||
return q.profileRepo.GetByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ type mockUserProfileRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -36,7 +36,7 @@ func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB,
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
@ -275,4 +275,4 @@ func TestNewService(t *testing.T) {
|
||||
assert.NotNil(t, service)
|
||||
assert.NotNil(t, service.Commands)
|
||||
assert.NotNil(t, service.Queries)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ import (
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// WorkCommands contains the command handlers for the work aggregate.
|
||||
@ -108,8 +110,8 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
|
||||
if work == nil {
|
||||
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
|
||||
}
|
||||
if work.ID == 0 {
|
||||
return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation)
|
||||
if work.ID == uuid.Nil {
|
||||
return fmt.Errorf("%w: work ID cannot be nil", domain.ErrValidation)
|
||||
}
|
||||
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
@ -149,10 +151,10 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
|
||||
}
|
||||
|
||||
// DeleteWork deletes a work by ID after performing an authorization check.
|
||||
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||
func (c *WorkCommands) DeleteWork(ctx context.Context, id uuid.UUID) error {
|
||||
ctx, span := c.tracer.Start(ctx, "DeleteWork")
|
||||
defer span.End()
|
||||
if id == 0 {
|
||||
if id == uuid.Nil {
|
||||
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||
}
|
||||
|
||||
@ -181,7 +183,7 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
// AnalyzeWork performs linguistic analysis on a work and its translations.
|
||||
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uuid.UUID) error {
|
||||
ctx, span := c.tracer.Start(ctx, "AnalyzeWork")
|
||||
defer span.End()
|
||||
logger := log.FromContext(ctx).With("workID", workID)
|
||||
@ -220,8 +222,9 @@ func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
|
||||
}
|
||||
|
||||
// MergeWork merges two works, moving all associations from the source to the target and deleting the source.
|
||||
//
|
||||
//nolint:gocyclo // Complex merge logic
|
||||
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) error {
|
||||
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uuid.UUID) error {
|
||||
ctx, span := c.tracer.Start(ctx, "MergeWork")
|
||||
defer span.End()
|
||||
if sourceID == targetID {
|
||||
@ -331,7 +334,7 @@ func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
||||
func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uuid.UUID) error {
|
||||
var sourceStats domain.WorkStats
|
||||
err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@ -352,7 +355,7 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// If target has no stats, create a new stats record for it.
|
||||
newStats := sourceStats
|
||||
newStats.ID = 0
|
||||
newStats.ID = uuid.Nil
|
||||
newStats.WorkID = targetWorkID
|
||||
if err = tx.Create(&newStats).Error; err != nil {
|
||||
return fmt.Errorf("failed to create new target stats: %w", err)
|
||||
@ -373,4 +376,4 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package work
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// WorkDTO is a read model for a work, containing only the data needed for API responses.
|
||||
type WorkDTO struct {
|
||||
ID uint
|
||||
ID uuid.UUID
|
||||
Title string
|
||||
Language string
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package work
|
||||
|
||||
// This file is intentionally left empty.
|
||||
// Mocks are defined in main_test.go to avoid redeclaration errors.
|
||||
// Mocks are defined in main_test.go to avoid redeclaration errors.
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// WorkQueries contains the query handlers for the work aggregate.
|
||||
@ -24,10 +26,10 @@ func NewWorkQueries(repo domain.WorkRepository) *WorkQueries {
|
||||
}
|
||||
|
||||
// GetWorkByID retrieves a work by ID.
|
||||
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*WorkDTO, error) {
|
||||
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uuid.UUID) (*WorkDTO, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "GetWorkByID")
|
||||
defer span.End()
|
||||
if id == 0 {
|
||||
if id == uuid.Nil {
|
||||
return nil, errors.New("invalid work ID")
|
||||
}
|
||||
|
||||
@ -75,10 +77,10 @@ func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domai
|
||||
}
|
||||
|
||||
// GetWorkWithTranslations retrieves a work with its translations.
|
||||
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "GetWorkWithTranslations")
|
||||
defer span.End()
|
||||
if id == 0 {
|
||||
if id == uuid.Nil {
|
||||
return nil, errors.New("invalid work ID")
|
||||
}
|
||||
return q.repo.GetWithTranslations(ctx, id)
|
||||
@ -95,20 +97,20 @@ func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]dom
|
||||
}
|
||||
|
||||
// FindWorksByAuthor finds works by author ID.
|
||||
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "FindWorksByAuthor")
|
||||
defer span.End()
|
||||
if authorID == 0 {
|
||||
if authorID == uuid.Nil {
|
||||
return nil, errors.New("invalid author ID")
|
||||
}
|
||||
return q.repo.FindByAuthor(ctx, authorID)
|
||||
}
|
||||
|
||||
// FindWorksByCategory finds works by category ID.
|
||||
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "FindWorksByCategory")
|
||||
defer span.End()
|
||||
if categoryID == 0 {
|
||||
if categoryID == uuid.Nil {
|
||||
return nil, errors.New("invalid category ID")
|
||||
}
|
||||
return q.repo.FindByCategory(ctx, categoryID)
|
||||
@ -125,10 +127,10 @@ func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string,
|
||||
}
|
||||
|
||||
// ListByCollectionID finds works by collection ID.
|
||||
func (q *WorkQueries) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
func (q *WorkQueries) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
|
||||
ctx, span := q.tracer.Start(ctx, "ListByCollectionID")
|
||||
defer span.End()
|
||||
if collectionID == 0 {
|
||||
if collectionID == uuid.Nil {
|
||||
return nil, errors.New("invalid collection ID")
|
||||
}
|
||||
return q.repo.ListByCollectionID(ctx, collectionID)
|
||||
|
||||
@ -159,4 +159,4 @@ func (s *WorkQueriesSuite) TestFindWorksByLanguage_Empty() {
|
||||
w, err := s.queries.FindWorksByLanguage(context.Background(), "", 1, 10)
|
||||
assert.Error(s.T(), err)
|
||||
assert.Nil(s.T(), w)
|
||||
}
|
||||
}
|
||||
|
||||
21
internal/data/cache/cached_author_repository.go
vendored
21
internal/data/cache/cached_author_repository.go
vendored
@ -7,6 +7,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
platform_cache "tercul/internal/platform/cache"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -52,7 +53,7 @@ func (r *CachedAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, en
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Author
|
||||
key := r.opt.Keys.EntityKey("author", id)
|
||||
@ -71,7 +72,7 @@ func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.
|
||||
return author, nil
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||
}
|
||||
|
||||
@ -97,7 +98,7 @@ func (r *CachedAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, en
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *CachedAuthorRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
err := r.inner.Delete(ctx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -108,7 +109,7 @@ func (r *CachedAuthorRepository) Delete(ctx context.Context, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -154,7 +155,7 @@ func (r *CachedAuthorRepository) CountWithOptions(ctx context.Context, options *
|
||||
return r.inner.CountWithOptions(ctx, options)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Author, error) {
|
||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
||||
}
|
||||
|
||||
@ -162,7 +163,7 @@ func (r *CachedAuthorRepository) GetAllForSync(ctx context.Context, batchSize, o
|
||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
func (r *CachedAuthorRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
return r.inner.Exists(ctx, id)
|
||||
}
|
||||
|
||||
@ -178,19 +179,19 @@ func (r *CachedAuthorRepository) FindByName(ctx context.Context, name string) (*
|
||||
return r.inner.FindByName(ctx, name)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
|
||||
return r.inner.ListByWorkID(ctx, workID)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
|
||||
return r.inner.ListByBookID(ctx, bookID)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
|
||||
return r.inner.ListByCountryID(ctx, countryID)
|
||||
}
|
||||
|
||||
func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Author
|
||||
key := r.opt.Keys.QueryKey("author", "withTranslations", id)
|
||||
|
||||
231
internal/data/cache/cached_author_repository_test.go
vendored
Normal file
231
internal/data/cache/cached_author_repository_test.go
vendored
Normal file
@ -0,0 +1,231 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tercul/internal/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// memCache (duplicated) - tiny in-memory Cache impl for tests
|
||||
type memCacheA struct {
|
||||
m map[string][]byte
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newMemCacheA() *memCacheA { return &memCacheA{m: map[string][]byte{}} }
|
||||
|
||||
func (c *memCacheA) Get(_ context.Context, key string, value interface{}) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
b, ok := c.m[key]
|
||||
if !ok {
|
||||
return errors.New("cache miss")
|
||||
}
|
||||
return json.Unmarshal(b, value)
|
||||
}
|
||||
func (c *memCacheA) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.m[key] = b
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheA) Delete(_ context.Context, key string) error {
|
||||
c.mu.Lock()
|
||||
delete(c.m, key)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheA) Clear(_ context.Context) error {
|
||||
c.mu.Lock()
|
||||
c.m = map[string][]byte{}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheA) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
|
||||
res := map[string][]byte{}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
for _, k := range keys {
|
||||
if v, ok := c.m[k]; ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
func (c *memCacheA) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||
for k, v := range items {
|
||||
b, _ := json.Marshal(v)
|
||||
c.m[k] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dummyAuthorRepo implements domain.AuthorRepository minimal methods for tests
|
||||
type dummyAuthorRepo struct {
|
||||
store map[uint]*domain.Author
|
||||
calls int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newDummyAuthorRepo() *dummyAuthorRepo { return &dummyAuthorRepo{store: map[uint]*domain.Author{}} }
|
||||
|
||||
func (d *dummyAuthorRepo) Create(_ context.Context, entity *domain.Author) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyAuthorRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
a, ok := d.store[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
cp := *a
|
||||
return &cp, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyAuthorRepo) Update(_ context.Context, entity *domain.Author) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[entity.ID]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyAuthorRepo) Delete(_ context.Context, id uint) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[id]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
delete(d.store, id)
|
||||
return nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyAuthorRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
|
||||
return &domain.PaginatedResult[domain.Author]{Items: []domain.Author{}}, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) ListAll(_ context.Context) ([]domain.Author, error) { return nil, nil }
|
||||
func (d *dummyAuthorRepo) Count(_ context.Context) (int64, error) { return 0, nil }
|
||||
func (d *dummyAuthorRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Author, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyAuthorRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) Exists(_ context.Context, id uint) (bool, error) {
|
||||
_, ok := d.store[id]
|
||||
return ok, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (d *dummyAuthorRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error { return fn(nil) }
|
||||
func (d *dummyAuthorRepo) FindByName(_ context.Context, name string) (*domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) ListByWorkID(_ context.Context, workID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) ListByBookID(_ context.Context, bookID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) ListByCountryID(_ context.Context, countryID uint) ([]domain.Author, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyAuthorRepo) GetWithTranslations(_ context.Context, id uint) (*domain.Author, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
|
||||
func TestCachedAuthor_GetByID_Caches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyAuthorRepo()
|
||||
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
|
||||
mc := newMemCacheA()
|
||||
ca := NewCachedAuthorRepository(d, mc, nil)
|
||||
|
||||
a, err := ca.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Tolstoy", a.Name)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// second read should hit cache
|
||||
a2, err := ca.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Tolstoy", a2.Name)
|
||||
require.Equal(t, 1, d.calls)
|
||||
}
|
||||
|
||||
func TestCachedAuthor_Update_Invalidates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyAuthorRepo()
|
||||
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
|
||||
mc := newMemCacheA()
|
||||
ca := NewCachedAuthorRepository(d, mc, nil)
|
||||
|
||||
_, err := ca.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// update underlying
|
||||
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Leo"}
|
||||
err = ca.Update(ctx, d.store[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
// next get should fetch again
|
||||
a, err := ca.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Leo", a.Name)
|
||||
require.Equal(t, 3, d.calls)
|
||||
}
|
||||
|
||||
func TestCachedAuthor_Delete_Invalidates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyAuthorRepo()
|
||||
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
|
||||
mc := newMemCacheA()
|
||||
ca := NewCachedAuthorRepository(d, mc, nil)
|
||||
|
||||
_, err := ca.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ca.Delete(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ca.GetByID(ctx, 1)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
platform_cache "tercul/internal/platform/cache"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -52,7 +53,7 @@ func (r *CachedTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.D
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Translation
|
||||
key := r.opt.Keys.EntityKey("translation", id)
|
||||
@ -71,7 +72,7 @@ func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uint) (*do
|
||||
return tr, nil
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Translation, error) {
|
||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||
}
|
||||
|
||||
@ -97,7 +98,7 @@ func (r *CachedTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.D
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *CachedTranslationRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
err := r.inner.Delete(ctx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -108,7 +109,7 @@ func (r *CachedTranslationRepository) Delete(ctx context.Context, id uint) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -154,7 +155,7 @@ func (r *CachedTranslationRepository) CountWithOptions(ctx context.Context, opti
|
||||
return r.inner.CountWithOptions(ctx, options)
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Translation, error) {
|
||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
||||
}
|
||||
|
||||
@ -162,7 +163,7 @@ func (r *CachedTranslationRepository) GetAllForSync(ctx context.Context, batchSi
|
||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
func (r *CachedTranslationRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
return r.inner.Exists(ctx, id)
|
||||
}
|
||||
|
||||
@ -174,7 +175,7 @@ func (r *CachedTranslationRepository) WithTx(ctx context.Context, fn func(tx *go
|
||||
return r.inner.WithTx(ctx, fn)
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached []domain.Translation
|
||||
key := r.opt.Keys.QueryKey("translation", "byWork", workID)
|
||||
@ -193,7 +194,7 @@ func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID u
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
lang := ""
|
||||
if language != nil {
|
||||
lang = *language
|
||||
@ -217,11 +218,11 @@ func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context,
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
|
||||
return r.inner.ListByEntity(ctx, entityType, entityID)
|
||||
}
|
||||
|
||||
func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||
func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
|
||||
return r.inner.ListByTranslatorID(ctx, translatorID)
|
||||
}
|
||||
|
||||
|
||||
256
internal/data/cache/cached_translation_repository_test.go
vendored
Normal file
256
internal/data/cache/cached_translation_repository_test.go
vendored
Normal file
@ -0,0 +1,256 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tercul/internal/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// memCacheT - small in-memory cache for translation tests
|
||||
type memCacheT struct {
|
||||
m map[string][]byte
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newMemCacheT() *memCacheT { return &memCacheT{m: map[string][]byte{}} }
|
||||
func (c *memCacheT) Get(_ context.Context, key string, value interface{}) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
b, ok := c.m[key]
|
||||
if !ok {
|
||||
return errors.New("cache miss")
|
||||
}
|
||||
return json.Unmarshal(b, value)
|
||||
}
|
||||
func (c *memCacheT) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.m[key] = b
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheT) Delete(_ context.Context, key string) error {
|
||||
c.mu.Lock()
|
||||
delete(c.m, key)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheT) Clear(_ context.Context) error {
|
||||
c.mu.Lock()
|
||||
c.m = map[string][]byte{}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (c *memCacheT) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
|
||||
res := map[string][]byte{}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
for _, k := range keys {
|
||||
if v, ok := c.m[k]; ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
func (c *memCacheT) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||
for k, v := range items {
|
||||
b, _ := json.Marshal(v)
|
||||
c.m[k] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dummyTranslationRepo implements domain.TranslationRepository minimal functionality for tests
|
||||
type dummyTranslationRepo struct {
|
||||
store map[uint]*domain.Translation
|
||||
calls int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newDummyTranslationRepo() *dummyTranslationRepo {
|
||||
return &dummyTranslationRepo{store: map[uint]*domain.Translation{}}
|
||||
}
|
||||
func (d *dummyTranslationRepo) Create(_ context.Context, entity *domain.Translation) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyTranslationRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
tr, ok := d.store[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
cp := *tr
|
||||
return &cp, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyTranslationRepo) Update(_ context.Context, entity *domain.Translation) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[entity.ID]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyTranslationRepo) Delete(_ context.Context, id uint) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[id]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
delete(d.store, id)
|
||||
return nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyTranslationRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
return &domain.PaginatedResult[domain.Translation]{Items: []domain.Translation{}}, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListAll(_ context.Context) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) Count(_ context.Context) (int64, error) { return 0, nil }
|
||||
func (d *dummyTranslationRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Translation, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyTranslationRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) Exists(_ context.Context, id uint) (bool, error) {
|
||||
_, ok := d.store[id]
|
||||
return ok, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (d *dummyTranslationRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListByWorkID(_ context.Context, workID uint) ([]domain.Translation, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
res := []domain.Translation{}
|
||||
for _, v := range d.store {
|
||||
if v.TranslatableID == workID {
|
||||
res = append(res, *v)
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListByWorkIDPaginated(_ context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
all, _ := d.ListByWorkID(context.Background(), workID)
|
||||
return &domain.PaginatedResult[domain.Translation]{Items: all, TotalCount: int64(len(all)), Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListByEntity(_ context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListByTranslatorID(_ context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) ListByStatus(_ context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyTranslationRepo) Upsert(_ context.Context, translation *domain.Translation) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if translation.ID == 0 {
|
||||
translation.ID = uint(len(d.store) + 1)
|
||||
}
|
||||
d.store[translation.ID] = translation
|
||||
d.calls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCachedTranslation_GetByID_Caches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyTranslationRepo()
|
||||
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "T1"}
|
||||
mc := newMemCacheT()
|
||||
ct := NewCachedTranslationRepository(d, mc, nil)
|
||||
|
||||
tr, err := ct.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "T1", tr.Title)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// second read should hit cache
|
||||
tr2, err := ct.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "T1", tr2.Title)
|
||||
require.Equal(t, 1, d.calls)
|
||||
}
|
||||
|
||||
func TestCachedTranslation_ListByWorkID_Caches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyTranslationRepo()
|
||||
d.store[11] = &domain.Translation{BaseModel: domain.BaseModel{ID: 11}, TranslatableID: 100, Language: "en", Title: "A"}
|
||||
d.store[12] = &domain.Translation{BaseModel: domain.BaseModel{ID: 12}, TranslatableID: 100, Language: "fr", Title: "B"}
|
||||
mc := newMemCacheT()
|
||||
ct := NewCachedTranslationRepository(d, mc, nil)
|
||||
|
||||
list, err := ct.ListByWorkID(ctx, 100)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 2)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// second call should hit cache
|
||||
list2, err := ct.ListByWorkID(ctx, 100)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list2, 2)
|
||||
require.Equal(t, 1, d.calls)
|
||||
}
|
||||
|
||||
func TestCachedTranslation_Update_Invalidates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyTranslationRepo()
|
||||
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "Old"}
|
||||
mc := newMemCacheT()
|
||||
ct := NewCachedTranslationRepository(d, mc, nil)
|
||||
|
||||
_, err := ct.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// update underlying
|
||||
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "New"}
|
||||
err = ct.Update(ctx, d.store[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
tr, err := ct.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "New", tr.Title)
|
||||
require.Equal(t, 3, d.calls)
|
||||
}
|
||||
27
internal/data/cache/cached_work_repository.go
vendored
27
internal/data/cache/cached_work_repository.go
vendored
@ -7,6 +7,7 @@ import (
|
||||
"tercul/internal/domain"
|
||||
platform_cache "tercul/internal/platform/cache"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -52,7 +53,7 @@ func (r *CachedWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Work
|
||||
key := r.opt.Keys.EntityKey("work", id)
|
||||
@ -72,7 +73,7 @@ func (r *CachedWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Wo
|
||||
return work, nil
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Work, error) {
|
||||
// Options can include varying preloads/where/order; avoid caching to prevent incorrect results.
|
||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||
}
|
||||
@ -99,7 +100,7 @@ func (r *CachedWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, enti
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) Delete(ctx context.Context, id uint) error {
|
||||
func (r *CachedWorkRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
err := r.inner.Delete(ctx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -110,7 +111,7 @@ func (r *CachedWorkRepository) Delete(ctx context.Context, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||
if err == nil {
|
||||
if r.opt.Cache != nil {
|
||||
@ -156,7 +157,7 @@ func (r *CachedWorkRepository) CountWithOptions(ctx context.Context, options *do
|
||||
return r.inner.CountWithOptions(ctx, options)
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Work, error) {
|
||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
||||
}
|
||||
|
||||
@ -164,7 +165,7 @@ func (r *CachedWorkRepository) GetAllForSync(ctx context.Context, batchSize, off
|
||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
func (r *CachedWorkRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||
return r.inner.Exists(ctx, id)
|
||||
}
|
||||
|
||||
@ -180,11 +181,11 @@ func (r *CachedWorkRepository) FindByTitle(ctx context.Context, title string) ([
|
||||
return r.inner.FindByTitle(ctx, title)
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
|
||||
return r.inner.FindByAuthor(ctx, authorID)
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
|
||||
return r.inner.FindByCategory(ctx, categoryID)
|
||||
}
|
||||
|
||||
@ -207,7 +208,7 @@ func (r *CachedWorkRepository) FindByLanguage(ctx context.Context, language stri
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Work
|
||||
key := r.opt.Keys.QueryKey("work", "withTranslations", id)
|
||||
@ -226,7 +227,7 @@ func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uint)
|
||||
return work, nil
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
if r.opt.Enabled && r.opt.Cache != nil {
|
||||
var cached domain.Work
|
||||
key := r.opt.Keys.QueryKey("work", "withAssociations", id)
|
||||
@ -245,7 +246,7 @@ func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uint)
|
||||
return work, nil
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
||||
func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) (*domain.Work, error) {
|
||||
// Tx-scoped reads should bypass cache.
|
||||
return r.inner.GetWithAssociationsInTx(ctx, tx, id)
|
||||
}
|
||||
@ -269,10 +270,10 @@ func (r *CachedWorkRepository) ListWithTranslations(ctx context.Context, page, p
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uuid.UUID, authorID uuid.UUID) (bool, error) {
|
||||
return r.inner.IsAuthor(ctx, workID, authorID)
|
||||
}
|
||||
|
||||
func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
|
||||
return r.inner.ListByCollectionID(ctx, collectionID)
|
||||
}
|
||||
|
||||
287
internal/data/cache/cached_work_repository_test.go
vendored
Normal file
287
internal/data/cache/cached_work_repository_test.go
vendored
Normal file
@ -0,0 +1,287 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tercul/internal/domain"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// memCache is a tiny in-memory implementation of platform_cache.Cache for tests.
|
||||
type memCache struct {
|
||||
m map[string][]byte
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newMemCache() *memCache { return &memCache{m: map[string][]byte{}} }
|
||||
|
||||
func (c *memCache) Get(_ context.Context, key string, value interface{}) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
b, ok := c.m[key]
|
||||
if !ok {
|
||||
return errors.New("cache miss")
|
||||
}
|
||||
return json.Unmarshal(b, value)
|
||||
}
|
||||
|
||||
func (c *memCache) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.m[key] = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memCache) Delete(_ context.Context, key string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.m, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *memCache) Clear(_ context.Context) error {
|
||||
c.mu.Lock()
|
||||
c.m = map[string][]byte{}
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
func (c *memCache) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
|
||||
res := map[string][]byte{}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
for _, k := range keys {
|
||||
if v, ok := c.m[k]; ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
func (c *memCache) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
|
||||
for k, v := range items {
|
||||
b, _ := json.Marshal(v)
|
||||
c.m[k] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dummyWorkRepo implements the minimal parts of domain.WorkRepository we need for tests.
|
||||
type dummyWorkRepo struct {
|
||||
store map[uint]*domain.Work
|
||||
calls int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newDummyWorkRepo() *dummyWorkRepo { return &dummyWorkRepo{store: map[uint]*domain.Work{}} }
|
||||
|
||||
func (d *dummyWorkRepo) Create(_ context.Context, entity *domain.Work) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyWorkRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyWorkRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Work, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
w, ok := d.store[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
// return a copy to ensure caching serializes
|
||||
cp := *w
|
||||
return &cp, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyWorkRepo) Update(_ context.Context, entity *domain.Work) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[entity.ID]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
d.store[entity.ID] = entity
|
||||
return nil
|
||||
}
|
||||
func (d *dummyWorkRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyWorkRepo) Delete(_ context.Context, id uint) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.calls++
|
||||
if _, ok := d.store[id]; !ok {
|
||||
return errors.New("not found")
|
||||
}
|
||||
delete(d.store, id)
|
||||
return nil
|
||||
}
|
||||
func (d *dummyWorkRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
func (d *dummyWorkRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) ListAll(_ context.Context) ([]domain.Work, error) { return nil, nil }
|
||||
func (d *dummyWorkRepo) Count(_ context.Context) (int64, error) { return 0, nil }
|
||||
func (d *dummyWorkRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Work, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyWorkRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) Exists(_ context.Context, id uint) (bool, error) {
|
||||
_, ok := d.store[id]
|
||||
return ok, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (d *dummyWorkRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (d *dummyWorkRepo) FindByTitle(_ context.Context, title string) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) FindByAuthor(_ context.Context, authorID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) FindByCategory(_ context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *dummyWorkRepo) GetWithTranslations(_ context.Context, id uint) (*domain.Work, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyWorkRepo) GetWithAssociations(_ context.Context, id uint) (*domain.Work, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyWorkRepo) GetWithAssociationsInTx(_ context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
||||
return d.GetByID(context.Background(), id)
|
||||
}
|
||||
func (d *dummyWorkRepo) ListWithTranslations(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
// return a simple paginated result of all works
|
||||
items := []domain.Work{}
|
||||
for _, w := range d.store {
|
||||
items = append(items, *w)
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + pageSize
|
||||
if start >= len(items) {
|
||||
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
||||
}
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (d *dummyWorkRepo) FindByLanguage(_ context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
items := []domain.Work{}
|
||||
for _, w := range d.store {
|
||||
if w.Language == language {
|
||||
items = append(items, *w)
|
||||
}
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + pageSize
|
||||
if start >= len(items) {
|
||||
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
|
||||
}
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) IsAuthor(_ context.Context, workID uint, authorID uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (d *dummyWorkRepo) ListByCollectionID(_ context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestCachedWork_GetByID_Caches(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyWorkRepo()
|
||||
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
||||
mc := newMemCache()
|
||||
cw := NewCachedWorkRepository(d, mc, nil)
|
||||
|
||||
w, err := cw.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "initial", w.Title)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// second read should hit cache and not increment inner calls
|
||||
w2, err := cw.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "initial", w2.Title)
|
||||
require.Equal(t, 1, d.calls)
|
||||
}
|
||||
|
||||
func TestCachedWork_Update_Invalidates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyWorkRepo()
|
||||
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
||||
mc := newMemCache()
|
||||
cw := NewCachedWorkRepository(d, mc, nil)
|
||||
|
||||
_, err := cw.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, d.calls)
|
||||
|
||||
// Update underlying entity
|
||||
d.store[1] = &domain.Work{Title: "updated", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
||||
err = cw.Update(ctx, d.store[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
// Next GetByID should call inner again (cache invalidated)
|
||||
w, err := cw.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated", w.Title)
|
||||
require.Equal(t, 3, d.calls, "expected inner to be called again for update invalidation")
|
||||
}
|
||||
|
||||
func TestCachedWork_Delete_Invalidates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
d := newDummyWorkRepo()
|
||||
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
|
||||
mc := newMemCache()
|
||||
cw := NewCachedWorkRepository(d, mc, nil)
|
||||
|
||||
_, err := cw.GetByID(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
// delete
|
||||
err = cw.Delete(ctx, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = cw.GetByID(ctx, 1)
|
||||
require.Error(t, err)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user