mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
Compare commits
No commits in common. "d50722dad54a136d93ab965e2a8a07b5657bbd6c" and "ad749d9184621ebdcb08e9a7c55dfc27a6761ca7" have entirely different histories.
d50722dad5
...
ad749d9184
@ -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/api"
|
cmd = "go build -o ./tmp/tercul ."
|
||||||
# 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
Normal file
16
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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
Normal file
769
.github/workflows/README.md
vendored
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
# 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
Normal file
46
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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
Normal file
60
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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
Normal file
65
.github/workflows/docker-build.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
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
Normal file
33
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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
Normal file
70
.github/workflows/security.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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
Normal file
116
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
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.25-alpine AS builder
|
FROM golang:1.24-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.25 AS development
|
FROM golang:1.24 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@v1.52.3
|
RUN go install github.com/air-verse/air@latest
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
13
Makefile
13
Makefile
@ -1,15 +1,4 @@
|
|||||||
.PHONY: lint-test dev dev-deps dev-down
|
.PHONY: lint-test
|
||||||
|
|
||||||
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..."
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import (
|
|||||||
"tercul/internal/app/translation"
|
"tercul/internal/app/translation"
|
||||||
"tercul/internal/app/user"
|
"tercul/internal/app/user"
|
||||||
"tercul/internal/app/work"
|
"tercul/internal/app/work"
|
||||||
datacache "tercul/internal/data/cache"
|
|
||||||
dbsql "tercul/internal/data/sql"
|
dbsql "tercul/internal/data/sql"
|
||||||
"tercul/internal/jobs/linguistics"
|
"tercul/internal/jobs/linguistics"
|
||||||
"tercul/internal/observability"
|
"tercul/internal/observability"
|
||||||
@ -126,36 +125,13 @@ 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)
|
||||||
|
|
||||||
@ -205,18 +181,12 @@ 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())
|
||||||
} else {
|
} else {
|
||||||
// Optional repository caching (opt-in)
|
|
||||||
if os.Getenv("REPO_CACHE_ENABLED") == "true" {
|
|
||||||
repos.Work = datacache.NewCachedWorkRepository(repos.Work, redisCache, nil)
|
|
||||||
repos.Author = datacache.NewCachedAuthorRepository(repos.Author, redisCache, nil)
|
|
||||||
repos.Translation = datacache.NewCachedTranslationRepository(repos.Translation, redisCache, nil)
|
|
||||||
app_log.Info("Repository caching enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
queryCache = &cache.GraphQLCacheAdapter{RedisCache: redisCache}
|
queryCache = &cache.GraphQLCacheAdapter{RedisCache: redisCache}
|
||||||
app_log.Info("Redis cache initialized for APQ")
|
app_log.Info("Redis cache initialized for APQ")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T15:37:36.561092+01:00"}
|
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-11-30T21:59:16.811419372Z"}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tercul/internal/data/sql"
|
"tercul/internal/data/sql"
|
||||||
@ -29,7 +30,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type checkpoint struct {
|
type checkpoint struct {
|
||||||
LastProcessedID string `json:"last_processed_id"`
|
LastProcessedID uint `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"`
|
||||||
}
|
}
|
||||||
@ -103,7 +104,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=%s, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed))
|
logger.Info(fmt.Sprintf("Resuming from checkpoint: last_id=%d, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,10 +268,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 != "" {
|
if cp != nil && cp.LastProcessedID > 0 {
|
||||||
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.String() > cp.LastProcessedID {
|
if t.ID > cp.LastProcessedID {
|
||||||
filtered = append(filtered, t)
|
filtered = append(filtered, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,11 +282,11 @@ func migrateTranslations(
|
|||||||
|
|
||||||
// Process translations in batches
|
// Process translations in batches
|
||||||
batch := make([]domain.Translation, 0, batchSize)
|
batch := make([]domain.Translation, 0, batchSize)
|
||||||
lastProcessedID := ""
|
lastProcessedID := uint(0)
|
||||||
|
|
||||||
for i, translation := range translations {
|
for i, translation := range translations {
|
||||||
batch = append(batch, translation)
|
batch = append(batch, translation)
|
||||||
lastProcessedID = translation.ID.String()
|
lastProcessedID = translation.ID
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -325,7 +326,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": t.ID.String(),
|
"id": strconv.FormatUint(uint64(t.ID), 10),
|
||||||
"title": t.Title,
|
"title": t.Title,
|
||||||
"content": t.Content,
|
"content": t.Content,
|
||||||
"description": t.Description,
|
"description": t.Description,
|
||||||
|
|||||||
@ -129,3 +129,4 @@ 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 uuid.UUID) (*domain.Translation, error) {
|
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*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,3 +114,4 @@ func TestRootCommand(t *testing.T) {
|
|||||||
assert.NotEmpty(t, cmd.Short)
|
assert.NotEmpty(t, cmd.Short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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"
|
||||||
@ -10,7 +11,6 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
entityIDUUID, err := uuid.Parse(entityID)
|
entityIDUint, err := strconv.ParseUint(entityID, 10, 64)
|
||||||
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 %s", entityType, entityIDUUID.String()))
|
log.Info(fmt.Sprintf("Enriching %s with ID %d", entityType, entityIDUint))
|
||||||
|
|
||||||
switch entityType {
|
switch entityType {
|
||||||
case "author":
|
case "author":
|
||||||
author, err := deps.Repos.Author.GetByID(ctx, entityIDUUID)
|
author, err := deps.Repos.Author.GetByID(ctx, uint(entityIDUint))
|
||||||
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,10 +6,7 @@ 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"
|
||||||
@ -76,47 +73,16 @@ 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)
|
||||||
linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
// Placeholder for other job handlers that might be added in the future
|
||||||
|
// 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.AnalysisRepository
|
AnalysisRepo *linguistics.GORMAnalysisRepository
|
||||||
SentimentProvider *linguistics.GoVADERSentimentProvider
|
SentimentProvider *linguistics.GoVADERSentimentProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,32 +61,12 @@ 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,3 +109,4 @@ func TestBootstrapWithMetrics(t *testing.T) {
|
|||||||
assert.NotNil(t, deps)
|
assert.NotNil(t, deps)
|
||||||
assert.NotNil(t, deps.Application)
|
assert.NotNil(t, deps.Application)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,13 +5,12 @@ 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() {
|
||||||
@ -25,7 +24,7 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
entityID, err := uuid.Parse(*entityIDStr)
|
entityID, err := strconv.ParseUint(*entityIDStr, 10, 64)
|
||||||
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)
|
||||||
@ -56,7 +55,7 @@ func main() {
|
|||||||
|
|
||||||
switch *entityType {
|
switch *entityType {
|
||||||
case "author":
|
case "author":
|
||||||
author, err := repos.Author.GetByID(ctx, entityID)
|
author, err := repos.Author.GetByID(ctx, uint(entityID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err, "Failed to get author")
|
log.Fatal(err, "Failed to get author")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,7 @@ 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"
|
||||||
@ -74,44 +70,13 @@ 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)
|
||||||
linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
// Placeholder for other job handlers that might be added in the future
|
||||||
|
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
|
||||||
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
// trending.RegisterTrendingHandlers(mux, analyticsService)
|
||||||
|
|
||||||
// Start the server in a goroutine
|
// Start the server in a goroutine
|
||||||
|
|||||||
43
content/blog/post1.json
Normal file
43
content/blog/post1.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Future of Artificial Intelligence",
|
||||||
|
"slug": "future-of-ai",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "A deep dive into the future of artificial intelligence, exploring its potential impact on society, industry, and our daily lives.",
|
||||||
|
"content": "<p>Artificial intelligence (AI) is no longer a concept confined to science fiction. It's a powerful force that's reshaping our world in countless ways. From the algorithms that power our social media feeds to the sophisticated systems that drive autonomous vehicles, AI is already here. But what does the future hold for this transformative technology?</p><p>In this post, we'll explore some of the most exciting advancements on the horizon, including the rise of general AI, the potential for AI-driven scientific discovery, and the ethical considerations that we must address as we move forward.</p>",
|
||||||
|
"publishDate": "2024-09-15",
|
||||||
|
"author": "Dr. Evelyn Reed",
|
||||||
|
"tags": ["AI", "Machine Learning", "Technology"],
|
||||||
|
"meta_title": "The Future of AI: A Comprehensive Overview",
|
||||||
|
"meta_description": "Learn about the future of artificial intelligence and its potential impact on our world."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-1",
|
||||||
|
"translation_group_id": "tg-future-of-ai",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-15T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/future-of-ai",
|
||||||
|
"og_title": "The Future of Artificial Intelligence",
|
||||||
|
"og_description": "A deep dive into the future of AI.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Technology", "Science"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/ai-future.jpg",
|
||||||
|
"alt": "An abstract image representing artificial intelligence."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post2.json
Normal file
43
content/blog/post2.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "A Guide to Sustainable Living",
|
||||||
|
"slug": "guide-to-sustainable-living",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Discover practical tips and simple changes you can make to live a more sustainable and eco-friendly lifestyle.",
|
||||||
|
"content": "<p>Living sustainably doesn't have to be complicated. It's about making conscious choices that reduce your environmental impact. In this guide, we'll cover everything from reducing your plastic consumption to creating a more energy-efficient home.</p><p>We'll also explore the benefits of a plant-based diet and how you can support local, sustainable businesses in your community.</p>",
|
||||||
|
"publishDate": "2024-09-18",
|
||||||
|
"author": "Liam Carter",
|
||||||
|
"tags": ["Sustainability", "Eco-Friendly", "Lifestyle"],
|
||||||
|
"meta_title": "Your Ultimate Guide to Sustainable Living",
|
||||||
|
"meta_description": "Learn how to live a more sustainable lifestyle with our comprehensive guide."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-2",
|
||||||
|
"translation_group_id": "tg-sustainable-living",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-18T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/guide-to-sustainable-living",
|
||||||
|
"og_title": "A Guide to Sustainable Living",
|
||||||
|
"og_description": "Discover practical tips for a more sustainable lifestyle.",
|
||||||
|
"twitter_card": "summary"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Lifestyle", "Environment"],
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-1", "post-3"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/sustainable-living.jpg",
|
||||||
|
"alt": "A person holding a reusable water bottle in a lush green environment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post3.json
Normal file
43
content/blog/post3.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Art of Mindful Meditation",
|
||||||
|
"slug": "art-of-mindful-meditation",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Learn the basics of mindful meditation and how it can help you reduce stress, improve focus, and cultivate a sense of inner peace.",
|
||||||
|
"content": "<p>In our fast-paced world, it's easy to get caught up in the chaos. Mindful meditation offers a powerful tool to ground yourself in the present moment and find a sense of calm amidst the noise.</p><p>This post will guide you through the fundamental principles of mindfulness and provide simple exercises to help you start your meditation practice.</p>",
|
||||||
|
"publishDate": "2024-09-22",
|
||||||
|
"author": "Isabella Rossi",
|
||||||
|
"tags": ["Mindfulness", "Meditation", "Wellness"],
|
||||||
|
"meta_title": "A Beginner's Guide to Mindful Meditation",
|
||||||
|
"meta_description": "Start your journey with mindful meditation and discover its many benefits."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-3",
|
||||||
|
"translation_group_id": "tg-mindful-meditation",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-22T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/art-of-mindful-meditation",
|
||||||
|
"og_title": "The Art of Mindful Meditation",
|
||||||
|
"og_description": "Learn the basics of mindful meditation.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Wellness", "Lifestyle"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2", "post-4"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/meditation.jpg",
|
||||||
|
"alt": "A person meditating peacefully in a serene setting."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post4.json
Normal file
43
content/blog/post4.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "Exploring the Wonders of the Cosmos",
|
||||||
|
"slug": "exploring-the-cosmos",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Join us on a journey through the cosmos as we explore distant galaxies, mysterious black holes, and the search for extraterrestrial life.",
|
||||||
|
"content": "<p>The universe is a vast and mysterious place, filled with wonders that we are only just beginning to understand. From the birth of stars to the formation of galaxies, the cosmos is a story of epic proportions.</p><p>In this post, we'll take a look at some of the most awe-inspiring discoveries in modern astronomy and consider the big questions that continue to drive our exploration of space.</p>",
|
||||||
|
"publishDate": "2024-09-25",
|
||||||
|
"author": "Dr. Kenji Tanaka",
|
||||||
|
"tags": ["Astronomy", "Space", "Science"],
|
||||||
|
"meta_title": "A Journey Through the Cosmos",
|
||||||
|
"meta_description": "Explore the wonders of the universe with our guide to modern astronomy."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-4",
|
||||||
|
"translation_group_id": "tg-exploring-the-cosmos",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-25T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/exploring-the-cosmos",
|
||||||
|
"og_title": "Exploring the Wonders of the Cosmos",
|
||||||
|
"og_description": "A journey through the cosmos.",
|
||||||
|
"twitter_card": "summary"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Science", "Astronomy"],
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-1", "post-5"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/cosmos.jpg",
|
||||||
|
"alt": "A stunning image of a spiral galaxy."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
content/blog/post5.json
Normal file
43
content/blog/post5.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"contentTypeSlug": "blog",
|
||||||
|
"title": "The Rise of Remote Work",
|
||||||
|
"slug": "rise-of-remote-work",
|
||||||
|
"status": "published",
|
||||||
|
"content": {
|
||||||
|
"excerpt": "Remote work is here to stay. In this post, we'll explore the benefits and challenges of working from home and how to create a productive and healthy remote work environment.",
|
||||||
|
"content": "<p>The way we work has been fundamentally transformed in recent years. Remote work has gone from a niche perk to a mainstream reality for millions of people around the world.</p><p>This shift has brought with it a host of new opportunities and challenges. We'll discuss how to stay focused and motivated while working from home, how to maintain a healthy work-life balance, and how companies can build strong remote teams.</p>",
|
||||||
|
"publishDate": "2024-09-28",
|
||||||
|
"author": "Chloe Davis",
|
||||||
|
"tags": ["Remote Work", "Productivity", "Future of Work"],
|
||||||
|
"meta_title": "Navigating the World of Remote Work",
|
||||||
|
"meta_description": "Learn how to thrive in a remote work environment."
|
||||||
|
},
|
||||||
|
"languageCode": "en-US",
|
||||||
|
"isDefault": true,
|
||||||
|
"id": "post-5",
|
||||||
|
"translation_group_id": "tg-remote-work",
|
||||||
|
"lifecycle": {
|
||||||
|
"state": "published",
|
||||||
|
"published_at": "2024-09-28T10:00:00Z",
|
||||||
|
"timezone": "UTC"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"canonical": "https://example.com/blog/rise-of-remote-work",
|
||||||
|
"og_title": "The Rise of Remote Work",
|
||||||
|
"og_description": "The benefits and challenges of working from home.",
|
||||||
|
"twitter_card": "summary_large_image"
|
||||||
|
},
|
||||||
|
"taxonomy": {
|
||||||
|
"categories": ["Work", "Productivity"],
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
"relations": {
|
||||||
|
"related_posts": ["post-2", "post-4"]
|
||||||
|
},
|
||||||
|
"assets": {
|
||||||
|
"hero_image": {
|
||||||
|
"url": "https://example.com/images/remote-work.jpg",
|
||||||
|
"alt": "A person working on a laptop in a comfortable home office setting."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,8 +14,7 @@ services:
|
|||||||
- DB_PASSWORD=postgres
|
- DB_PASSWORD=postgres
|
||||||
- DB_NAME=tercul
|
- DB_NAME=tercul
|
||||||
- REDIS_ADDR=redis:6379
|
- REDIS_ADDR=redis:6379
|
||||||
- WEAVIATE_HOST=weaviate:8080
|
- WEAVIATE_HOST=http://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.25.3
|
go 1.24.10
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.72
|
github.com/99designs/gqlgen v0.17.72
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
package graphql
|
package graphql
|
||||||
|
|
||||||
import (
|
import "context"
|
||||||
"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 uuid.UUID, preferredLanguage string) *string {
|
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, 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 uuid.UUID) (*domain.Like, error) {
|
func (m *mockLikeRepository) GetByID(ctx context.Context, id uint) (*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,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strconv"
|
||||||
"tercul/internal/adapters/graphql/model"
|
"tercul/internal/adapters/graphql/model"
|
||||||
"tercul/internal/app/auth"
|
"tercul/internal/app/auth"
|
||||||
"tercul/internal/app/author"
|
"tercul/internal/app/author"
|
||||||
@ -25,27 +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 {
|
|
||||||
switch strings.ToLower(string(role)) {
|
|
||||||
case "reader":
|
|
||||||
return model.UserRoleReader
|
|
||||||
case "contributor":
|
|
||||||
return model.UserRoleContributor
|
|
||||||
case "reviewer":
|
|
||||||
return model.UserRoleReviewer
|
|
||||||
case "editor":
|
|
||||||
return model.UserRoleEditor
|
|
||||||
case "admin":
|
|
||||||
return model.UserRoleAdmin
|
|
||||||
default:
|
|
||||||
return model.UserRoleReader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register is the resolver for the register field.
|
// Register is the resolver for the register field.
|
||||||
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
|
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
|
||||||
// Convert GraphQL input to service input
|
// Convert GraphQL input to service input
|
||||||
@ -73,7 +54,7 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
|
|||||||
FirstName: &authResponse.User.FirstName,
|
FirstName: &authResponse.User.FirstName,
|
||||||
LastName: &authResponse.User.LastName,
|
LastName: &authResponse.User.LastName,
|
||||||
DisplayName: &authResponse.User.DisplayName,
|
DisplayName: &authResponse.User.DisplayName,
|
||||||
Role: toModelUserRole(authResponse.User.Role),
|
Role: model.UserRole(authResponse.User.Role),
|
||||||
Verified: authResponse.User.Verified,
|
Verified: authResponse.User.Verified,
|
||||||
Active: authResponse.User.Active,
|
Active: authResponse.User.Active,
|
||||||
},
|
},
|
||||||
@ -104,7 +85,7 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
|
|||||||
FirstName: &authResponse.User.FirstName,
|
FirstName: &authResponse.User.FirstName,
|
||||||
LastName: &authResponse.User.LastName,
|
LastName: &authResponse.User.LastName,
|
||||||
DisplayName: &authResponse.User.DisplayName,
|
DisplayName: &authResponse.User.DisplayName,
|
||||||
Role: toModelUserRole(authResponse.User.Role),
|
Role: model.UserRole(authResponse.User.Role),
|
||||||
Verified: authResponse.User.Verified,
|
Verified: authResponse.User.Verified,
|
||||||
Active: authResponse.User.Active,
|
Active: authResponse.User.Active,
|
||||||
},
|
},
|
||||||
@ -145,7 +126,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: createdWork.ID.String(),
|
ID: fmt.Sprintf("%d", createdWork.ID),
|
||||||
Name: createdWork.Title,
|
Name: createdWork.Title,
|
||||||
Language: createdWork.Language,
|
Language: createdWork.Language,
|
||||||
Content: input.Content,
|
Content: input.Content,
|
||||||
@ -158,7 +139,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := uuid.Parse(id)
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -166,7 +147,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: workID},
|
BaseModel: domain.BaseModel{ID: uint(workID)},
|
||||||
Language: input.Language,
|
Language: input.Language,
|
||||||
},
|
},
|
||||||
Title: input.Name,
|
Title: input.Name,
|
||||||
@ -189,12 +170,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 := uuid.Parse(id)
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, workID)
|
err = r.App.Work.Commands.DeleteWork(ctx, uint(workID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -208,7 +189,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := uuid.Parse(input.WorkID)
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -222,7 +203,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: workID,
|
TranslatableID: uint(workID),
|
||||||
TranslatableType: "works",
|
TranslatableType: "works",
|
||||||
}
|
}
|
||||||
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
|
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
|
||||||
@ -231,7 +212,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), workID); err != nil {
|
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), uint(workID)); err != nil {
|
||||||
log.Error(err, "failed to increment work translation count")
|
log.Error(err, "failed to increment work translation count")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -251,7 +232,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := uuid.Parse(input.WorkID)
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -265,7 +246,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: workID,
|
TranslatableID: uint(workID),
|
||||||
TranslatableType: "works",
|
TranslatableType: "works",
|
||||||
}
|
}
|
||||||
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
|
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
|
||||||
@ -284,12 +265,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 := uuid.Parse(id)
|
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, translationID)
|
err = r.App.Translation.Commands.DeleteTranslation(ctx, uint(translationID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -330,13 +311,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bookID, err := uuid.Parse(id)
|
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: bookID,
|
ID: uint(bookID),
|
||||||
Title: &input.Name,
|
Title: &input.Name,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
Language: &input.Language,
|
Language: &input.Language,
|
||||||
@ -359,12 +340,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 := uuid.Parse(id)
|
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, bookID)
|
err = r.App.Book.Commands.DeleteBook(ctx, uint(bookID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -397,13 +378,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 := uuid.Parse(id)
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: authorID,
|
ID: uint(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)
|
||||||
@ -420,12 +401,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 := uuid.Parse(id)
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, authorID)
|
err = r.App.Author.Commands.DeleteAuthor(ctx, uint(authorID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -439,13 +420,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := uuid.Parse(id)
|
userID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: userID,
|
ID: uint(userID),
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
Email: input.Email,
|
Email: input.Email,
|
||||||
Password: input.Password,
|
Password: input.Password,
|
||||||
@ -464,25 +445,28 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.CountryID != nil {
|
if input.CountryID != nil {
|
||||||
countryID, err := uuid.Parse(*input.CountryID)
|
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.CountryID = &countryID
|
uid := uint(countryID)
|
||||||
|
updateInput.CountryID = &uid
|
||||||
}
|
}
|
||||||
if input.CityID != nil {
|
if input.CityID != nil {
|
||||||
cityID, err := uuid.Parse(*input.CityID)
|
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.CityID = &cityID
|
uid := uint(cityID)
|
||||||
|
updateInput.CityID = &uid
|
||||||
}
|
}
|
||||||
if input.AddressID != nil {
|
if input.AddressID != nil {
|
||||||
addressID, err := uuid.Parse(*input.AddressID)
|
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.AddressID = &addressID
|
uid := uint(addressID)
|
||||||
|
updateInput.AddressID = &uid
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
||||||
@ -499,7 +483,7 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
|
|||||||
DisplayName: &updatedUser.DisplayName,
|
DisplayName: &updatedUser.DisplayName,
|
||||||
Bio: &updatedUser.Bio,
|
Bio: &updatedUser.Bio,
|
||||||
AvatarURL: &updatedUser.AvatarURL,
|
AvatarURL: &updatedUser.AvatarURL,
|
||||||
Role: toModelUserRole(updatedUser.Role),
|
Role: model.UserRole(updatedUser.Role),
|
||||||
Verified: updatedUser.Verified,
|
Verified: updatedUser.Verified,
|
||||||
Active: updatedUser.Active,
|
Active: updatedUser.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -507,12 +491,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 := uuid.Parse(id)
|
userID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, userID)
|
err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -556,13 +540,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionID, err := uuid.Parse(id)
|
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: collectionID,
|
ID: uint(collectionID),
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
@ -591,12 +575,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
|
|||||||
return false, fmt.Errorf("unauthorized")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collectionID, err := uuid.Parse(id)
|
collectionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, collectionID, userID)
|
err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -611,18 +595,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collID, err := uuid.Parse(collectionID)
|
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||||
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 := uuid.Parse(workID)
|
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||||
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: collID,
|
CollectionID: uint(collID),
|
||||||
WorkID: wID,
|
WorkID: uint(wID),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
|
||||||
@ -630,7 +614,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
|
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -649,18 +633,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
collID, err := uuid.Parse(collectionID)
|
collID, err := strconv.ParseUint(collectionID, 10, 32)
|
||||||
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 := uuid.Parse(workID)
|
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||||
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: collID,
|
CollectionID: uint(collID),
|
||||||
WorkID: wID,
|
WorkID: uint(wID),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
|
||||||
@ -668,7 +652,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
|
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -696,25 +680,28 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := uuid.Parse(*input.WorkID)
|
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||||
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.WorkID = &workID
|
wID := uint(workID)
|
||||||
|
createInput.WorkID = &wID
|
||||||
}
|
}
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := uuid.Parse(*input.TranslationID)
|
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
createInput.TranslationID = &translationID
|
tID := uint(translationID)
|
||||||
|
createInput.TranslationID = &tID
|
||||||
}
|
}
|
||||||
if input.ParentCommentID != nil {
|
if input.ParentCommentID != nil {
|
||||||
parentCommentID, err := uuid.Parse(*input.ParentCommentID)
|
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
|
||||||
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)
|
||||||
}
|
}
|
||||||
createInput.ParentID = &parentCommentID
|
pID := uint(parentCommentID)
|
||||||
|
createInput.ParentID = &pID
|
||||||
}
|
}
|
||||||
|
|
||||||
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
|
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
|
||||||
@ -749,12 +736,12 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
commentID, err := uuid.Parse(id)
|
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, commentID)
|
commentModel, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -767,7 +754,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateInput := comment.UpdateCommentInput{
|
updateInput := comment.UpdateCommentInput{
|
||||||
ID: commentID,
|
ID: uint(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)
|
||||||
@ -791,12 +778,12 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
|
|||||||
return false, fmt.Errorf("unauthorized")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
commentID, err := uuid.Parse(id)
|
commentID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, commentID)
|
comment, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -808,7 +795,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, commentID)
|
err = r.App.Comment.Commands.DeleteComment(ctx, uint(commentID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -834,25 +821,28 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := uuid.Parse(*input.WorkID)
|
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||||
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.WorkID = &workID
|
wID := uint(workID)
|
||||||
|
createInput.WorkID = &wID
|
||||||
}
|
}
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := uuid.Parse(*input.TranslationID)
|
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
createInput.TranslationID = &translationID
|
tID := uint(translationID)
|
||||||
|
createInput.TranslationID = &tID
|
||||||
}
|
}
|
||||||
if input.CommentID != nil {
|
if input.CommentID != nil {
|
||||||
commentID, err := uuid.Parse(*input.CommentID)
|
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
return nil, fmt.Errorf("invalid comment ID: %v", err)
|
||||||
}
|
}
|
||||||
createInput.CommentID = &commentID
|
cID := uint(commentID)
|
||||||
|
createInput.CommentID = &cID
|
||||||
}
|
}
|
||||||
|
|
||||||
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
|
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
|
||||||
@ -884,12 +874,12 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
|
|||||||
return false, fmt.Errorf("unauthorized")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
likeID, err := uuid.Parse(id)
|
likeID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, likeID)
|
like, err := r.App.Like.Queries.Like(ctx, uint(likeID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -901,7 +891,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, likeID)
|
err = r.App.Like.Commands.DeleteLike(ctx, uint(likeID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -916,14 +906,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
|||||||
return nil, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
workID, err := uuid.Parse(input.WorkID)
|
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
|
||||||
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: workID,
|
WorkID: uint(workID),
|
||||||
}
|
}
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
createInput.Name = *input.Name
|
createInput.Name = *input.Name
|
||||||
@ -934,7 +924,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, workID); err != nil {
|
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, uint(workID)); err != nil {
|
||||||
log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
|
log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -953,12 +943,12 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
|
|||||||
return false, fmt.Errorf("unauthorized")
|
return false, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkID, err := uuid.Parse(id)
|
bookmarkID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, bookmarkID)
|
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, uint(bookmarkID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -970,7 +960,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, bookmarkID)
|
err = r.App.Bookmark.Commands.DeleteBookmark(ctx, uint(bookmarkID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -990,19 +980,21 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.WorkID != nil {
|
if input.WorkID != nil {
|
||||||
workID, err := uuid.Parse(*input.WorkID)
|
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
|
||||||
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.WorkID = &workID
|
wID := uint(workID)
|
||||||
|
createInput.WorkID = &wID
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.TranslationID != nil {
|
if input.TranslationID != nil {
|
||||||
translationID, err := uuid.Parse(*input.TranslationID)
|
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
return nil, fmt.Errorf("invalid translation ID: %v", err)
|
||||||
}
|
}
|
||||||
createInput.TranslationID = &translationID
|
tID := uint(translationID)
|
||||||
|
createInput.TranslationID = &tID
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Status != nil {
|
if input.Status != nil {
|
||||||
@ -1033,13 +1025,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
|
|||||||
return nil, domain.ErrUnauthorized
|
return nil, domain.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
contributionID, err := uuid.Parse(id)
|
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: contributionID,
|
ID: uint(contributionID),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Name: &input.Name,
|
Name: &input.Name,
|
||||||
}
|
}
|
||||||
@ -1071,12 +1063,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
|
|||||||
return false, domain.ErrUnauthorized
|
return false, domain.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
contributionID, err := uuid.Parse(id)
|
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, contributionID, userID)
|
err = r.App.Contribution.Commands.DeleteContribution(ctx, uint(contributionID), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -1086,13 +1078,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 := uuid.Parse(id)
|
contributionID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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: contributionID,
|
ID: uint(contributionID),
|
||||||
Status: status.String(),
|
Status: status.String(),
|
||||||
Feedback: feedback,
|
Feedback: feedback,
|
||||||
}
|
}
|
||||||
@ -1137,7 +1129,7 @@ func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload
|
|||||||
FirstName: &authResponse.User.FirstName,
|
FirstName: &authResponse.User.FirstName,
|
||||||
LastName: &authResponse.User.LastName,
|
LastName: &authResponse.User.LastName,
|
||||||
DisplayName: &authResponse.User.DisplayName,
|
DisplayName: &authResponse.User.DisplayName,
|
||||||
Role: toModelUserRole(authResponse.User.Role),
|
Role: model.UserRole(authResponse.User.Role),
|
||||||
Verified: authResponse.User.Verified,
|
Verified: authResponse.User.Verified,
|
||||||
Active: authResponse.User.Active,
|
Active: authResponse.User.Active,
|
||||||
},
|
},
|
||||||
@ -1201,25 +1193,28 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.CountryID != nil {
|
if input.CountryID != nil {
|
||||||
countryID, err := uuid.Parse(*input.CountryID)
|
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid country ID: %v", err)
|
return nil, fmt.Errorf("invalid country ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.CountryID = &countryID
|
uid := uint(countryID)
|
||||||
|
updateInput.CountryID = &uid
|
||||||
}
|
}
|
||||||
if input.CityID != nil {
|
if input.CityID != nil {
|
||||||
cityID, err := uuid.Parse(*input.CityID)
|
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid city ID: %v", err)
|
return nil, fmt.Errorf("invalid city ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.CityID = &cityID
|
uid := uint(cityID)
|
||||||
|
updateInput.CityID = &uid
|
||||||
}
|
}
|
||||||
if input.AddressID != nil {
|
if input.AddressID != nil {
|
||||||
addressID, err := uuid.Parse(*input.AddressID)
|
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid address ID: %v", err)
|
return nil, fmt.Errorf("invalid address ID: %v", err)
|
||||||
}
|
}
|
||||||
updateInput.AddressID = &addressID
|
uid := uint(addressID)
|
||||||
|
updateInput.AddressID = &uid
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
|
||||||
@ -1236,7 +1231,7 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
|
|||||||
DisplayName: &updatedUser.DisplayName,
|
DisplayName: &updatedUser.DisplayName,
|
||||||
Bio: &updatedUser.Bio,
|
Bio: &updatedUser.Bio,
|
||||||
AvatarURL: &updatedUser.AvatarURL,
|
AvatarURL: &updatedUser.AvatarURL,
|
||||||
Role: toModelUserRole(updatedUser.Role),
|
Role: model.UserRole(updatedUser.Role),
|
||||||
Verified: updatedUser.Verified,
|
Verified: updatedUser.Verified,
|
||||||
Active: updatedUser.Active,
|
Active: updatedUser.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1265,12 +1260,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 := uuid.Parse(id)
|
workID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, workID)
|
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(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
|
||||||
@ -1279,7 +1274,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(), workID); err != nil {
|
if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil {
|
||||||
log.Error(err, "failed to increment work views")
|
log.Error(err, "failed to increment work views")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -1325,12 +1320,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 := uuid.Parse(id)
|
translationID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, translationID)
|
translationDTO, err := r.App.Translation.Queries.Translation(ctx, uint(translationID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1339,7 +1334,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), translationID); err != nil {
|
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), uint(translationID)); err != nil {
|
||||||
log.Error(err, "failed to increment translation views")
|
log.Error(err, "failed to increment translation views")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -1355,7 +1350,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 := uuid.Parse(workID)
|
wID, err := strconv.ParseUint(workID, 10, 32)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -1369,7 +1364,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, wID, language, page, pageSize)
|
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, uint(wID), language, page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1389,12 +1384,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 := uuid.Parse(id)
|
bookID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, bookID)
|
bookRecord, err := r.App.Book.Queries.Book(ctx, uint(bookID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1434,12 +1429,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 := uuid.Parse(id)
|
authorID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, authorID)
|
authorRecord, err := r.App.Author.Queries.Author(ctx, uint(authorID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1468,17 +1463,18 @@ 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 countryIDUUID *uuid.UUID
|
var countryIDUint *uint
|
||||||
|
|
||||||
if countryID != nil {
|
if countryID != nil {
|
||||||
parsedID, err := uuid.Parse(*countryID)
|
parsedID, err := strconv.ParseUint(*countryID, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
countryIDUUID = &parsedID
|
uid := uint(parsedID)
|
||||||
|
countryIDUint = &uid
|
||||||
}
|
}
|
||||||
|
|
||||||
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUUID)
|
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1507,12 +1503,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 := uuid.Parse(id)
|
userID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, userID)
|
userRecord, err := r.App.User.Queries.User(ctx, uint(userID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1529,7 +1525,7 @@ func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error
|
|||||||
DisplayName: &userRecord.DisplayName,
|
DisplayName: &userRecord.DisplayName,
|
||||||
Bio: &userRecord.Bio,
|
Bio: &userRecord.Bio,
|
||||||
AvatarURL: &userRecord.AvatarURL,
|
AvatarURL: &userRecord.AvatarURL,
|
||||||
Role: toModelUserRole(userRecord.Role),
|
Role: model.UserRole(userRecord.Role),
|
||||||
Verified: userRecord.Verified,
|
Verified: userRecord.Verified,
|
||||||
Active: userRecord.Active,
|
Active: userRecord.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1554,7 +1550,7 @@ func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.U
|
|||||||
DisplayName: &userRecord.DisplayName,
|
DisplayName: &userRecord.DisplayName,
|
||||||
Bio: &userRecord.Bio,
|
Bio: &userRecord.Bio,
|
||||||
AvatarURL: &userRecord.AvatarURL,
|
AvatarURL: &userRecord.AvatarURL,
|
||||||
Role: toModelUserRole(userRecord.Role),
|
Role: model.UserRole(userRecord.Role),
|
||||||
Verified: userRecord.Verified,
|
Verified: userRecord.Verified,
|
||||||
Active: userRecord.Active,
|
Active: userRecord.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1579,7 +1575,7 @@ func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*m
|
|||||||
DisplayName: &userRecord.DisplayName,
|
DisplayName: &userRecord.DisplayName,
|
||||||
Bio: &userRecord.Bio,
|
Bio: &userRecord.Bio,
|
||||||
AvatarURL: &userRecord.AvatarURL,
|
AvatarURL: &userRecord.AvatarURL,
|
||||||
Role: toModelUserRole(userRecord.Role),
|
Role: model.UserRole(userRecord.Role),
|
||||||
Verified: userRecord.Verified,
|
Verified: userRecord.Verified,
|
||||||
Active: userRecord.Active,
|
Active: userRecord.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1668,7 +1664,7 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
|
|||||||
DisplayName: &userRecord.DisplayName,
|
DisplayName: &userRecord.DisplayName,
|
||||||
Bio: &userRecord.Bio,
|
Bio: &userRecord.Bio,
|
||||||
AvatarURL: &userRecord.AvatarURL,
|
AvatarURL: &userRecord.AvatarURL,
|
||||||
Role: toModelUserRole(userRecord.Role),
|
Role: model.UserRole(userRecord.Role),
|
||||||
Verified: userRecord.Verified,
|
Verified: userRecord.Verified,
|
||||||
Active: userRecord.Active,
|
Active: userRecord.Active,
|
||||||
}, nil
|
}, nil
|
||||||
@ -1676,12 +1672,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 := uuid.Parse(userID)
|
uID, err := strconv.ParseUint(userID, 10, 32)
|
||||||
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, uID)
|
profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1689,12 +1685,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, uID)
|
user, err := r.App.User.Queries.User(ctx, uint(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 %s", profile.ID.String())
|
return nil, fmt.Errorf("user not found for profile %d", profile.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.UserProfile{
|
return &model.UserProfile{
|
||||||
@ -1709,7 +1705,7 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
|
|||||||
DisplayName: &user.DisplayName,
|
DisplayName: &user.DisplayName,
|
||||||
Bio: &user.Bio,
|
Bio: &user.Bio,
|
||||||
AvatarURL: &user.AvatarURL,
|
AvatarURL: &user.AvatarURL,
|
||||||
Role: toModelUserRole(user.Role),
|
Role: model.UserRole(user.Role),
|
||||||
Verified: user.Verified,
|
Verified: user.Verified,
|
||||||
Active: user.Active,
|
Active: user.Active,
|
||||||
},
|
},
|
||||||
@ -1724,12 +1720,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 := uuid.Parse(id)
|
collID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, collID)
|
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1737,7 +1733,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, collID)
|
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, uint(collID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1770,11 +1766,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 := uuid.Parse(*userID)
|
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
||||||
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, uID)
|
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uint(uID))
|
||||||
} else {
|
} else {
|
||||||
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
|
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
|
||||||
}
|
}
|
||||||
@ -1815,18 +1811,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 := uuid.Parse(id)
|
tagID, err := strconv.ParseUint(id, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tag, err := r.App.Tag.Queries.Tag(ctx, tagID)
|
tag, err := r.App.Tag.Queries.Tag(ctx, uint(tagID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.Tag{
|
return &model.Tag{
|
||||||
ID: tag.ID.String(),
|
ID: fmt.Sprintf("%d", tag.ID),
|
||||||
Name: tag.Name,
|
Name: tag.Name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1851,12 +1847,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 := uuid.Parse(id)
|
categoryID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, categoryID)
|
category, err := r.App.Category.Queries.Category(ctx, uint(categoryID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1865,7 +1861,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.Category{
|
return &model.Category{
|
||||||
ID: category.ID.String(),
|
ID: fmt.Sprintf("%d", category.ID),
|
||||||
Name: category.Name,
|
Name: category.Name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1890,12 +1886,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 := uuid.Parse(id)
|
cID, err := strconv.ParseUint(id, 10, 32)
|
||||||
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, cID)
|
commentRecord, err := r.App.Comment.Queries.Comment(ctx, uint(cID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1904,10 +1900,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &model.Comment{
|
return &model.Comment{
|
||||||
ID: commentRecord.ID.String(),
|
ID: fmt.Sprintf("%d", commentRecord.ID),
|
||||||
Text: commentRecord.Text,
|
Text: commentRecord.Text,
|
||||||
User: &model.User{
|
User: &model.User{
|
||||||
ID: commentRecord.UserID.String(),
|
ID: fmt.Sprintf("%d", commentRecord.UserID),
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1918,23 +1914,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 := uuid.Parse(*workID)
|
wID, idErr := strconv.ParseUint(*workID, 10, 32)
|
||||||
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, wID)
|
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, uint(wID))
|
||||||
} else if translationID != nil {
|
} else if translationID != nil {
|
||||||
tID, idErr := uuid.Parse(*translationID)
|
tID, idErr := strconv.ParseUint(*translationID, 10, 32)
|
||||||
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, tID)
|
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, uint(tID))
|
||||||
} else if userID != nil {
|
} else if userID != nil {
|
||||||
uID, idErr := uuid.Parse(*userID)
|
uID, idErr := strconv.ParseUint(*userID, 10, 32)
|
||||||
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, uID)
|
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uint(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"
|
||||||
"tercul/internal/adapters/graphql/model"
|
"testing"
|
||||||
"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 uuid.UUID) (*domain.User, error) {
|
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uint) (*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 uuid.UUID) (*domain.UserProfile, error) {
|
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*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 uuid.UUID) (*domain.UserProfile, error) {
|
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*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 uuid.UUID) (*domain.Work, error) {
|
func (m *mockWorkRepository) GetByID(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 {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ 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"
|
||||||
@ -10,7 +11,6 @@ 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 uuid.UUID) (*domain.Work, error) {
|
func (m *mockWorkRepository) GetByID(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 {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
@ -52,18 +52,10 @@ 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) {
|
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { return nil, nil }
|
||||||
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) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[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) 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 {
|
||||||
@ -71,47 +63,24 @@ 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) {
|
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) { return nil, nil }
|
||||||
return nil, nil
|
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { return nil, nil }
|
||||||
}
|
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil }
|
||||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { return nil, nil }
|
||||||
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) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
||||||
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) {
|
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) { return nil, nil }
|
||||||
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) {
|
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||||
return 0, nil
|
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { return nil, nil }
|
||||||
}
|
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { return nil, nil }
|
||||||
func (m *mockWorkRepository) 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 {
|
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockAuthorRepository struct{ mock.Mock }
|
type mockAuthorRepository struct{ mock.Mock }
|
||||||
|
|
||||||
@ -127,111 +96,57 @@ 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 uuid.UUID) (*domain.Author, error) {
|
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
||||||
return nil, nil
|
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { return nil, nil }
|
||||||
}
|
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { return nil, nil }
|
||||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { return nil, nil }
|
||||||
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) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { return nil, nil }
|
||||||
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 {
|
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil }
|
||||||
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 {
|
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||||
return nil
|
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { return nil, nil }
|
||||||
}
|
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { return nil, nil }
|
||||||
func (m *mockAuthorRepository) 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) {
|
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||||
return 0, nil
|
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { return nil, nil }
|
||||||
}
|
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { return nil, nil }
|
||||||
func (m *mockAuthorRepository) 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 {
|
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockUserRepository struct{ mock.Mock }
|
type mockUserRepository struct{ mock.Mock }
|
||||||
|
|
||||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
|
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*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) {
|
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { return nil, nil }
|
||||||
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) 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 {
|
func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil }
|
||||||
return nil
|
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) { return nil, 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 {
|
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil }
|
||||||
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) {
|
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[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) 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) {
|
func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||||
return 0, nil
|
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) { return nil, nil }
|
||||||
}
|
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) { return nil, nil }
|
||||||
func (m *mockUserRepository) 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 {
|
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockSearchClient struct{ mock.Mock }
|
type mockSearchClient struct{ mock.Mock }
|
||||||
|
|
||||||
@ -247,6 +162,7 @@ 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 {
|
||||||
@ -258,62 +174,26 @@ 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 {
|
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error { return nil }
|
||||||
return nil
|
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil }
|
||||||
}
|
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil }
|
||||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error { return nil }
|
||||||
return nil
|
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||||
}
|
|
||||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||||
return nil
|
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil }
|
||||||
}
|
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error { return nil }
|
||||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { return nil, nil }
|
||||||
return 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) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error { return nil }
|
||||||
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) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { return nil }
|
||||||
return nil, nil
|
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return 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) {
|
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) { return nil, nil }
|
||||||
return nil, nil
|
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { return 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 }
|
||||||
|
|
||||||
@ -321,69 +201,30 @@ 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 uuid.UUID) (*domain.Translation, error) {
|
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil }
|
||||||
return nil, nil
|
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil }
|
||||||
}
|
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
||||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { return nil, 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) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { return nil }
|
||||||
return nil, 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) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error { return nil }
|
||||||
return nil, nil
|
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return 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 {
|
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||||
return nil
|
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
||||||
}
|
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { return nil, nil }
|
||||||
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) { return nil, nil }
|
||||||
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) {
|
func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||||
return 0, nil
|
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return nil, nil }
|
||||||
}
|
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { return nil, nil }
|
||||||
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) {
|
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||||
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 {
|
func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||||
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 {
|
||||||
|
|||||||
1
internal/adapters/http/.keep
Normal file
1
internal/adapters/http/.keep
Normal file
@ -0,0 +1 @@
|
|||||||
|
# This file is created to ensure the directory structure is in place.
|
||||||
@ -4,19 +4,17 @@ 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 uuid.UUID, field string, value int) error
|
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error
|
||||||
IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error
|
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error
|
||||||
UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
|
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||||
UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error
|
UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
||||||
GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error)
|
GetOrCreateUserEngagement(ctx context.Context, userID uint, 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,36 +13,34 @@ 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 uuid.UUID) error
|
IncrementWorkViews(ctx context.Context, workID uint) error
|
||||||
IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
IncrementWorkLikes(ctx context.Context, workID uint) error
|
||||||
IncrementWorkComments(ctx context.Context, workID uuid.UUID) error
|
IncrementWorkComments(ctx context.Context, workID uint) error
|
||||||
IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error
|
IncrementWorkBookmarks(ctx context.Context, workID uint) error
|
||||||
IncrementWorkShares(ctx context.Context, workID uuid.UUID) error
|
IncrementWorkShares(ctx context.Context, workID uint) error
|
||||||
IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error
|
IncrementWorkTranslationCount(ctx context.Context, workID uint) error
|
||||||
IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error
|
IncrementTranslationViews(ctx context.Context, translationID uint) error
|
||||||
IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
IncrementTranslationLikes(ctx context.Context, translationID uint) error
|
||||||
DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error
|
DecrementWorkLikes(ctx context.Context, workID uint) error
|
||||||
DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
|
DecrementTranslationLikes(ctx context.Context, translationID uint) error
|
||||||
IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error
|
IncrementTranslationComments(ctx context.Context, translationID uint) error
|
||||||
IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error
|
IncrementTranslationShares(ctx context.Context, translationID uint) error
|
||||||
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
|
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||||
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
|
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
|
||||||
|
|
||||||
UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error
|
UpdateWorkReadingTime(ctx context.Context, workID uint) error
|
||||||
UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error
|
UpdateWorkComplexity(ctx context.Context, workID uint) error
|
||||||
UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error
|
UpdateWorkSentiment(ctx context.Context, workID uint) error
|
||||||
UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error
|
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error
|
||||||
UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error
|
UpdateTranslationSentiment(ctx context.Context, translationID uint) error
|
||||||
|
|
||||||
UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error
|
UpdateUserEngagement(ctx context.Context, userID uint, 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 uuid.UUID, stats domain.WorkStats) error
|
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
@ -65,91 +63,91 @@ func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, tr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) IncrementWorkViews(ctx context.Context, workID uuid.UUID) error {
|
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) DecrementWorkLikes(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) 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 uuid.UUID) error {
|
func (s *service) DecrementTranslationLikes(ctx context.Context, translationID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) 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 uuid.UUID) error {
|
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) 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 uuid.UUID) (*domain.WorkStats, error) {
|
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*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 uuid.UUID) (*domain.TranslationStats, error) {
|
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*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 uuid.UUID) error {
|
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) 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)
|
||||||
@ -176,7 +174,7 @@ func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) e
|
|||||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error {
|
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) 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)
|
||||||
@ -200,7 +198,7 @@ func (s *service) UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) er
|
|||||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error {
|
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) 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)
|
||||||
@ -229,7 +227,7 @@ func (s *service) UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) err
|
|||||||
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
return s.repo.UpdateWorkStats(ctx, workID, *stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error {
|
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) 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)
|
||||||
@ -257,7 +255,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 uuid.UUID) error {
|
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) 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)
|
||||||
@ -284,7 +282,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 uuid.UUID, eventType string) error {
|
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, 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)
|
||||||
@ -317,7 +315,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 uuid.UUID, stats domain.WorkStats) error {
|
func (s *service) UpdateWorkStats(ctx context.Context, workID uint, 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,7 +11,6 @@ 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"
|
||||||
)
|
)
|
||||||
@ -179,7 +178,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 uuid.UUID
|
UserID uint
|
||||||
CurrentPassword string
|
CurrentPassword string
|
||||||
NewPassword string
|
NewPassword string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ 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 uuid.UUID) (*domain.User, error) {
|
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||||
if m.getByIDFunc != nil {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -36,7 +34,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 uuid.UUID
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +53,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 uuid.UUID) error {
|
func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uint) error {
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,12 +16,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 uuid.UUID) (*domain.Author, error) {
|
func (q *AuthorQueries) Author(ctx context.Context, id uint) (*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 *uuid.UUID) ([]*domain.Author, error) {
|
func (q *AuthorQueries) Authors(ctx context.Context, countryID *uint) ([]*domain.Author, error) {
|
||||||
var authors []domain.Author
|
var authors []domain.Author
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -45,6 +43,6 @@ func (q *AuthorQueries) Authors(ctx context.Context, countryID *uuid.UUID) ([]*d
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 uuid.UUID) (*domain.Author, error) {
|
func (q *AuthorQueries) AuthorWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||||
return q.repo.GetWithTranslations(ctx, id)
|
return q.repo.GetWithTranslations(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@ 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.
|
||||||
@ -28,7 +26,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 uuid.UUID, work *domain.Work) (bool, error) {
|
func (s *Service) CanEditWork(ctx context.Context, userID uint, 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
|
||||||
@ -63,7 +61,7 @@ func (s *Service) CanEditWork(ctx context.Context, userID uuid.UUID, work *domai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 uuid.UUID, work *domain.Work) (bool, error) {
|
func (s *Service) CanDeleteWork(ctx context.Context, userID uint, 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
|
||||||
@ -98,7 +96,7 @@ func (s *Service) CanDeleteWork(ctx context.Context, userID uuid.UUID, work *dom
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 uuid.UUID, translatableType string, translatableID uuid.UUID) (bool, error) {
|
func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableType string, translatableID uint) (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.
|
||||||
@ -116,7 +114,7 @@ func (s *Service) CanEditEntity(ctx context.Context, userID uuid.UUID, translata
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 uuid.UUID, translationID uuid.UUID) (bool, error) {
|
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uint, translationID uint) (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
|
||||||
@ -159,7 +157,7 @@ func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CanEditTranslation(ctx context.Context, userID uuid.UUID, translationID uuid.UUID) (bool, error) {
|
func (s *Service) CanEditTranslation(ctx context.Context, userID uint, translationID uint) (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
|
||||||
@ -213,7 +211,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 uuid.UUID) (bool, error) {
|
func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint) (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
|
||||||
@ -234,7 +232,7 @@ func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uuid.
|
|||||||
|
|
||||||
// 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 uuid.UUID, comment *domain.Comment) (bool, error) {
|
func (s *Service) CanDeleteComment(ctx context.Context, userID uint, 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,8 +4,6 @@ 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.
|
||||||
@ -28,7 +26,7 @@ type CreateBookInput struct {
|
|||||||
Description string
|
Description string
|
||||||
Language string
|
Language string
|
||||||
ISBN *string
|
ISBN *string
|
||||||
AuthorIDs []uuid.UUID
|
AuthorIDs []uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBook creates a new book.
|
// CreateBook creates a new book.
|
||||||
@ -64,12 +62,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 uuid.UUID
|
ID uint
|
||||||
Title *string
|
Title *string
|
||||||
Description *string
|
Description *string
|
||||||
Language *string
|
Language *string
|
||||||
ISBN *string
|
ISBN *string
|
||||||
AuthorIDs []uuid.UUID
|
AuthorIDs []uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBook updates an existing book.
|
// UpdateBook updates an existing book.
|
||||||
@ -108,7 +106,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 uuid.UUID) error {
|
func (c *BookCommands) DeleteBook(ctx context.Context, id uint) error {
|
||||||
can, err := c.authzSvc.CanDeleteBook(ctx)
|
can, err := c.authzSvc.CanDeleteBook(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,7 +16,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 uuid.UUID) (*domain.Book, error) {
|
func (q *BookQueries) Book(ctx context.Context, id uint) (*domain.Book, error) {
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ 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.
|
||||||
@ -26,8 +24,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 uuid.UUID
|
UserID uint
|
||||||
WorkID uuid.UUID
|
WorkID uint
|
||||||
Notes string
|
Notes string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +55,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 uuid.UUID
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
Notes string
|
Notes string
|
||||||
}
|
}
|
||||||
@ -78,6 +76,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 uuid.UUID) error {
|
func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uint) error {
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,16 +16,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 uuid.UUID) (*domain.Bookmark, error) {
|
func (q *BookmarkQueries) Bookmark(ctx context.Context, id uint) (*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 uuid.UUID) ([]domain.Bookmark, error) {
|
func (q *BookmarkQueries) BookmarksByUserID(ctx context.Context, userID uint) ([]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 uuid.UUID) ([]domain.Bookmark, error) {
|
func (q *BookmarkQueries) BookmarksByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) {
|
||||||
return q.repo.ListByWorkID(ctx, workID)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -21,7 +19,7 @@ func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
|
|||||||
type CreateCategoryInput struct {
|
type CreateCategoryInput struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
ParentID *uuid.UUID
|
ParentID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCategory creates a new category.
|
// CreateCategory creates a new category.
|
||||||
@ -40,10 +38,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 uuid.UUID
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
ParentID *uuid.UUID
|
ParentID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCategory updates an existing category.
|
// UpdateCategory updates an existing category.
|
||||||
@ -63,6 +61,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 uuid.UUID) error {
|
func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uint) error {
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,7 +16,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 uuid.UUID) (*domain.Category, error) {
|
func (q *CategoryQueries) Category(ctx context.Context, id uint) (*domain.Category, error) {
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,12 +26,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 uuid.UUID) ([]domain.Category, error) {
|
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uint) ([]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 *uuid.UUID) ([]domain.Category, error) {
|
func (q *CategoryQueries) CategoriesByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) {
|
||||||
return q.repo.ListByParentID(ctx, parentID)
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,6 @@ 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.
|
||||||
@ -22,7 +20,7 @@ func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands
|
|||||||
type CreateCollectionInput struct {
|
type CreateCollectionInput struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
UserID uuid.UUID
|
UserID uint
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
CoverImageURL string
|
CoverImageURL string
|
||||||
}
|
}
|
||||||
@ -45,12 +43,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 uuid.UUID
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
IsPublic bool
|
IsPublic bool
|
||||||
CoverImageURL string
|
CoverImageURL string
|
||||||
UserID uuid.UUID
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCollection updates an existing collection.
|
// UpdateCollection updates an existing collection.
|
||||||
@ -60,7 +58,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 %s cannot update collection %s", input.UserID, input.ID)
|
return nil, fmt.Errorf("unauthorized: user %d cannot update collection %d", input.UserID, input.ID)
|
||||||
}
|
}
|
||||||
collection.Name = input.Name
|
collection.Name = input.Name
|
||||||
collection.Description = input.Description
|
collection.Description = input.Description
|
||||||
@ -74,22 +72,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 uuid.UUID, userID uuid.UUID) error {
|
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint, userID uint) 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 %s cannot delete collection %s", userID, id)
|
return fmt.Errorf("unauthorized: user %d cannot delete collection %d", 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 uuid.UUID
|
CollectionID uint
|
||||||
WorkID uuid.UUID
|
WorkID uint
|
||||||
UserID uuid.UUID
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWorkToCollection adds a work to a collection.
|
// AddWorkToCollection adds a work to a collection.
|
||||||
@ -99,16 +97,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 %s cannot add work to collection %s", input.UserID, input.CollectionID)
|
return fmt.Errorf("unauthorized: user %d cannot add work to collection %d", 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 uuid.UUID
|
CollectionID uint
|
||||||
WorkID uuid.UUID
|
WorkID uint
|
||||||
UserID uuid.UUID
|
UserID uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveWorkFromCollection removes a work from a collection.
|
// RemoveWorkFromCollection removes a work from a collection.
|
||||||
@ -118,7 +116,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 %s cannot remove work from collection %s", input.UserID, input.CollectionID)
|
return fmt.Errorf("unauthorized: user %d cannot remove work from collection %d", input.UserID, input.CollectionID)
|
||||||
}
|
}
|
||||||
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,12 +16,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 uuid.UUID) (*domain.Collection, error) {
|
func (q *CollectionQueries) Collection(ctx context.Context, id uint) (*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 uuid.UUID) ([]domain.Collection, error) {
|
func (q *CollectionQueries) CollectionsByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) {
|
||||||
return q.repo.ListByUserID(ctx, userID)
|
return q.repo.ListByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +31,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 uuid.UUID) ([]domain.Collection, error) {
|
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
|
||||||
return q.repo.ListByWorkID(ctx, workID)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,6 @@ 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.
|
||||||
@ -32,10 +30,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 uuid.UUID
|
UserID uint
|
||||||
WorkID *uuid.UUID
|
WorkID *uint
|
||||||
TranslationID *uuid.UUID
|
TranslationID *uint
|
||||||
ParentID *uuid.UUID
|
ParentID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment creates a new comment.
|
// CreateComment creates a new comment.
|
||||||
@ -74,7 +72,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 uuid.UUID
|
ID uint
|
||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +86,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 %s not found", domain.ErrEntityNotFound, input.ID)
|
return nil, fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, input.ID)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -110,7 +108,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 uuid.UUID) error {
|
func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.ErrUnauthorized
|
return domain.ErrUnauthorized
|
||||||
@ -119,7 +117,7 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uuid.UUID) 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 %s not found", domain.ErrEntityNotFound, id)
|
return fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, id)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,27 +16,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 uuid.UUID) (*domain.Comment, error) {
|
func (q *CommentQueries) Comment(ctx context.Context, id uint) (*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 uuid.UUID) ([]domain.Comment, error) {
|
func (q *CommentQueries) CommentsByUserID(ctx context.Context, userID uint) ([]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 uuid.UUID) ([]domain.Comment, error) {
|
func (q *CommentQueries) CommentsByWorkID(ctx context.Context, workID uint) ([]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 uuid.UUID) ([]domain.Comment, error) {
|
func (q *CommentQueries) CommentsByTranslationID(ctx context.Context, translationID uint) ([]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 uuid.UUID) ([]domain.Comment, error) {
|
func (q *CommentQueries) CommentsByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) {
|
||||||
return q.repo.ListByParentID(ctx, parentID)
|
return q.repo.ListByParentID(ctx, parentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,6 @@ 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.
|
||||||
@ -27,8 +25,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 *uuid.UUID
|
WorkID *uint
|
||||||
TranslationID *uuid.UUID
|
TranslationID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateContribution creates a new contribution.
|
// CreateContribution creates a new contribution.
|
||||||
@ -58,8 +56,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 uuid.UUID
|
ID uint
|
||||||
UserID uuid.UUID
|
UserID uint
|
||||||
Name *string
|
Name *string
|
||||||
Status *string
|
Status *string
|
||||||
}
|
}
|
||||||
@ -91,7 +89,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 uuid.UUID, userID uuid.UUID) error {
|
func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint, userID uint) 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
|
||||||
@ -107,7 +105,7 @@ func (c *Commands) DeleteContribution(ctx context.Context, contributionID uuid.U
|
|||||||
|
|
||||||
// ReviewContributionInput represents the input for reviewing a contribution.
|
// ReviewContributionInput represents the input for reviewing a contribution.
|
||||||
type ReviewContributionInput struct {
|
type ReviewContributionInput struct {
|
||||||
ID uuid.UUID
|
ID uint
|
||||||
Status string
|
Status string
|
||||||
Feedback *string
|
Feedback *string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ 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.
|
||||||
@ -39,8 +37,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 == uuid.Nil {
|
if copyright.ID == 0 {
|
||||||
return errors.New("copyright ID cannot be nil")
|
return errors.New("copyright ID cannot be zero")
|
||||||
}
|
}
|
||||||
if copyright.Name == "" {
|
if copyright.Name == "" {
|
||||||
return errors.New("copyright name cannot be empty")
|
return errors.New("copyright name cannot be empty")
|
||||||
@ -53,8 +51,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 uuid.UUID) error {
|
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error {
|
||||||
if id == uuid.Nil {
|
if id == 0 {
|
||||||
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")
|
||||||
@ -62,8 +60,8 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uuid.UUID) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddCopyrightToWork adds a copyright to a work.
|
// AddCopyrightToWork adds a copyright to a work.
|
||||||
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
if workID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -71,8 +69,8 @@ func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uuid.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCopyrightFromWork removes a copyright from a work.
|
// RemoveCopyrightFromWork removes a copyright from a work.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error {
|
||||||
if workID == uuid.Nil || copyrightID == uuid.Nil {
|
if workID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -80,8 +78,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
if authorID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -89,8 +87,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error {
|
||||||
if authorID == uuid.Nil || copyrightID == uuid.Nil {
|
if authorID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -98,8 +96,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
if bookID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -107,8 +105,8 @@ func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uuid.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCopyrightFromBook removes a copyright from a book.
|
// RemoveCopyrightFromBook removes a copyright from a book.
|
||||||
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error {
|
||||||
if bookID == uuid.Nil || copyrightID == uuid.Nil {
|
if bookID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -116,8 +114,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -125,8 +123,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error {
|
||||||
if publisherID == uuid.Nil || copyrightID == uuid.Nil {
|
if publisherID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -134,8 +132,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -143,8 +141,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 uuid.UUID, copyrightID uuid.UUID) error {
|
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error {
|
||||||
if sourceID == uuid.Nil || copyrightID == uuid.Nil {
|
if sourceID == 0 || copyrightID == 0 {
|
||||||
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")
|
||||||
@ -156,8 +154,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 == uuid.Nil {
|
if translation.CopyrightID == 0 {
|
||||||
return errors.New("copyright ID cannot be nil")
|
return errors.New("copyright ID cannot be zero")
|
||||||
}
|
}
|
||||||
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 uuid.UUID) (*domain.Copyright, error) {
|
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||||
if m.getByIDFunc != nil {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
@ -165,9 +165,7 @@ 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) {
|
func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||||
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
|
||||||
@ -184,48 +182,40 @@ 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,8 +5,6 @@ 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.
|
||||||
@ -25,8 +23,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 uuid.UUID) (*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) {
|
||||||
if id == uuid.Nil {
|
if id == 0 {
|
||||||
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")
|
||||||
@ -42,7 +40,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 uuid.UUID) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*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 {
|
||||||
@ -52,7 +50,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uuid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
// GetCopyrightsForAuthor gets all copyrights for a specific author.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uuid.UUID) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*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 {
|
||||||
@ -62,7 +60,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 uuid.UUID) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*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 {
|
||||||
@ -72,7 +70,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uuid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
// GetCopyrightsForPublisher gets all copyrights for a specific publisher.
|
||||||
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uuid.UUID) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*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 {
|
||||||
@ -82,7 +80,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 uuid.UUID) ([]*domain.Copyright, error) {
|
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*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 {
|
||||||
@ -92,8 +90,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 uuid.UUID) ([]domain.CopyrightTranslation, error) {
|
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
|
||||||
if copyrightID == uuid.Nil {
|
if copyrightID == 0 {
|
||||||
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")
|
||||||
@ -101,8 +99,8 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uuid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTranslationByLanguage gets a specific translation by language code.
|
// GetTranslationByLanguage gets a specific translation by language code.
|
||||||
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uuid.UUID, languageCode string) (*domain.CopyrightTranslation, error) {
|
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
|
||||||
if copyrightID == uuid.Nil {
|
if copyrightID == 0 {
|
||||||
return nil, errors.New("invalid copyright ID")
|
return nil, errors.New("invalid copyright ID")
|
||||||
}
|
}
|
||||||
if languageCode == "" {
|
if languageCode == "" {
|
||||||
|
|||||||
@ -6,8 +6,6 @@ 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.
|
||||||
@ -26,10 +24,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 uuid.UUID
|
UserID uint
|
||||||
WorkID *uuid.UUID
|
WorkID *uint
|
||||||
TranslationID *uuid.UUID
|
TranslationID *uint
|
||||||
CommentID *uuid.UUID
|
CommentID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLike creates a new like and increments the relevant counter.
|
// CreateLike creates a new like and increments the relevant counter.
|
||||||
@ -71,7 +69,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 uuid.UUID) error {
|
func (c *LikeCommands) DeleteLike(ctx context.Context, id uint) 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,8 +3,6 @@ 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.
|
||||||
@ -18,27 +16,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 uuid.UUID) (*domain.Like, error) {
|
func (q *LikeQueries) Like(ctx context.Context, id uint) (*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 uuid.UUID) ([]domain.Like, error) {
|
func (q *LikeQueries) LikesByUserID(ctx context.Context, userID uint) ([]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 uuid.UUID) ([]domain.Like, error) {
|
func (q *LikeQueries) LikesByWorkID(ctx context.Context, workID uint) ([]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 uuid.UUID) ([]domain.Like, error) {
|
func (q *LikeQueries) LikesByTranslationID(ctx context.Context, translationID uint) ([]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 uuid.UUID) ([]domain.Like, error) {
|
func (q *LikeQueries) LikesByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) {
|
||||||
return q.repo.ListByCommentID(ctx, commentID)
|
return q.repo.ListByCommentID(ctx, commentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -28,11 +26,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 uuid.UUID, language string) (string, error) {
|
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, 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 uuid.UUID, language string) (string, error) {
|
func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) {
|
||||||
return q.repo.GetWorkContent(ctx, workID, language)
|
return q.repo.GetWorkContent(ctx, workID, language)
|
||||||
}
|
}
|
||||||
@ -5,8 +5,6 @@ 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.
|
||||||
@ -20,8 +18,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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
if workID == 0 || monetizationID == 0 {
|
||||||
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")
|
||||||
@ -29,72 +27,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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error {
|
||||||
if workID == uuid.Nil || monetizationID == uuid.Nil {
|
if workID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
if authorID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error {
|
||||||
if authorID == uuid.Nil || monetizationID == uuid.Nil {
|
if authorID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
if bookID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error {
|
||||||
if bookID == uuid.Nil || monetizationID == uuid.Nil {
|
if bookID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error {
|
||||||
if publisherID == uuid.Nil || monetizationID == uuid.Nil {
|
if publisherID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID, monetizationID uuid.UUID) error {
|
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error {
|
||||||
if sourceID == uuid.Nil || monetizationID == uuid.Nil {
|
if sourceID == 0 || monetizationID == 0 {
|
||||||
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 uuid.UUID) (*domain.Monetization, error) {
|
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
if m.getByIDFunc != nil {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
@ -107,48 +107,40 @@ 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,8 +5,6 @@ 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.
|
||||||
@ -25,8 +23,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 uuid.UUID) (*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) (*domain.Monetization, error) {
|
||||||
if id == uuid.Nil {
|
if id == 0 {
|
||||||
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")
|
||||||
@ -39,7 +37,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 uuid.UUID) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*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 {
|
||||||
@ -48,7 +46,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 uuid.UUID) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*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 {
|
||||||
@ -57,7 +55,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 uuid.UUID) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*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 {
|
||||||
@ -66,7 +64,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 uuid.UUID) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*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 {
|
||||||
@ -75,7 +73,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context,
|
|||||||
return publisher.Monetizations, nil
|
return publisher.Monetizations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uuid.UUID) ([]*domain.Monetization, error) {
|
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*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,8 +3,6 @@ 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.
|
||||||
@ -38,7 +36,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 uuid.UUID
|
ID uint
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
@ -59,6 +57,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 uuid.UUID) error {
|
func (c *TagCommands) DeleteTag(ctx context.Context, id uint) error {
|
||||||
return c.repo.Delete(ctx, id)
|
return c.repo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ 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.
|
||||||
@ -18,7 +16,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 uuid.UUID) (*domain.Tag, error) {
|
func (q *TagQueries) Tag(ctx context.Context, id uint) (*domain.Tag, error) {
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +26,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 uuid.UUID) ([]domain.Tag, error) {
|
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
|
||||||
return q.repo.ListByWorkID(ctx, workID)
|
return q.repo.ListByWorkID(ctx, workID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ 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"
|
||||||
)
|
)
|
||||||
@ -35,9 +34,9 @@ type CreateOrUpdateTranslationInput struct {
|
|||||||
Description string
|
Description string
|
||||||
Language string
|
Language string
|
||||||
Status domain.TranslationStatus
|
Status domain.TranslationStatus
|
||||||
TranslatableID uuid.UUID
|
TranslatableID uint
|
||||||
TranslatableType string
|
TranslatableType string
|
||||||
TranslatorID *uuid.UUID
|
TranslatorID *uint
|
||||||
IsOriginalLanguage bool
|
IsOriginalLanguage bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,8 +49,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 == uuid.Nil {
|
if input.TranslatableID == 0 {
|
||||||
return nil, fmt.Errorf("%w: translatable ID cannot be nil", domain.ErrValidation)
|
return nil, fmt.Errorf("%w: translatable ID cannot be zero", 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)
|
||||||
@ -71,7 +70,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
|||||||
return nil, domain.ErrForbidden
|
return nil, domain.ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
var translatorID uuid.UUID
|
var translatorID uint
|
||||||
if input.TranslatorID != nil {
|
if input.TranslatorID != nil {
|
||||||
translatorID = *input.TranslatorID
|
translatorID = *input.TranslatorID
|
||||||
} else {
|
} else {
|
||||||
@ -98,7 +97,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 uuid.UUID) error {
|
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) 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 uuid.UUID) (*domain.Author, error) {
|
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*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,12 +1,10 @@
|
|||||||
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 uuid.UUID
|
ID uint
|
||||||
Title string
|
Title string
|
||||||
Language string
|
Language string
|
||||||
Content string
|
Content string
|
||||||
TranslatableID uuid.UUID
|
TranslatableID uint
|
||||||
}
|
}
|
||||||
@ -2,8 +2,8 @@ package translation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"gorm.io/gorm"
|
|
||||||
"tercul/internal/domain"
|
"tercul/internal/domain"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 uuid.UUID) (*domain.Translation, error) {
|
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||||
if m.getByIDFunc != nil {
|
if m.getByIDFunc != nil {
|
||||||
return m.getByIDFunc(ctx, id)
|
return m.getByIDFunc(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ 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"
|
||||||
)
|
)
|
||||||
@ -24,7 +23,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 uuid.UUID) (*TranslationDTO, error) {
|
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*TranslationDTO, error) {
|
||||||
ctx, span := q.tracer.Start(ctx, "Translation")
|
ctx, span := q.tracer.Start(ctx, "Translation")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@ -46,21 +45,21 @@ func (q *TranslationQueries) Translation(ctx context.Context, id uuid.UUID) (*Tr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TranslationsByWorkID returns all translations for a work.
|
// TranslationsByWorkID returns all translations for a work.
|
||||||
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]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 uuid.UUID) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uint) ([]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 uuid.UUID) ([]domain.Translation, error) {
|
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]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)
|
||||||
@ -81,7 +80,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 uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[TranslationDTO], error) {
|
func (q *TranslationQueries) ListTranslations(ctx context.Context, workID uint, 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,8 +7,6 @@ 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.
|
||||||
@ -54,7 +52,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 uuid.UUID
|
ID uint
|
||||||
Username *string
|
Username *string
|
||||||
Email *string
|
Email *string
|
||||||
Password *string
|
Password *string
|
||||||
@ -66,13 +64,12 @@ type UpdateUserInput struct {
|
|||||||
Role *domain.UserRole
|
Role *domain.UserRole
|
||||||
Verified *bool
|
Verified *bool
|
||||||
Active *bool
|
Active *bool
|
||||||
CountryID *uuid.UUID
|
CountryID *uint
|
||||||
CityID *uuid.UUID
|
CityID *uint
|
||||||
AddressID *uuid.UUID
|
AddressID *uint
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@ -150,7 +147,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 uuid.UUID) error {
|
func (c *UserCommands) DeleteUser(ctx context.Context, id uint) 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 uuid.UUID) (*domain.User, error) {
|
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*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,8 +3,6 @@ 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.
|
||||||
@ -19,7 +17,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 uuid.UUID) (*domain.User, error) {
|
func (q *UserQueries) User(ctx context.Context, id uint) (*domain.User, error) {
|
||||||
return q.repo.GetByID(ctx, id)
|
return q.repo.GetByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +42,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 uuid.UUID) (*domain.UserProfile, error) {
|
func (q *UserQueries) UserProfile(ctx context.Context, userID uint) (*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 uuid.UUID) (*domain.UserProfile, error) {
|
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*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 uuid.UUID) (*domain.UserProfile, error) {
|
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*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,8 +14,6 @@ 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.
|
||||||
@ -110,8 +108,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 == uuid.Nil {
|
if work.ID == 0 {
|
||||||
return fmt.Errorf("%w: work ID cannot be nil", domain.ErrValidation)
|
return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||||
@ -151,10 +149,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 uuid.UUID) error {
|
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||||
ctx, span := c.tracer.Start(ctx, "DeleteWork")
|
ctx, span := c.tracer.Start(ctx, "DeleteWork")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if id == uuid.Nil {
|
if id == 0 {
|
||||||
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +181,7 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uuid.UUID) 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 uuid.UUID) error {
|
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) 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)
|
||||||
@ -222,9 +220,8 @@ func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uuid.UUID) 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 uuid.UUID) error {
|
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) 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 {
|
||||||
@ -334,7 +331,7 @@ func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uuid.UU
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uuid.UUID) error {
|
func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) 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) {
|
||||||
@ -355,7 +352,7 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uuid.UUID) 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 = uuid.Nil
|
newStats.ID = 0
|
||||||
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,10 +1,8 @@
|
|||||||
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 uuid.UUID
|
ID uint
|
||||||
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 uuid.UUID) (*domain.Work, error) {
|
func (m *mockWorkRepository) GetByID(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 {
|
||||||
return nil, args.Error(1)
|
return nil, args.Error(1)
|
||||||
|
|||||||
@ -7,8 +7,6 @@ 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.
|
||||||
@ -26,10 +24,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 uuid.UUID) (*WorkDTO, error) {
|
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*WorkDTO, error) {
|
||||||
ctx, span := q.tracer.Start(ctx, "GetWorkByID")
|
ctx, span := q.tracer.Start(ctx, "GetWorkByID")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
if id == uuid.Nil {
|
if id == 0 {
|
||||||
return nil, errors.New("invalid work ID")
|
return nil, errors.New("invalid work ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,10 +75,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 uuid.UUID) (*domain.Work, error) {
|
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*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 == uuid.Nil {
|
if id == 0 {
|
||||||
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)
|
||||||
@ -97,20 +95,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 uuid.UUID) ([]domain.Work, error) {
|
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]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 == uuid.Nil {
|
if authorID == 0 {
|
||||||
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 uuid.UUID) ([]domain.Work, error) {
|
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]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 == uuid.Nil {
|
if categoryID == 0 {
|
||||||
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)
|
||||||
@ -127,10 +125,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 uuid.UUID) ([]domain.Work, error) {
|
func (q *WorkQueries) ListByCollectionID(ctx context.Context, collectionID uint) ([]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 == uuid.Nil {
|
if collectionID == 0 {
|
||||||
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)
|
||||||
|
|||||||
211
internal/data/cache/cached_author_repository.go
vendored
211
internal/data/cache/cached_author_repository.go
vendored
@ -1,211 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"tercul/internal/domain"
|
|
||||||
platform_cache "tercul/internal/platform/cache"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CachedAuthorRepository struct {
|
|
||||||
inner domain.AuthorRepository
|
|
||||||
opt Options
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCachedAuthorRepository(inner domain.AuthorRepository, c platform_cache.Cache, opt *Options) *CachedAuthorRepository {
|
|
||||||
resolved := DefaultOptions(c)
|
|
||||||
if opt != nil {
|
|
||||||
resolved = *opt
|
|
||||||
if resolved.Cache == nil {
|
|
||||||
resolved.Cache = c
|
|
||||||
}
|
|
||||||
if resolved.Keys == nil {
|
|
||||||
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
|
|
||||||
}
|
|
||||||
if resolved.EntityTTL == 0 {
|
|
||||||
resolved.EntityTTL = 1 * time.Hour
|
|
||||||
}
|
|
||||||
if resolved.ListTTL == 0 {
|
|
||||||
resolved.ListTTL = 5 * time.Minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CachedAuthorRepository{inner: inner, opt: resolved}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) Create(ctx context.Context, entity *domain.Author) error {
|
|
||||||
err := r.inner.Create(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
|
||||||
err := r.inner.CreateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Author
|
|
||||||
key := r.opt.Keys.EntityKey("author", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
author, err := r.inner.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && author != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("author", id), author, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return author, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Author, error) {
|
|
||||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) Update(ctx context.Context, entity *domain.Author) error {
|
|
||||||
err := r.inner.Update(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
|
||||||
err := r.inner.UpdateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
err := r.inner.Delete(ctx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
|
||||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "author")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Author]
|
|
||||||
key := r.opt.Keys.ListKey("author", page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.List(ctx, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("author", page, pageSize), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
|
|
||||||
return r.inner.ListWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) {
|
|
||||||
return r.inner.ListAll(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) Count(ctx context.Context) (int64, error) {
|
|
||||||
return r.inner.Count(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
|
||||||
return r.inner.CountWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Author, error) {
|
|
||||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
|
|
||||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
|
||||||
return r.inner.Exists(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
|
||||||
return r.inner.BeginTx(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
|
||||||
return r.inner.WithTx(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
|
|
||||||
return r.inner.FindByName(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
|
|
||||||
return r.inner.ListByWorkID(ctx, workID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
|
|
||||||
return r.inner.ListByBookID(ctx, bookID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
|
|
||||||
return r.inner.ListByCountryID(ctx, countryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Author
|
|
||||||
key := r.opt.Keys.QueryKey("author", "withTranslations", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
author, err := r.inner.GetWithTranslations(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && author != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("author", "withTranslations", id), author, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return author, nil
|
|
||||||
}
|
|
||||||
231
internal/data/cache/cached_author_repository_test.go
vendored
231
internal/data/cache/cached_author_repository_test.go
vendored
@ -1,231 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
242
internal/data/cache/cached_translation_repository.go
vendored
242
internal/data/cache/cached_translation_repository.go
vendored
@ -1,242 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"tercul/internal/domain"
|
|
||||||
platform_cache "tercul/internal/platform/cache"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CachedTranslationRepository struct {
|
|
||||||
inner domain.TranslationRepository
|
|
||||||
opt Options
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCachedTranslationRepository(inner domain.TranslationRepository, c platform_cache.Cache, opt *Options) *CachedTranslationRepository {
|
|
||||||
resolved := DefaultOptions(c)
|
|
||||||
if opt != nil {
|
|
||||||
resolved = *opt
|
|
||||||
if resolved.Cache == nil {
|
|
||||||
resolved.Cache = c
|
|
||||||
}
|
|
||||||
if resolved.Keys == nil {
|
|
||||||
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
|
|
||||||
}
|
|
||||||
if resolved.EntityTTL == 0 {
|
|
||||||
resolved.EntityTTL = 1 * time.Hour
|
|
||||||
}
|
|
||||||
if resolved.ListTTL == 0 {
|
|
||||||
resolved.ListTTL = 5 * time.Minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CachedTranslationRepository{inner: inner, opt: resolved}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
|
|
||||||
err := r.inner.Create(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
|
||||||
err := r.inner.CreateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Translation
|
|
||||||
key := r.opt.Keys.EntityKey("translation", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr, err := r.inner.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && tr != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("translation", id), tr, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return tr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Translation, error) {
|
|
||||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error {
|
|
||||||
err := r.inner.Update(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
|
||||||
err := r.inner.UpdateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
err := r.inner.Delete(ctx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
|
||||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Translation]
|
|
||||||
key := r.opt.Keys.ListKey("translation", page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.List(ctx, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("translation", page, pageSize), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
|
|
||||||
return r.inner.ListWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) {
|
|
||||||
return r.inner.ListAll(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Count(ctx context.Context) (int64, error) {
|
|
||||||
return r.inner.Count(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
|
||||||
return r.inner.CountWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Translation, error) {
|
|
||||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) {
|
|
||||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
|
||||||
return r.inner.Exists(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
|
||||||
return r.inner.BeginTx(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
|
||||||
return r.inner.WithTx(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached []domain.Translation
|
|
||||||
key := r.opt.Keys.QueryKey("translation", "byWork", workID)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.ListByWorkID(ctx, workID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("translation", "byWork", workID), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
|
||||||
lang := ""
|
|
||||||
if language != nil {
|
|
||||||
lang = *language
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Translation]
|
|
||||||
key := r.opt.Keys.QueryKey("translation", "byWorkPaged", workID, lang, page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.ListByWorkIDPaginated(ctx, workID, language, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
key := r.opt.Keys.QueryKey("translation", "byWorkPaged", workID, lang, page, pageSize)
|
|
||||||
_ = r.opt.Cache.Set(ctx, key, res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
|
|
||||||
return r.inner.ListByEntity(ctx, entityType, entityID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
|
|
||||||
return r.inner.ListByTranslatorID(ctx, translatorID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
|
||||||
return r.inner.ListByStatus(ctx, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error {
|
|
||||||
err := r.inner.Upsert(ctx, translation)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil && translation != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", translation.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "translation")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@ -1,256 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
279
internal/data/cache/cached_work_repository.go
vendored
279
internal/data/cache/cached_work_repository.go
vendored
@ -1,279 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"tercul/internal/domain"
|
|
||||||
platform_cache "tercul/internal/platform/cache"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CachedWorkRepository struct {
|
|
||||||
inner domain.WorkRepository
|
|
||||||
opt Options
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCachedWorkRepository(inner domain.WorkRepository, c platform_cache.Cache, opt *Options) *CachedWorkRepository {
|
|
||||||
resolved := DefaultOptions(c)
|
|
||||||
if opt != nil {
|
|
||||||
resolved = *opt
|
|
||||||
if resolved.Cache == nil {
|
|
||||||
resolved.Cache = c
|
|
||||||
}
|
|
||||||
if resolved.Keys == nil {
|
|
||||||
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
|
|
||||||
}
|
|
||||||
if resolved.EntityTTL == 0 {
|
|
||||||
resolved.EntityTTL = 1 * time.Hour
|
|
||||||
}
|
|
||||||
if resolved.ListTTL == 0 {
|
|
||||||
resolved.ListTTL = 5 * time.Minute
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CachedWorkRepository{inner: inner, opt: resolved}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) Create(ctx context.Context, entity *domain.Work) error {
|
|
||||||
err := r.inner.Create(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
|
||||||
err := r.inner.CreateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Work
|
|
||||||
key := r.opt.Keys.EntityKey("work", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
work, err := r.inner.GetByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("work", id), work, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return work, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Work, error) {
|
|
||||||
// Options can include varying preloads/where/order; avoid caching to prevent incorrect results.
|
|
||||||
return r.inner.GetByIDWithOptions(ctx, id, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) Update(ctx context.Context, entity *domain.Work) error {
|
|
||||||
err := r.inner.Update(ctx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
|
||||||
err := r.inner.UpdateInTx(ctx, tx, entity)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", entity.ID))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
err := r.inner.Delete(ctx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
|
|
||||||
err := r.inner.DeleteInTx(ctx, tx, id)
|
|
||||||
if err == nil {
|
|
||||||
if r.opt.Cache != nil {
|
|
||||||
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", id))
|
|
||||||
}
|
|
||||||
invalidateEntityType(ctx, r.opt.Cache, "work")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Work]
|
|
||||||
key := r.opt.Keys.ListKey("work", page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.List(ctx, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("work", page, pageSize), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
|
|
||||||
return r.inner.ListWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) {
|
|
||||||
return r.inner.ListAll(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) Count(ctx context.Context) (int64, error) {
|
|
||||||
return r.inner.Count(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
|
||||||
return r.inner.CountWithOptions(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Work, error) {
|
|
||||||
return r.inner.FindWithPreload(ctx, preloads, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
|
|
||||||
return r.inner.GetAllForSync(ctx, batchSize, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
|
|
||||||
return r.inner.Exists(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
|
||||||
return r.inner.BeginTx(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
|
||||||
return r.inner.WithTx(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
|
|
||||||
return r.inner.FindByTitle(ctx, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
|
|
||||||
return r.inner.FindByAuthor(ctx, authorID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
|
|
||||||
return r.inner.FindByCategory(ctx, categoryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Work]
|
|
||||||
key := r.opt.Keys.QueryKey("work", "lang", language, page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.FindByLanguage(ctx, language, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "lang", language, page, pageSize), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Work
|
|
||||||
key := r.opt.Keys.QueryKey("work", "withTranslations", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
work, err := r.inner.GetWithTranslations(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "withTranslations", id), work, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return work, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.Work
|
|
||||||
key := r.opt.Keys.QueryKey("work", "withAssociations", id)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
work, err := r.inner.GetWithAssociations(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "withAssociations", id), work, r.opt.EntityTTL)
|
|
||||||
}
|
|
||||||
return work, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) (*domain.Work, error) {
|
|
||||||
// Tx-scoped reads should bypass cache.
|
|
||||||
return r.inner.GetWithAssociationsInTx(ctx, tx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil {
|
|
||||||
var cached domain.PaginatedResult[domain.Work]
|
|
||||||
key := r.opt.Keys.QueryKey("work", "listWithTranslations", page, pageSize)
|
|
||||||
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
|
|
||||||
return &cached, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.inner.ListWithTranslations(ctx, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
|
|
||||||
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "listWithTranslations", page, pageSize), res, r.opt.ListTTL)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uuid.UUID, authorID uuid.UUID) (bool, error) {
|
|
||||||
return r.inner.IsAuthor(ctx, workID, authorID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
|
|
||||||
return r.inner.ListByCollectionID(ctx, collectionID)
|
|
||||||
}
|
|
||||||
287
internal/data/cache/cached_work_repository_test.go
vendored
287
internal/data/cache/cached_work_repository_test.go
vendored
@ -1,287 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
20
internal/data/cache/invalidate.go
vendored
20
internal/data/cache/invalidate.go
vendored
@ -1,20 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
platform_cache "tercul/internal/platform/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
type entityTypeInvalidator interface {
|
|
||||||
InvalidateEntityType(ctx context.Context, entityType string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidateEntityType(ctx context.Context, c platform_cache.Cache, entityType string) {
|
|
||||||
if c == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if inv, ok := c.(entityTypeInvalidator); ok {
|
|
||||||
_ = inv.InvalidateEntityType(ctx, entityType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
internal/data/cache/options.go
vendored
25
internal/data/cache/options.go
vendored
@ -1,25 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
platform_cache "tercul/internal/platform/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Enabled bool
|
|
||||||
Cache platform_cache.Cache
|
|
||||||
Keys platform_cache.KeyGenerator
|
|
||||||
EntityTTL time.Duration
|
|
||||||
ListTTL time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultOptions(c platform_cache.Cache) Options {
|
|
||||||
return Options{
|
|
||||||
Enabled: c != nil,
|
|
||||||
Cache: c,
|
|
||||||
Keys: platform_cache.NewDefaultKeyGenerator("tercul:repo:"),
|
|
||||||
EntityTTL: 1 * time.Hour,
|
|
||||||
ListTTL: 5 * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
78
internal/data/cache/redis_integration_test.go
vendored
78
internal/data/cache/redis_integration_test.go
vendored
@ -1,78 +0,0 @@
|
|||||||
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,99 +1,96 @@
|
|||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- Enable UUID extension
|
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);
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
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 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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "work_copyrights" ("work_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_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 "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 "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 "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 "work_copyrights" ("work_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_id"));
|
CREATE TABLE "tags" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"slug" text);
|
||||||
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 "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 "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 "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 "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 "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 "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_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 "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 "work_monetizations" ("work_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_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 "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 "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 "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_monetizations" ("work_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_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 "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_monetizations" ("author_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_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 "author_copyrights" ("author_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_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_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 "author_monetizations" ("author_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_id"));
|
CREATE TABLE "book_monetizations" ("book_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id"));
|
||||||
CREATE TABLE "author_copyrights" ("author_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_id"));
|
CREATE TABLE "book_copyrights" ("book_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_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_monetizations" ("publisher_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id"));
|
||||||
CREATE TABLE "book_monetizations" ("book_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id"));
|
CREATE TABLE "publisher_copyrights" ("publisher_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id"));
|
||||||
CREATE TABLE "book_copyrights" ("book_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_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 "publisher_monetizations" ("publisher_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id"));
|
CREATE TABLE "source_monetizations" ("source_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id"));
|
||||||
CREATE TABLE "publisher_copyrights" ("publisher_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id"));
|
CREATE TABLE "source_copyrights" ("source_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_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 "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 "source_monetizations" ("source_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id"));
|
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_copyrights" ("source_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "series" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
||||||
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 "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 "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 "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 "series" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" 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 "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_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 "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 "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 "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 "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 "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 "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_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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "concepts" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
||||||
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 "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 "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 "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 "concepts" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
|
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 "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 "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 "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 "language_entities" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"language" text NOT NULL);
|
||||||
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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "moods" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL);
|
||||||
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 "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 "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 "topic_clusters" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"keywords" text);
|
||||||
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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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,7 +8,6 @@ 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"
|
||||||
@ -42,7 +41,7 @@ var allowedTranslationCounterFields = map[string]bool{
|
|||||||
"shares": true,
|
"shares": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error {
|
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, 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] {
|
||||||
@ -84,7 +83,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
return []*domain.Work{}, nil
|
return []*domain.Work{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
workIDs := make([]uuid.UUID, len(trendingWorks))
|
workIDs := make([]uint, len(trendingWorks))
|
||||||
for i, tw := range trendingWorks {
|
for i, tw := range trendingWorks {
|
||||||
workIDs[i] = tw.EntityID
|
workIDs[i] = tw.EntityID
|
||||||
}
|
}
|
||||||
@ -96,7 +95,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[uuid.UUID]*domain.Work)
|
workMap := make(map[uint]*domain.Work)
|
||||||
for _, w := range works {
|
for _, w := range works {
|
||||||
workMap[w.ID] = w
|
workMap[w.ID] = w
|
||||||
}
|
}
|
||||||
@ -111,7 +110,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
|
|||||||
return orderedWorks, err
|
return orderedWorks, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error {
|
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, 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] {
|
||||||
@ -133,19 +132,19 @@ func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, t
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error {
|
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, 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 uuid.UUID, stats domain.TranslationStats) error {
|
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, 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 uuid.UUID) (*domain.WorkStats, error) {
|
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*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
|
||||||
@ -153,7 +152,7 @@ func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID u
|
|||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error) {
|
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*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
|
||||||
@ -161,7 +160,7 @@ func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, t
|
|||||||
return &stats, err
|
return &stats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error) {
|
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, 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,7 +6,6 @@ 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"
|
||||||
@ -24,7 +23,7 @@ func NewAuthRepository(db *gorm.DB, cfg *config.Config) domain.AuthRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *authRepository) StoreToken(ctx context.Context, userID uuid.UUID, token string, expiresAt time.Time) error {
|
func (r *authRepository) StoreToken(ctx context.Context, userID uint, 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,8 +5,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,27 +34,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 uuid.UUID) ([]domain.Author, error) {
|
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]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 uuid.UUID) ([]domain.Author, error) {
|
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]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 uuid.UUID) ([]domain.Author, error) {
|
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]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 uuid.UUID) (*domain.Author, error) {
|
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*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"
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user