mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11: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]
|
[build]
|
||||||
# Just plain old shell command. You could use `make` as well.
|
# 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`.
|
# Binary file yields from `cmd`.
|
||||||
bin = "tmp/tercul"
|
bin = "tmp/tercul"
|
||||||
# Customize binary.
|
# Customize binary.
|
||||||
|
|||||||
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
|
# Install git and required dependencies
|
||||||
RUN apk add --no-cache git build-base
|
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)
|
# 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
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
13
Makefile
13
Makefile
@ -1,4 +1,15 @@
|
|||||||
.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:
|
lint-test:
|
||||||
@echo "Running linter..."
|
@echo "Running linter..."
|
||||||
|
|||||||
@ -126,13 +126,36 @@ func main() {
|
|||||||
// Create repositories
|
// Create repositories
|
||||||
repos := dbsql.NewRepositories(database, cfg)
|
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
|
// Create linguistics dependencies
|
||||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
|
||||||
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
|
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
|
||||||
if sErr != nil {
|
if sErr != nil {
|
||||||
app_log.Fatal(sErr, "Failed to create sentiment provider")
|
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
|
// Create platform components
|
||||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||||
|
|
||||||
@ -182,8 +205,6 @@ func main() {
|
|||||||
App: application,
|
App: application,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Redis Cache for APQ
|
|
||||||
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
|
|
||||||
var queryCache gql.Cache[string]
|
var queryCache gql.Cache[string]
|
||||||
if cacheErr != nil {
|
if cacheErr != nil {
|
||||||
app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error())
|
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"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
@ -30,7 +29,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type checkpoint struct {
|
type checkpoint struct {
|
||||||
LastProcessedID uint `json:"last_processed_id"`
|
LastProcessedID string `json:"last_processed_id"`
|
||||||
TotalProcessed int `json:"total_processed"`
|
TotalProcessed int `json:"total_processed"`
|
||||||
LastUpdated time.Time `json:"last_updated"`
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
}
|
}
|
||||||
@ -104,7 +103,7 @@ Example:
|
|||||||
if resume {
|
if resume {
|
||||||
cp = loadCheckpoint()
|
cp = loadCheckpoint()
|
||||||
if cp != nil {
|
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))
|
logger.Info(fmt.Sprintf("Found %d translations", totalTranslations))
|
||||||
|
|
||||||
// Filter translations if resuming from checkpoint
|
// Filter translations if resuming from checkpoint
|
||||||
if cp != nil && cp.LastProcessedID > 0 {
|
if cp != nil && cp.LastProcessedID != "" {
|
||||||
filtered := make([]domain.Translation, 0, len(translations))
|
filtered := make([]domain.Translation, 0, len(translations))
|
||||||
for _, t := range translations {
|
for _, t := range translations {
|
||||||
if t.ID > cp.LastProcessedID {
|
if t.ID.String() > cp.LastProcessedID {
|
||||||
filtered = append(filtered, t)
|
filtered = append(filtered, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -282,11 +281,11 @@ func migrateTranslations(
|
|||||||
|
|
||||||
// Process translations in batches
|
// Process translations in batches
|
||||||
batch := make([]domain.Translation, 0, batchSize)
|
batch := make([]domain.Translation, 0, batchSize)
|
||||||
lastProcessedID := uint(0)
|
lastProcessedID := ""
|
||||||
|
|
||||||
for i, translation := range translations {
|
for i, translation := range translations {
|
||||||
batch = append(batch, translation)
|
batch = append(batch, translation)
|
||||||
lastProcessedID = translation.ID
|
lastProcessedID = translation.ID.String()
|
||||||
|
|
||||||
// Process batch when it reaches the batch size or at the end
|
// Process batch when it reaches the batch size or at the end
|
||||||
if len(batch) >= batchSize || i == len(translations)-1 {
|
if len(batch) >= batchSize || i == len(translations)-1 {
|
||||||
@ -326,7 +325,7 @@ func indexBatch(index bleve.Index, translations []domain.Translation) error {
|
|||||||
batch := index.NewBatch()
|
batch := index.NewBatch()
|
||||||
for _, t := range translations {
|
for _, t := range translations {
|
||||||
doc := map[string]interface{}{
|
doc := map[string]interface{}{
|
||||||
"id": strconv.FormatUint(uint64(t.ID), 10),
|
"id": t.ID.String(),
|
||||||
"title": t.Title,
|
"title": t.Title,
|
||||||
"content": t.Content,
|
"content": t.Content,
|
||||||
"description": t.Description,
|
"description": t.Description,
|
||||||
|
|||||||
@ -129,4 +129,3 @@ func TestCheckpoint_InvalidJSON(t *testing.T) {
|
|||||||
// This would require mocking file system, but for now we test the happy path
|
// This would require mocking file system, but for now we test the happy path
|
||||||
// Invalid JSON handling is tested implicitly through file operations
|
// 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
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
|
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)
|
assert.NotEmpty(t, cmd.Short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"tercul/cmd/cli/internal/bootstrap"
|
"tercul/cmd/cli/internal/bootstrap"
|
||||||
"tercul/internal/enrichment"
|
"tercul/internal/enrichment"
|
||||||
@ -11,6 +10,7 @@ import (
|
|||||||
"tercul/internal/platform/db"
|
"tercul/internal/platform/db"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ Example:
|
|||||||
return fmt.Errorf("both --type and --id are required")
|
return fmt.Errorf("both --type and --id are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
entityIDUint, err := strconv.ParseUint(entityID, 10, 64)
|
entityIDUUID, err := uuid.Parse(entityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid entity ID: %w", err)
|
return fmt.Errorf("invalid entity ID: %w", err)
|
||||||
}
|
}
|
||||||
@ -71,11 +71,11 @@ Example:
|
|||||||
|
|
||||||
// Fetch, enrich, and save the entity
|
// Fetch, enrich, and save the entity
|
||||||
ctx := context.Background()
|
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 {
|
switch entityType {
|
||||||
case "author":
|
case "author":
|
||||||
author, err := deps.Repos.Author.GetByID(ctx, uint(entityIDUint))
|
author, err := deps.Repos.Author.GetByID(ctx, entityIDUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get author: %w", err)
|
return fmt.Errorf("failed to get author: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,10 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"tercul/cmd/cli/internal/bootstrap"
|
"tercul/cmd/cli/internal/bootstrap"
|
||||||
|
dbsql "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/jobs/sync"
|
"tercul/internal/jobs/sync"
|
||||||
|
"tercul/internal/platform/cache"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/platform/db"
|
"tercul/internal/platform/db"
|
||||||
app_log "tercul/internal/platform/log"
|
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
|
// Create SyncJob with all dependencies
|
||||||
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
|
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
|
||||||
|
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
|
||||||
|
|
||||||
// Create a new ServeMux for routing jobs
|
// Create a new ServeMux for routing jobs
|
||||||
mux := asynq.NewServeMux()
|
mux := asynq.NewServeMux()
|
||||||
|
|
||||||
// Register all job handlers
|
// Register all job handlers
|
||||||
sync.RegisterQueueHandlers(mux, syncJob)
|
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)
|
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
||||||
|
|
||||||
// Start the server in a goroutine
|
// Start the server in a goroutine
|
||||||
|
|||||||
@ -48,7 +48,7 @@ type Dependencies struct {
|
|||||||
Repos *dbsql.Repositories
|
Repos *dbsql.Repositories
|
||||||
Application *app.Application
|
Application *app.Application
|
||||||
JWTManager *platform_auth.JWTManager
|
JWTManager *platform_auth.JWTManager
|
||||||
AnalysisRepo *linguistics.GORMAnalysisRepository
|
AnalysisRepo linguistics.AnalysisRepository
|
||||||
SentimentProvider *linguistics.GoVADERSentimentProvider
|
SentimentProvider *linguistics.GoVADERSentimentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,12 +61,32 @@ func Bootstrap(cfg *config.Config, database *gorm.DB, weaviateClient *weaviate.C
|
|||||||
repos := dbsql.NewRepositories(database, cfg)
|
repos := dbsql.NewRepositories(database, cfg)
|
||||||
|
|
||||||
// Create linguistics dependencies
|
// Create linguistics dependencies
|
||||||
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
|
|
||||||
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// Create platform components
|
||||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||||
|
|
||||||
|
|||||||
@ -109,4 +109,3 @@ func TestBootstrapWithMetrics(t *testing.T) {
|
|||||||
assert.NotNil(t, deps)
|
assert.NotNil(t, deps)
|
||||||
assert.NotNil(t, deps.Application)
|
assert.NotNil(t, deps.Application)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,13 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/enrichment"
|
"tercul/internal/enrichment"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/platform/db"
|
"tercul/internal/platform/db"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -24,7 +25,7 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
entityID, err := strconv.ParseUint(*entityIDStr, 10, 64)
|
entityID, err := uuid.Parse(*entityIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Invalid entity ID: %v\n", err)
|
fmt.Printf("Invalid entity ID: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -55,7 +56,7 @@ func main() {
|
|||||||
|
|
||||||
switch *entityType {
|
switch *entityType {
|
||||||
case "author":
|
case "author":
|
||||||
author, err := repos.Author.GetByID(ctx, uint(entityID))
|
author, err := repos.Author.GetByID(ctx, entityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err, "Failed to get author")
|
log.Fatal(err, "Failed to get author")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
dbsql "tercul/internal/data/sql"
|
||||||
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/jobs/sync"
|
"tercul/internal/jobs/sync"
|
||||||
|
"tercul/internal/platform/cache"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/platform/db"
|
"tercul/internal/platform/db"
|
||||||
app_log "tercul/internal/platform/log"
|
app_log "tercul/internal/platform/log"
|
||||||
@ -70,13 +74,44 @@ func main() {
|
|||||||
// Create SyncJob with all dependencies
|
// Create SyncJob with all dependencies
|
||||||
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
|
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
|
// Create a new ServeMux for routing jobs
|
||||||
mux := asynq.NewServeMux()
|
mux := asynq.NewServeMux()
|
||||||
|
|
||||||
// Register all job handlers
|
// Register all job handlers
|
||||||
sync.RegisterQueueHandlers(mux, syncJob)
|
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)
|
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
||||||
|
|
||||||
// Start the server in a goroutine
|
// Start the server in a goroutine
|
||||||
|
|||||||
@ -14,7 +14,8 @@ services:
|
|||||||
- DB_PASSWORD=postgres
|
- DB_PASSWORD=postgres
|
||||||
- DB_NAME=tercul
|
- DB_NAME=tercul
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- WEAVIATE_HOST=http://weaviate:8080
|
- WEAVIATE_HOST=weaviate:8080
|
||||||
|
- WEAVIATE_SCHEME=http
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module tercul
|
module tercul
|
||||||
|
|
||||||
go 1.24.10
|
go 1.25.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.72
|
github.com/99designs/gqlgen v0.17.72
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
package graphql
|
package graphql
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
// resolveWorkContent uses the Work service to fetch preferred content for a work.
|
// 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 {
|
if r.App.Work == nil || r.App.Work.Queries == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ func (m *mockLikeRepository) Create(ctx context.Context, entity *domain.Like) er
|
|||||||
args := m.Called(ctx, entity)
|
args := m.Called(ctx, entity)
|
||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"tercul/internal/adapters/graphql/model"
|
"tercul/internal/adapters/graphql/model"
|
||||||
"tercul/internal/app/auth"
|
"tercul/internal/app/auth"
|
||||||
@ -26,6 +25,8 @@ import (
|
|||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toModelUserRole(role domain.UserRole) model.UserRole {
|
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
|
// Convert to GraphQL model
|
||||||
return &model.Work{
|
return &model.Work{
|
||||||
ID: fmt.Sprintf("%d", createdWork.ID),
|
ID: createdWork.ID.String(),
|
||||||
Name: createdWork.Title,
|
Name: createdWork.Title,
|
||||||
Language: createdWork.Language,
|
Language: createdWork.Language,
|
||||||
Content: input.Content,
|
Content: input.Content,
|
||||||
@ -157,7 +158,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(id, 10, 32)
|
workID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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
|
// Create domain model
|
||||||
workModel := &domain.Work{
|
workModel := &domain.Work{
|
||||||
TranslatableModel: domain.TranslatableModel{
|
TranslatableModel: domain.TranslatableModel{
|
||||||
BaseModel: domain.BaseModel{ID: uint(workID)},
|
BaseModel: domain.BaseModel{ID: workID},
|
||||||
Language: input.Language,
|
Language: input.Language,
|
||||||
},
|
},
|
||||||
Title: input.Name,
|
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.
|
// DeleteWork is the resolver for the deleteWork field.
|
||||||
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
|
||||||
workID, err := strconv.ParseUint(id, 10, 32)
|
workID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -207,7 +208,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
workID, err := uuid.Parse(input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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,
|
Title: input.Name,
|
||||||
Content: content,
|
Content: content,
|
||||||
Language: input.Language,
|
Language: input.Language,
|
||||||
TranslatableID: uint(workID),
|
TranslatableID: workID,
|
||||||
TranslatableType: "works",
|
TranslatableType: "works",
|
||||||
}
|
}
|
||||||
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
|
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() {
|
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")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
workID, err := uuid.Parse(input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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,
|
Title: input.Name,
|
||||||
Content: content,
|
Content: content,
|
||||||
Language: input.Language,
|
Language: input.Language,
|
||||||
TranslatableID: uint(workID),
|
TranslatableID: workID,
|
||||||
TranslatableType: "works",
|
TranslatableType: "works",
|
||||||
}
|
}
|
||||||
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
|
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.
|
// DeleteTranslation is the resolver for the deleteTranslation field.
|
||||||
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
|
||||||
translationID, err := strconv.ParseUint(id, 10, 32)
|
translationID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid translation ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -329,13 +330,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bookID, err := strconv.ParseUint(id, 10, 32)
|
bookID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput := book.UpdateBookInput{
|
updateInput := book.UpdateBookInput{
|
||||||
ID: uint(bookID),
|
ID: bookID,
|
||||||
Title: &input.Name,
|
Title: &input.Name,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
Language: &input.Language,
|
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.
|
// DeleteBook is the resolver for the deleteBook field.
|
||||||
func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) {
|
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 {
|
if err != nil {
|
||||||
return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -396,13 +397,13 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
|
|||||||
if err := Validate(input); err != nil {
|
if err := Validate(input); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
authorID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid author ID: %v", err)
|
return nil, fmt.Errorf("invalid author ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput := author.UpdateAuthorInput{
|
updateInput := author.UpdateAuthorInput{
|
||||||
ID: uint(authorID),
|
ID: authorID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
}
|
}
|
||||||
updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput)
|
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.
|
// DeleteAuthor is the resolver for the deleteAuthor field.
|
||||||
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
|
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
|
||||||
authorID, err := strconv.ParseUint(id, 10, 32)
|
authorID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid author ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -438,13 +439,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := strconv.ParseUint(id, 10, 32)
|
userID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid user ID: %v", err)
|
return nil, fmt.Errorf("invalid user ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput := user.UpdateUserInput{
|
updateInput := user.UpdateUserInput{
|
||||||
ID: uint(userID),
|
ID: userID,
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Password: input.Password,
|
Password: input.Password,
|
||||||
@ -463,28 +464,25 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.CountryID != nil {
|
if input.CountryID != nil {
|
||||||
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
countryID, err := uuid.Parse(*input.CountryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(countryID)
|
updateInput.CountryID = &countryID
|
||||||
updateInput.CountryID = &uid
|
|
||||||
}
|
}
|
||||||
if input.CityID != nil {
|
if input.CityID != nil {
|
||||||
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
cityID, err := uuid.Parse(*input.CityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(cityID)
|
updateInput.CityID = &cityID
|
||||||
updateInput.CityID = &uid
|
|
||||||
}
|
}
|
||||||
if input.AddressID != nil {
|
if input.AddressID != nil {
|
||||||
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
addressID, err := uuid.Parse(*input.AddressID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(addressID)
|
updateInput.AddressID = &addressID
|
||||||
updateInput.AddressID = &uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
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.
|
// DeleteUser is the resolver for the deleteUser field.
|
||||||
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
|
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 {
|
if err != nil {
|
||||||
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -558,13 +556,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionID, err := strconv.ParseUint(id, 10, 32)
|
collectionID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput := collection.UpdateCollectionInput{
|
updateInput := collection.UpdateCollectionInput{
|
||||||
ID: uint(collectionID),
|
ID: collectionID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
@ -593,12 +591,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
|
|||||||
return false, fmt.Errorf("unauthorized")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionID, err := strconv.ParseUint(id, 10, 32)
|
collectionID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid collection ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -613,18 +611,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
collID, err := uuid.Parse(collectionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
}
|
}
|
||||||
wID, err := strconv.ParseUint(workID, 10, 32)
|
wID, err := uuid.Parse(workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addInput := collection.AddWorkToCollectionInput{
|
addInput := collection.AddWorkToCollectionInput{
|
||||||
CollectionID: uint(collID),
|
CollectionID: collID,
|
||||||
WorkID: uint(wID),
|
WorkID: wID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
||||||
@ -632,7 +630,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -651,18 +649,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
collID, err := uuid.Parse(collectionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
return nil, fmt.Errorf("invalid collection ID: %v", err)
|
||||||
}
|
}
|
||||||
wID, err := strconv.ParseUint(workID, 10, 32)
|
wID, err := uuid.Parse(workID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInput := collection.RemoveWorkFromCollectionInput{
|
removeInput := collection.RemoveWorkFromCollectionInput{
|
||||||
CollectionID: uint(collID),
|
CollectionID: collID,
|
||||||
WorkID: uint(wID),
|
WorkID: wID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
||||||
@ -670,7 +668,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -698,28 +696,25 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
workID, err := uuid.Parse(*input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
wID := uint(workID)
|
createInput.WorkID = &workID
|
||||||
createInput.WorkID = &wID
|
|
||||||
}
|
}
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
translationID, err := uuid.Parse(*input.TranslationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
tID := uint(translationID)
|
createInput.TranslationID = &translationID
|
||||||
createInput.TranslationID = &tID
|
|
||||||
}
|
}
|
||||||
if input.ParentCommentID != nil {
|
if input.ParentCommentID != nil {
|
||||||
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
|
parentCommentID, err := uuid.Parse(*input.ParentCommentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid parent comment ID: %v", err)
|
return nil, fmt.Errorf("invalid parent comment ID: %v", err)
|
||||||
}
|
}
|
||||||
pID := uint(parentCommentID)
|
createInput.ParentID = &parentCommentID
|
||||||
createInput.ParentID = &pID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
|
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")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
commentID, err := strconv.ParseUint(id, 10, 32)
|
commentID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -772,7 +767,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateInput := comment.UpdateCommentInput{
|
updateInput := comment.UpdateCommentInput{
|
||||||
ID: uint(commentID),
|
ID: commentID,
|
||||||
Text: input.Text,
|
Text: input.Text,
|
||||||
}
|
}
|
||||||
updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput)
|
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")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
commentID, err := strconv.ParseUint(id, 10, 32)
|
commentID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid comment ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -813,7 +808,7 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
|
|||||||
return false, fmt.Errorf("unauthorized")
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -839,28 +834,25 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
workID, err := uuid.Parse(*input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
wID := uint(workID)
|
createInput.WorkID = &workID
|
||||||
createInput.WorkID = &wID
|
|
||||||
}
|
}
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
translationID, err := uuid.Parse(*input.TranslationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
tID := uint(translationID)
|
createInput.TranslationID = &translationID
|
||||||
createInput.TranslationID = &tID
|
|
||||||
}
|
}
|
||||||
if input.CommentID != nil {
|
if input.CommentID != nil {
|
||||||
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
|
commentID, err := uuid.Parse(*input.CommentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||||
}
|
}
|
||||||
cID := uint(commentID)
|
createInput.CommentID = &commentID
|
||||||
createInput.CommentID = &cID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
|
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")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
likeID, err := strconv.ParseUint(id, 10, 32)
|
likeID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid like ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -909,7 +901,7 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
|
|||||||
return false, fmt.Errorf("unauthorized")
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -924,14 +916,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
workID, err := uuid.Parse(input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
createInput := bookmark.CreateBookmarkInput{
|
createInput := bookmark.CreateBookmarkInput{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
WorkID: uint(workID),
|
WorkID: workID,
|
||||||
}
|
}
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
createInput.Name = *input.Name
|
createInput.Name = *input.Name
|
||||||
@ -942,7 +934,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
|||||||
return nil, err
|
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")
|
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")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkID, err := strconv.ParseUint(id, 10, 32)
|
bookmarkID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("invalid bookmark ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -978,7 +970,7 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
|
|||||||
return false, fmt.Errorf("unauthorized")
|
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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -998,21 +990,19 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
workID, err := uuid.Parse(*input.WorkID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
return nil, fmt.Errorf("invalid work ID: %v", err)
|
||||||
}
|
}
|
||||||
wID := uint(workID)
|
createInput.WorkID = &workID
|
||||||
createInput.WorkID = &wID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
translationID, err := uuid.Parse(*input.TranslationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
tID := uint(translationID)
|
createInput.TranslationID = &translationID
|
||||||
createInput.TranslationID = &tID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Status != nil {
|
if input.Status != nil {
|
||||||
@ -1043,13 +1033,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
|
|||||||
return nil, domain.ErrUnauthorized
|
return nil, domain.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
contributionID, err := strconv.ParseUint(id, 10, 32)
|
contributionID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInput := contribution.UpdateContributionInput{
|
updateInput := contribution.UpdateContributionInput{
|
||||||
ID: uint(contributionID),
|
ID: contributionID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: &input.Name,
|
Name: &input.Name,
|
||||||
}
|
}
|
||||||
@ -1081,12 +1071,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
|
|||||||
return false, domain.ErrUnauthorized
|
return false, domain.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
contributionID, err := strconv.ParseUint(id, 10, 32)
|
contributionID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return false, err
|
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.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
reviewInput := contribution.ReviewContributionInput{
|
reviewInput := contribution.ReviewContributionInput{
|
||||||
ID: uint(contributionID),
|
ID: contributionID,
|
||||||
Status: status.String(),
|
Status: status.String(),
|
||||||
Feedback: feedback,
|
Feedback: feedback,
|
||||||
}
|
}
|
||||||
@ -1211,28 +1201,25 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.CountryID != nil {
|
if input.CountryID != nil {
|
||||||
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
countryID, err := uuid.Parse(*input.CountryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(countryID)
|
updateInput.CountryID = &countryID
|
||||||
updateInput.CountryID = &uid
|
|
||||||
}
|
}
|
||||||
if input.CityID != nil {
|
if input.CityID != nil {
|
||||||
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
cityID, err := uuid.Parse(*input.CityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(cityID)
|
updateInput.CityID = &cityID
|
||||||
updateInput.CityID = &uid
|
|
||||||
}
|
}
|
||||||
if input.AddressID != nil {
|
if input.AddressID != nil {
|
||||||
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
addressID, err := uuid.Parse(*input.AddressID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||||
}
|
}
|
||||||
uid := uint(addressID)
|
updateInput.AddressID = &addressID
|
||||||
updateInput.AddressID = &uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
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.
|
// Work is the resolver for the work field.
|
||||||
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid work ID: %v", err)
|
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 err != nil {
|
||||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -1292,7 +1279,7 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
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")
|
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.
|
// Translation is the resolver for the translation field.
|
||||||
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1352,7 +1339,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
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")
|
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.
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// Book is the resolver for the book field.
|
||||||
func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// Author is the resolver for the author field.
|
||||||
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
|
||||||
var authors []*domain.Author
|
var authors []*domain.Author
|
||||||
var err error
|
var err error
|
||||||
var countryIDUint *uint
|
var countryIDUUID *uuid.UUID
|
||||||
|
|
||||||
if countryID != nil {
|
if countryID != nil {
|
||||||
parsedID, err := strconv.ParseUint(*countryID, 10, 32)
|
parsedID, err := uuid.Parse(*countryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
uid := uint(parsedID)
|
countryIDUUID = &parsedID
|
||||||
countryIDUint = &uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint)
|
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// User is the resolver for the user field.
|
||||||
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// UserProfile is the resolver for the userProfile field.
|
||||||
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1703,12 +1689,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
|
|||||||
return nil, nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if user == nil {
|
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{
|
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.
|
// Collection is the resolver for the collection field.
|
||||||
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1751,7 +1737,7 @@ func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Colle
|
|||||||
return nil, nil
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1784,11 +1770,11 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if userID != nil {
|
if userID != nil {
|
||||||
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
uID, idErr := uuid.Parse(*userID)
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
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 {
|
} else {
|
||||||
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
|
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.
|
// Tag is the resolver for the tag field.
|
||||||
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.Tag{
|
return &model.Tag{
|
||||||
ID: fmt.Sprintf("%d", tag.ID),
|
ID: tag.ID.String(),
|
||||||
Name: tag.Name,
|
Name: tag.Name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1865,12 +1851,12 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) (
|
|||||||
|
|
||||||
// Category is the resolver for the category field.
|
// Category is the resolver for the category field.
|
||||||
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid category ID: %v", err)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1879,7 +1865,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.Category{
|
return &model.Category{
|
||||||
ID: fmt.Sprintf("%d", category.ID),
|
ID: category.ID.String(),
|
||||||
Name: category.Name,
|
Name: category.Name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1904,12 +1890,12 @@ func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *in
|
|||||||
|
|
||||||
// Comment is the resolver for the comment field.
|
// Comment is the resolver for the comment field.
|
||||||
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1918,10 +1904,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.Comment{
|
return &model.Comment{
|
||||||
ID: fmt.Sprintf("%d", commentRecord.ID),
|
ID: commentRecord.ID.String(),
|
||||||
Text: commentRecord.Text,
|
Text: commentRecord.Text,
|
||||||
User: &model.User{
|
User: &model.User{
|
||||||
ID: fmt.Sprintf("%d", commentRecord.UserID),
|
ID: commentRecord.UserID.String(),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1932,23 +1918,23 @@ func (r *queryResolver) Comments(ctx context.Context, workID *string, translatio
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if workID != nil {
|
if workID != nil {
|
||||||
wID, idErr := strconv.ParseUint(*workID, 10, 32)
|
wID, idErr := uuid.Parse(*workID)
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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 {
|
} else if translationID != nil {
|
||||||
tID, idErr := strconv.ParseUint(*translationID, 10, 32)
|
tID, idErr := uuid.Parse(*translationID)
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation)
|
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 {
|
} else if userID != nil {
|
||||||
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
uID, idErr := uuid.Parse(*userID)
|
||||||
if idErr != nil {
|
if idErr != nil {
|
||||||
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
|
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 {
|
} else {
|
||||||
commentRecords, err = r.App.Comment.Queries.Comments(ctx)
|
commentRecords, err = r.App.Comment.Queries.Comments(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,13 @@ package graphql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"tercul/internal/adapters/graphql/model"
|
||||||
"tercul/internal/app"
|
"tercul/internal/app"
|
||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/app/user"
|
"tercul/internal/app/user"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/adapters/graphql/model"
|
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -20,7 +20,7 @@ import (
|
|||||||
type mockUserRepositoryForUserResolver struct{ mock.Mock }
|
type mockUserRepositoryForUserResolver struct{ mock.Mock }
|
||||||
|
|
||||||
// Implement domain.UserRepository
|
// 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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
@ -106,7 +106,7 @@ func (m *mockUserRepositoryForUserResolver) WithTx(ctx context.Context, fn func(
|
|||||||
type mockUserProfileRepository struct{ mock.Mock }
|
type mockUserProfileRepository struct{ mock.Mock }
|
||||||
|
|
||||||
// Implement domain.UserProfileRepository
|
// 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)
|
args := m.Called(ctx, userID)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
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 {
|
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||||
return nil
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
|
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
|
||||||
|
|||||||
@ -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 {
|
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||||
return m.Create(ctx, entity)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package graphql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/adapters/graphql/model"
|
"tercul/internal/adapters/graphql/model"
|
||||||
"tercul/internal/app"
|
"tercul/internal/app"
|
||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
@ -11,6 +10,7 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
domainsearch "tercul/internal/domain/search"
|
domainsearch "tercul/internal/domain/search"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@ -26,7 +26,7 @@ func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) erro
|
|||||||
work.ID = 1
|
work.ID = 1
|
||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
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)
|
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) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
||||||
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { return nil, nil }
|
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) 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) {
|
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
args := m.Called(ctx, id)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
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)
|
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) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { return nil, nil }
|
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) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
||||||
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
return nil, 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) 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) 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) 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) 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) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { return nil, nil }
|
return 0, nil
|
||||||
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { return nil, 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) 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) 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) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockAuthorRepository struct{ mock.Mock }
|
type mockAuthorRepository struct{ mock.Mock }
|
||||||
|
|
||||||
@ -96,57 +127,111 @@ func (m *mockAuthorRepository) Create(ctx context.Context, author *domain.Author
|
|||||||
author.ID = 1
|
author.ID = 1
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { return nil, nil }
|
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) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
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) 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) 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) 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) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { return nil, nil }
|
return nil
|
||||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { return nil, 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) 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) 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) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { return nil, nil }
|
return 0, nil
|
||||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { return nil, 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) 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) 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) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockUserRepository struct{ mock.Mock }
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
}
|
}
|
||||||
return args.Get(0).(*domain.User), 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) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil }
|
return nil, nil
|
||||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]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) 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) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||||
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) { return nil, nil }
|
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) 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) 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) 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) 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) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) { return nil, nil }
|
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) 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) 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) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) { return nil, nil }
|
return 0, nil
|
||||||
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) { return nil, 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) 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 }
|
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockSearchClient struct{ mock.Mock }
|
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)
|
return args.Get(0).(*domainsearch.SearchResults), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type mockAnalyticsService struct{ mock.Mock }
|
type mockAnalyticsService struct{ mock.Mock }
|
||||||
|
|
||||||
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
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)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
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) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil }
|
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) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
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) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil }
|
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) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return nil, nil }
|
return 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) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error { return nil }
|
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) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return nil }
|
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) 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) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { return nil }
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type mockTranslationRepository struct{ mock.Mock }
|
type mockTranslationRepository struct{ mock.Mock }
|
||||||
|
|
||||||
@ -201,30 +321,69 @@ func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *dom
|
|||||||
args := m.Called(ctx, translation)
|
args := m.Called(ctx, translation)
|
||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil }
|
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
||||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil }
|
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) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||||
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { return nil, nil }
|
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) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||||
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil }
|
return nil, 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) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||||
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil }
|
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) 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) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||||
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
return 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) 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) 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) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||||
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return nil, nil }
|
return 0, 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) 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) 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.
|
// WorkResolversUnitSuite is a unit test suite for the work resolvers.
|
||||||
type WorkResolversUnitSuite struct {
|
type WorkResolversUnitSuite struct {
|
||||||
|
|||||||
@ -4,17 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnalyticsRepository defines the data access layer for analytics.
|
// AnalyticsRepository defines the data access layer for analytics.
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
|
IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error
|
||||||
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
|
IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error
|
||||||
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
|
||||||
UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error
|
UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
|
||||||
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error)
|
GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error)
|
||||||
UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error
|
UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error
|
||||||
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error
|
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error
|
||||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, 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"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
IncrementWorkViews(ctx context.Context, workID uint) error
|
IncrementWorkViews(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementWorkLikes(ctx context.Context, workID uint) error
|
IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementWorkComments(ctx context.Context, workID uint) error
|
IncrementWorkComments(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementWorkBookmarks(ctx context.Context, workID uint) error
|
IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementWorkShares(ctx context.Context, workID uint) error
|
IncrementWorkShares(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementWorkTranslationCount(ctx context.Context, workID uint) error
|
IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error
|
||||||
IncrementTranslationViews(ctx context.Context, translationID uint) error
|
IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error
|
||||||
IncrementTranslationLikes(ctx context.Context, translationID uint) error
|
IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
||||||
DecrementWorkLikes(ctx context.Context, workID uint) error
|
DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
||||||
DecrementTranslationLikes(ctx context.Context, translationID uint) error
|
DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
||||||
IncrementTranslationComments(ctx context.Context, translationID uint) error
|
IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error
|
||||||
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
|
||||||
|
|
||||||
UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error
|
||||||
UpdateWorkComplexity(ctx context.Context, workID uint) error
|
UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error
|
||||||
UpdateWorkSentiment(ctx context.Context, workID uint) error
|
UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error
|
||||||
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error
|
UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error
|
||||||
UpdateTranslationSentiment(ctx context.Context, translationID uint) 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
|
UpdateTrending(ctx context.Context) error
|
||||||
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, 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 {
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkViews")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkLikes")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "DecrementWorkLikes")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "likes", -1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkComments")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkBookmarks")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkShares")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementWorkTranslationCount")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationViews")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationLikes")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "DecrementTranslationLikes")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", -1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationComments")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "IncrementTranslationShares")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
|
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")
|
ctx, span := s.tracer.Start(ctx, "GetOrCreateWorkStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.GetOrCreateWorkStats(ctx, workID)
|
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")
|
ctx, span := s.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkReadingTime")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkComplexity")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
logger := log.FromContext(ctx).With("workID", workID)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkSentiment")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
logger := log.FromContext(ctx).With("workID", workID)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateTranslationReadingTime")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateTranslationSentiment")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateUserEngagement")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
today := time.Now().UTC().Truncate(24 * time.Hour)
|
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)
|
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")
|
ctx, span := s.tracer.Start(ctx, "UpdateWorkStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return s.repo.UpdateWorkStats(ctx, workID, stats)
|
return s.repo.UpdateWorkStats(ctx, workID, stats)
|
||||||
|
|||||||
@ -3,13 +3,13 @@ package analytics_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
|
||||||
"tercul/internal/app/analytics"
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/jobs/linguistics"
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
@ -178,7 +179,7 @@ func (c *AuthCommands) ResendVerificationEmail(ctx context.Context, email string
|
|||||||
|
|
||||||
// ChangePasswordInput represents the input for changing a password.
|
// ChangePasswordInput represents the input for changing a password.
|
||||||
type ChangePasswordInput struct {
|
type ChangePasswordInput struct {
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
CurrentPassword string
|
CurrentPassword string
|
||||||
NewPassword string
|
NewPassword string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type AuthCommandsSuite struct {
|
type AuthCommandsSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
userRepo *mockUserRepository
|
userRepo *mockUserRepository
|
||||||
|
|||||||
@ -71,7 +71,7 @@ func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) erro
|
|||||||
return errors.New("user not found")
|
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 {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package author
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorCommands contains the command handlers for the author aggregate.
|
// 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.
|
// UpdateAuthorInput represents the input for updating an existing author.
|
||||||
type UpdateAuthorInput struct {
|
type UpdateAuthorInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +55,6 @@ func (c *AuthorCommands) UpdateAuthor(ctx context.Context, input UpdateAuthorInp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAuthor deletes an author by ID.
|
// 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)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package author
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorQueries contains the query handlers for the author aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authors returns all authors, with optional filtering by country.
|
// 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 authors []domain.Author
|
||||||
var err error
|
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.
|
// 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)
|
return q.repo.GetWithTranslations(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service provides authorization checks for the application.
|
// 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.
|
// 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.
|
// 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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
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.
|
// 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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
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.
|
// 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 {
|
switch translatableType {
|
||||||
case "works":
|
case "works":
|
||||||
// For works, we can reuse the CanEditWork logic.
|
// 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.
|
// 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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
return false, domain.ErrUnauthorized
|
||||||
@ -157,7 +159,7 @@ func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
|
|||||||
return true, nil
|
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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
return false, domain.ErrUnauthorized
|
||||||
@ -211,7 +213,7 @@ func (s *Service) CanDeleteBook(ctx context.Context) (bool, error) {
|
|||||||
return false, domain.ErrForbidden
|
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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
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.
|
// 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.
|
// 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)
|
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, domain.ErrUnauthorized
|
return false, domain.ErrUnauthorized
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BookCommands contains the command handlers for the book aggregate.
|
// BookCommands contains the command handlers for the book aggregate.
|
||||||
@ -26,7 +28,7 @@ type CreateBookInput struct {
|
|||||||
Description string
|
Description string
|
||||||
Language string
|
Language string
|
||||||
ISBN *string
|
ISBN *string
|
||||||
AuthorIDs []uint
|
AuthorIDs []uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBook creates a new book.
|
// 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.
|
// UpdateBookInput represents the input for updating an existing book.
|
||||||
type UpdateBookInput struct {
|
type UpdateBookInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Title *string
|
Title *string
|
||||||
Description *string
|
Description *string
|
||||||
Language *string
|
Language *string
|
||||||
ISBN *string
|
ISBN *string
|
||||||
AuthorIDs []uint
|
AuthorIDs []uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBook updates an existing book.
|
// UpdateBook updates an existing book.
|
||||||
@ -106,7 +108,7 @@ func (c *BookCommands) UpdateBook(ctx context.Context, input UpdateBookInput) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBook deletes a book by ID.
|
// 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)
|
can, err := c.authzSvc.CanDeleteBook(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package book
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BookQueries contains the query handlers for the book aggregate.
|
// BookQueries contains the query handlers for the book aggregate.
|
||||||
@ -16,7 +18,7 @@ func NewBookQueries(repo domain.BookRepository) *BookQueries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Book retrieves a book by its ID.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"tercul/internal/app/analytics"
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BookmarkCommands contains the command handlers for the bookmark aggregate.
|
// 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.
|
// CreateBookmarkInput represents the input for creating a new bookmark.
|
||||||
type CreateBookmarkInput struct {
|
type CreateBookmarkInput struct {
|
||||||
Name string
|
Name string
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
WorkID uint
|
WorkID uuid.UUID
|
||||||
Notes string
|
Notes string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ func (c *BookmarkCommands) CreateBookmark(ctx context.Context, input CreateBookm
|
|||||||
|
|
||||||
// UpdateBookmarkInput represents the input for updating an existing bookmark.
|
// UpdateBookmarkInput represents the input for updating an existing bookmark.
|
||||||
type UpdateBookmarkInput struct {
|
type UpdateBookmarkInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Notes string
|
Notes string
|
||||||
}
|
}
|
||||||
@ -76,6 +78,6 @@ func (c *BookmarkCommands) UpdateBookmark(ctx context.Context, input UpdateBookm
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBookmark deletes a bookmark by ID.
|
// 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)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package bookmark
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BookmarkQueries contains the query handlers for the bookmark aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookmarksByUserID returns all bookmarks for a user.
|
// 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)
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookmarksByWorkID returns all bookmarks for a work.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package category
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CategoryCommands contains the command handlers for the category aggregate.
|
// CategoryCommands contains the command handlers for the category aggregate.
|
||||||
@ -19,7 +21,7 @@ func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
|
|||||||
type CreateCategoryInput struct {
|
type CreateCategoryInput struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
ParentID *uint
|
ParentID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCategory creates a new category.
|
// 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.
|
// UpdateCategoryInput represents the input for updating an existing category.
|
||||||
type UpdateCategoryInput struct {
|
type UpdateCategoryInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
ParentID *uint
|
ParentID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCategory updates an existing category.
|
// UpdateCategory updates an existing category.
|
||||||
@ -61,6 +63,6 @@ func (c *CategoryCommands) UpdateCategory(ctx context.Context, input UpdateCateg
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCategory deletes a category by ID.
|
// 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)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package category
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CategoryQueries contains the query handlers for the category aggregate.
|
// 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.
|
// 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)
|
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.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CategoriesByParentID returns all categories for a parent.
|
// 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)
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CollectionCommands contains the command handlers for the collection aggregate.
|
// CollectionCommands contains the command handlers for the collection aggregate.
|
||||||
@ -20,7 +22,7 @@ func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands
|
|||||||
type CreateCollectionInput struct {
|
type CreateCollectionInput struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
CoverImageURL string
|
CoverImageURL string
|
||||||
}
|
}
|
||||||
@ -43,12 +45,12 @@ func (c *CollectionCommands) CreateCollection(ctx context.Context, input CreateC
|
|||||||
|
|
||||||
// UpdateCollectionInput represents the input for updating an existing collection.
|
// UpdateCollectionInput represents the input for updating an existing collection.
|
||||||
type UpdateCollectionInput struct {
|
type UpdateCollectionInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
CoverImageURL string
|
CoverImageURL string
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCollection updates an existing collection.
|
// UpdateCollection updates an existing collection.
|
||||||
@ -58,7 +60,7 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if collection.UserID != input.UserID {
|
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.Name = input.Name
|
||||||
collection.Description = input.Description
|
collection.Description = input.Description
|
||||||
@ -72,22 +74,22 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCollection deletes a collection by ID.
|
// 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)
|
collection, err := c.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if collection.UserID != userID {
|
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)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWorkToCollectionInput represents the input for adding a work to a collection.
|
// AddWorkToCollectionInput represents the input for adding a work to a collection.
|
||||||
type AddWorkToCollectionInput struct {
|
type AddWorkToCollectionInput struct {
|
||||||
CollectionID uint
|
CollectionID uuid.UUID
|
||||||
WorkID uint
|
WorkID uuid.UUID
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWorkToCollection adds a work to a collection.
|
// AddWorkToCollection adds a work to a collection.
|
||||||
@ -97,16 +99,16 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if collection.UserID != input.UserID {
|
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)
|
return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollectionInput represents the input for removing a work from a collection.
|
// RemoveWorkFromCollectionInput represents the input for removing a work from a collection.
|
||||||
type RemoveWorkFromCollectionInput struct {
|
type RemoveWorkFromCollectionInput struct {
|
||||||
CollectionID uint
|
CollectionID uuid.UUID
|
||||||
WorkID uint
|
WorkID uuid.UUID
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollection removes a work from a collection.
|
// RemoveWorkFromCollection removes a work from a collection.
|
||||||
@ -116,7 +118,7 @@ func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if collection.UserID != input.UserID {
|
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)
|
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package collection
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CollectionQueries contains the query handlers for the collection aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CollectionsByUserID returns all collections for a user.
|
// 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)
|
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.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentCommands contains the command handlers for the comment aggregate.
|
// 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.
|
// CreateCommentInput represents the input for creating a new comment.
|
||||||
type CreateCommentInput struct {
|
type CreateCommentInput struct {
|
||||||
Text string
|
Text string
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
WorkID *uint
|
WorkID *uuid.UUID
|
||||||
TranslationID *uint
|
TranslationID *uuid.UUID
|
||||||
ParentID *uint
|
ParentID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment creates a new comment.
|
// 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.
|
// UpdateCommentInput represents the input for updating an existing comment.
|
||||||
type UpdateCommentInput struct {
|
type UpdateCommentInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
|
|||||||
comment, err := c.repo.GetByID(ctx, input.ID)
|
comment, err := c.repo.GetByID(ctx, input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
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
|
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.
|
// 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)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.ErrUnauthorized
|
return domain.ErrUnauthorized
|
||||||
@ -117,7 +119,7 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
|||||||
comment, err := c.repo.GetByID(ctx, id)
|
comment, err := c.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package comment
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentQueries contains the query handlers for the comment aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentsByUserID returns all comments for a user.
|
// 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)
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentsByWorkID returns all comments for a work.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentsByTranslationID returns all comments for a translation.
|
// 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)
|
return q.repo.ListByTranslationID(ctx, translationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentsByParentID returns all comments for a parent.
|
// 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)
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Commands contains the command handlers for the contribution aggregate.
|
// 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 {
|
type CreateContributionInput struct {
|
||||||
Name string
|
Name string
|
||||||
Status string
|
Status string
|
||||||
WorkID *uint
|
WorkID *uuid.UUID
|
||||||
TranslationID *uint
|
TranslationID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateContribution creates a new contribution.
|
// 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.
|
// UpdateContributionInput represents the input for updating a contribution.
|
||||||
type UpdateContributionInput struct {
|
type UpdateContributionInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
Name *string
|
Name *string
|
||||||
Status *string
|
Status *string
|
||||||
}
|
}
|
||||||
@ -89,7 +91,7 @@ func (c *Commands) UpdateContribution(ctx context.Context, input UpdateContribut
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteContribution deletes a contribution.
|
// 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)
|
contribution, err := c.repo.GetByID(ctx, contributionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -105,7 +107,7 @@ func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint,
|
|||||||
|
|
||||||
// ReviewContributionInput represents the input for reviewing a contribution.
|
// ReviewContributionInput represents the input for reviewing a contribution.
|
||||||
type ReviewContributionInput struct {
|
type ReviewContributionInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Status string
|
Status string
|
||||||
Feedback *string
|
Feedback *string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyrightCommands contains the command handlers for copyright.
|
// CopyrightCommands contains the command handlers for copyright.
|
||||||
@ -37,8 +39,8 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
|
|||||||
if copyright == nil {
|
if copyright == nil {
|
||||||
return errors.New("copyright cannot be nil")
|
return errors.New("copyright cannot be nil")
|
||||||
}
|
}
|
||||||
if copyright.ID == 0 {
|
if copyright.ID == uuid.Nil {
|
||||||
return errors.New("copyright ID cannot be zero")
|
return errors.New("copyright ID cannot be nil")
|
||||||
}
|
}
|
||||||
if copyright.Name == "" {
|
if copyright.Name == "" {
|
||||||
return errors.New("copyright name cannot be empty")
|
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.
|
// DeleteCopyright deletes a copyright.
|
||||||
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error {
|
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uuid.UUID) error {
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return errors.New("invalid copyright ID")
|
return errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.FromContext(ctx).With("id", id).Debug("Deleting copyright")
|
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.
|
// AddCopyrightToWork adds a copyright to a work.
|
||||||
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if workID == 0 || copyrightID == 0 {
|
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid work ID or copyright ID")
|
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")
|
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.
|
// RemoveCopyrightFromWork removes a copyright from a work.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if workID == 0 || copyrightID == 0 {
|
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid work ID or copyright ID")
|
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")
|
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.
|
// AddCopyrightToAuthor adds a copyright to an author.
|
||||||
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if authorID == 0 || copyrightID == 0 {
|
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid author ID or copyright ID")
|
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")
|
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.
|
// RemoveCopyrightFromAuthor removes a copyright from an author.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if authorID == 0 || copyrightID == 0 {
|
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid author ID or copyright ID")
|
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")
|
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.
|
// AddCopyrightToBook adds a copyright to a book.
|
||||||
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if bookID == 0 || copyrightID == 0 {
|
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid book ID or copyright ID")
|
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")
|
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.
|
// RemoveCopyrightFromBook removes a copyright from a book.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if bookID == 0 || copyrightID == 0 {
|
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid book ID or copyright ID")
|
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")
|
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.
|
// AddCopyrightToPublisher adds a copyright to a publisher.
|
||||||
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if publisherID == 0 || copyrightID == 0 {
|
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid publisher ID or copyright ID")
|
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")
|
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.
|
// RemoveCopyrightFromPublisher removes a copyright from a publisher.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if publisherID == 0 || copyrightID == 0 {
|
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid publisher ID or copyright ID")
|
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")
|
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.
|
// AddCopyrightToSource adds a copyright to a source.
|
||||||
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if sourceID == 0 || copyrightID == 0 {
|
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid source ID or copyright ID")
|
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")
|
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.
|
// RemoveCopyrightFromSource removes a copyright from a source.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
|
||||||
if sourceID == 0 || copyrightID == 0 {
|
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
||||||
return errors.New("invalid source ID or copyright ID")
|
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")
|
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 {
|
if translation == nil {
|
||||||
return errors.New("translation cannot be nil")
|
return errors.New("translation cannot be nil")
|
||||||
}
|
}
|
||||||
if translation.CopyrightID == 0 {
|
if translation.CopyrightID == uuid.Nil {
|
||||||
return errors.New("copyright ID cannot be zero")
|
return errors.New("copyright ID cannot be nil")
|
||||||
}
|
}
|
||||||
if translation.LanguageCode == "" {
|
if translation.LanguageCode == "" {
|
||||||
return errors.New("language code cannot be empty")
|
return errors.New("language code cannot be empty")
|
||||||
|
|||||||
@ -4,10 +4,10 @@ package copyright_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/app/copyright"
|
"tercul/internal/app/copyright"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -111,7 +111,7 @@ func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translatio
|
|||||||
}
|
}
|
||||||
return nil
|
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 {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
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) {
|
func (m *mockCopyrightRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Copyright, error) {
|
||||||
return nil, nil
|
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) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||||
func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||||
return nil
|
return nil
|
||||||
@ -182,40 +184,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockAuthorRepository struct {
|
type mockAuthorRepository struct {
|
||||||
domain.AuthorRepository
|
domain.AuthorRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
|
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) {
|
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockBookRepository struct {
|
type mockBookRepository struct {
|
||||||
domain.BookRepository
|
domain.BookRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
|
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) {
|
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockPublisherRepository struct {
|
type mockPublisherRepository struct {
|
||||||
domain.PublisherRepository
|
domain.PublisherRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
|
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) {
|
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockSourceRepository struct {
|
type mockSourceRepository struct {
|
||||||
domain.SourceRepository
|
domain.SourceRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
|
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) {
|
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CopyrightQueries contains the query handlers for copyright.
|
// 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.
|
// GetCopyrightByID retrieves a copyright by ID.
|
||||||
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.FromContext(ctx).With("id", id).Debug("Getting copyright by 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.
|
// 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")
|
log.FromContext(ctx).With("work_id", workID).Debug("Getting copyrights for work")
|
||||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -50,7 +52,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
// 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")
|
log.FromContext(ctx).With("author_id", authorID).Debug("Getting copyrights for author")
|
||||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,7 +62,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForBook gets all copyrights for a specific book.
|
// 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")
|
log.FromContext(ctx).With("book_id", bookID).Debug("Getting copyrights for book")
|
||||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,7 +72,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
// 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")
|
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting copyrights for publisher")
|
||||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -80,7 +82,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForSource gets all copyrights for a specific source.
|
// 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")
|
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting copyrights for source")
|
||||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,8 +92,8 @@ func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTranslations gets all translations for a copyright.
|
// GetTranslations gets all translations for a copyright.
|
||||||
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uuid.UUID) ([]domain.CopyrightTranslation, error) {
|
||||||
if copyrightID == 0 {
|
if copyrightID == uuid.Nil {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
log.FromContext(ctx).With("copyright_id", copyrightID).Debug("Getting translations for copyright")
|
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.
|
// GetTranslationByLanguage gets a specific translation by language code.
|
||||||
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uuid.UUID, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
if copyrightID == 0 {
|
if copyrightID == uuid.Nil {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
if languageCode == "" {
|
if languageCode == "" {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"tercul/internal/app/analytics"
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LikeCommands contains the command handlers for the like aggregate.
|
// 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.
|
// CreateLikeInput represents the input for creating a new like.
|
||||||
type CreateLikeInput struct {
|
type CreateLikeInput struct {
|
||||||
UserID uint
|
UserID uuid.UUID
|
||||||
WorkID *uint
|
WorkID *uuid.UUID
|
||||||
TranslationID *uint
|
TranslationID *uuid.UUID
|
||||||
CommentID *uint
|
CommentID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLike creates a new like and increments the relevant counter.
|
// 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.
|
// 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.
|
// First, get the like to determine what it was attached to.
|
||||||
like, err := c.repo.GetByID(ctx, id)
|
like, err := c.repo.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package like
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LikeQueries contains the query handlers for the like aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LikesByUserID returns all likes for a user.
|
// 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)
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LikesByWorkID returns all likes for a work.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LikesByTranslationID returns all likes for a translation.
|
// 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)
|
return q.repo.ListByTranslationID(ctx, translationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LikesByCommentID returns all likes for a comment.
|
// 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)
|
return q.repo.ListByCommentID(ctx, commentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package localization
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalizationQueries contains the query handlers for the localization aggregate.
|
// 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.
|
// 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)
|
return q.repo.GetAuthorBiography(ctx, authorID, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkContent returns the content of a work in a specific 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)
|
return q.repo.GetWorkContent(ctx, workID, language)
|
||||||
}
|
}
|
||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MonetizationCommands contains the command handlers for monetization.
|
// MonetizationCommands contains the command handlers for monetization.
|
||||||
@ -18,8 +20,8 @@ func NewMonetizationCommands(repo domain.MonetizationRepository) *MonetizationCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddMonetizationToWork adds a monetization to a work.
|
// AddMonetizationToWork adds a monetization to a work.
|
||||||
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if workID == 0 || monetizationID == 0 {
|
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid work ID or monetization ID")
|
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")
|
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.
|
// RemoveMonetizationFromWork removes a monetization from a work.
|
||||||
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if workID == 0 || monetizationID == 0 {
|
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid work ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Removing monetization from work")
|
||||||
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if authorID == 0 || monetizationID == 0 {
|
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid author ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Adding monetization to author")
|
||||||
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if authorID == 0 || monetizationID == 0 {
|
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid author ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Removing monetization from author")
|
||||||
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if bookID == 0 || monetizationID == 0 {
|
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid book ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Adding monetization to book")
|
||||||
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if bookID == 0 || monetizationID == 0 {
|
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid book ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Removing monetization from book")
|
||||||
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if publisherID == 0 || monetizationID == 0 {
|
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid publisher ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Adding monetization to publisher")
|
||||||
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if publisherID == 0 || monetizationID == 0 {
|
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid publisher ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Removing monetization from publisher")
|
||||||
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if sourceID == 0 || monetizationID == 0 {
|
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid source ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Adding monetization to source")
|
||||||
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
|
||||||
if sourceID == 0 || monetizationID == 0 {
|
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
||||||
return errors.New("invalid source ID or monetization ID")
|
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")
|
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Removing monetization from source")
|
||||||
|
|||||||
@ -4,10 +4,10 @@ package monetization_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/app/monetization"
|
"tercul/internal/app/monetization"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ type mockMonetizationRepository struct {
|
|||||||
listAllFunc func(ctx context.Context) ([]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 {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
@ -107,40 +107,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockAuthorRepository struct {
|
type mockAuthorRepository struct {
|
||||||
domain.AuthorRepository
|
domain.AuthorRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
|
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) {
|
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockBookRepository struct {
|
type mockBookRepository struct {
|
||||||
domain.BookRepository
|
domain.BookRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
|
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) {
|
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockPublisherRepository struct {
|
type mockPublisherRepository struct {
|
||||||
domain.PublisherRepository
|
domain.PublisherRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
|
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) {
|
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockSourceRepository struct {
|
type mockSourceRepository struct {
|
||||||
domain.SourceRepository
|
domain.SourceRepository
|
||||||
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
|
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) {
|
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
|
||||||
if m.getByIDWithOptionsFunc != nil {
|
if m.getByIDWithOptionsFunc != nil {
|
||||||
return m.getByIDWithOptionsFunc(ctx, id, options)
|
return m.getByIDWithOptionsFunc(ctx, id, options)
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MonetizationQueries contains the query handlers for monetization.
|
// MonetizationQueries contains the query handlers for monetization.
|
||||||
@ -23,8 +25,8 @@ func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo domain.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMonetizationByID retrieves a monetization by ID.
|
// GetMonetizationByID retrieves a monetization by ID.
|
||||||
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return nil, errors.New("invalid monetization ID")
|
return nil, errors.New("invalid monetization ID")
|
||||||
}
|
}
|
||||||
log.FromContext(ctx).With("id", id).Debug("Getting monetization by 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)
|
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")
|
log.FromContext(ctx).With("work_id", workID).Debug("Getting monetizations for work")
|
||||||
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -46,7 +48,7 @@ func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workI
|
|||||||
return workRecord.Monetizations, nil
|
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")
|
log.FromContext(ctx).With("author_id", authorID).Debug("Getting monetizations for author")
|
||||||
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -55,7 +57,7 @@ func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, aut
|
|||||||
return author.Monetizations, nil
|
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")
|
log.FromContext(ctx).With("book_id", bookID).Debug("Getting monetizations for book")
|
||||||
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -64,7 +66,7 @@ func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookI
|
|||||||
return book.Monetizations, nil
|
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")
|
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting monetizations for publisher")
|
||||||
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,7 +75,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context,
|
|||||||
return publisher.Monetizations, nil
|
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")
|
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting monetizations for source")
|
||||||
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package tag
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagCommands contains the command handlers for the tag aggregate.
|
// 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.
|
// UpdateTagInput represents the input for updating an existing tag.
|
||||||
type UpdateTagInput struct {
|
type UpdateTagInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
@ -57,6 +59,6 @@ func (c *TagCommands) UpdateTag(ctx context.Context, input UpdateTagInput) (*dom
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTag deletes a tag by ID.
|
// 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)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package tag
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TagQueries contains the query handlers for the tag aggregate.
|
// 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.
|
// 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)
|
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.
|
// 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)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
@ -34,9 +35,9 @@ type CreateOrUpdateTranslationInput struct {
|
|||||||
Description string
|
Description string
|
||||||
Language string
|
Language string
|
||||||
Status domain.TranslationStatus
|
Status domain.TranslationStatus
|
||||||
TranslatableID uint
|
TranslatableID uuid.UUID
|
||||||
TranslatableType string
|
TranslatableType string
|
||||||
TranslatorID *uint
|
TranslatorID *uuid.UUID
|
||||||
IsOriginalLanguage bool
|
IsOriginalLanguage bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,8 +50,8 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
|||||||
if input.Language == "" {
|
if input.Language == "" {
|
||||||
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
if input.TranslatableID == 0 {
|
if input.TranslatableID == uuid.Nil {
|
||||||
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: translatable ID cannot be nil", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
if input.TranslatableType == "" {
|
if input.TranslatableType == "" {
|
||||||
return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation)
|
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
|
return nil, domain.ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
var translatorID uint
|
var translatorID uuid.UUID
|
||||||
if input.TranslatorID != nil {
|
if input.TranslatorID != nil {
|
||||||
translatorID = *input.TranslatorID
|
translatorID = *input.TranslatorID
|
||||||
} else {
|
} else {
|
||||||
@ -97,7 +98,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTranslation deletes a translation by ID.
|
// 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")
|
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
|
|||||||
args := m.Called(ctx, tx, entity)
|
args := m.Called(ctx, tx, entity)
|
||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
package translation
|
package translation
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
// TranslationDTO is a read model for a translation.
|
// TranslationDTO is a read model for a translation.
|
||||||
type TranslationDTO struct {
|
type TranslationDTO struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Title string
|
Title string
|
||||||
Language string
|
Language string
|
||||||
Content string
|
Content string
|
||||||
TranslatableID uint
|
TranslatableID uuid.UUID
|
||||||
}
|
}
|
||||||
@ -2,8 +2,8 @@ package translation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"tercul/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockTranslationRepository struct {
|
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)
|
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 {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
@ -23,7 +24,7 @@ func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQuerie
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Translation returns a translation by ID.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "Translation")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@ -45,21 +46,21 @@ func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*Transla
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByWorkID returns all translations for a work.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "TranslationsByWorkID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return q.repo.ListByWorkID(ctx, workID)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByEntity returns all translations for an entity.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "TranslationsByEntity")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return q.repo.ListByEntity(ctx, entityType, entityID)
|
return q.repo.ListByEntity(ctx, entityType, entityID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByTranslatorID returns all translations for a translator.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return q.repo.ListByTranslatorID(ctx, translatorID)
|
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.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "ListTranslations")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
"tercul/internal/app/authz"
|
"tercul/internal/app/authz"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
platform_auth "tercul/internal/platform/auth"
|
platform_auth "tercul/internal/platform/auth"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserCommands contains the command handlers for the user aggregate.
|
// 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.
|
// UpdateUserInput represents the input for updating an existing user.
|
||||||
type UpdateUserInput struct {
|
type UpdateUserInput struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Username *string
|
Username *string
|
||||||
Email *string
|
Email *string
|
||||||
Password *string
|
Password *string
|
||||||
@ -64,12 +66,13 @@ type UpdateUserInput struct {
|
|||||||
Role *domain.UserRole
|
Role *domain.UserRole
|
||||||
Verified *bool
|
Verified *bool
|
||||||
Active *bool
|
Active *bool
|
||||||
CountryID *uint
|
CountryID *uuid.UUID
|
||||||
CityID *uint
|
CityID *uuid.UUID
|
||||||
AddressID *uint
|
AddressID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser updates an existing user.
|
// UpdateUser updates an existing user.
|
||||||
|
//
|
||||||
//nolint:gocyclo // Complex update logic
|
//nolint:gocyclo // Complex update logic
|
||||||
func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) {
|
func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) {
|
||||||
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
|
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.
|
// 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)
|
actorID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.ErrUnauthorized
|
return domain.ErrUnauthorized
|
||||||
|
|||||||
@ -23,7 +23,7 @@ func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
|
|||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserQueries contains the query handlers for the user aggregate.
|
// 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.
|
// 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)
|
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.
|
// 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)
|
return q.profileRepo.GetByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ type mockUserProfileRepository struct {
|
|||||||
mock.Mock
|
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)
|
args := m.Called(ctx, userID)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
@ -36,7 +36,7 @@ func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB,
|
|||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import (
|
|||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WorkCommands contains the command handlers for the work aggregate.
|
// 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 {
|
if work == nil {
|
||||||
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
|
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
if work.ID == 0 {
|
if work.ID == uuid.Nil {
|
||||||
return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation)
|
return fmt.Errorf("%w: work ID cannot be nil", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
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.
|
// 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")
|
ctx, span := c.tracer.Start(ctx, "DeleteWork")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
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.
|
// 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")
|
ctx, span := c.tracer.Start(ctx, "AnalyzeWork")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
logger := log.FromContext(ctx).With("workID", workID)
|
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.
|
// MergeWork merges two works, moving all associations from the source to the target and deleting the source.
|
||||||
|
//
|
||||||
//nolint:gocyclo // Complex merge logic
|
//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")
|
ctx, span := c.tracer.Start(ctx, "MergeWork")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if sourceID == targetID {
|
if sourceID == targetID {
|
||||||
@ -331,7 +334,7 @@ func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) e
|
|||||||
return nil
|
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
|
var sourceStats domain.WorkStats
|
||||||
err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
|
err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
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 errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// If target has no stats, create a new stats record for it.
|
// If target has no stats, create a new stats record for it.
|
||||||
newStats := sourceStats
|
newStats := sourceStats
|
||||||
newStats.ID = 0
|
newStats.ID = uuid.Nil
|
||||||
newStats.WorkID = targetWorkID
|
newStats.WorkID = targetWorkID
|
||||||
if err = tx.Create(&newStats).Error; err != nil {
|
if err = tx.Create(&newStats).Error; err != nil {
|
||||||
return fmt.Errorf("failed to create new target stats: %w", err)
|
return fmt.Errorf("failed to create new target stats: %w", err)
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package work
|
package work
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
// WorkDTO is a read model for a work, containing only the data needed for API responses.
|
// WorkDTO is a read model for a work, containing only the data needed for API responses.
|
||||||
type WorkDTO struct {
|
type WorkDTO struct {
|
||||||
ID uint
|
ID uuid.UUID
|
||||||
Title string
|
Title string
|
||||||
Language string
|
Language string
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
|
|||||||
args := m.Called(ctx, tx, entity)
|
args := m.Called(ctx, tx, entity)
|
||||||
return args.Error(0)
|
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)
|
args := m.Called(ctx, id)
|
||||||
if args.Get(0) == nil {
|
if args.Get(0) == nil {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WorkQueries contains the query handlers for the work aggregate.
|
// 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.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "GetWorkByID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return nil, errors.New("invalid work ID")
|
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.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "GetWorkWithTranslations")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return nil, errors.New("invalid work ID")
|
return nil, errors.New("invalid work ID")
|
||||||
}
|
}
|
||||||
return q.repo.GetWithTranslations(ctx, 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.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "FindWorksByAuthor")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if authorID == 0 {
|
if authorID == uuid.Nil {
|
||||||
return nil, errors.New("invalid author ID")
|
return nil, errors.New("invalid author ID")
|
||||||
}
|
}
|
||||||
return q.repo.FindByAuthor(ctx, authorID)
|
return q.repo.FindByAuthor(ctx, authorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindWorksByCategory finds works by category ID.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "FindWorksByCategory")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if categoryID == 0 {
|
if categoryID == uuid.Nil {
|
||||||
return nil, errors.New("invalid category ID")
|
return nil, errors.New("invalid category ID")
|
||||||
}
|
}
|
||||||
return q.repo.FindByCategory(ctx, categoryID)
|
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.
|
// 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")
|
ctx, span := q.tracer.Start(ctx, "ListByCollectionID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if collectionID == 0 {
|
if collectionID == uuid.Nil {
|
||||||
return nil, errors.New("invalid collection ID")
|
return nil, errors.New("invalid collection ID")
|
||||||
}
|
}
|
||||||
return q.repo.ListByCollectionID(ctx, collectionID)
|
return q.repo.ListByCollectionID(ctx, collectionID)
|
||||||
|
|||||||
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"
|
"tercul/internal/domain"
|
||||||
platform_cache "tercul/internal/platform/cache"
|
platform_cache "tercul/internal/platform/cache"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ func (r *CachedAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, en
|
|||||||
return err
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Author
|
var cached domain.Author
|
||||||
key := r.opt.Keys.EntityKey("author", id)
|
key := r.opt.Keys.EntityKey("author", id)
|
||||||
@ -71,7 +72,7 @@ func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.
|
|||||||
return author, nil
|
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)
|
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +98,7 @@ func (r *CachedAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, en
|
|||||||
return err
|
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)
|
err := r.inner.Delete(ctx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != nil {
|
if r.opt.Cache != nil {
|
||||||
@ -108,7 +109,7 @@ func (r *CachedAuthorRepository) Delete(ctx context.Context, id uint) error {
|
|||||||
return err
|
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)
|
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != nil {
|
if r.opt.Cache != nil {
|
||||||
@ -154,7 +155,7 @@ func (r *CachedAuthorRepository) CountWithOptions(ctx context.Context, options *
|
|||||||
return r.inner.CountWithOptions(ctx, 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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Author
|
var cached domain.Author
|
||||||
key := r.opt.Keys.QueryKey("author", "withTranslations", id)
|
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"
|
"tercul/internal/domain"
|
||||||
platform_cache "tercul/internal/platform/cache"
|
platform_cache "tercul/internal/platform/cache"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ func (r *CachedTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.D
|
|||||||
return err
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Translation
|
var cached domain.Translation
|
||||||
key := r.opt.Keys.EntityKey("translation", id)
|
key := r.opt.Keys.EntityKey("translation", id)
|
||||||
@ -71,7 +72,7 @@ func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uint) (*do
|
|||||||
return tr, nil
|
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)
|
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +98,7 @@ func (r *CachedTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.D
|
|||||||
return err
|
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)
|
err := r.inner.Delete(ctx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != nil {
|
if r.opt.Cache != nil {
|
||||||
@ -108,7 +109,7 @@ func (r *CachedTranslationRepository) Delete(ctx context.Context, id uint) error
|
|||||||
return err
|
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)
|
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != nil {
|
if r.opt.Cache != nil {
|
||||||
@ -154,7 +155,7 @@ func (r *CachedTranslationRepository) CountWithOptions(ctx context.Context, opti
|
|||||||
return r.inner.CountWithOptions(ctx, options)
|
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)
|
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)
|
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)
|
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)
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached []domain.Translation
|
var cached []domain.Translation
|
||||||
key := r.opt.Keys.QueryKey("translation", "byWork", workID)
|
key := r.opt.Keys.QueryKey("translation", "byWork", workID)
|
||||||
@ -193,7 +194,7 @@ func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID u
|
|||||||
return res, nil
|
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 := ""
|
lang := ""
|
||||||
if language != nil {
|
if language != nil {
|
||||||
lang = *language
|
lang = *language
|
||||||
@ -217,11 +218,11 @@ func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context,
|
|||||||
return res, nil
|
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)
|
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)
|
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"
|
"tercul/internal/domain"
|
||||||
platform_cache "tercul/internal/platform/cache"
|
platform_cache "tercul/internal/platform/cache"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ func (r *CachedWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
|
|||||||
return err
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Work
|
var cached domain.Work
|
||||||
key := r.opt.Keys.EntityKey("work", id)
|
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
|
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.
|
// Options can include varying preloads/where/order; avoid caching to prevent incorrect results.
|
||||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
return r.inner.GetByIDWithOptions(ctx, id, options)
|
||||||
}
|
}
|
||||||
@ -99,7 +100,7 @@ func (r *CachedWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, enti
|
|||||||
return err
|
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)
|
err := r.inner.Delete(ctx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != nil {
|
if r.opt.Cache != nil {
|
||||||
@ -110,7 +111,7 @@ func (r *CachedWorkRepository) Delete(ctx context.Context, id uint) error {
|
|||||||
return err
|
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)
|
err := r.inner.DeleteInTx(ctx, tx, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.opt.Cache != 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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
return r.inner.FindByCategory(ctx, categoryID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +208,7 @@ func (r *CachedWorkRepository) FindByLanguage(ctx context.Context, language stri
|
|||||||
return res, nil
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Work
|
var cached domain.Work
|
||||||
key := r.opt.Keys.QueryKey("work", "withTranslations", id)
|
key := r.opt.Keys.QueryKey("work", "withTranslations", id)
|
||||||
@ -226,7 +227,7 @@ func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uint)
|
|||||||
return work, nil
|
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 {
|
if r.opt.Enabled && r.opt.Cache != nil {
|
||||||
var cached domain.Work
|
var cached domain.Work
|
||||||
key := r.opt.Keys.QueryKey("work", "withAssociations", id)
|
key := r.opt.Keys.QueryKey("work", "withAssociations", id)
|
||||||
@ -245,7 +246,7 @@ func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uint)
|
|||||||
return work, nil
|
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.
|
// Tx-scoped reads should bypass cache.
|
||||||
return r.inner.GetWithAssociationsInTx(ctx, tx, id)
|
return r.inner.GetWithAssociationsInTx(ctx, tx, id)
|
||||||
}
|
}
|
||||||
@ -269,10 +270,10 @@ func (r *CachedWorkRepository) ListWithTranslations(ctx context.Context, page, p
|
|||||||
return res, nil
|
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)
|
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)
|
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)
|
||||||
|
}
|
||||||
78
internal/data/cache/redis_integration_test.go
vendored
Normal file
78
internal/data/cache/redis_integration_test.go
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tercul/internal/domain"
|
||||||
|
platform_cache "tercul/internal/platform/cache"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
"github.com/testcontainers/testcontainers-go/wait"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRedisIntegration_CachedWorkRepository(t *testing.T) {
|
||||||
|
if os.Getenv("INTEGRATION_TESTS") != "true" {
|
||||||
|
t.Skip("skipping integration test; set INTEGRATION_TESTS=true to run")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
req := testcontainers.ContainerRequest{
|
||||||
|
Image: "redis:7.0",
|
||||||
|
ExposedPorts: []string{"6379/tcp"},
|
||||||
|
WaitingFor: wait.ForListeningPort("6379/tcp").WithStartupTimeout(30 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer redisC.Terminate(ctx)
|
||||||
|
|
||||||
|
host, err := redisC.Host(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
port, err := redisC.MappedPort(ctx, "6379")
|
||||||
|
require.NoError(t, err)
|
||||||
|
addr := fmt.Sprintf("%s:%s", host, port.Port())
|
||||||
|
|
||||||
|
rcClient := redis.NewClient(&redis.Options{Addr: addr})
|
||||||
|
defer rcClient.Close()
|
||||||
|
|
||||||
|
require.NoError(t, rcClient.Ping(ctx).Err())
|
||||||
|
|
||||||
|
// create platform redis cache using key prefix that caches expect
|
||||||
|
keyGen := platform_cache.NewDefaultKeyGenerator("tercul:repo:")
|
||||||
|
rc := platform_cache.NewRedisCache(rcClient, keyGen, 0)
|
||||||
|
|
||||||
|
// create dummy repo and wrap
|
||||||
|
d := newDummyWorkRepo()
|
||||||
|
d.store[1] = &domain.Work{Title: "intwork", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}, Language: "en"}}
|
||||||
|
|
||||||
|
cw := NewCachedWorkRepository(d, rc, nil)
|
||||||
|
|
||||||
|
// call and assert Redis stored entity
|
||||||
|
w, err := cw.GetByID(ctx, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "intwork", w.Title)
|
||||||
|
|
||||||
|
key := keyGen.EntityKey("work", 1)
|
||||||
|
val, err := rcClient.Get(ctx, key).Bytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
var stored domain.Work
|
||||||
|
require.NoError(t, json.Unmarshal(val, &stored))
|
||||||
|
require.Equal(t, "intwork", stored.Title)
|
||||||
|
|
||||||
|
// list caching test
|
||||||
|
d.store[2] = &domain.Work{Title: "intwork2", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 2}, Language: "en"}}
|
||||||
|
|
||||||
|
// call list
|
||||||
|
_, err = cw.List(ctx, 1, 10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
listKey := keyGen.ListKey("work", 1, 10)
|
||||||
|
_, err = rcClient.Get(ctx, listKey).Bytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@ -1,96 +1,99 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
CREATE TABLE "countries" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"code" text NOT NULL,"phone_code" text,"currency" text,"continent" text);
|
-- Enable UUID extension
|
||||||
CREATE TABLE "cities" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"country_id" bigint,CONSTRAINT "fk_countries_cities" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
CREATE TABLE "addresses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"street" text,"street_number" text,"postal_code" text,"country_id" bigint,"city_id" bigint,"latitude" real,"longitude" real,CONSTRAINT "fk_cities_addresses" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_addresses" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
|
||||||
CREATE TABLE "users" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"username" text NOT NULL,"email" text NOT NULL,"password" text NOT NULL,"first_name" text,"last_name" text,"display_name" text,"bio" text,"avatar_url" text,"role" text DEFAULT 'reader',"last_login_at" timestamptz,"verified" boolean DEFAULT false,"active" boolean DEFAULT true,"country_id" bigint,"city_id" bigint,"address_id" bigint,CONSTRAINT "fk_users_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_users_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"),CONSTRAINT "fk_users_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "uni_users_username" UNIQUE ("username"),CONSTRAINT "uni_users_email" UNIQUE ("email"));
|
CREATE TABLE "countries" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"code" text NOT NULL,"phone_code" text,"currency" text,"continent" text);
|
||||||
CREATE TABLE "user_sessions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"ip" text,"user_agent" text,"expires_at" timestamptz NOT NULL,CONSTRAINT "fk_user_sessions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "cities" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"country_id" UUID,CONSTRAINT "fk_countries_cities" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "password_resets" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_password_resets_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "addresses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"street" text,"street_number" text,"postal_code" text,"country_id" UUID,"city_id" UUID,"latitude" real,"longitude" real,CONSTRAINT "fk_cities_addresses" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_addresses" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "email_verifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_email_verifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "users" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"username" text NOT NULL,"email" text NOT NULL,"password" text NOT NULL,"first_name" text,"last_name" text,"display_name" text,"bio" text,"avatar_url" text,"role" text DEFAULT 'reader',"last_login_at" timestamptz,"verified" boolean DEFAULT false,"active" boolean DEFAULT true,"country_id" UUID,"city_id" UUID,"address_id" UUID,CONSTRAINT "fk_users_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_users_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"),CONSTRAINT "fk_users_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "uni_users_username" UNIQUE ("username"),CONSTRAINT "uni_users_email" UNIQUE ("email"));
|
||||||
CREATE TABLE "works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"type" text DEFAULT 'other',"status" text DEFAULT 'draft',"published_at" timestamptz);
|
CREATE TABLE "user_sessions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"ip" text,"user_agent" text,"expires_at" timestamptz NOT NULL,CONSTRAINT "fk_user_sessions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "work_copyrights" ("work_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_id"));
|
CREATE TABLE "password_resets" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_password_resets_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "categories" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"parent_id" bigint,"path" text,"slug" text,CONSTRAINT "fk_categories_children" FOREIGN KEY ("parent_id") REFERENCES "categories"("id"));
|
CREATE TABLE "email_verifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_email_verifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "work_categories" ("category_id" bigint,"work_id" bigint,PRIMARY KEY ("category_id","work_id"),CONSTRAINT "fk_work_categories_category" FOREIGN KEY ("category_id") REFERENCES "categories"("id"),CONSTRAINT "fk_work_categories_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"type" text DEFAULT 'other',"status" text DEFAULT 'draft',"published_at" timestamptz);
|
||||||
CREATE TABLE "tags" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"slug" text);
|
CREATE TABLE "work_copyrights" ("work_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_id"));
|
||||||
CREATE TABLE "work_tags" ("tag_id" bigint,"work_id" bigint,PRIMARY KEY ("tag_id","work_id"),CONSTRAINT "fk_work_tags_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_tags_tag" FOREIGN KEY ("tag_id") REFERENCES "tags"("id"));
|
CREATE TABLE "categories" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"parent_id" UUID,"path" text,"slug" text,CONSTRAINT "fk_categories_children" FOREIGN KEY ("parent_id") REFERENCES "categories"("id"));
|
||||||
CREATE TABLE "places" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"latitude" real,"longitude" real,"country_id" bigint,"city_id" bigint,CONSTRAINT "fk_cities_places" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_places" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
CREATE TABLE "work_categories" ("category_id" UUID,"work_id" UUID,PRIMARY KEY ("category_id","work_id"),CONSTRAINT "fk_work_categories_category" FOREIGN KEY ("category_id") REFERENCES "categories"("id"),CONSTRAINT "fk_work_categories_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"status" text DEFAULT 'active',"birth_date" timestamptz,"death_date" timestamptz,"country_id" bigint,"city_id" bigint,"place_id" bigint,"address_id" bigint,CONSTRAINT "fk_authors_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "fk_authors_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_authors_place" FOREIGN KEY ("place_id") REFERENCES "places"("id"),CONSTRAINT "fk_authors_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"));
|
CREATE TABLE "tags" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"slug" text);
|
||||||
CREATE TABLE "work_authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"author_id" bigint,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_work_authors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
|
CREATE TABLE "work_tags" ("tag_id" UUID,"work_id" UUID,PRIMARY KEY ("tag_id","work_id"),CONSTRAINT "fk_work_tags_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_tags_tag" FOREIGN KEY ("tag_id") REFERENCES "tags"("id"));
|
||||||
CREATE TABLE "work_monetizations" ("work_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_id"));
|
CREATE TABLE "places" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"latitude" real,"longitude" real,"country_id" UUID,"city_id" UUID,CONSTRAINT "fk_cities_places" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_places" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "publishers" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"status" text DEFAULT 'active',"country_id" bigint,CONSTRAINT "fk_publishers_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
CREATE TABLE "authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"status" text DEFAULT 'active',"birth_date" timestamptz,"death_date" timestamptz,"country_id" UUID,"city_id" UUID,"place_id" UUID,"address_id" UUID,CONSTRAINT "fk_authors_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "fk_authors_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_authors_place" FOREIGN KEY ("place_id") REFERENCES "places"("id"),CONSTRAINT "fk_authors_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"));
|
||||||
CREATE TABLE "books" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"isbn" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"publisher_id" bigint,CONSTRAINT "fk_publishers_books" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id"));
|
CREATE TABLE "work_authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"author_id" UUID,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_work_authors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
|
||||||
CREATE TABLE "book_authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"book_id" bigint,"author_id" bigint,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_book_authors_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
|
CREATE TABLE "work_monetizations" ("work_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_id"));
|
||||||
CREATE TABLE "author_monetizations" ("author_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_id"));
|
CREATE TABLE "publishers" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"status" text DEFAULT 'active',"country_id" UUID,CONSTRAINT "fk_publishers_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "author_copyrights" ("author_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_id"));
|
CREATE TABLE "books" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"isbn" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"publisher_id" UUID,CONSTRAINT "fk_publishers_books" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id"));
|
||||||
CREATE TABLE "book_works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"book_id" bigint,"work_id" bigint,"order" integer DEFAULT 0,CONSTRAINT "fk_book_works_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "book_authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"book_id" UUID,"author_id" UUID,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_book_authors_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
|
||||||
CREATE TABLE "book_monetizations" ("book_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id"));
|
CREATE TABLE "author_monetizations" ("author_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_id"));
|
||||||
CREATE TABLE "book_copyrights" ("book_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_id"));
|
CREATE TABLE "author_copyrights" ("author_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_id"));
|
||||||
CREATE TABLE "publisher_monetizations" ("publisher_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id"));
|
CREATE TABLE "book_works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"book_id" UUID,"work_id" UUID,"order" integer DEFAULT 0,CONSTRAINT "fk_book_works_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "publisher_copyrights" ("publisher_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id"));
|
CREATE TABLE "book_monetizations" ("book_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id"));
|
||||||
CREATE TABLE "sources" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"url" text,"status" text DEFAULT 'active');
|
CREATE TABLE "book_copyrights" ("book_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_id"));
|
||||||
CREATE TABLE "source_monetizations" ("source_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id"));
|
CREATE TABLE "publisher_monetizations" ("publisher_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id"));
|
||||||
CREATE TABLE "source_copyrights" ("source_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_id"));
|
CREATE TABLE "publisher_copyrights" ("publisher_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id"));
|
||||||
CREATE TABLE "work_sources" ("source_id" bigint,"work_id" bigint,PRIMARY KEY ("source_id","work_id"),CONSTRAINT "fk_work_sources_source" FOREIGN KEY ("source_id") REFERENCES "sources"("id"),CONSTRAINT "fk_work_sources_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "sources" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"url" text,"status" text DEFAULT 'active');
|
||||||
CREATE TABLE "editions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"description" text,"isbn" text,"version" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"book_id" bigint,CONSTRAINT "fk_editions_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
|
CREATE TABLE "source_monetizations" ("source_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id"));
|
||||||
CREATE TABLE "translations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"content" text,"description" text,"language" text NOT NULL,"status" text DEFAULT 'draft',"published_at" timestamptz,"translatable_id" bigint NOT NULL,"translatable_type" text NOT NULL,"translator_id" bigint,"is_original_language" boolean DEFAULT false,"audio_url" text,"date_translated" timestamptz,CONSTRAINT "fk_users_translations" FOREIGN KEY ("translator_id") REFERENCES "users"("id"));
|
CREATE TABLE "source_copyrights" ("source_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_id"));
|
||||||
CREATE TABLE "text_blocks" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"translation_id" bigint,"index" bigint,"type" text,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"text" text,CONSTRAINT "fk_text_blocks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_text_blocks_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "work_sources" ("source_id" UUID,"work_id" UUID,PRIMARY KEY ("source_id","work_id"),CONSTRAINT "fk_work_sources_source" FOREIGN KEY ("source_id") REFERENCES "sources"("id"),CONSTRAINT "fk_work_sources_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "comments" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"line_number" bigint,"text_block_id" bigint,"parent_id" bigint,CONSTRAINT "fk_comments_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_comments_children" FOREIGN KEY ("parent_id") REFERENCES "comments"("id"),CONSTRAINT "fk_users_comments" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_comments_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "editions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"description" text,"isbn" text,"version" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"book_id" UUID,CONSTRAINT "fk_editions_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
|
||||||
CREATE TABLE "likes" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"comment_id" bigint,CONSTRAINT "fk_users_likes" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_likes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_likes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_likes" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
|
CREATE TABLE "translations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"content" text,"description" text,"language" text NOT NULL,"status" text DEFAULT 'draft',"published_at" timestamptz,"translatable_id" UUID NOT NULL,"translatable_type" text NOT NULL,"translator_id" UUID,"is_original_language" boolean DEFAULT false,"audio_url" text,"date_translated" timestamptz,CONSTRAINT "fk_users_translations" FOREIGN KEY ("translator_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "bookmarks" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text,"user_id" bigint,"work_id" bigint,"notes" text,"last_read_at" timestamptz,"progress" integer DEFAULT 0,CONSTRAINT "fk_bookmarks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_users_bookmarks" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "text_blocks" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"translation_id" UUID,"index" bigint,"type" text,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"text" text,CONSTRAINT "fk_text_blocks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_text_blocks_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
CREATE TABLE "collections" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"user_id" bigint,"is_public" boolean DEFAULT true,"cover_image_url" text,CONSTRAINT "fk_users_collections" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "comments" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"line_number" bigint,"text_block_id" UUID,"parent_id" UUID,CONSTRAINT "fk_comments_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_comments_children" FOREIGN KEY ("parent_id") REFERENCES "comments"("id"),CONSTRAINT "fk_users_comments" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_comments_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "collection_works" ("collection_id" bigint,"work_id" bigint,PRIMARY KEY ("collection_id","work_id"),CONSTRAINT "fk_collection_works_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"),CONSTRAINT "fk_collection_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "likes" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"comment_id" UUID,CONSTRAINT "fk_users_likes" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_likes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_likes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_likes" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
|
||||||
CREATE TABLE "contributions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"status" text DEFAULT 'draft',"user_id" bigint,"work_id" bigint,"translation_id" bigint,"reviewer_id" bigint,"reviewed_at" timestamptz,"feedback" text,CONSTRAINT "fk_contributions_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"),CONSTRAINT "fk_users_contributions" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributions_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "bookmarks" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text,"user_id" UUID,"work_id" UUID,"notes" text,"last_read_at" timestamptz,"progress" integer DEFAULT 0,CONSTRAINT "fk_bookmarks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_users_bookmarks" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "languages" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"code" text NOT NULL,"name" text NOT NULL,"script" text,"direction" text);
|
CREATE TABLE "collections" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"user_id" UUID,"is_public" boolean DEFAULT true,"cover_image_url" text,CONSTRAINT "fk_users_collections" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "series" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
CREATE TABLE "collection_works" ("collection_id" UUID,"work_id" UUID,PRIMARY KEY ("collection_id","work_id"),CONSTRAINT "fk_collection_works_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"),CONSTRAINT "fk_collection_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "work_series" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"series_id" bigint,"number_in_series" integer DEFAULT 0,CONSTRAINT "fk_work_series_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_series_series" FOREIGN KEY ("series_id") REFERENCES "series"("id"));
|
CREATE TABLE "contributions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"status" text DEFAULT 'draft',"user_id" UUID,"work_id" UUID,"translation_id" UUID,"reviewer_id" UUID,"reviewed_at" timestamptz,"feedback" text,CONSTRAINT "fk_contributions_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"),CONSTRAINT "fk_users_contributions" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributions_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
CREATE TABLE "translation_fields" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"translation_id" bigint,"field_name" text NOT NULL,"field_value" text NOT NULL,"language" text NOT NULL,CONSTRAINT "fk_translation_fields_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "languages" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"code" text NOT NULL,"name" text NOT NULL,"script" text,"direction" text);
|
||||||
CREATE TABLE "copyrights" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"identificator" text NOT NULL,"name" text NOT NULL,"description" text,"license" text,"start_date" timestamptz,"end_date" timestamptz);
|
CREATE TABLE "series" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
||||||
CREATE TABLE "copyright_translations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"copyright_id" bigint,"language_code" text NOT NULL,"message" text NOT NULL,"description" text,CONSTRAINT "fk_copyrights_translations" FOREIGN KEY ("copyright_id") REFERENCES "copyrights"("id"));
|
CREATE TABLE "work_series" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"series_id" UUID,"number_in_series" integer DEFAULT 0,CONSTRAINT "fk_work_series_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_series_series" FOREIGN KEY ("series_id") REFERENCES "series"("id"));
|
||||||
CREATE TABLE "copyright_claims" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"details" text NOT NULL,"status" text DEFAULT 'pending',"claim_date" timestamptz NOT NULL,"resolution" text,"resolved_at" timestamptz,"user_id" bigint,CONSTRAINT "fk_copyright_claims_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "translation_fields" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"translation_id" UUID,"field_name" text NOT NULL,"field_value" text NOT NULL,"language" text NOT NULL,CONSTRAINT "fk_translation_fields_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
CREATE TABLE "monetizations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"amount" decimal(10,2) DEFAULT 0,"currency" text DEFAULT 'USD',"type" text,"status" text DEFAULT 'active',"start_date" timestamptz,"end_date" timestamptz,"language" text NOT NULL);
|
CREATE TABLE "copyrights" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"identificator" text NOT NULL,"name" text NOT NULL,"description" text,"license" text,"start_date" timestamptz,"end_date" timestamptz);
|
||||||
CREATE TABLE "licenses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"spdx_identifier" text,"name" text NOT NULL,"url" text,"description" text);
|
CREATE TABLE "copyright_translations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"copyright_id" UUID,"language_code" text NOT NULL,"message" text NOT NULL,"description" text,CONSTRAINT "fk_copyrights_translations" FOREIGN KEY ("copyright_id") REFERENCES "copyrights"("id"));
|
||||||
CREATE TABLE "moderation_flags" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"target_type" text NOT NULL,"target_id" bigint NOT NULL,"reason" text,"status" text DEFAULT 'open',"reviewer_id" bigint,"notes" text,CONSTRAINT "fk_moderation_flags_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"));
|
CREATE TABLE "copyright_claims" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"details" text NOT NULL,"status" text DEFAULT 'pending',"claim_date" timestamptz NOT NULL,"resolution" text,"resolved_at" timestamptz,"user_id" UUID,CONSTRAINT "fk_copyright_claims_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "audit_logs" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"actor_id" bigint,"action" text NOT NULL,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"before" jsonb DEFAULT '{}',"after" jsonb DEFAULT '{}',"at" timestamptz,CONSTRAINT "fk_audit_logs_actor" FOREIGN KEY ("actor_id") REFERENCES "users"("id"));
|
CREATE TABLE "monetizations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"amount" decimal(10,2) DEFAULT 0,"currency" text DEFAULT 'USD',"type" text,"status" text DEFAULT 'active',"start_date" timestamptz,"end_date" timestamptz,"language" text NOT NULL);
|
||||||
CREATE TABLE "work_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"bookmarks" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"translation_count" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"complexity" decimal(5,2) DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"work_id" bigint,CONSTRAINT "fk_work_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id") ON DELETE CASCADE ON UPDATE CASCADE);
|
CREATE TABLE "licenses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"spdx_identifier" text,"name" text NOT NULL,"url" text,"description" text);
|
||||||
CREATE TABLE "translation_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"translation_id" bigint,CONSTRAINT "fk_translation_stats_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id") ON DELETE CASCADE ON UPDATE CASCADE);
|
CREATE TABLE "moderation_flags" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"target_type" text NOT NULL,"target_id" UUID NOT NULL,"reason" text,"status" text DEFAULT 'open',"reviewer_id" UUID,"notes" text,CONSTRAINT "fk_moderation_flags_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "user_engagements" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"date" date,"works_read" integer DEFAULT 0,"comments_made" integer DEFAULT 0,"likes_given" integer DEFAULT 0,"bookmarks_made" integer DEFAULT 0,"translations_made" integer DEFAULT 0,CONSTRAINT "fk_user_engagements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "audit_logs" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"actor_id" UUID,"action" text NOT NULL,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"before" jsonb DEFAULT '{}',"after" jsonb DEFAULT '{}',"at" timestamptz,CONSTRAINT "fk_audit_logs_actor" FOREIGN KEY ("actor_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "trendings" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"rank" integer NOT NULL,"score" decimal(10,2) DEFAULT 0,"time_period" text NOT NULL,"date" date);
|
CREATE TABLE "work_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"bookmarks" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"translation_count" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"complexity" decimal(5,2) DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"work_id" UUID,CONSTRAINT "fk_work_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id") ON DELETE CASCADE ON UPDATE CASCADE);
|
||||||
CREATE TABLE "book_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"sales" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"book_id" bigint,CONSTRAINT "fk_book_stats_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
|
CREATE TABLE "translation_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"translation_id" UUID,CONSTRAINT "fk_translation_stats_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id") ON DELETE CASCADE ON UPDATE CASCADE);
|
||||||
CREATE TABLE "collection_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"items" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"collection_id" bigint,CONSTRAINT "fk_collection_stats_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
|
CREATE TABLE "user_engagements" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"date" date,"works_read" integer DEFAULT 0,"comments_made" integer DEFAULT 0,"likes_given" integer DEFAULT 0,"bookmarks_made" integer DEFAULT 0,"translations_made" integer DEFAULT 0,CONSTRAINT "fk_user_engagements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "media_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"downloads" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"media_id" bigint);
|
CREATE TABLE "trendings" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"rank" integer NOT NULL,"score" decimal(10,2) DEFAULT 0,"time_period" text NOT NULL,"date" date);
|
||||||
CREATE TABLE "author_countries" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"author_id" bigint,"country_id" bigint,CONSTRAINT "fk_author_countries_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_author_countries_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
CREATE TABLE "book_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"sales" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"book_id" UUID,CONSTRAINT "fk_book_stats_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
|
||||||
CREATE TABLE "readability_scores" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"score" decimal(5,2),"language" text NOT NULL,"method" text,"work_id" bigint,CONSTRAINT "fk_readability_scores_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "collection_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"items" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"collection_id" UUID,CONSTRAINT "fk_collection_stats_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
|
||||||
CREATE TABLE "writing_styles" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"work_id" bigint,CONSTRAINT "fk_writing_styles_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "media_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"downloads" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"media_id" UUID);
|
||||||
CREATE TABLE "linguistic_layers" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"type" text,"work_id" bigint,"data" jsonb DEFAULT '{}',CONSTRAINT "fk_linguistic_layers_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "author_countries" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"author_id" UUID,"country_id" UUID,CONSTRAINT "fk_author_countries_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_author_countries_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "text_metadata" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"analysis" text,"language" text NOT NULL,"word_count" integer DEFAULT 0,"sentence_count" integer DEFAULT 0,"paragraph_count" integer DEFAULT 0,"average_word_length" decimal(5,2),"average_sentence_length" decimal(5,2),"work_id" bigint,CONSTRAINT "fk_text_metadata_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "readability_scores" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"score" decimal(5,2),"language" text NOT NULL,"method" text,"work_id" UUID,CONSTRAINT "fk_readability_scores_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "poetic_analyses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"structure" text,"language" text NOT NULL,"rhyme_scheme" text,"meter_type" text,"stanza_count" integer DEFAULT 0,"line_count" integer DEFAULT 0,"work_id" bigint,CONSTRAINT "fk_poetic_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "writing_styles" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"work_id" UUID,CONSTRAINT "fk_writing_styles_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "concepts" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
CREATE TABLE "linguistic_layers" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"type" text,"work_id" UUID,"data" jsonb DEFAULT '{}',CONSTRAINT "fk_linguistic_layers_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "words" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"language" text NOT NULL,"part_of_speech" text,"lemma" text,"concept_id" bigint,CONSTRAINT "fk_concepts_words" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"));
|
CREATE TABLE "text_metadata" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"analysis" text,"language" text NOT NULL,"word_count" integer DEFAULT 0,"sentence_count" integer DEFAULT 0,"paragraph_count" integer DEFAULT 0,"average_word_length" decimal(5,2),"average_sentence_length" decimal(5,2),"work_id" UUID,CONSTRAINT "fk_text_metadata_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "work_words" ("word_id" bigint,"work_id" bigint,PRIMARY KEY ("word_id","work_id"),CONSTRAINT "fk_work_words_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_words_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
|
CREATE TABLE "poetic_analyses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"structure" text,"language" text NOT NULL,"rhyme_scheme" text,"meter_type" text,"stanza_count" integer DEFAULT 0,"line_count" integer DEFAULT 0,"work_id" UUID,CONSTRAINT "fk_poetic_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "word_occurrences" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" bigint,"word_id" bigint,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"lemma" text,"part_of_speech" text,CONSTRAINT "fk_word_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_word_occurrences_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
|
CREATE TABLE "concepts" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
||||||
CREATE TABLE "work_concepts" ("concept_id" bigint,"work_id" bigint,PRIMARY KEY ("concept_id","work_id"),CONSTRAINT "fk_work_concepts_concept" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"),CONSTRAINT "fk_work_concepts_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "words" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"language" text NOT NULL,"part_of_speech" text,"lemma" text,"concept_id" UUID,CONSTRAINT "fk_concepts_words" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"));
|
||||||
CREATE TABLE "language_entities" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"language" text NOT NULL);
|
CREATE TABLE "work_words" ("word_id" UUID,"work_id" UUID,PRIMARY KEY ("word_id","work_id"),CONSTRAINT "fk_work_words_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_words_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
|
||||||
CREATE TABLE "work_language_entities" ("language_entity_id" bigint,"work_id" bigint,PRIMARY KEY ("language_entity_id","work_id"),CONSTRAINT "fk_work_language_entities_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"),CONSTRAINT "fk_work_language_entities_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "word_occurrences" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" UUID,"word_id" UUID,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"lemma" text,"part_of_speech" text,CONSTRAINT "fk_word_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_word_occurrences_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
|
||||||
CREATE TABLE "entity_occurrences" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" bigint,"language_entity_id" bigint,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,CONSTRAINT "fk_entity_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_entity_occurrences_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"));
|
CREATE TABLE "work_concepts" ("concept_id" UUID,"work_id" UUID,PRIMARY KEY ("concept_id","work_id"),CONSTRAINT "fk_work_concepts_concept" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"),CONSTRAINT "fk_work_concepts_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "language_analyses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text NOT NULL,"analysis" jsonb DEFAULT '{}',"work_id" bigint,CONSTRAINT "fk_language_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "language_entities" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"language" text NOT NULL);
|
||||||
CREATE TABLE "gamifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"points" integer DEFAULT 0,"level" integer DEFAULT 1,"badges" jsonb DEFAULT '{}',"streaks" integer DEFAULT 0,"last_active" timestamptz,"user_id" bigint,CONSTRAINT "fk_gamifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "work_language_entities" ("language_entity_id" UUID,"work_id" UUID,PRIMARY KEY ("language_entity_id","work_id"),CONSTRAINT "fk_work_language_entities_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"),CONSTRAINT "fk_work_language_entities_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"data" jsonb DEFAULT '{}',"period" text,"start_date" timestamptz,"end_date" timestamptz,"user_id" bigint,"work_id" bigint,CONSTRAINT "fk_stats_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "entity_occurrences" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" UUID,"language_entity_id" UUID,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,CONSTRAINT "fk_entity_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_entity_occurrences_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"));
|
||||||
CREATE TABLE "search_documents" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text,"entity_id" bigint,"language_code" text,"title" text,"body" text,"keywords" text);
|
CREATE TABLE "language_analyses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text NOT NULL,"analysis" jsonb DEFAULT '{}',"work_id" UUID,CONSTRAINT "fk_language_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "emotions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"intensity" decimal(5,2) DEFAULT 0,"user_id" bigint,"work_id" bigint,"collection_id" bigint,CONSTRAINT "fk_emotions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_emotions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_emotions_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
|
CREATE TABLE "gamifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"points" integer DEFAULT 0,"level" integer DEFAULT 1,"badges" jsonb DEFAULT '{}',"streaks" integer DEFAULT 0,"last_active" timestamptz,"user_id" UUID,CONSTRAINT "fk_gamifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "moods" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL);
|
CREATE TABLE "stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"data" jsonb DEFAULT '{}',"period" text,"start_date" timestamptz,"end_date" timestamptz,"user_id" UUID,"work_id" UUID,CONSTRAINT "fk_stats_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "work_moods" ("mood_id" bigint,"work_id" bigint,PRIMARY KEY ("mood_id","work_id"),CONSTRAINT "fk_work_moods_mood" FOREIGN KEY ("mood_id") REFERENCES "moods"("id"),CONSTRAINT "fk_work_moods_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "search_documents" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text,"entity_id" UUID,"language_code" text,"title" text,"body" text,"keywords" text);
|
||||||
CREATE TABLE "topic_clusters" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"keywords" text);
|
CREATE TABLE "emotions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"intensity" decimal(5,2) DEFAULT 0,"user_id" UUID,"work_id" UUID,"collection_id" UUID,CONSTRAINT "fk_emotions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_emotions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_emotions_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
|
||||||
CREATE TABLE "work_topic_clusters" ("topic_cluster_id" bigint,"work_id" bigint,PRIMARY KEY ("topic_cluster_id","work_id"),CONSTRAINT "fk_work_topic_clusters_topic_cluster" FOREIGN KEY ("topic_cluster_id") REFERENCES "topic_clusters"("id"),CONSTRAINT "fk_work_topic_clusters_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
CREATE TABLE "moods" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL);
|
||||||
CREATE TABLE "edges" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"source_table" text NOT NULL,"source_id" bigint NOT NULL,"target_table" text NOT NULL,"target_id" bigint NOT NULL,"relation" text NOT NULL DEFAULT 'ASSOCIATED_WITH',"language" text DEFAULT 'en',"extra" jsonb DEFAULT '{}');
|
CREATE TABLE "work_moods" ("mood_id" UUID,"work_id" UUID,PRIMARY KEY ("mood_id","work_id"),CONSTRAINT "fk_work_moods_mood" FOREIGN KEY ("mood_id") REFERENCES "moods"("id"),CONSTRAINT "fk_work_moods_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "embeddings" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"external_id" text,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"model" text NOT NULL,"dim" integer DEFAULT 0,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_embeddings_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_embeddings_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "topic_clusters" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"keywords" text);
|
||||||
CREATE TABLE "localizations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"key" text NOT NULL,"value" text NOT NULL,"language" text NOT NULL);
|
CREATE TABLE "work_topic_clusters" ("topic_cluster_id" UUID,"work_id" UUID,PRIMARY KEY ("topic_cluster_id","work_id"),CONSTRAINT "fk_work_topic_clusters_topic_cluster" FOREIGN KEY ("topic_cluster_id") REFERENCES "topic_clusters"("id"),CONSTRAINT "fk_work_topic_clusters_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
|
||||||
CREATE TABLE "media" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"url" text NOT NULL,"type" text NOT NULL,"mime_type" text,"size" bigint DEFAULT 0,"title" text,"description" text,"language" text NOT NULL,"author_id" bigint,"translation_id" bigint,"country_id" bigint,"city_id" bigint,CONSTRAINT "fk_media_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_media_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_media_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_media_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
CREATE TABLE "edges" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"source_table" text NOT NULL,"source_id" UUID NOT NULL,"target_table" text NOT NULL,"target_id" UUID NOT NULL,"relation" text NOT NULL DEFAULT 'ASSOCIATED_WITH',"language" text DEFAULT 'en',"extra" jsonb DEFAULT '{}');
|
||||||
CREATE TABLE "notifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"message" text NOT NULL,"type" text,"read" boolean DEFAULT false,"language" text NOT NULL,"user_id" bigint,"related_id" bigint,"related_type" text,CONSTRAINT "fk_notifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "embeddings" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"external_id" text,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"model" text NOT NULL,"dim" integer DEFAULT 0,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_embeddings_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_embeddings_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
CREATE TABLE "editorial_workflows" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"stage" text NOT NULL,"notes" text,"language" text NOT NULL,"work_id" bigint,"translation_id" bigint,"user_id" bigint,"assigned_to_id" bigint,"due_date" timestamptz,"completed_at" timestamptz,CONSTRAINT "fk_editorial_workflows_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_editorial_workflows_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_editorial_workflows_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_editorial_workflows_assigned_to" FOREIGN KEY ("assigned_to_id") REFERENCES "users"("id"));
|
CREATE TABLE "localizations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"key" text NOT NULL,"value" text NOT NULL,"language" text NOT NULL);
|
||||||
CREATE TABLE "admins" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"role" text NOT NULL,"permissions" jsonb DEFAULT '{}',CONSTRAINT "fk_admins_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "media" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"url" text NOT NULL,"type" text NOT NULL,"mime_type" text,"size" bigint DEFAULT 0,"title" text,"description" text,"language" text NOT NULL,"author_id" UUID,"translation_id" UUID,"country_id" UUID,"city_id" UUID,CONSTRAINT "fk_media_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_media_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_media_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_media_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
|
||||||
CREATE TABLE "votes" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"value" integer DEFAULT 0,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"comment_id" bigint,CONSTRAINT "fk_votes_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_votes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_votes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_votes_comment" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
|
CREATE TABLE "notifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"message" text NOT NULL,"type" text,"read" boolean DEFAULT false,"language" text NOT NULL,"user_id" UUID,"related_id" UUID,"related_type" text,CONSTRAINT "fk_notifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "contributors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"role" text,"user_id" bigint,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_contributors_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributors_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "editorial_workflows" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"stage" text NOT NULL,"notes" text,"language" text NOT NULL,"work_id" UUID,"translation_id" UUID,"user_id" UUID,"assigned_to_id" UUID,"due_date" timestamptz,"completed_at" timestamptz,CONSTRAINT "fk_editorial_workflows_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_editorial_workflows_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_editorial_workflows_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_editorial_workflows_assigned_to" FOREIGN KEY ("assigned_to_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "interaction_events" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"target_type" text NOT NULL,"target_id" bigint NOT NULL,"kind" text NOT NULL,"occurred_at" timestamptz,CONSTRAINT "fk_interaction_events_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
CREATE TABLE "admins" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"role" text NOT NULL,"permissions" jsonb DEFAULT '{}',CONSTRAINT "fk_admins_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
CREATE TABLE "hybrid_entity_works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_hybrid_entity_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_hybrid_entity_works_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
CREATE TABLE "votes" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"value" integer DEFAULT 0,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"comment_id" UUID,CONSTRAINT "fk_votes_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_votes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_votes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_votes_comment" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
|
||||||
|
CREATE TABLE "contributors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"role" text,"user_id" UUID,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_contributors_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributors_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
|
CREATE TABLE "interaction_events" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"target_type" text NOT NULL,"target_id" UUID NOT NULL,"kind" text NOT NULL,"occurred_at" timestamptz,CONSTRAINT "fk_interaction_events_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
|
||||||
|
CREATE TABLE "hybrid_entity_works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_hybrid_entity_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_hybrid_entity_works_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
DROP TABLE IF EXISTS "hybrid_entity_works";
|
DROP TABLE IF EXISTS "hybrid_entity_works";
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -41,7 +42,7 @@ var allowedTranslationCounterFields = map[string]bool{
|
|||||||
"shares": true,
|
"shares": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error {
|
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error {
|
||||||
ctx, span := r.tracer.Start(ctx, "IncrementWorkCounter")
|
ctx, span := r.tracer.Start(ctx, "IncrementWorkCounter")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if !allowedWorkCounterFields[field] {
|
if !allowedWorkCounterFields[field] {
|
||||||
@ -83,7 +84,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
return []*domain.Work{}, nil
|
return []*domain.Work{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
workIDs := make([]uint, len(trendingWorks))
|
workIDs := make([]uuid.UUID, len(trendingWorks))
|
||||||
for i, tw := range trendingWorks {
|
for i, tw := range trendingWorks {
|
||||||
workIDs[i] = tw.EntityID
|
workIDs[i] = tw.EntityID
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
|
|
||||||
// This part is tricky because the order from the IN clause is not guaranteed.
|
// This part is tricky because the order from the IN clause is not guaranteed.
|
||||||
// We need to re-order the works based on the trending rank.
|
// We need to re-order the works based on the trending rank.
|
||||||
workMap := make(map[uint]*domain.Work)
|
workMap := make(map[uuid.UUID]*domain.Work)
|
||||||
for _, w := range works {
|
for _, w := range works {
|
||||||
workMap[w.ID] = w
|
workMap[w.ID] = w
|
||||||
}
|
}
|
||||||
@ -110,7 +111,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
return orderedWorks, err
|
return orderedWorks, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error {
|
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error {
|
||||||
ctx, span := r.tracer.Start(ctx, "IncrementTranslationCounter")
|
ctx, span := r.tracer.Start(ctx, "IncrementTranslationCounter")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if !allowedTranslationCounterFields[field] {
|
if !allowedTranslationCounterFields[field] {
|
||||||
@ -132,19 +133,19 @@ func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, t
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error {
|
||||||
ctx, span := r.tracer.Start(ctx, "UpdateWorkStats")
|
ctx, span := r.tracer.Start(ctx, "UpdateWorkStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error {
|
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error {
|
||||||
ctx, span := r.tracer.Start(ctx, "UpdateTranslationStats")
|
ctx, span := r.tracer.Start(ctx, "UpdateTranslationStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "GetOrCreateWorkStats")
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateWorkStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var stats domain.WorkStats
|
var stats domain.WorkStats
|
||||||
@ -152,7 +153,7 @@ func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID u
|
|||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateTranslationStats")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var stats domain.TranslationStats
|
var stats domain.TranslationStats
|
||||||
@ -160,7 +161,7 @@ func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, t
|
|||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) {
|
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "GetOrCreateUserEngagement")
|
ctx, span := r.tracer.Start(ctx, "GetOrCreateUserEngagement")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var engagement domain.UserEngagement
|
var engagement domain.UserEngagement
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package sql_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/app/analytics"
|
"tercul/internal/app/analytics"
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -23,7 +24,7 @@ func NewAuthRepository(db *gorm.DB, cfg *config.Config) domain.AuthRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authRepository) StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error {
|
func (r *authRepository) StoreToken(ctx context.Context, userID uuid.UUID, token string, expiresAt time.Time) error {
|
||||||
ctx, span := r.tracer.Start(ctx, "StoreToken")
|
ctx, span := r.tracer.Start(ctx, "StoreToken")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
session := &domain.UserSession{
|
session := &domain.UserSession{
|
||||||
|
|||||||
@ -2,10 +2,10 @@ package sql_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,27 +36,27 @@ func (r *authorRepository) FindByName(ctx context.Context, name string) (*domain
|
|||||||
return &author, nil
|
return &author, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
||||||
Where("work_authors.work_id = ?", workID).Find(&authors).Error
|
Where("work_authors.work_id = ?", workID).Find(&authors).Error
|
||||||
return authors, err
|
return authors, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
||||||
Where("book_authors.book_id = ?", bookID).Find(&authors).Error
|
Where("book_authors.book_id = ?", bookID).Find(&authors).Error
|
||||||
return authors, err
|
return authors, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error
|
err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error
|
||||||
return authors, err
|
return authors, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
||||||
var author domain.Author
|
var author domain.Author
|
||||||
err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error
|
err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package sql_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"tercul/internal/platform/log"
|
"tercul/internal/platform/log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -39,8 +40,8 @@ func (r *BaseRepositoryImpl[T]) validateContext(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateID ensures ID is valid
|
// validateID ensures ID is valid
|
||||||
func (r *BaseRepositoryImpl[T]) validateID(id uint) error {
|
func (r *BaseRepositoryImpl[T]) validateID(id uuid.UUID) error {
|
||||||
if id == 0 {
|
if id == uuid.Nil {
|
||||||
return domain.ErrValidation
|
return domain.ErrValidation
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -158,7 +159,7 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByID retrieves an entity by its ID
|
// GetByID retrieves an entity by its ID
|
||||||
func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error) {
|
func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uuid.UUID) (*T, error) {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -187,7 +188,7 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByIDWithOptions retrieves an entity by its ID with query options
|
// GetByIDWithOptions retrieves an entity by its ID with query options
|
||||||
func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*T, error) {
|
func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*T, error) {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -268,7 +269,7 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes an entity by its ID
|
// Delete removes an entity by its ID
|
||||||
func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
|
func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -298,7 +299,7 @@ func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteInTx removes an entity by its ID within a transaction
|
// DeleteInTx removes an entity by its ID within a transaction
|
||||||
func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -475,7 +476,7 @@ func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FindWithPreload retrieves an entity by its ID with preloaded relationships
|
// FindWithPreload retrieves an entity by its ID with preloaded relationships
|
||||||
func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error) {
|
func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*T, error) {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -541,7 +542,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exists checks if an entity exists by ID
|
// Exists checks if an entity exists by ID
|
||||||
func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uint) (bool, error) {
|
func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
||||||
if err := r.validateContext(ctx); err != nil {
|
if err := r.validateContext(ctx); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,11 @@ package sql_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -27,7 +28,7 @@ func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByAuthorID finds books by author ID
|
// ListByAuthorID finds books by author ID
|
||||||
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uuid.UUID) ([]domain.Book, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByAuthorID")
|
ctx, span := r.tracer.Start(ctx, "ListByAuthorID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
@ -40,7 +41,7 @@ func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByPublisherID finds books by publisher ID
|
// ListByPublisherID finds books by publisher ID
|
||||||
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uuid.UUID) ([]domain.Book, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByPublisherID")
|
ctx, span := r.tracer.Start(ctx, "ListByPublisherID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
@ -51,7 +52,7 @@ func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds books by work ID
|
// ListByWorkID finds books by work ID
|
||||||
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) {
|
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Book, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var books []domain.Book
|
var books []domain.Book
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package sql_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -26,7 +27,7 @@ func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepos
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByUserID finds bookmarks by user ID
|
// ListByUserID finds bookmarks by user ID
|
||||||
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) {
|
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Bookmark, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
ctx, span := r.tracer.Start(ctx, "ListByUserID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var bookmarks []domain.Bookmark
|
var bookmarks []domain.Bookmark
|
||||||
@ -37,7 +38,7 @@ func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds bookmarks by work ID
|
// ListByWorkID finds bookmarks by work ID
|
||||||
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Bookmark, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var bookmarks []domain.Bookmark
|
var bookmarks []domain.Bookmark
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
|
||||||
repo "tercul/internal/data/sql"
|
repo "tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -41,7 +42,7 @@ func (r *categoryRepository) FindByName(ctx context.Context, name string) (*doma
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByWorkID finds categories by work ID
|
// ListByWorkID finds categories by work ID
|
||||||
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
|
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Category, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var categories []domain.Category
|
var categories []domain.Category
|
||||||
@ -54,7 +55,7 @@ func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListByParentID finds categories by parent ID
|
// ListByParentID finds categories by parent ID
|
||||||
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error) {
|
||||||
ctx, span := r.tracer.Start(ctx, "ListByParentID")
|
ctx, span := r.tracer.Start(ctx, "ListByParentID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
var categories []domain.Category
|
var categories []domain.Category
|
||||||
|
|||||||
@ -2,11 +2,11 @@ package sql_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
"tercul/internal/platform/config"
|
"tercul/internal/platform/config"
|
||||||
"tercul/internal/testutil"
|
"tercul/internal/testutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user