Refactor ID handling to use UUIDs across the application
Some checks failed
Test / Integration Tests (push) Successful in 4s
Build / Build Binary (push) Failing after 2m9s
Docker Build / Build Docker Image (push) Failing after 2m32s
Test / Unit Tests (push) Failing after 3m12s
Lint / Go Lint (push) Failing after 1m0s

- Updated database models and repositories to replace uint IDs with UUIDs.
- Modified test fixtures to generate and use UUIDs for authors, translations, users, and works.
- Adjusted mock implementations to align with the new UUID structure.
- Ensured all relevant functions and methods are updated to handle UUIDs correctly.
- Added necessary imports for UUID handling in various files.
This commit is contained in:
Damir Mukimov 2025-12-27 00:33:34 +01:00
parent 6fdf0a97fd
commit d50722dad5
No known key found for this signature in database
GPG Key ID: 42996CC7C73BC750
202 changed files with 3221 additions and 2826 deletions

View File

@ -6,7 +6,7 @@ tmp_dir = "tmp"
[build] [build]
# Just plain old shell command. You could use `make` as well. # Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/tercul ." cmd = "go build -o ./tmp/tercul ./cmd/api"
# Binary file yields from `cmd`. # Binary file yields from `cmd`.
bin = "tmp/tercul" bin = "tmp/tercul"
# Customize binary. # Customize binary.

View File

@ -1,16 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "ci"
include: "scope"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "deps"
include: "scope"

View File

@ -1,769 +0,0 @@
# GitHub Actions CI/CD Documentation
## Overview
This document describes the GitHub Actions CI/CD pipeline for the Tercul backend
project, updated to 2025 best practices. The pipeline ensures code quality,
security, and reliable deployments through automated testing, linting, security
scanning, and containerized deployments.
### Quick Reference
| Workflow | File | Purpose | Triggers |
|----------|------|---------|----------|
| **Lint** | `lint.yml` | Code quality & style | Push/PR to main, develop |
| **Test** | `test.yml` | Unit tests & compatibility | Push/PR to main, develop |
| **Build** | `build.yml` | Binary compilation | Push/PR to main, develop |
| **Security** | `security.yml` | CodeQL scanning | Push/PR to main + Weekly |
| **Docker Build** | `docker-build.yml` | Container images | Push to main, Tags, PRs |
| **Deploy** | `deploy.yml` | Production deployment | Tags (v*), Manual |
## Architecture
The CI/CD pipeline follows the **Single Responsibility Principle** with focused workflows:
1. **Lint** (`lint.yml`) - Code quality and style enforcement
2. **Test** (`test.yml`) - Unit tests and compatibility matrix
3. **Build** (`build.yml`) - Binary compilation and verification
4. **Security** (`security.yml`) - CodeQL security scanning
5. **Docker Build** (`docker-build.yml`) - Container image building and publishing
6. **Deploy** (`deploy.yml`) - Production deployment orchestration
## Workflows
### Lint Workflow (`lint.yml`)
**Purpose**: Ensures code quality and consistent style across the codebase.
**Triggers**:
- Push to `main` and `develop` branches
- Pull requests targeting `main` and `develop` branches
**Jobs**:
- `golangci-lint`: Go linting with golangci-lint
- Checkout code
- Setup Go 1.25 with caching
- Install dependencies
- Tidy modules (ensures go.mod/go.sum are clean)
- Run linter with 5-minute timeout
**Configuration**:
- **Timeout**: 5 minutes
- **Target**: All Go files (`./...`)
- **Cache**: Enabled for faster runs
### Test Workflow (`test.yml`)
**Purpose**: Validates code functionality through comprehensive testing.
**Triggers**:
- Push to `main` and `develop` branches
- Pull requests targeting `main` and `develop` branches
**Jobs**:
#### Unit Tests
- **Environment**: Ubuntu with PostgreSQL 15 and Redis 7
- **Features**:
- Race detection enabled
- Code coverage reporting (atomic mode)
- HTML coverage report generation
- Test result summaries in GitHub UI
- 30-day artifact retention
**Services**:
- PostgreSQL 15 with health checks
- Redis 7-alpine with health checks
#### Compatibility Matrix
- **Trigger**: Push to `main` branch only
- **Strategy**: Tests across Go versions 1.22, 1.23, 1.24, 1.25
- **Purpose**: Ensures compatibility with multiple Go versions
- **Fail-fast**: Disabled (all versions tested even if one fails)
### Build Workflow (`build.yml`)
**Purpose**: Compiles the application binary and validates the build.
**Triggers**:
- Push to `main` and `develop` branches
- Pull requests targeting `main` and `develop` branches
**Jobs**:
- `build-binary`: Binary compilation and verification
- Dependency verification with `go mod verify`
- Build to `bin/tercul-backend`
- Binary validation test
- Artifact upload (30-day retention)
**Permissions**:
- `contents: read` - Read repository code
- `attestations: write` - Future SLSA attestation support
- `id-token: write` - OIDC token for attestations
### Security Workflow (`security.yml`)
**Purpose**: Automated security vulnerability detection with CodeQL.
**Triggers**:
- Push to `main` branch
- Pull requests targeting `main` branch
- Scheduled: Every Monday at 14:20 UTC
**Jobs**:
- `codeql-analysis`: CodeQL security scanning for Go
- Setup Go 1.25 (must run before CodeQL init)
- Initialize CodeQL with Go language support
- Build code for analysis
- Perform security scan
- Category: "backend-security" for tracking
- Continues on error (warns if code scanning not enabled)
**Important Notes**:
- **Go Setup Order**: Go must be set up BEFORE CodeQL initialization to ensure version compatibility
- **Code Scanning**: Must be enabled in repository settings (Settings > Security > Code scanning)
- **Error Handling**: Workflow continues on CodeQL errors to allow scanning even if upload fails
**CodeQL Configuration**:
The workflow can be customized with additional query suites:
```yaml
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
# Run security-extended suite for more comprehensive scanning
queries: security-extended
# Or use security-and-quality for maintainability checks
# queries: security-and-quality
```
**Available Query Suites**:
- `security-extended`: Default queries plus lower severity/precision queries
- `security-and-quality`: Security queries plus maintainability and reliability
**Custom Query Packs**:
Add custom CodeQL query packs for specialized analysis:
```yaml
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
packs: my-org/go-security-queries@1.0.0
```
### Docker Build Workflow (`docker-build.yml`)
**Purpose**: Builds and publishes multi-architecture Docker images.
**Triggers**:
- Push to `main` branch
- Tag pushes with `v*` pattern
- Pull requests targeting `main` branch
**Jobs**:
- `build-image`: Multi-platform Docker image building
- Docker Buildx setup for multi-arch builds
- Login to GitHub Container Registry
- Metadata extraction for tags and labels
- Build for AMD64 and ARM64 architectures
- Push to registry (except for PRs)
- Generate build provenance attestation
**Image Tagging Strategy**:
- `main` branch pushes → `main` + `sha-<hash>` tags
- Tag pushes (`v1.2.3`) → `v1.2.3`, `1.2.3`, `1.2`, `sha-<hash>` tags
- Pull requests → `pr-<number>` tag (build only, not pushed)
**Push Behavior**:
- **Pushes to main/tags**: Build and push to registry
- **Pull requests**: Build only (validation, no push)
**Platforms**: linux/amd64, linux/arm64
### Deploy Workflow (`deploy.yml`)
**Purpose**: Production deployment orchestration to Docker Swarm.
**Triggers**:
- Tag pushes with `v*` pattern
- Manual dispatch with version input
**Jobs**:
- `deploy-production`: Deployment to production environment
- Version extraction from tag or manual input
- Docker Swarm service update (SSH-based deployment template)
- Deployment summary with timestamp
- Environment protection and tracking
**Environment**:
- Name: `production`
- URL: Configurable production URL
- Protection: Supports required reviewers and wait timers
**Manual Deployment**:
Deployments can be triggered manually from the Actions tab with a specific version.
**Required Secrets**:
- `SWARM_HOST`: Docker Swarm manager hostname/IP
- `SWARM_SSH_KEY`: SSH private key for Swarm access
## Workflow Execution Order
**On Pull Request**:
1. Lint → Validates code style
2. Test → Runs unit tests with coverage
3. Build → Compiles binary
4. Security → CodeQL analysis (main branch PRs only)
5. Docker Build → Builds image (no push)
**On Push to main**:
1. Lint → Code quality check
2. Test → Unit tests + compatibility matrix
3. Build → Binary compilation
4. Security → CodeQL scan
5. Docker Build → Build and push image
**On Tag Push (v\*)**:
1. Docker Build → Build and push versioned image
2. Deploy → Deploy to production
## Security Features
### Permissions Management
- **Principle of Least Privilege**: Each workflow has minimal required permissions
- **GITHUB_TOKEN Restrictions**: Read-only by default, elevated only when necessary
- **Workflow Separation**: Each workflow operates independently
- **Attestation Permissions**: For build provenance and SLSA compliance
### Code Security
- **CodeQL Integration**: Automated security scanning for Go code
- **Dependency Verification**: `go mod verify` ensures integrity
- **Module Tidying**: `go mod tidy` prevents dependency drift
### Container Security
- **Multi-platform Builds**: Ensures compatibility and security across architectures
- **Provenance Attestation**: Cryptographic proof of build integrity
- **Registry Security**: GitHub Container Registry with token-based authentication
### Secrets Management
- **No Hardcoded Secrets**: All sensitive data uses GitHub secrets
- **Environment Variables**: Proper isolation of configuration
- **GITHUB_TOKEN**: Automatic authentication for package registries
- **Granular Permissions**: Package-level access control
### Package Registry Security
- **GITHUB_TOKEN Authentication**: No personal access tokens required
- **Automatic Permissions**: Packages inherit repository visibility
- **Repository Scoped**: Packages linked to source repository
- **Granular Access**: Fine-grained permissions per package
- **Artifact Attestation**: Cryptographic proof of build provenance
- **OIDC Support**: Token-based authentication without long-lived credentials
## Best Practices Implemented
### 2025 Updates
- **Semantic Versioning**: Actions pinned to major versions (e.g., `@v5`) instead of SHA
- **Caching Optimization**: Go module and Docker layer caching
- **Matrix Testing**: Cross-version compatibility validation
- **Service Health Checks**: Database and Redis readiness verification
- **Artifact Management**: Proper retention policies and naming
### Performance Optimizations
- **Dependency Caching**: Reduces setup time significantly
- **Parallel Jobs**: Independent jobs run concurrently
- **Conditional Execution**: Security scans only on main branch
- **Artifact Upload**: Efficient storage and retrieval
### Reliability Features
- **Timeout Configuration**: Prevents hanging jobs
- **Error Handling**: Proper exit codes and logging
- **Health Checks**: Service readiness validation
- **Retention Policies**: Balanced storage management
## Configuration Details
### Go Version
- Primary: Go 1.25
- Matrix: Go 1.22, 1.23, 1.24, 1.25
### Services
- **PostgreSQL**: Version 15 with health checks
- **Redis**: Version 7-alpine with ping health checks
### Tools
- **Linter**: golangci-lint latest with 5-minute timeout
- **Testing**: `go test` with race detection and coverage
- **Building**: `go build` with verbose output
- **Security**: CodeQL for Go analysis
### Caching
- **Go Modules**: Automatic caching via setup-go action
- **Docker Layers**: GitHub Actions cache with GHA type
- **CodeQL Databases**: Stored in `${{ github.runner_temp }}/codeql_databases`
## Maintenance
### Dependabot Configuration
- **GitHub Actions**: Weekly updates with "ci" prefix
- **Go Modules**: Weekly updates with "deps" prefix
- **Automated PRs**: Keeps dependencies current and secure
### Monitoring
- **Workflow Runs**: GitHub Actions tab for execution monitoring
- **Security Alerts**: Code scanning results and dependency alerts
- **Coverage Reports**: Artifact downloads for test coverage analysis
### Troubleshooting
#### Common Issues
1. **Cache Misses**: Clear caches if corruption suspected
2. **Service Failures**: Check health check configurations
3. **Permission Errors**: Verify GITHUB_TOKEN scopes
4. **Timeout Issues**: Adjust timeout values in workflow configurations
5. **Docker Push Failures**: Check package write permissions
6. **Registry Authentication**: Ensure `packages: write` permission is set
#### Package Registry Issues
**Problem**: Cannot push to GitHub Container Registry
```yaml
# Solution: Ensure proper permissions
permissions:
contents: read
packages: write
id-token: write
attestations: write
```
**Problem**: Package not visible after push
- Check package visibility settings (public/private/internal)
- Verify repository is linked to package
- Ensure workflow completed successfully
**Problem**: Cannot pull package in workflow
```yaml
# Solution: Login to registry first
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
```
#### Debugging
- **Manual Dispatch**: Use `workflow_dispatch` for testing
- **Log Analysis**: Review step outputs for error details
- **Artifact Inspection**: Download build artifacts for verification
- **Package Logs**: Check package activity in GitHub UI
## Future Enhancements
### Planned Features
- **SLSA Integration**: Enhanced build attestation
- **Dependency Review**: Automated dependency vulnerability checking
- **Performance Testing**: Load testing integration
- **Multi-environment Deployment**: Staging and production separation
### Scalability Considerations
- **Self-hosted Runners**: For resource-intensive jobs
- **Job Parallelization**: Further optimization of concurrent execution
- **Cache Optimization**: Advanced caching strategies
## Docker Image Usage
### Pulling Images
Pull the latest image:
```bash
docker pull ghcr.io/<owner>/<repository>:latest
```
Pull a specific version:
```bash
docker pull ghcr.io/<owner>/<repository>:1.2.3
```
### Running Locally
Run the container:
```bash
docker run -d \
--name tercul-backend \
-p 8080:8080 \
-e DATABASE_URL="postgres://..." \
ghcr.io/<owner>/<repository>:latest
```
### Docker Swarm Deployment
Deploy as a stack:
```bash
docker stack deploy -c docker-compose.yml tercul
```
Update running service:
```bash
docker service update \
--image ghcr.io/<owner>/<repository>:1.2.3 \
tercul_backend
```
### Verifying Attestations
Verify build provenance:
```bash
gh attestation verify \
oci://ghcr.io/<owner>/<repository>:latest \
--owner <owner>
```
## Contributing
When modifying workflows:
1. Test changes using `workflow_dispatch`
2. Ensure backward compatibility
3. Update this documentation
4. Follow security best practices
5. Use semantic versioning for action references
6. Test Docker builds locally before pushing
7. Verify package permissions after changes
8. Review CodeQL alerts before merging PRs
9. Update query packs regularly for latest security rules
10. Test configuration changes with `workflow_dispatch`
## CodeQL Advanced Configuration
### Custom Configuration File
For complex CodeQL setups, use a configuration file (`.github/codeql/codeql-config.yml`):
```yaml
name: "CodeQL Config"
# Disable default queries to run only custom queries
disable-default-queries: false
# Specify query packs
packs:
- scope/go-security-pack
- scope/go-compliance-pack@1.2.3
# Add custom queries
queries:
- uses: security-and-quality
- uses: ./custom-queries
# Filter queries by severity
query-filters:
- exclude:
problem.severity:
- warning
- recommendation
- exclude:
id: go/redundant-assignment
# Scan specific directories
paths:
- internal
- cmd
- pkg
paths-ignore:
- "**/*_test.go"
- vendor
- "**/testdata/**"
# Extend threat model (preview)
threat-models: local
```
Reference the config in your workflow:
```yaml
- uses: github/codeql-action/init@v3
with:
config-file: ./.github/codeql/codeql-config.yml
```
### Inline Configuration
Alternatively, specify configuration inline:
```yaml
- uses: github/codeql-action/init@v3
with:
languages: go
config: |
disable-default-queries: false
queries:
- uses: security-extended
query-filters:
- exclude:
problem.severity:
- recommendation
```
### Scheduling CodeQL Scans
Run CodeQL on a schedule for regular security audits:
```yaml
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Run at 14:20 UTC every Monday
- cron: '20 14 * * 1'
```
### Avoiding Unnecessary Scans
Skip CodeQL for specific file changes:
```yaml
on:
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- '**/*.txt'
- 'docs/**'
```
### Analysis Categories
Categorize multiple analyses in monorepos:
```yaml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "backend-api"
```
### External Query Packs
Use query packs from GitHub Enterprise Server:
```yaml
- uses: github/codeql-action/init@v3
with:
registries: |
- url: https://containers.GHEHOSTNAME/v2/
packages:
- my-company/*
token: ${{ secrets.GHES_TOKEN }}
packs: my-company/go-queries
```
### Query Suite Examples
**Security-focused**:
```yaml
queries:
- uses: security-extended
```
**Quality and security**:
```yaml
queries:
- uses: security-and-quality
```
**Custom suite**:
```yaml
queries:
- uses: ./custom-queries/critical-security.qls
```
## Advanced Workflow Techniques
### Workflow Commands
GitHub Actions supports workflow commands for advanced functionality:
#### Debug Logging
Enable detailed debugging with the `ACTIONS_STEP_DEBUG` secret:
```yaml
- name: Debug step
run: echo "::debug::Detailed debugging information"
```
#### Annotations
Create annotations for notices, warnings, and errors:
```yaml
# Notice annotation
- run: echo "::notice file=app.go,line=10::Consider refactoring"
# Warning annotation
- run: echo "::warning file=main.go,line=5,col=10::Deprecated function"
# Error annotation
- run: echo "::error file=handler.go,line=20,title=Build Error::Missing import"
```
#### Grouping Log Lines
Organize logs with collapsible groups:
```yaml
- name: Build application
run: |
echo "::group::Compiling Go code"
go build -v ./...
echo "::endgroup::"
```
#### Masking Secrets
Prevent sensitive values from appearing in logs:
```yaml
- name: Generate token
run: |
TOKEN=$(generate_token)
echo "::add-mask::$TOKEN"
echo "TOKEN=$TOKEN" >> $GITHUB_ENV
```
#### Job Summaries
Add Markdown summaries to workflow runs:
```yaml
- name: Test summary
run: |
echo "### Test Results :white_check_mark:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Total: 150 tests" >> $GITHUB_STEP_SUMMARY
echo "- Passed: 148" >> $GITHUB_STEP_SUMMARY
echo "- Failed: 2" >> $GITHUB_STEP_SUMMARY
```
### Environment Files
Use environment files for dynamic configuration:
```yaml
# Set environment variable for subsequent steps
- name: Set build info
run: |
echo "BUILD_TIME=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_ENV
echo "COMMIT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_ENV
# Use in later steps
- name: Deploy
run: echo "Deploying $COMMIT_SHA built at $BUILD_TIME"
```
### Output Parameters
Share data between steps:
```yaml
- name: Calculate version
id: version
run: echo "VERSION=1.2.3" >> $GITHUB_OUTPUT
- name: Use version
run: echo "Building version ${{ steps.version.outputs.VERSION }}"
```
### Multiline Values
Handle multiline strings safely:
```yaml
- name: Store API response
run: |
{
echo 'API_RESPONSE<<EOF'
curl https://api.example.com/data
echo EOF
} >> $GITHUB_ENV
```
## References
- [GitHub Actions Documentation](https://docs.github.com/en/actions)
- [Go CI/CD Best Practices](https://github.com/golang/go/wiki/Go-Release-Cycle)
- [Security Hardening Guide](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions)
- [Dependency Caching](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows)
- [Workflow Commands Reference](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions)
- [Publishing Packages with Actions](https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions)
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
- [Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
- [CodeQL Configuration Reference](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning)
- [CodeQL Query Suites](https://docs.github.com/en/code-security/code-scanning/managing-your-code-scanning-configuration/codeql-query-suites)
- [CodeQL CLI Reference](https://docs.github.com/en/code-security/codeql-cli)

View File

@ -1,46 +0,0 @@
name: Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
build-binary:
name: Build Binary
runs-on: ubuntu-latest
permissions:
contents: read
attestations: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache: true
- name: Install dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Build application
run: |
go build -v -o bin/tercul-backend ./cmd/api
ls -la bin/
- name: Test binary
run: ./bin/tercul-backend --help || echo "Binary built successfully"
- name: Upload build artifacts
uses: actions/upload-artifact@v5
with:
name: tercul-backend-binary
path: bin/
retention-days: 30

View File

@ -1,60 +0,0 @@
name: Deploy
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
version:
description: "Version to deploy (e.g., v1.2.3)"
required: true
type: string
jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment:
name: production
url: https://tercul.example.com
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Extract version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "VERSION=${{ inputs.version }}" >> $GITHUB_OUTPUT
else
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
fi
- name: Deploy to Docker Swarm
env:
SWARM_HOST: ${{ secrets.SWARM_HOST }}
SWARM_SSH_KEY: ${{ secrets.SWARM_SSH_KEY }}
IMAGE_TAG: ${{ steps.version.outputs.VERSION }}
run: |
# Uncomment and configure for actual Docker Swarm deployment
# echo "$SWARM_SSH_KEY" > swarm_key
# chmod 600 swarm_key
# ssh -i swarm_key -o StrictHostKeyChecking=no \
# deploy@$SWARM_HOST \
# "docker service update \
# --image ghcr.io/${{ github.repository }}:${IMAGE_TAG} \
# tercul-backend"
# rm swarm_key
echo "Deploying version ${{ steps.version.outputs.VERSION }} to production"
echo "Image: ghcr.io/${{ github.repository }}:${IMAGE_TAG}"
- name: Deployment summary
run: |
echo "### Deployment Complete :rocket:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY
echo "- **Deployed at**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY

View File

@ -1,65 +0,0 @@
name: Docker Build
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
jobs:
build-image:
name: Build Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=long
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Generate artifact attestation
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3
with:
subject-name: ghcr.io/${{ github.repository}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@ -1,33 +0,0 @@
name: Lint
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
golangci-lint:
name: Go Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache: true
- name: Install dependencies
run: go mod download
- name: Tidy modules
run: go mod tidy
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m ./...

View File

@ -1,70 +0,0 @@
name: Security
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Run CodeQL scan every Monday at 14:20 UTC
- cron: "20 14 * * 1"
jobs:
codeql-analysis:
name: CodeQL Security Scan
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache: true
- name: Verify Go installation
run: |
echo "Go version: $(go version)"
echo "Go path: $(which go)"
echo "GOROOT: $GOROOT"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
# CodeQL will use the Go version installed by setup-go above
# Optionally use security-extended for more comprehensive scanning
# queries: security-extended
- name: Install dependencies
run: go mod download
- name: Build for analysis
run: go build -v ./...
- name: Perform CodeQL Analysis
id: codeql-analysis
uses: github/codeql-action/analyze@v3
with:
category: "backend-security"
continue-on-error: true
- name: Check CodeQL Results
if: steps.codeql-analysis.outcome == 'failure'
run: |
echo "⚠️ CodeQL analysis completed with warnings/errors"
echo "This may be due to:"
echo " 1. Code scanning not enabled in repository settings"
echo " 2. Security alerts that need review"
echo ""
echo "To enable code scanning:"
echo " Go to Settings > Security > Code security and analysis"
echo " Click 'Set up' under Code scanning"
echo ""
echo "Analysis results are still available in the workflow artifacts."

View File

@ -1,116 +0,0 @@
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: "1.25"
cache: true
- name: Install dependencies
run: go mod download
- name: Run tests with coverage
run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
- name: Generate test summary
if: always()
run: |
echo "### Test Results :test_tube:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Coverage**: See artifact for detailed report" >> $GITHUB_STEP_SUMMARY
echo "- **Race Detection**: Enabled" >> $GITHUB_STEP_SUMMARY
echo "- **Go Version**: 1.25" >> $GITHUB_STEP_SUMMARY
- name: Upload coverage reports
uses: actions/upload-artifact@v5
with:
name: coverage-report
path: |
coverage.out
coverage.html
retention-days: 30
compatibility-matrix:
name: Go ${{ matrix.go-version }} Compatibility
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
go-version: ["1.22", "1.23", "1.24", "1.25"]
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v -race ./...
- name: Build
run: go build -v ./...

View File

@ -1,4 +1,4 @@
FROM golang:1.24-alpine AS builder FROM golang:1.25-alpine AS builder
# Install git and required dependencies # Install git and required dependencies
RUN apk add --no-cache git build-base RUN apk add --no-cache git build-base

View File

@ -1,7 +1,7 @@
FROM golang:1.24 AS development FROM golang:1.25 AS development
# Install Air for hot reloading (using the updated repository) # Install Air for hot reloading (using the updated repository)
RUN go install github.com/air-verse/air@latest RUN go install github.com/air-verse/air@v1.52.3
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

View File

@ -1,4 +1,15 @@
.PHONY: lint-test .PHONY: lint-test dev dev-deps dev-down
DOCKER_COMPOSE := $(shell command -v docker-compose >/dev/null 2>&1 && echo docker-compose || echo "docker compose")
dev-deps:
$(DOCKER_COMPOSE) up -d
dev-down:
$(DOCKER_COMPOSE) down
dev: dev-deps
go run ./cmd/cli serve
lint-test: lint-test:
@echo "Running linter..." @echo "Running linter..."

View File

@ -126,13 +126,36 @@ func main() {
// Create repositories // Create repositories
repos := dbsql.NewRepositories(database, cfg) repos := dbsql.NewRepositories(database, cfg)
// Initialize Redis cache once (shared by APQ, optional repo caching, and linguistics)
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
// Create linguistics dependencies // Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider() sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
if sErr != nil { if sErr != nil {
app_log.Fatal(sErr, "Failed to create sentiment provider") app_log.Fatal(sErr, "Failed to create sentiment provider")
} }
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
redisCache,
2,
true,
sentimentProvider,
workAnalyticsDeps,
)
analysisRepo := linguisticsFactory.GetAnalysisRepository()
// Create platform components // Create platform components
jwtManager := platform_auth.NewJWTManager(cfg) jwtManager := platform_auth.NewJWTManager(cfg)
@ -182,8 +205,6 @@ func main() {
App: application, App: application,
} }
// Initialize Redis Cache for APQ
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
var queryCache gql.Cache[string] var queryCache gql.Cache[string]
if cacheErr != nil { if cacheErr != nil {
app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error()) app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error())

View File

@ -1 +1 @@
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T12:33:05.005477+01:00"} {"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T15:37:36.561092+01:00"}

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"tercul/internal/data/sql" "tercul/internal/data/sql"
@ -30,7 +29,7 @@ const (
) )
type checkpoint struct { type checkpoint struct {
LastProcessedID uint `json:"last_processed_id"` LastProcessedID string `json:"last_processed_id"`
TotalProcessed int `json:"total_processed"` TotalProcessed int `json:"total_processed"`
LastUpdated time.Time `json:"last_updated"` LastUpdated time.Time `json:"last_updated"`
} }
@ -104,7 +103,7 @@ Example:
if resume { if resume {
cp = loadCheckpoint() cp = loadCheckpoint()
if cp != nil { if cp != nil {
logger.Info(fmt.Sprintf("Resuming from checkpoint: last_id=%d, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed)) logger.Info(fmt.Sprintf("Resuming from checkpoint: last_id=%s, total_processed=%d", cp.LastProcessedID, cp.TotalProcessed))
} }
} }
@ -268,10 +267,10 @@ func migrateTranslations(
logger.Info(fmt.Sprintf("Found %d translations", totalTranslations)) logger.Info(fmt.Sprintf("Found %d translations", totalTranslations))
// Filter translations if resuming from checkpoint // Filter translations if resuming from checkpoint
if cp != nil && cp.LastProcessedID > 0 { if cp != nil && cp.LastProcessedID != "" {
filtered := make([]domain.Translation, 0, len(translations)) filtered := make([]domain.Translation, 0, len(translations))
for _, t := range translations { for _, t := range translations {
if t.ID > cp.LastProcessedID { if t.ID.String() > cp.LastProcessedID {
filtered = append(filtered, t) filtered = append(filtered, t)
} }
} }
@ -282,11 +281,11 @@ func migrateTranslations(
// Process translations in batches // Process translations in batches
batch := make([]domain.Translation, 0, batchSize) batch := make([]domain.Translation, 0, batchSize)
lastProcessedID := uint(0) lastProcessedID := ""
for i, translation := range translations { for i, translation := range translations {
batch = append(batch, translation) batch = append(batch, translation)
lastProcessedID = translation.ID lastProcessedID = translation.ID.String()
// Process batch when it reaches the batch size or at the end // Process batch when it reaches the batch size or at the end
if len(batch) >= batchSize || i == len(translations)-1 { if len(batch) >= batchSize || i == len(translations)-1 {
@ -326,7 +325,7 @@ func indexBatch(index bleve.Index, translations []domain.Translation) error {
batch := index.NewBatch() batch := index.NewBatch()
for _, t := range translations { for _, t := range translations {
doc := map[string]interface{}{ doc := map[string]interface{}{
"id": strconv.FormatUint(uint64(t.ID), 10), "id": t.ID.String(),
"title": t.Title, "title": t.Title,
"content": t.Content, "content": t.Content,
"description": t.Description, "description": t.Description,

View File

@ -129,4 +129,3 @@ func TestCheckpoint_InvalidJSON(t *testing.T) {
// This would require mocking file system, but for now we test the happy path // This would require mocking file system, but for now we test the happy path
// Invalid JSON handling is tested implicitly through file operations // Invalid JSON handling is tested implicitly through file operations
} }

View File

@ -31,7 +31,7 @@ func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Trans
} }
// Implement other required methods with minimal implementations // Implement other required methods with minimal implementations
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
return nil, nil return nil, nil
} }
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {

View File

@ -114,4 +114,3 @@ func TestRootCommand(t *testing.T) {
assert.NotEmpty(t, cmd.Short) assert.NotEmpty(t, cmd.Short)
} }
} }

View File

@ -3,7 +3,6 @@ package commands
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"tercul/cmd/cli/internal/bootstrap" "tercul/cmd/cli/internal/bootstrap"
"tercul/internal/enrichment" "tercul/internal/enrichment"
@ -11,6 +10,7 @@ import (
"tercul/internal/platform/db" "tercul/internal/platform/db"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -33,7 +33,7 @@ Example:
return fmt.Errorf("both --type and --id are required") return fmt.Errorf("both --type and --id are required")
} }
entityIDUint, err := strconv.ParseUint(entityID, 10, 64) entityIDUUID, err := uuid.Parse(entityID)
if err != nil { if err != nil {
return fmt.Errorf("invalid entity ID: %w", err) return fmt.Errorf("invalid entity ID: %w", err)
} }
@ -71,11 +71,11 @@ Example:
// Fetch, enrich, and save the entity // Fetch, enrich, and save the entity
ctx := context.Background() ctx := context.Background()
log.Info(fmt.Sprintf("Enriching %s with ID %d", entityType, entityIDUint)) log.Info(fmt.Sprintf("Enriching %s with ID %s", entityType, entityIDUUID.String()))
switch entityType { switch entityType {
case "author": case "author":
author, err := deps.Repos.Author.GetByID(ctx, uint(entityIDUint)) author, err := deps.Repos.Author.GetByID(ctx, entityIDUUID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get author: %w", err) return fmt.Errorf("failed to get author: %w", err)
} }

View File

@ -6,7 +6,10 @@ import (
"syscall" "syscall"
"tercul/cmd/cli/internal/bootstrap" "tercul/cmd/cli/internal/bootstrap"
dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics"
"tercul/internal/jobs/sync" "tercul/internal/jobs/sync"
"tercul/internal/platform/cache"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/platform/db" "tercul/internal/platform/db"
app_log "tercul/internal/platform/log" app_log "tercul/internal/platform/log"
@ -73,16 +76,47 @@ func NewWorkerCommand() *cobra.Command {
}, },
) )
repos := dbsql.NewRepositories(database, cfg)
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
if cacheErr != nil {
app_log.Warn("Redis cache initialization failed for linguistics: " + cacheErr.Error())
}
sentimentProvider, spErr := linguistics.NewGoVADERSentimentProvider()
if spErr != nil {
return spErr
}
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
redisCache,
2,
true,
sentimentProvider,
workAnalyticsDeps,
)
// Create SyncJob with all dependencies // Create SyncJob with all dependencies
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient) syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
// Create a new ServeMux for routing jobs // Create a new ServeMux for routing jobs
mux := asynq.NewServeMux() mux := asynq.NewServeMux()
// Register all job handlers // Register all job handlers
sync.RegisterQueueHandlers(mux, syncJob) sync.RegisterQueueHandlers(mux, syncJob)
// Placeholder for other job handlers that might be added in the future linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// trending.RegisterTrendingHandlers(mux, analyticsService) // trending.RegisterTrendingHandlers(mux, analyticsService)
// Start the server in a goroutine // Start the server in a goroutine

View File

@ -48,7 +48,7 @@ type Dependencies struct {
Repos *dbsql.Repositories Repos *dbsql.Repositories
Application *app.Application Application *app.Application
JWTManager *platform_auth.JWTManager JWTManager *platform_auth.JWTManager
AnalysisRepo *linguistics.GORMAnalysisRepository AnalysisRepo linguistics.AnalysisRepository
SentimentProvider *linguistics.GoVADERSentimentProvider SentimentProvider *linguistics.GoVADERSentimentProvider
} }
@ -61,12 +61,32 @@ func Bootstrap(cfg *config.Config, database *gorm.DB, weaviateClient *weaviate.C
repos := dbsql.NewRepositories(database, cfg) repos := dbsql.NewRepositories(database, cfg)
// Create linguistics dependencies // Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
if err != nil { if err != nil {
return nil, err return nil, err
} }
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
nil, // optional cache
2,
false,
sentimentProvider,
workAnalyticsDeps,
)
analysisRepo := linguisticsFactory.GetAnalysisRepository()
// Create platform components // Create platform components
jwtManager := platform_auth.NewJWTManager(cfg) jwtManager := platform_auth.NewJWTManager(cfg)

View File

@ -109,4 +109,3 @@ func TestBootstrapWithMetrics(t *testing.T) {
assert.NotNil(t, deps) assert.NotNil(t, deps)
assert.NotNil(t, deps.Application) assert.NotNil(t, deps.Application)
} }

View File

@ -5,12 +5,13 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"strconv"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/enrichment" "tercul/internal/enrichment"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/platform/db" "tercul/internal/platform/db"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
func main() { func main() {
@ -24,7 +25,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
entityID, err := strconv.ParseUint(*entityIDStr, 10, 64) entityID, err := uuid.Parse(*entityIDStr)
if err != nil { if err != nil {
fmt.Printf("Invalid entity ID: %v\n", err) fmt.Printf("Invalid entity ID: %v\n", err)
os.Exit(1) os.Exit(1)
@ -55,7 +56,7 @@ func main() {
switch *entityType { switch *entityType {
case "author": case "author":
author, err := repos.Author.GetByID(ctx, uint(entityID)) author, err := repos.Author.GetByID(ctx, entityID)
if err != nil { if err != nil {
log.Fatal(err, "Failed to get author") log.Fatal(err, "Failed to get author")
} }

View File

@ -5,7 +5,11 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics"
"tercul/internal/jobs/sync" "tercul/internal/jobs/sync"
"tercul/internal/platform/cache"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/platform/db" "tercul/internal/platform/db"
app_log "tercul/internal/platform/log" app_log "tercul/internal/platform/log"
@ -70,13 +74,44 @@ func main() {
// Create SyncJob with all dependencies // Create SyncJob with all dependencies
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient) syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
repos := dbsql.NewRepositories(database, cfg)
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
if cacheErr != nil {
app_log.Warn("Redis cache initialization failed for linguistics: " + cacheErr.Error())
}
sentimentProvider, spErr := linguistics.NewGoVADERSentimentProvider()
if spErr != nil {
app_log.Fatal(spErr, "Failed to create sentiment provider")
}
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
redisCache,
2,
true,
sentimentProvider,
workAnalyticsDeps,
)
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
// Create a new ServeMux for routing jobs // Create a new ServeMux for routing jobs
mux := asynq.NewServeMux() mux := asynq.NewServeMux()
// Register all job handlers // Register all job handlers
sync.RegisterQueueHandlers(mux, syncJob) sync.RegisterQueueHandlers(mux, syncJob)
// Placeholder for other job handlers that might be added in the future linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// trending.RegisterTrendingHandlers(mux, analyticsService) // trending.RegisterTrendingHandlers(mux, analyticsService)
// Start the server in a goroutine // Start the server in a goroutine

View File

@ -14,7 +14,8 @@ services:
- DB_PASSWORD=postgres - DB_PASSWORD=postgres
- DB_NAME=tercul - DB_NAME=tercul
- REDIS_ADDR=redis:6379 - REDIS_ADDR=redis:6379
- WEAVIATE_HOST=http://weaviate:8080 - WEAVIATE_HOST=weaviate:8080
- WEAVIATE_SCHEME=http
depends_on: depends_on:
- postgres - postgres
- redis - redis

2
go.mod
View File

@ -1,6 +1,6 @@
module tercul module tercul
go 1.24.10 go 1.25.3
require ( require (
github.com/99designs/gqlgen v0.17.72 github.com/99designs/gqlgen v0.17.72

View File

@ -1,9 +1,13 @@
package graphql package graphql
import "context" import (
"context"
"github.com/google/uuid"
)
// resolveWorkContent uses the Work service to fetch preferred content for a work. // resolveWorkContent uses the Work service to fetch preferred content for a work.
func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uint, preferredLanguage string) *string { func (r *queryResolver) resolveWorkContent(ctx context.Context, workID uuid.UUID, preferredLanguage string) *string {
if r.App.Work == nil || r.App.Work.Queries == nil { if r.App.Work == nil || r.App.Work.Queries == nil {
return nil return nil
} }

View File

@ -17,7 +17,7 @@ func (m *mockLikeRepository) Create(ctx context.Context, entity *domain.Like) er
args := m.Called(ctx, entity) args := m.Called(ctx, entity)
return args.Error(0) return args.Error(0)
} }
func (m *mockLikeRepository) GetByID(ctx context.Context, id uint) (*domain.Like, error) { func (m *mockLikeRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Like, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -8,7 +8,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"tercul/internal/adapters/graphql/model" "tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth" "tercul/internal/app/auth"
@ -26,6 +25,8 @@ import (
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"time" "time"
"github.com/google/uuid"
) )
func toModelUserRole(role domain.UserRole) model.UserRole { func toModelUserRole(role domain.UserRole) model.UserRole {
@ -144,7 +145,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
// Convert to GraphQL model // Convert to GraphQL model
return &model.Work{ return &model.Work{
ID: fmt.Sprintf("%d", createdWork.ID), ID: createdWork.ID.String(),
Name: createdWork.Title, Name: createdWork.Title,
Language: createdWork.Language, Language: createdWork.Language,
Content: input.Content, Content: input.Content,
@ -157,7 +158,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
workID, err := strconv.ParseUint(id, 10, 32) workID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -165,7 +166,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
// Create domain model // Create domain model
workModel := &domain.Work{ workModel := &domain.Work{
TranslatableModel: domain.TranslatableModel{ TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: uint(workID)}, BaseModel: domain.BaseModel{ID: workID},
Language: input.Language, Language: input.Language,
}, },
Title: input.Name, Title: input.Name,
@ -188,12 +189,12 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
// DeleteWork is the resolver for the deleteWork field. // DeleteWork is the resolver for the deleteWork field.
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
workID, err := strconv.ParseUint(id, 10, 32) workID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
err = r.App.Work.Commands.DeleteWork(ctx, uint(workID)) err = r.App.Work.Commands.DeleteWork(ctx, workID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -207,7 +208,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
return nil, err return nil, err
} }
workID, err := strconv.ParseUint(input.WorkID, 10, 32) workID, err := uuid.Parse(input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -221,7 +222,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
Title: input.Name, Title: input.Name,
Content: content, Content: content,
Language: input.Language, Language: input.Language,
TranslatableID: uint(workID), TranslatableID: workID,
TranslatableType: "works", TranslatableType: "works",
} }
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput) createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
@ -230,7 +231,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
} }
go func() { go func() {
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), uint(workID)); err != nil { if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), workID); err != nil {
log.Error(err, "failed to increment work translation count") log.Error(err, "failed to increment work translation count")
} }
}() }()
@ -250,7 +251,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
return nil, err return nil, err
} }
workID, err := strconv.ParseUint(input.WorkID, 10, 32) workID, err := uuid.Parse(input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -264,7 +265,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
Title: input.Name, Title: input.Name,
Content: content, Content: content,
Language: input.Language, Language: input.Language,
TranslatableID: uint(workID), TranslatableID: workID,
TranslatableType: "works", TranslatableType: "works",
} }
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput) updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
@ -283,12 +284,12 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
// DeleteTranslation is the resolver for the deleteTranslation field. // DeleteTranslation is the resolver for the deleteTranslation field.
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
translationID, err := strconv.ParseUint(id, 10, 32) translationID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid translation ID: %v", err) return false, fmt.Errorf("invalid translation ID: %v", err)
} }
err = r.App.Translation.Commands.DeleteTranslation(ctx, uint(translationID)) err = r.App.Translation.Commands.DeleteTranslation(ctx, translationID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -329,13 +330,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
bookID, err := strconv.ParseUint(id, 10, 32) bookID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
updateInput := book.UpdateBookInput{ updateInput := book.UpdateBookInput{
ID: uint(bookID), ID: bookID,
Title: &input.Name, Title: &input.Name,
Description: input.Description, Description: input.Description,
Language: &input.Language, Language: &input.Language,
@ -358,12 +359,12 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
// DeleteBook is the resolver for the deleteBook field. // DeleteBook is the resolver for the deleteBook field.
func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) {
bookID, err := strconv.ParseUint(id, 10, 32) bookID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
err = r.App.Book.Commands.DeleteBook(ctx, uint(bookID)) err = r.App.Book.Commands.DeleteBook(ctx, bookID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -396,13 +397,13 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
if err := Validate(input); err != nil { if err := Validate(input); err != nil {
return nil, err return nil, err
} }
authorID, err := strconv.ParseUint(id, 10, 32) authorID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid author ID: %v", err) return nil, fmt.Errorf("invalid author ID: %v", err)
} }
updateInput := author.UpdateAuthorInput{ updateInput := author.UpdateAuthorInput{
ID: uint(authorID), ID: authorID,
Name: input.Name, Name: input.Name,
} }
updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput) updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput)
@ -419,12 +420,12 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
// DeleteAuthor is the resolver for the deleteAuthor field. // DeleteAuthor is the resolver for the deleteAuthor field.
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
authorID, err := strconv.ParseUint(id, 10, 32) authorID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid author ID: %v", err) return false, fmt.Errorf("invalid author ID: %v", err)
} }
err = r.App.Author.Commands.DeleteAuthor(ctx, uint(authorID)) err = r.App.Author.Commands.DeleteAuthor(ctx, authorID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -438,13 +439,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
userID, err := strconv.ParseUint(id, 10, 32) userID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid user ID: %v", err) return nil, fmt.Errorf("invalid user ID: %v", err)
} }
updateInput := user.UpdateUserInput{ updateInput := user.UpdateUserInput{
ID: uint(userID), ID: userID,
Username: input.Username, Username: input.Username,
Email: input.Email, Email: input.Email,
Password: input.Password, Password: input.Password,
@ -463,28 +464,25 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
} }
if input.CountryID != nil { if input.CountryID != nil {
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32) countryID, err := uuid.Parse(*input.CountryID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err) return nil, fmt.Errorf("invalid country ID: %v", err)
} }
uid := uint(countryID) updateInput.CountryID = &countryID
updateInput.CountryID = &uid
} }
if input.CityID != nil { if input.CityID != nil {
cityID, err := strconv.ParseUint(*input.CityID, 10, 32) cityID, err := uuid.Parse(*input.CityID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err) return nil, fmt.Errorf("invalid city ID: %v", err)
} }
uid := uint(cityID) updateInput.CityID = &cityID
updateInput.CityID = &uid
} }
if input.AddressID != nil { if input.AddressID != nil {
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32) addressID, err := uuid.Parse(*input.AddressID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err) return nil, fmt.Errorf("invalid address ID: %v", err)
} }
uid := uint(addressID) updateInput.AddressID = &addressID
updateInput.AddressID = &uid
} }
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput) updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
@ -509,12 +507,12 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
// DeleteUser is the resolver for the deleteUser field. // DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
userID, err := strconv.ParseUint(id, 10, 32) userID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
err = r.App.User.Commands.DeleteUser(ctx, uint(userID)) err = r.App.User.Commands.DeleteUser(ctx, userID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -558,13 +556,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collectionID, err := strconv.ParseUint(id, 10, 32) collectionID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
updateInput := collection.UpdateCollectionInput{ updateInput := collection.UpdateCollectionInput{
ID: uint(collectionID), ID: collectionID,
Name: input.Name, Name: input.Name,
UserID: userID, UserID: userID,
} }
@ -593,12 +591,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
collectionID, err := strconv.ParseUint(id, 10, 32) collectionID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid collection ID: %v", err) return false, fmt.Errorf("invalid collection ID: %v", err)
} }
err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID) err = r.App.Collection.Commands.DeleteCollection(ctx, collectionID, userID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -613,18 +611,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collID, err := strconv.ParseUint(collectionID, 10, 32) collID, err := uuid.Parse(collectionID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
wID, err := strconv.ParseUint(workID, 10, 32) wID, err := uuid.Parse(workID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
addInput := collection.AddWorkToCollectionInput{ addInput := collection.AddWorkToCollectionInput{
CollectionID: uint(collID), CollectionID: collID,
WorkID: uint(wID), WorkID: wID,
UserID: userID, UserID: userID,
} }
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput) err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
@ -632,7 +630,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
return nil, err return nil, err
} }
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID)) updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -651,18 +649,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collID, err := strconv.ParseUint(collectionID, 10, 32) collID, err := uuid.Parse(collectionID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
wID, err := strconv.ParseUint(workID, 10, 32) wID, err := uuid.Parse(workID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
removeInput := collection.RemoveWorkFromCollectionInput{ removeInput := collection.RemoveWorkFromCollectionInput{
CollectionID: uint(collID), CollectionID: collID,
WorkID: uint(wID), WorkID: wID,
UserID: userID, UserID: userID,
} }
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput) err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
@ -670,7 +668,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
return nil, err return nil, err
} }
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID)) updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -698,28 +696,25 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
UserID: userID, UserID: userID,
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32) workID, err := uuid.Parse(*input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
wID := uint(workID) createInput.WorkID = &workID
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32) translationID, err := uuid.Parse(*input.TranslationID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
tID := uint(translationID) createInput.TranslationID = &translationID
createInput.TranslationID = &tID
} }
if input.ParentCommentID != nil { if input.ParentCommentID != nil {
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32) parentCommentID, err := uuid.Parse(*input.ParentCommentID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid parent comment ID: %v", err) return nil, fmt.Errorf("invalid parent comment ID: %v", err)
} }
pID := uint(parentCommentID) createInput.ParentID = &parentCommentID
createInput.ParentID = &pID
} }
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput) createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
@ -754,12 +749,12 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
commentID, err := strconv.ParseUint(id, 10, 32) commentID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err) return nil, fmt.Errorf("invalid comment ID: %v", err)
} }
commentModel, err := r.App.Comment.Queries.Comment(ctx, uint(commentID)) commentModel, err := r.App.Comment.Queries.Comment(ctx, commentID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -772,7 +767,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
} }
updateInput := comment.UpdateCommentInput{ updateInput := comment.UpdateCommentInput{
ID: uint(commentID), ID: commentID,
Text: input.Text, Text: input.Text,
} }
updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput) updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput)
@ -796,12 +791,12 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
commentID, err := strconv.ParseUint(id, 10, 32) commentID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid comment ID: %v", err) return false, fmt.Errorf("invalid comment ID: %v", err)
} }
comment, err := r.App.Comment.Queries.Comment(ctx, uint(commentID)) comment, err := r.App.Comment.Queries.Comment(ctx, commentID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -813,7 +808,7 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Comment.Commands.DeleteComment(ctx, uint(commentID)) err = r.App.Comment.Commands.DeleteComment(ctx, commentID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -839,28 +834,25 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
UserID: userID, UserID: userID,
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32) workID, err := uuid.Parse(*input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
wID := uint(workID) createInput.WorkID = &workID
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32) translationID, err := uuid.Parse(*input.TranslationID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
tID := uint(translationID) createInput.TranslationID = &translationID
createInput.TranslationID = &tID
} }
if input.CommentID != nil { if input.CommentID != nil {
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32) commentID, err := uuid.Parse(*input.CommentID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err) return nil, fmt.Errorf("invalid comment ID: %v", err)
} }
cID := uint(commentID) createInput.CommentID = &commentID
createInput.CommentID = &cID
} }
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput) createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
@ -892,12 +884,12 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
likeID, err := strconv.ParseUint(id, 10, 32) likeID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid like ID: %v", err) return false, fmt.Errorf("invalid like ID: %v", err)
} }
like, err := r.App.Like.Queries.Like(ctx, uint(likeID)) like, err := r.App.Like.Queries.Like(ctx, likeID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -909,7 +901,7 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Like.Commands.DeleteLike(ctx, uint(likeID)) err = r.App.Like.Commands.DeleteLike(ctx, likeID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -924,14 +916,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
workID, err := strconv.ParseUint(input.WorkID, 10, 32) workID, err := uuid.Parse(input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
createInput := bookmark.CreateBookmarkInput{ createInput := bookmark.CreateBookmarkInput{
UserID: userID, UserID: userID,
WorkID: uint(workID), WorkID: workID,
} }
if input.Name != nil { if input.Name != nil {
createInput.Name = *input.Name createInput.Name = *input.Name
@ -942,7 +934,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
return nil, err return nil, err
} }
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, uint(workID)); err != nil { if err := r.App.Analytics.IncrementWorkBookmarks(ctx, workID); err != nil {
log.FromContext(ctx).Error(err, "failed to increment work bookmarks") log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
} }
@ -961,12 +953,12 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
bookmarkID, err := strconv.ParseUint(id, 10, 32) bookmarkID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid bookmark ID: %v", err) return false, fmt.Errorf("invalid bookmark ID: %v", err)
} }
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, uint(bookmarkID)) bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, bookmarkID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -978,7 +970,7 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Bookmark.Commands.DeleteBookmark(ctx, uint(bookmarkID)) err = r.App.Bookmark.Commands.DeleteBookmark(ctx, bookmarkID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -998,21 +990,19 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := strconv.ParseUint(*input.WorkID, 10, 32) workID, err := uuid.Parse(*input.WorkID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
wID := uint(workID) createInput.WorkID = &workID
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32) translationID, err := uuid.Parse(*input.TranslationID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
tID := uint(translationID) createInput.TranslationID = &translationID
createInput.TranslationID = &tID
} }
if input.Status != nil { if input.Status != nil {
@ -1043,13 +1033,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
return nil, domain.ErrUnauthorized return nil, domain.ErrUnauthorized
} }
contributionID, err := strconv.ParseUint(id, 10, 32) contributionID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
updateInput := contribution.UpdateContributionInput{ updateInput := contribution.UpdateContributionInput{
ID: uint(contributionID), ID: contributionID,
UserID: userID, UserID: userID,
Name: &input.Name, Name: &input.Name,
} }
@ -1081,12 +1071,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
} }
contributionID, err := strconv.ParseUint(id, 10, 32) contributionID, err := uuid.Parse(id)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
err = r.App.Contribution.Commands.DeleteContribution(ctx, uint(contributionID), userID) err = r.App.Contribution.Commands.DeleteContribution(ctx, contributionID, userID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1096,13 +1086,13 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
// ReviewContribution is the resolver for the reviewContribution field. // ReviewContribution is the resolver for the reviewContribution field.
func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) { func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) {
contributionID, err := strconv.ParseUint(id, 10, 32) contributionID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
reviewInput := contribution.ReviewContributionInput{ reviewInput := contribution.ReviewContributionInput{
ID: uint(contributionID), ID: contributionID,
Status: status.String(), Status: status.String(),
Feedback: feedback, Feedback: feedback,
} }
@ -1211,28 +1201,25 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
} }
if input.CountryID != nil { if input.CountryID != nil {
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32) countryID, err := uuid.Parse(*input.CountryID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err) return nil, fmt.Errorf("invalid country ID: %v", err)
} }
uid := uint(countryID) updateInput.CountryID = &countryID
updateInput.CountryID = &uid
} }
if input.CityID != nil { if input.CityID != nil {
cityID, err := strconv.ParseUint(*input.CityID, 10, 32) cityID, err := uuid.Parse(*input.CityID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err) return nil, fmt.Errorf("invalid city ID: %v", err)
} }
uid := uint(cityID) updateInput.CityID = &cityID
updateInput.CityID = &uid
} }
if input.AddressID != nil { if input.AddressID != nil {
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32) addressID, err := uuid.Parse(*input.AddressID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err) return nil, fmt.Errorf("invalid address ID: %v", err)
} }
uid := uint(addressID) updateInput.AddressID = &addressID
updateInput.AddressID = &uid
} }
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput) updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
@ -1278,12 +1265,12 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword s
// Work is the resolver for the work field. // Work is the resolver for the work field.
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) { func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
workID, err := strconv.ParseUint(id, 10, 32) workID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID)) workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, workID)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrEntityNotFound) { if errors.Is(err, domain.ErrEntityNotFound) {
return nil, nil return nil, nil
@ -1292,7 +1279,7 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
} }
go func() { go func() {
if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil { if err := r.App.Analytics.IncrementWorkViews(context.Background(), workID); err != nil {
log.Error(err, "failed to increment work views") log.Error(err, "failed to increment work views")
} }
}() }()
@ -1338,12 +1325,12 @@ func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32,
// Translation is the resolver for the translation field. // Translation is the resolver for the translation field.
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) { func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
translationID, err := strconv.ParseUint(id, 10, 32) translationID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
translationDTO, err := r.App.Translation.Queries.Translation(ctx, uint(translationID)) translationDTO, err := r.App.Translation.Queries.Translation(ctx, translationID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1352,7 +1339,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
} }
go func() { go func() {
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), uint(translationID)); err != nil { if err := r.App.Analytics.IncrementTranslationViews(context.Background(), translationID); err != nil {
log.Error(err, "failed to increment translation views") log.Error(err, "failed to increment translation views")
} }
}() }()
@ -1368,7 +1355,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
// Translations is the resolver for the translations field. // Translations is the resolver for the translations field.
func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) { func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) {
wID, err := strconv.ParseUint(workID, 10, 32) wID, err := uuid.Parse(workID)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -1382,7 +1369,7 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
page = int(*offset)/pageSize + 1 page = int(*offset)/pageSize + 1
} }
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, uint(wID), language, page, pageSize) paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, wID, language, page, pageSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1402,12 +1389,12 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
// Book is the resolver for the book field. // Book is the resolver for the book field.
func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) { func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) {
bookID, err := strconv.ParseUint(id, 10, 32) bookID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
bookRecord, err := r.App.Book.Queries.Book(ctx, uint(bookID)) bookRecord, err := r.App.Book.Queries.Book(ctx, bookID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1447,12 +1434,12 @@ func (r *queryResolver) Books(ctx context.Context, limit *int32, offset *int32)
// Author is the resolver for the author field. // Author is the resolver for the author field.
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) { func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
authorID, err := strconv.ParseUint(id, 10, 32) authorID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation)
} }
authorRecord, err := r.App.Author.Queries.Author(ctx, uint(authorID)) authorRecord, err := r.App.Author.Queries.Author(ctx, authorID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1481,18 +1468,17 @@ func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, e
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) { func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
var authors []*domain.Author var authors []*domain.Author
var err error var err error
var countryIDUint *uint var countryIDUUID *uuid.UUID
if countryID != nil { if countryID != nil {
parsedID, err := strconv.ParseUint(*countryID, 10, 32) parsedID, err := uuid.Parse(*countryID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
uid := uint(parsedID) countryIDUUID = &parsedID
countryIDUint = &uid
} }
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint) authors, err = r.App.Author.Queries.Authors(ctx, countryIDUUID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1521,12 +1507,12 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32
// User is the resolver for the user field. // User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
userID, err := strconv.ParseUint(id, 10, 32) userID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
userRecord, err := r.App.User.Queries.User(ctx, uint(userID)) userRecord, err := r.App.User.Queries.User(ctx, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1690,12 +1676,12 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
// UserProfile is the resolver for the userProfile field. // UserProfile is the resolver for the userProfile field.
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) { func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
uID, err := strconv.ParseUint(userID, 10, 32) uID, err := uuid.Parse(userID)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID)) profile, err := r.App.User.Queries.UserProfile(ctx, uID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1703,12 +1689,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
return nil, nil return nil, nil
} }
user, err := r.App.User.Queries.User(ctx, uint(uID)) user, err := r.App.User.Queries.User(ctx, uID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil { if user == nil {
return nil, fmt.Errorf("user not found for profile %d", profile.ID) return nil, fmt.Errorf("user not found for profile %s", profile.ID.String())
} }
return &model.UserProfile{ return &model.UserProfile{
@ -1738,12 +1724,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
// Collection is the resolver for the collection field. // Collection is the resolver for the collection field.
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) { func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
collID, err := strconv.ParseUint(id, 10, 32) collID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation)
} }
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, uint(collID)) collectionRecord, err := r.App.Collection.Queries.Collection(ctx, collID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1751,7 +1737,7 @@ func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Colle
return nil, nil return nil, nil
} }
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, uint(collID)) workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, collID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1784,11 +1770,11 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
var err error var err error
if userID != nil { if userID != nil {
uID, idErr := strconv.ParseUint(*userID, 10, 32) uID, idErr := uuid.Parse(*userID)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uint(uID)) collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uID)
} else { } else {
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx) collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
} }
@ -1829,18 +1815,18 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
// Tag is the resolver for the tag field. // Tag is the resolver for the tag field.
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) { func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
tagID, err := strconv.ParseUint(id, 10, 32) tagID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag, err := r.App.Tag.Queries.Tag(ctx, uint(tagID)) tag, err := r.App.Tag.Queries.Tag(ctx, tagID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &model.Tag{ return &model.Tag{
ID: fmt.Sprintf("%d", tag.ID), ID: tag.ID.String(),
Name: tag.Name, Name: tag.Name,
}, nil }, nil
} }
@ -1865,12 +1851,12 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) (
// Category is the resolver for the category field. // Category is the resolver for the category field.
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) { func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
categoryID, err := strconv.ParseUint(id, 10, 32) categoryID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid category ID: %v", err) return nil, fmt.Errorf("invalid category ID: %v", err)
} }
category, err := r.App.Category.Queries.Category(ctx, uint(categoryID)) category, err := r.App.Category.Queries.Category(ctx, categoryID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1879,7 +1865,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
} }
return &model.Category{ return &model.Category{
ID: fmt.Sprintf("%d", category.ID), ID: category.ID.String(),
Name: category.Name, Name: category.Name,
}, nil }, nil
} }
@ -1904,12 +1890,12 @@ func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *in
// Comment is the resolver for the comment field. // Comment is the resolver for the comment field.
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) { func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
cID, err := strconv.ParseUint(id, 10, 32) cID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation)
} }
commentRecord, err := r.App.Comment.Queries.Comment(ctx, uint(cID)) commentRecord, err := r.App.Comment.Queries.Comment(ctx, cID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1918,10 +1904,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
} }
return &model.Comment{ return &model.Comment{
ID: fmt.Sprintf("%d", commentRecord.ID), ID: commentRecord.ID.String(),
Text: commentRecord.Text, Text: commentRecord.Text,
User: &model.User{ User: &model.User{
ID: fmt.Sprintf("%d", commentRecord.UserID), ID: commentRecord.UserID.String(),
}, },
}, nil }, nil
} }
@ -1932,23 +1918,23 @@ func (r *queryResolver) Comments(ctx context.Context, workID *string, translatio
var err error var err error
if workID != nil { if workID != nil {
wID, idErr := strconv.ParseUint(*workID, 10, 32) wID, idErr := uuid.Parse(*workID)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, uint(wID)) commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, wID)
} else if translationID != nil { } else if translationID != nil {
tID, idErr := strconv.ParseUint(*translationID, 10, 32) tID, idErr := uuid.Parse(*translationID)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, uint(tID)) commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, tID)
} else if userID != nil { } else if userID != nil {
uID, idErr := strconv.ParseUint(*userID, 10, 32) uID, idErr := uuid.Parse(*userID)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uint(uID)) commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uID)
} else { } else {
commentRecords, err = r.App.Comment.Queries.Comments(ctx) commentRecords, err = r.App.Comment.Queries.Comments(ctx)
} }

View File

@ -3,13 +3,13 @@ package graphql
import ( import (
"context" "context"
"fmt" "fmt"
"testing" "tercul/internal/adapters/graphql/model"
"tercul/internal/app" "tercul/internal/app"
"tercul/internal/app/authz" "tercul/internal/app/authz"
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/adapters/graphql/model"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"testing"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -20,7 +20,7 @@ import (
type mockUserRepositoryForUserResolver struct{ mock.Mock } type mockUserRepositoryForUserResolver struct{ mock.Mock }
// Implement domain.UserRepository // Implement domain.UserRepository
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uint) (*domain.User, error) { func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)
@ -106,7 +106,7 @@ func (m *mockUserRepositoryForUserResolver) WithTx(ctx context.Context, fn func(
type mockUserProfileRepository struct{ mock.Mock } type mockUserProfileRepository struct{ mock.Mock }
// Implement domain.UserProfileRepository // Implement domain.UserProfileRepository
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) { func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
args := m.Called(ctx, userID) args := m.Called(ctx, userID)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)
@ -121,7 +121,7 @@ func (m *mockUserProfileRepository) Create(ctx context.Context, entity *domain.U
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error { func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
return nil return nil
} }
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) { func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserProfile, error) {
return nil, nil return nil, nil
} }
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) { func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {

View File

@ -20,7 +20,7 @@ func (m *mockWorkRepository) Create(ctx context.Context, entity *domain.Work) er
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
return m.Create(ctx, entity) return m.Create(ctx, entity)
} }
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -2,7 +2,6 @@ package graphql
import ( import (
"context" "context"
"testing"
"tercul/internal/adapters/graphql/model" "tercul/internal/adapters/graphql/model"
"tercul/internal/app" "tercul/internal/app"
"tercul/internal/app/authz" "tercul/internal/app/authz"
@ -11,6 +10,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
domainsearch "tercul/internal/domain/search" domainsearch "tercul/internal/domain/search"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"testing"
"time" "time"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
@ -26,7 +26,7 @@ func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) erro
work.ID = 1 work.ID = 1
return args.Error(0) return args.Error(0)
} }
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)
@ -52,10 +52,18 @@ func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*dom
} }
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1) return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
} }
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { return nil, nil } func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { return nil, nil } return nil, nil
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { return nil, nil } }
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil } func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
return nil, nil
}
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) { func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
@ -63,24 +71,47 @@ func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (
} }
return args.Get(0).(*domain.Work), args.Error(1) return args.Get(0).(*domain.Work), args.Error(1)
} }
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) { return nil, nil } func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) {
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { return nil, nil } return nil, nil
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil } }
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { return nil, nil } func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil } return nil, nil
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { return nil, nil } }
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil } func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
return nil, nil
}
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
return nil
}
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
return nil
}
func (m *mockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } func (m *mockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) { return nil, nil } func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) { return nil, nil } func (m *mockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) { return nil, nil }
func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) { return 0, nil } func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil } func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { return nil, nil } return 0, nil
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { return nil, nil } }
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
return nil, nil
}
func (m *mockWorkRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } func (m *mockWorkRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil } func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
}
type mockAuthorRepository struct{ mock.Mock } type mockAuthorRepository struct{ mock.Mock }
@ -96,57 +127,111 @@ func (m *mockAuthorRepository) Create(ctx context.Context, author *domain.Author
author.ID = 1 author.ID = 1
return args.Error(0) return args.Error(0)
} }
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil } func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { return nil, nil } return nil, nil
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { return nil, nil } }
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { return nil, nil } func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil } return nil, nil
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil } }
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { return nil, nil } func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
return nil
}
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error { return nil } func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error { return nil }
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil } func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
return nil
}
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error { return nil } func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error { return nil }
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { return nil, nil } return nil
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { return nil, nil } }
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
return nil, nil
}
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) { return nil, nil } func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) { return nil, nil }
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { return 0, nil } func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil } func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { return nil, nil } return 0, nil
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { return nil, nil } }
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
return nil, nil
}
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil } func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
}
type mockUserRepository struct{ mock.Mock } type mockUserRepository struct{ mock.Mock }
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) { func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)
} }
return args.Get(0).(*domain.User), args.Error(1) return args.Get(0).(*domain.User), args.Error(1)
} }
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { return nil, nil } func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil } return nil, nil
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) { return nil, nil } }
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) error { return nil } func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) error { return nil }
func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil } func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) { return nil, nil } return nil
}
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) Update(ctx context.Context, entity *domain.User) error { return nil } func (m *mockUserRepository) Update(ctx context.Context, entity *domain.User) error { return nil }
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil } func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
return nil
}
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil } func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil }
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) { return nil, nil } func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) { return nil, nil } return nil, nil
}
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil } func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil }
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil } func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil } func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) { return nil, nil } return 0, nil
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) { return nil, nil } }
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
return nil, nil
}
func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil } func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
}
type mockSearchClient struct{ mock.Mock } type mockSearchClient struct{ mock.Mock }
@ -162,7 +247,6 @@ func (m *mockSearchClient) Search(ctx context.Context, params domainsearch.Searc
return args.Get(0).(*domainsearch.SearchResults), args.Error(1) return args.Get(0).(*domainsearch.SearchResults), args.Error(1)
} }
type mockAnalyticsService struct{ mock.Mock } type mockAnalyticsService struct{ mock.Mock }
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error { func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
@ -174,26 +258,62 @@ func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID ui
return args.Error(0) return args.Error(0)
} }
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil }
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error {
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil } return nil
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil } }
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil } return nil
}
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error {
return nil
}
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error {
return nil
}
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
return nil
}
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil } return nil
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error { return nil } }
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { return nil, nil } func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error {
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return nil, nil } return nil
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error { return nil } }
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error {
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error { return nil } return nil
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { return nil } }
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return nil } return nil, nil
}
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
return nil, nil
}
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
return nil
}
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error {
return nil
}
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error {
return nil
}
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
return nil
}
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
return nil
}
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
return nil
}
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil } func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil }
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) { return nil, nil } func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { return nil } return nil, nil
}
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
return nil
}
type mockTranslationRepository struct{ mock.Mock } type mockTranslationRepository struct{ mock.Mock }
@ -201,30 +321,69 @@ func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *dom
args := m.Called(ctx, translation) args := m.Called(ctx, translation)
return args.Error(0) return args.Error(0)
} }
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil } return nil, nil
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil } }
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { return nil, nil } return nil, nil
func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) { return nil, nil } }
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil } return nil, nil
func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { return nil, nil } }
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil } return nil, nil
}
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
return nil
}
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
return nil
}
func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error {
return nil
}
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
return nil
}
func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil } func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil }
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil } return nil
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { return nil, nil } }
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
return nil, nil
}
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) { return 0, nil } func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil } func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return nil, nil } return 0, nil
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { return nil, nil } }
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) {
return nil, nil
}
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) {
return false, nil
}
func (m *mockTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil } func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
}
// WorkResolversUnitSuite is a unit test suite for the work resolvers. // WorkResolversUnitSuite is a unit test suite for the work resolvers.
type WorkResolversUnitSuite struct { type WorkResolversUnitSuite struct {

View File

@ -4,17 +4,19 @@ import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"time" "time"
"github.com/google/uuid"
) )
// AnalyticsRepository defines the data access layer for analytics. // AnalyticsRepository defines the data access layer for analytics.
type Repository interface { type Repository interface {
IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error
IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error)
UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error UpdateUserEngagement(ctx context.Context, userEngagement *domain.UserEngagement) error
UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error UpdateTrendingWorks(ctx context.Context, timePeriod string, trending []*domain.Trending) error
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)

View File

@ -13,34 +13,36 @@ import (
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/google/uuid"
) )
type Service interface { type Service interface {
IncrementWorkViews(ctx context.Context, workID uint) error IncrementWorkViews(ctx context.Context, workID uuid.UUID) error
IncrementWorkLikes(ctx context.Context, workID uint) error IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error
IncrementWorkComments(ctx context.Context, workID uint) error IncrementWorkComments(ctx context.Context, workID uuid.UUID) error
IncrementWorkBookmarks(ctx context.Context, workID uint) error IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error
IncrementWorkShares(ctx context.Context, workID uint) error IncrementWorkShares(ctx context.Context, workID uuid.UUID) error
IncrementWorkTranslationCount(ctx context.Context, workID uint) error IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error
IncrementTranslationViews(ctx context.Context, translationID uint) error IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationLikes(ctx context.Context, translationID uint) error IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
DecrementWorkLikes(ctx context.Context, workID uint) error DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error
DecrementTranslationLikes(ctx context.Context, translationID uint) error DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationComments(ctx context.Context, translationID uint) error IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationShares(ctx context.Context, translationID uint) error IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
UpdateWorkReadingTime(ctx context.Context, workID uint) error UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error
UpdateWorkComplexity(ctx context.Context, workID uint) error UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error
UpdateWorkSentiment(ctx context.Context, workID uint) error UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error
UpdateTranslationSentiment(ctx context.Context, translationID uint) error UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error
UpdateTrending(ctx context.Context) error UpdateTrending(ctx context.Context) error
GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error)
UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error
} }
type service struct { type service struct {
@ -63,91 +65,91 @@ func NewService(repo Repository, analysisRepo linguistics.AnalysisRepository, tr
} }
} }
func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error { func (s *service) IncrementWorkViews(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkViews") ctx, span := s.tracer.Start(ctx, "IncrementWorkViews")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "views", 1) return s.repo.IncrementWorkCounter(ctx, workID, "views", 1)
} }
func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error { func (s *service) IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkLikes") ctx, span := s.tracer.Start(ctx, "IncrementWorkLikes")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1) return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1)
} }
func (s *service) DecrementWorkLikes(ctx context.Context, workID uint) error { func (s *service) DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "DecrementWorkLikes") ctx, span := s.tracer.Start(ctx, "DecrementWorkLikes")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "likes", -1) return s.repo.IncrementWorkCounter(ctx, workID, "likes", -1)
} }
func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error { func (s *service) IncrementWorkComments(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkComments") ctx, span := s.tracer.Start(ctx, "IncrementWorkComments")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1) return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1)
} }
func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error { func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkBookmarks") ctx, span := s.tracer.Start(ctx, "IncrementWorkBookmarks")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1) return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1)
} }
func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error { func (s *service) IncrementWorkShares(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkShares") ctx, span := s.tracer.Start(ctx, "IncrementWorkShares")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1) return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1)
} }
func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error { func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementWorkTranslationCount") ctx, span := s.tracer.Start(ctx, "IncrementWorkTranslationCount")
defer span.End() defer span.End()
return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1) return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1)
} }
func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error { func (s *service) IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementTranslationViews") ctx, span := s.tracer.Start(ctx, "IncrementTranslationViews")
defer span.End() defer span.End()
return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1) return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1)
} }
func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error { func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementTranslationLikes") ctx, span := s.tracer.Start(ctx, "IncrementTranslationLikes")
defer span.End() defer span.End()
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1) return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1)
} }
func (s *service) DecrementTranslationLikes(ctx context.Context, translationID uint) error { func (s *service) DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "DecrementTranslationLikes") ctx, span := s.tracer.Start(ctx, "DecrementTranslationLikes")
defer span.End() defer span.End()
return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", -1) return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", -1)
} }
func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error { func (s *service) IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementTranslationComments") ctx, span := s.tracer.Start(ctx, "IncrementTranslationComments")
defer span.End() defer span.End()
return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1) return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1)
} }
func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error { func (s *service) IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "IncrementTranslationShares") ctx, span := s.tracer.Start(ctx, "IncrementTranslationShares")
defer span.End() defer span.End()
return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1) return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1)
} }
func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error) {
ctx, span := s.tracer.Start(ctx, "GetOrCreateWorkStats") ctx, span := s.tracer.Start(ctx, "GetOrCreateWorkStats")
defer span.End() defer span.End()
return s.repo.GetOrCreateWorkStats(ctx, workID) return s.repo.GetOrCreateWorkStats(ctx, workID)
} }
func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error) {
ctx, span := s.tracer.Start(ctx, "GetOrCreateTranslationStats") ctx, span := s.tracer.Start(ctx, "GetOrCreateTranslationStats")
defer span.End() defer span.End()
return s.repo.GetOrCreateTranslationStats(ctx, translationID) return s.repo.GetOrCreateTranslationStats(ctx, translationID)
} }
func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error { func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "UpdateWorkReadingTime") ctx, span := s.tracer.Start(ctx, "UpdateWorkReadingTime")
defer span.End() defer span.End()
stats, err := s.repo.GetOrCreateWorkStats(ctx, workID) stats, err := s.repo.GetOrCreateWorkStats(ctx, workID)
@ -174,7 +176,7 @@ func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error
return s.repo.UpdateWorkStats(ctx, workID, *stats) return s.repo.UpdateWorkStats(ctx, workID, *stats)
} }
func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error { func (s *service) UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "UpdateWorkComplexity") ctx, span := s.tracer.Start(ctx, "UpdateWorkComplexity")
defer span.End() defer span.End()
logger := log.FromContext(ctx).With("workID", workID) logger := log.FromContext(ctx).With("workID", workID)
@ -198,7 +200,7 @@ func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error {
return s.repo.UpdateWorkStats(ctx, workID, *stats) return s.repo.UpdateWorkStats(ctx, workID, *stats)
} }
func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error { func (s *service) UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "UpdateWorkSentiment") ctx, span := s.tracer.Start(ctx, "UpdateWorkSentiment")
defer span.End() defer span.End()
logger := log.FromContext(ctx).With("workID", workID) logger := log.FromContext(ctx).With("workID", workID)
@ -227,7 +229,7 @@ func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error {
return s.repo.UpdateWorkStats(ctx, workID, *stats) return s.repo.UpdateWorkStats(ctx, workID, *stats)
} }
func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "UpdateTranslationReadingTime") ctx, span := s.tracer.Start(ctx, "UpdateTranslationReadingTime")
defer span.End() defer span.End()
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID) stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
@ -255,7 +257,7 @@ func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationI
return s.repo.UpdateTranslationStats(ctx, translationID, *stats) return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
} }
func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error {
ctx, span := s.tracer.Start(ctx, "UpdateTranslationSentiment") ctx, span := s.tracer.Start(ctx, "UpdateTranslationSentiment")
defer span.End() defer span.End()
stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID) stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID)
@ -282,7 +284,7 @@ func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID
return s.repo.UpdateTranslationStats(ctx, translationID, *stats) return s.repo.UpdateTranslationStats(ctx, translationID, *stats)
} }
func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { func (s *service) UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error {
ctx, span := s.tracer.Start(ctx, "UpdateUserEngagement") ctx, span := s.tracer.Start(ctx, "UpdateUserEngagement")
defer span.End() defer span.End()
today := time.Now().UTC().Truncate(24 * time.Hour) today := time.Now().UTC().Truncate(24 * time.Hour)
@ -315,7 +317,7 @@ func (s *service) GetTrendingWorks(ctx context.Context, timePeriod string, limit
return s.repo.GetTrendingWorks(ctx, timePeriod, limit) return s.repo.GetTrendingWorks(ctx, timePeriod, limit)
} }
func (s *service) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { func (s *service) UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error {
ctx, span := s.tracer.Start(ctx, "UpdateWorkStats") ctx, span := s.tracer.Start(ctx, "UpdateWorkStats")
defer span.End() defer span.End()
return s.repo.UpdateWorkStats(ctx, workID, stats) return s.repo.UpdateWorkStats(ctx, workID, stats)

View File

@ -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"
) )

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -178,7 +179,7 @@ func (c *AuthCommands) ResendVerificationEmail(ctx context.Context, email string
// ChangePasswordInput represents the input for changing a password. // ChangePasswordInput represents the input for changing a password.
type ChangePasswordInput struct { type ChangePasswordInput struct {
UserID uint UserID uuid.UUID
CurrentPassword string CurrentPassword string
NewPassword string NewPassword string
} }

View File

@ -9,7 +9,6 @@ import (
"testing" "testing"
) )
type AuthCommandsSuite struct { type AuthCommandsSuite struct {
suite.Suite suite.Suite
userRepo *mockUserRepository userRepo *mockUserRepository

View File

@ -71,7 +71,7 @@ func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) erro
return errors.New("user not found") return errors.New("user not found")
} }
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) { func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
if m.getByIDFunc != nil { if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id) return m.getByIDFunc(ctx, id)
} }

View File

@ -3,6 +3,8 @@ package author
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// AuthorCommands contains the command handlers for the author aggregate. // AuthorCommands contains the command handlers for the author aggregate.
@ -34,7 +36,7 @@ func (c *AuthorCommands) CreateAuthor(ctx context.Context, input CreateAuthorInp
// UpdateAuthorInput represents the input for updating an existing author. // UpdateAuthorInput represents the input for updating an existing author.
type UpdateAuthorInput struct { type UpdateAuthorInput struct {
ID uint ID uuid.UUID
Name string Name string
} }
@ -53,6 +55,6 @@ func (c *AuthorCommands) UpdateAuthor(ctx context.Context, input UpdateAuthorInp
} }
// DeleteAuthor deletes an author by ID. // DeleteAuthor deletes an author by ID.
func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uint) error { func (c *AuthorCommands) DeleteAuthor(ctx context.Context, id uuid.UUID) error {
return c.repo.Delete(ctx, id) return c.repo.Delete(ctx, id)
} }

View File

@ -3,6 +3,8 @@ package author
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// AuthorQueries contains the query handlers for the author aggregate. // AuthorQueries contains the query handlers for the author aggregate.
@ -16,12 +18,12 @@ func NewAuthorQueries(repo domain.AuthorRepository) *AuthorQueries {
} }
// Author returns an author by ID. // Author returns an author by ID.
func (q *AuthorQueries) Author(ctx context.Context, id uint) (*domain.Author, error) { func (q *AuthorQueries) Author(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
// Authors returns all authors, with optional filtering by country. // Authors returns all authors, with optional filtering by country.
func (q *AuthorQueries) Authors(ctx context.Context, countryID *uint) ([]*domain.Author, error) { func (q *AuthorQueries) Authors(ctx context.Context, countryID *uuid.UUID) ([]*domain.Author, error) {
var authors []domain.Author var authors []domain.Author
var err error var err error
@ -43,6 +45,6 @@ func (q *AuthorQueries) Authors(ctx context.Context, countryID *uint) ([]*domain
} }
// AuthorWithTranslations returns an author by ID with its translations. // AuthorWithTranslations returns an author by ID with its translations.
func (q *AuthorQueries) AuthorWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { func (q *AuthorQueries) AuthorWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
return q.repo.GetWithTranslations(ctx, id) return q.repo.GetWithTranslations(ctx, id)
} }

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
) )
// Service provides authorization checks for the application. // Service provides authorization checks for the application.
@ -26,7 +28,7 @@ func NewService(workRepo domain.WorkRepository, authorRepo domain.AuthorReposito
// CanEditWork checks if a user has permission to edit a work. // CanEditWork checks if a user has permission to edit a work.
// For now, we'll implement a simple rule: only an admin or the work's author can edit it. // For now, we'll implement a simple rule: only an admin or the work's author can edit it.
func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) { func (s *Service) CanEditWork(ctx context.Context, userID uuid.UUID, work *domain.Work) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
@ -61,7 +63,7 @@ func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Wor
} }
// CanDeleteWork checks if a user has permission to delete a work. // CanDeleteWork checks if a user has permission to delete a work.
func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) { func (s *Service) CanDeleteWork(ctx context.Context, userID uuid.UUID, work *domain.Work) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
@ -96,7 +98,7 @@ func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.W
} }
// CanEditEntity checks if a user has permission to edit a specific translatable entity. // CanEditEntity checks if a user has permission to edit a specific translatable entity.
func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableType string, translatableID uint) (bool, error) { func (s *Service) CanEditEntity(ctx context.Context, userID uuid.UUID, translatableType string, translatableID uuid.UUID) (bool, error) {
switch translatableType { switch translatableType {
case "works": case "works":
// For works, we can reuse the CanEditWork logic. // For works, we can reuse the CanEditWork logic.
@ -114,7 +116,7 @@ func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableTy
} }
// CanDeleteTranslation checks if a user can delete a translation. // CanDeleteTranslation checks if a user can delete a translation.
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) { func (s *Service) CanDeleteTranslation(ctx context.Context, userID uuid.UUID, translationID uuid.UUID) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
@ -157,7 +159,7 @@ func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
return true, nil return true, nil
} }
func (s *Service) CanEditTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) { func (s *Service) CanEditTranslation(ctx context.Context, userID uuid.UUID, translationID uuid.UUID) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
@ -211,7 +213,7 @@ func (s *Service) CanDeleteBook(ctx context.Context) (bool, error) {
return false, domain.ErrForbidden return false, domain.ErrForbidden
} }
func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint) (bool, error) { func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uuid.UUID) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
@ -232,7 +234,7 @@ func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint)
// CanDeleteComment checks if a user has permission to delete a comment. // CanDeleteComment checks if a user has permission to delete a comment.
// For now, we'll implement a simple rule: only an admin or the comment's author can delete it. // For now, we'll implement a simple rule: only an admin or the comment's author can delete it.
func (s *Service) CanDeleteComment(ctx context.Context, userID uint, comment *domain.Comment) (bool, error) { func (s *Service) CanDeleteComment(ctx context.Context, userID uuid.UUID, comment *domain.Comment) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx) claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok { if !ok {
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"tercul/internal/app/authz" "tercul/internal/app/authz"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// BookCommands contains the command handlers for the book aggregate. // BookCommands contains the command handlers for the book aggregate.
@ -26,7 +28,7 @@ type CreateBookInput struct {
Description string Description string
Language string Language string
ISBN *string ISBN *string
AuthorIDs []uint AuthorIDs []uuid.UUID
} }
// CreateBook creates a new book. // CreateBook creates a new book.
@ -62,12 +64,12 @@ func (c *BookCommands) CreateBook(ctx context.Context, input CreateBookInput) (*
// UpdateBookInput represents the input for updating an existing book. // UpdateBookInput represents the input for updating an existing book.
type UpdateBookInput struct { type UpdateBookInput struct {
ID uint ID uuid.UUID
Title *string Title *string
Description *string Description *string
Language *string Language *string
ISBN *string ISBN *string
AuthorIDs []uint AuthorIDs []uuid.UUID
} }
// UpdateBook updates an existing book. // UpdateBook updates an existing book.
@ -106,7 +108,7 @@ func (c *BookCommands) UpdateBook(ctx context.Context, input UpdateBookInput) (*
} }
// DeleteBook deletes a book by ID. // DeleteBook deletes a book by ID.
func (c *BookCommands) DeleteBook(ctx context.Context, id uint) error { func (c *BookCommands) DeleteBook(ctx context.Context, id uuid.UUID) error {
can, err := c.authzSvc.CanDeleteBook(ctx) can, err := c.authzSvc.CanDeleteBook(ctx)
if err != nil { if err != nil {
return err return err

View File

@ -3,6 +3,8 @@ package book
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// BookQueries contains the query handlers for the book aggregate. // BookQueries contains the query handlers for the book aggregate.
@ -16,7 +18,7 @@ func NewBookQueries(repo domain.BookRepository) *BookQueries {
} }
// Book retrieves a book by its ID. // Book retrieves a book by its ID.
func (q *BookQueries) Book(ctx context.Context, id uint) (*domain.Book, error) { func (q *BookQueries) Book(ctx context.Context, id uuid.UUID) (*domain.Book, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }

View File

@ -5,6 +5,8 @@ import (
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// BookmarkCommands contains the command handlers for the bookmark aggregate. // BookmarkCommands contains the command handlers for the bookmark aggregate.
@ -24,8 +26,8 @@ func NewBookmarkCommands(repo domain.BookmarkRepository, analyticsSvc analytics.
// CreateBookmarkInput represents the input for creating a new bookmark. // CreateBookmarkInput represents the input for creating a new bookmark.
type CreateBookmarkInput struct { type CreateBookmarkInput struct {
Name string Name string
UserID uint UserID uuid.UUID
WorkID uint WorkID uuid.UUID
Notes string Notes string
} }
@ -55,7 +57,7 @@ func (c *BookmarkCommands) CreateBookmark(ctx context.Context, input CreateBookm
// UpdateBookmarkInput represents the input for updating an existing bookmark. // UpdateBookmarkInput represents the input for updating an existing bookmark.
type UpdateBookmarkInput struct { type UpdateBookmarkInput struct {
ID uint ID uuid.UUID
Name string Name string
Notes string Notes string
} }
@ -76,6 +78,6 @@ func (c *BookmarkCommands) UpdateBookmark(ctx context.Context, input UpdateBookm
} }
// DeleteBookmark deletes a bookmark by ID. // DeleteBookmark deletes a bookmark by ID.
func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uint) error { func (c *BookmarkCommands) DeleteBookmark(ctx context.Context, id uuid.UUID) error {
return c.repo.Delete(ctx, id) return c.repo.Delete(ctx, id)
} }

View File

@ -3,6 +3,8 @@ package bookmark
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// BookmarkQueries contains the query handlers for the bookmark aggregate. // BookmarkQueries contains the query handlers for the bookmark aggregate.
@ -16,16 +18,16 @@ func NewBookmarkQueries(repo domain.BookmarkRepository) *BookmarkQueries {
} }
// Bookmark returns a bookmark by ID. // Bookmark returns a bookmark by ID.
func (q *BookmarkQueries) Bookmark(ctx context.Context, id uint) (*domain.Bookmark, error) { func (q *BookmarkQueries) Bookmark(ctx context.Context, id uuid.UUID) (*domain.Bookmark, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
// BookmarksByUserID returns all bookmarks for a user. // BookmarksByUserID returns all bookmarks for a user.
func (q *BookmarkQueries) BookmarksByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) { func (q *BookmarkQueries) BookmarksByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Bookmark, error) {
return q.repo.ListByUserID(ctx, userID) return q.repo.ListByUserID(ctx, userID)
} }
// BookmarksByWorkID returns all bookmarks for a work. // BookmarksByWorkID returns all bookmarks for a work.
func (q *BookmarkQueries) BookmarksByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) { func (q *BookmarkQueries) BookmarksByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Bookmark, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }

View File

@ -3,6 +3,8 @@ package category
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// CategoryCommands contains the command handlers for the category aggregate. // CategoryCommands contains the command handlers for the category aggregate.
@ -19,7 +21,7 @@ func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
type CreateCategoryInput struct { type CreateCategoryInput struct {
Name string Name string
Description string Description string
ParentID *uint ParentID *uuid.UUID
} }
// CreateCategory creates a new category. // CreateCategory creates a new category.
@ -38,10 +40,10 @@ func (c *CategoryCommands) CreateCategory(ctx context.Context, input CreateCateg
// UpdateCategoryInput represents the input for updating an existing category. // UpdateCategoryInput represents the input for updating an existing category.
type UpdateCategoryInput struct { type UpdateCategoryInput struct {
ID uint ID uuid.UUID
Name string Name string
Description string Description string
ParentID *uint ParentID *uuid.UUID
} }
// UpdateCategory updates an existing category. // UpdateCategory updates an existing category.
@ -61,6 +63,6 @@ func (c *CategoryCommands) UpdateCategory(ctx context.Context, input UpdateCateg
} }
// DeleteCategory deletes a category by ID. // DeleteCategory deletes a category by ID.
func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uint) error { func (c *CategoryCommands) DeleteCategory(ctx context.Context, id uuid.UUID) error {
return c.repo.Delete(ctx, id) return c.repo.Delete(ctx, id)
} }

View File

@ -3,6 +3,8 @@ package category
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// CategoryQueries contains the query handlers for the category aggregate. // CategoryQueries contains the query handlers for the category aggregate.
@ -16,7 +18,7 @@ func NewCategoryQueries(repo domain.CategoryRepository) *CategoryQueries {
} }
// Category returns a category by ID. // Category returns a category by ID.
func (q *CategoryQueries) Category(ctx context.Context, id uint) (*domain.Category, error) { func (q *CategoryQueries) Category(ctx context.Context, id uuid.UUID) (*domain.Category, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
@ -26,12 +28,12 @@ func (q *CategoryQueries) CategoryByName(ctx context.Context, name string) (*dom
} }
// CategoriesByWorkID returns all categories for a work. // CategoriesByWorkID returns all categories for a work.
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) { func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Category, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }
// CategoriesByParentID returns all categories for a parent. // CategoriesByParentID returns all categories for a parent.
func (q *CategoryQueries) CategoriesByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) { func (q *CategoryQueries) CategoriesByParentID(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error) {
return q.repo.ListByParentID(ctx, parentID) return q.repo.ListByParentID(ctx, parentID)
} }

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"fmt" "fmt"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// CollectionCommands contains the command handlers for the collection aggregate. // CollectionCommands contains the command handlers for the collection aggregate.
@ -20,7 +22,7 @@ func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands
type CreateCollectionInput struct { type CreateCollectionInput struct {
Name string Name string
Description string Description string
UserID uint UserID uuid.UUID
IsPublic bool IsPublic bool
CoverImageURL string CoverImageURL string
} }
@ -43,12 +45,12 @@ func (c *CollectionCommands) CreateCollection(ctx context.Context, input CreateC
// UpdateCollectionInput represents the input for updating an existing collection. // UpdateCollectionInput represents the input for updating an existing collection.
type UpdateCollectionInput struct { type UpdateCollectionInput struct {
ID uint ID uuid.UUID
Name string Name string
Description string Description string
IsPublic bool IsPublic bool
CoverImageURL string CoverImageURL string
UserID uint UserID uuid.UUID
} }
// UpdateCollection updates an existing collection. // UpdateCollection updates an existing collection.
@ -58,7 +60,7 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
return nil, err return nil, err
} }
if collection.UserID != input.UserID { if collection.UserID != input.UserID {
return nil, fmt.Errorf("unauthorized: user %d cannot update collection %d", input.UserID, input.ID) return nil, fmt.Errorf("unauthorized: user %s cannot update collection %s", input.UserID, input.ID)
} }
collection.Name = input.Name collection.Name = input.Name
collection.Description = input.Description collection.Description = input.Description
@ -72,22 +74,22 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
} }
// DeleteCollection deletes a collection by ID. // DeleteCollection deletes a collection by ID.
func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uint, userID uint) error { func (c *CollectionCommands) DeleteCollection(ctx context.Context, id uuid.UUID, userID uuid.UUID) error {
collection, err := c.repo.GetByID(ctx, id) collection, err := c.repo.GetByID(ctx, id)
if err != nil { if err != nil {
return err return err
} }
if collection.UserID != userID { if collection.UserID != userID {
return fmt.Errorf("unauthorized: user %d cannot delete collection %d", userID, id) return fmt.Errorf("unauthorized: user %s cannot delete collection %s", userID, id)
} }
return c.repo.Delete(ctx, id) return c.repo.Delete(ctx, id)
} }
// AddWorkToCollectionInput represents the input for adding a work to a collection. // AddWorkToCollectionInput represents the input for adding a work to a collection.
type AddWorkToCollectionInput struct { type AddWorkToCollectionInput struct {
CollectionID uint CollectionID uuid.UUID
WorkID uint WorkID uuid.UUID
UserID uint UserID uuid.UUID
} }
// AddWorkToCollection adds a work to a collection. // AddWorkToCollection adds a work to a collection.
@ -97,16 +99,16 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW
return err return err
} }
if collection.UserID != input.UserID { if collection.UserID != input.UserID {
return fmt.Errorf("unauthorized: user %d cannot add work to collection %d", input.UserID, input.CollectionID) return fmt.Errorf("unauthorized: user %s cannot add work to collection %s", input.UserID, input.CollectionID)
} }
return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID) return c.repo.AddWorkToCollection(ctx, input.CollectionID, input.WorkID)
} }
// RemoveWorkFromCollectionInput represents the input for removing a work from a collection. // RemoveWorkFromCollectionInput represents the input for removing a work from a collection.
type RemoveWorkFromCollectionInput struct { type RemoveWorkFromCollectionInput struct {
CollectionID uint CollectionID uuid.UUID
WorkID uint WorkID uuid.UUID
UserID uint UserID uuid.UUID
} }
// RemoveWorkFromCollection removes a work from a collection. // RemoveWorkFromCollection removes a work from a collection.
@ -116,7 +118,7 @@ func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input
return err return err
} }
if collection.UserID != input.UserID { if collection.UserID != input.UserID {
return fmt.Errorf("unauthorized: user %d cannot remove work from collection %d", input.UserID, input.CollectionID) return fmt.Errorf("unauthorized: user %s cannot remove work from collection %s", input.UserID, input.CollectionID)
} }
return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID) return c.repo.RemoveWorkFromCollection(ctx, input.CollectionID, input.WorkID)
} }

View File

@ -3,6 +3,8 @@ package collection
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// CollectionQueries contains the query handlers for the collection aggregate. // CollectionQueries contains the query handlers for the collection aggregate.
@ -16,12 +18,12 @@ func NewCollectionQueries(repo domain.CollectionRepository) *CollectionQueries {
} }
// Collection returns a collection by ID. // Collection returns a collection by ID.
func (q *CollectionQueries) Collection(ctx context.Context, id uint) (*domain.Collection, error) { func (q *CollectionQueries) Collection(ctx context.Context, id uuid.UUID) (*domain.Collection, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
// CollectionsByUserID returns all collections for a user. // CollectionsByUserID returns all collections for a user.
func (q *CollectionQueries) CollectionsByUserID(ctx context.Context, userID uint) ([]domain.Collection, error) { func (q *CollectionQueries) CollectionsByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Collection, error) {
return q.repo.ListByUserID(ctx, userID) return q.repo.ListByUserID(ctx, userID)
} }
@ -31,7 +33,7 @@ func (q *CollectionQueries) PublicCollections(ctx context.Context) ([]domain.Col
} }
// CollectionsByWorkID returns all collections for a work. // CollectionsByWorkID returns all collections for a work.
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) { func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Collection, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }

View File

@ -9,6 +9,8 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// CommentCommands contains the command handlers for the comment aggregate. // CommentCommands contains the command handlers for the comment aggregate.
@ -30,10 +32,10 @@ func NewCommentCommands(repo domain.CommentRepository, authzSvc *authz.Service,
// CreateCommentInput represents the input for creating a new comment. // CreateCommentInput represents the input for creating a new comment.
type CreateCommentInput struct { type CreateCommentInput struct {
Text string Text string
UserID uint UserID uuid.UUID
WorkID *uint WorkID *uuid.UUID
TranslationID *uint TranslationID *uuid.UUID
ParentID *uint ParentID *uuid.UUID
} }
// CreateComment creates a new comment. // CreateComment creates a new comment.
@ -72,7 +74,7 @@ func (c *CommentCommands) CreateComment(ctx context.Context, input CreateComment
// UpdateCommentInput represents the input for updating an existing comment. // UpdateCommentInput represents the input for updating an existing comment.
type UpdateCommentInput struct { type UpdateCommentInput struct {
ID uint ID uuid.UUID
Text string Text string
} }
@ -86,7 +88,7 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
comment, err := c.repo.GetByID(ctx, input.ID) comment, err := c.repo.GetByID(ctx, input.ID)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrEntityNotFound) { if errors.Is(err, domain.ErrEntityNotFound) {
return nil, fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, input.ID) return nil, fmt.Errorf("%w: comment with id %s not found", domain.ErrEntityNotFound, input.ID)
} }
return nil, err return nil, err
} }
@ -108,7 +110,7 @@ func (c *CommentCommands) UpdateComment(ctx context.Context, input UpdateComment
} }
// DeleteComment deletes a comment by ID after an authorization check. // DeleteComment deletes a comment by ID after an authorization check.
func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error { func (c *CommentCommands) DeleteComment(ctx context.Context, id uuid.UUID) error {
userID, ok := platform_auth.GetUserIDFromContext(ctx) userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok { if !ok {
return domain.ErrUnauthorized return domain.ErrUnauthorized
@ -117,7 +119,7 @@ func (c *CommentCommands) DeleteComment(ctx context.Context, id uint) error {
comment, err := c.repo.GetByID(ctx, id) comment, err := c.repo.GetByID(ctx, id)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrEntityNotFound) { if errors.Is(err, domain.ErrEntityNotFound) {
return fmt.Errorf("%w: comment with id %d not found", domain.ErrEntityNotFound, id) return fmt.Errorf("%w: comment with id %s not found", domain.ErrEntityNotFound, id)
} }
return err return err
} }

View File

@ -3,6 +3,8 @@ package comment
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// CommentQueries contains the query handlers for the comment aggregate. // CommentQueries contains the query handlers for the comment aggregate.
@ -16,27 +18,27 @@ func NewCommentQueries(repo domain.CommentRepository) *CommentQueries {
} }
// Comment returns a comment by ID. // Comment returns a comment by ID.
func (q *CommentQueries) Comment(ctx context.Context, id uint) (*domain.Comment, error) { func (q *CommentQueries) Comment(ctx context.Context, id uuid.UUID) (*domain.Comment, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
// CommentsByUserID returns all comments for a user. // CommentsByUserID returns all comments for a user.
func (q *CommentQueries) CommentsByUserID(ctx context.Context, userID uint) ([]domain.Comment, error) { func (q *CommentQueries) CommentsByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Comment, error) {
return q.repo.ListByUserID(ctx, userID) return q.repo.ListByUserID(ctx, userID)
} }
// CommentsByWorkID returns all comments for a work. // CommentsByWorkID returns all comments for a work.
func (q *CommentQueries) CommentsByWorkID(ctx context.Context, workID uint) ([]domain.Comment, error) { func (q *CommentQueries) CommentsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Comment, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }
// CommentsByTranslationID returns all comments for a translation. // CommentsByTranslationID returns all comments for a translation.
func (q *CommentQueries) CommentsByTranslationID(ctx context.Context, translationID uint) ([]domain.Comment, error) { func (q *CommentQueries) CommentsByTranslationID(ctx context.Context, translationID uuid.UUID) ([]domain.Comment, error) {
return q.repo.ListByTranslationID(ctx, translationID) return q.repo.ListByTranslationID(ctx, translationID)
} }
// CommentsByParentID returns all comments for a parent. // CommentsByParentID returns all comments for a parent.
func (q *CommentQueries) CommentsByParentID(ctx context.Context, parentID uint) ([]domain.Comment, error) { func (q *CommentQueries) CommentsByParentID(ctx context.Context, parentID uuid.UUID) ([]domain.Comment, error) {
return q.repo.ListByParentID(ctx, parentID) return q.repo.ListByParentID(ctx, parentID)
} }

View File

@ -5,6 +5,8 @@ import (
"tercul/internal/app/authz" "tercul/internal/app/authz"
"tercul/internal/domain" "tercul/internal/domain"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
) )
// Commands contains the command handlers for the contribution aggregate. // Commands contains the command handlers for the contribution aggregate.
@ -25,8 +27,8 @@ func NewCommands(repo domain.ContributionRepository, authzSvc *authz.Service) *C
type CreateContributionInput struct { type CreateContributionInput struct {
Name string Name string
Status string Status string
WorkID *uint WorkID *uuid.UUID
TranslationID *uint TranslationID *uuid.UUID
} }
// CreateContribution creates a new contribution. // CreateContribution creates a new contribution.
@ -56,8 +58,8 @@ func (c *Commands) CreateContribution(ctx context.Context, input CreateContribut
// UpdateContributionInput represents the input for updating a contribution. // UpdateContributionInput represents the input for updating a contribution.
type UpdateContributionInput struct { type UpdateContributionInput struct {
ID uint ID uuid.UUID
UserID uint UserID uuid.UUID
Name *string Name *string
Status *string Status *string
} }
@ -89,7 +91,7 @@ func (c *Commands) UpdateContribution(ctx context.Context, input UpdateContribut
} }
// DeleteContribution deletes a contribution. // DeleteContribution deletes a contribution.
func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint, userID uint) error { func (c *Commands) DeleteContribution(ctx context.Context, contributionID uuid.UUID, userID uuid.UUID) error {
contribution, err := c.repo.GetByID(ctx, contributionID) contribution, err := c.repo.GetByID(ctx, contributionID)
if err != nil { if err != nil {
return err return err
@ -105,7 +107,7 @@ func (c *Commands) DeleteContribution(ctx context.Context, contributionID uint,
// ReviewContributionInput represents the input for reviewing a contribution. // ReviewContributionInput represents the input for reviewing a contribution.
type ReviewContributionInput struct { type ReviewContributionInput struct {
ID uint ID uuid.UUID
Status string Status string
Feedback *string Feedback *string
} }

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// CopyrightCommands contains the command handlers for copyright. // CopyrightCommands contains the command handlers for copyright.
@ -37,8 +39,8 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
if copyright == nil { if copyright == nil {
return errors.New("copyright cannot be nil") return errors.New("copyright cannot be nil")
} }
if copyright.ID == 0 { if copyright.ID == uuid.Nil {
return errors.New("copyright ID cannot be zero") return errors.New("copyright ID cannot be nil")
} }
if copyright.Name == "" { if copyright.Name == "" {
return errors.New("copyright name cannot be empty") return errors.New("copyright name cannot be empty")
@ -51,8 +53,8 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma
} }
// DeleteCopyright deletes a copyright. // DeleteCopyright deletes a copyright.
func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error { func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uuid.UUID) error {
if id == 0 { if id == uuid.Nil {
return errors.New("invalid copyright ID") return errors.New("invalid copyright ID")
} }
log.FromContext(ctx).With("id", id).Debug("Deleting copyright") log.FromContext(ctx).With("id", id).Debug("Deleting copyright")
@ -60,8 +62,8 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error
} }
// AddCopyrightToWork adds a copyright to a work. // AddCopyrightToWork adds a copyright to a work.
func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error { func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
if workID == 0 || copyrightID == 0 { if workID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid work ID or copyright ID") return errors.New("invalid work ID or copyright ID")
} }
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Adding copyright to work") log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Adding copyright to work")
@ -69,8 +71,8 @@ func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint,
} }
// RemoveCopyrightFromWork removes a copyright from a work. // RemoveCopyrightFromWork removes a copyright from a work.
func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error { func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID uuid.UUID, copyrightID uuid.UUID) error {
if workID == 0 || copyrightID == 0 { if workID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid work ID or copyright ID") return errors.New("invalid work ID or copyright ID")
} }
log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Removing copyright from work") log.FromContext(ctx).With("work_id", workID).With("copyright_id", copyrightID).Debug("Removing copyright from work")
@ -78,8 +80,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID
} }
// AddCopyrightToAuthor adds a copyright to an author. // AddCopyrightToAuthor adds a copyright to an author.
func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error { func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
if authorID == 0 || copyrightID == 0 { if authorID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid author ID or copyright ID") return errors.New("invalid author ID or copyright ID")
} }
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Adding copyright to author") log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Adding copyright to author")
@ -87,8 +89,8 @@ func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID u
} }
// RemoveCopyrightFromAuthor removes a copyright from an author. // RemoveCopyrightFromAuthor removes a copyright from an author.
func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error { func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, authorID uuid.UUID, copyrightID uuid.UUID) error {
if authorID == 0 || copyrightID == 0 { if authorID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid author ID or copyright ID") return errors.New("invalid author ID or copyright ID")
} }
log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Removing copyright from author") log.FromContext(ctx).With("author_id", authorID).With("copyright_id", copyrightID).Debug("Removing copyright from author")
@ -96,8 +98,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, autho
} }
// AddCopyrightToBook adds a copyright to a book. // AddCopyrightToBook adds a copyright to a book.
func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error { func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
if bookID == 0 || copyrightID == 0 { if bookID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid book ID or copyright ID") return errors.New("invalid book ID or copyright ID")
} }
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Adding copyright to book") log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Adding copyright to book")
@ -105,8 +107,8 @@ func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint,
} }
// RemoveCopyrightFromBook removes a copyright from a book. // RemoveCopyrightFromBook removes a copyright from a book.
func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error { func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID uuid.UUID, copyrightID uuid.UUID) error {
if bookID == 0 || copyrightID == 0 { if bookID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid book ID or copyright ID") return errors.New("invalid book ID or copyright ID")
} }
log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Removing copyright from book") log.FromContext(ctx).With("book_id", bookID).With("copyright_id", copyrightID).Debug("Removing copyright from book")
@ -114,8 +116,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID
} }
// AddCopyrightToPublisher adds a copyright to a publisher. // AddCopyrightToPublisher adds a copyright to a publisher.
func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error { func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
if publisherID == 0 || copyrightID == 0 { if publisherID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid publisher ID or copyright ID") return errors.New("invalid publisher ID or copyright ID")
} }
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Adding copyright to publisher") log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Adding copyright to publisher")
@ -123,8 +125,8 @@ func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publish
} }
// RemoveCopyrightFromPublisher removes a copyright from a publisher. // RemoveCopyrightFromPublisher removes a copyright from a publisher.
func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error { func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uuid.UUID, copyrightID uuid.UUID) error {
if publisherID == 0 || copyrightID == 0 { if publisherID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid publisher ID or copyright ID") return errors.New("invalid publisher ID or copyright ID")
} }
log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Removing copyright from publisher") log.FromContext(ctx).With("publisher_id", publisherID).With("copyright_id", copyrightID).Debug("Removing copyright from publisher")
@ -132,8 +134,8 @@ func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, pu
} }
// AddCopyrightToSource adds a copyright to a source. // AddCopyrightToSource adds a copyright to a source.
func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error { func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
if sourceID == 0 || copyrightID == 0 { if sourceID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid source ID or copyright ID") return errors.New("invalid source ID or copyright ID")
} }
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Adding copyright to source") log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Adding copyright to source")
@ -141,8 +143,8 @@ func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID u
} }
// RemoveCopyrightFromSource removes a copyright from a source. // RemoveCopyrightFromSource removes a copyright from a source.
func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error { func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourceID uuid.UUID, copyrightID uuid.UUID) error {
if sourceID == 0 || copyrightID == 0 { if sourceID == uuid.Nil || copyrightID == uuid.Nil {
return errors.New("invalid source ID or copyright ID") return errors.New("invalid source ID or copyright ID")
} }
log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Removing copyright from source") log.FromContext(ctx).With("source_id", sourceID).With("copyright_id", copyrightID).Debug("Removing copyright from source")
@ -154,8 +156,8 @@ func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *dom
if translation == nil { if translation == nil {
return errors.New("translation cannot be nil") return errors.New("translation cannot be nil")
} }
if translation.CopyrightID == 0 { if translation.CopyrightID == uuid.Nil {
return errors.New("copyright ID cannot be zero") return errors.New("copyright ID cannot be nil")
} }
if translation.LanguageCode == "" { if translation.LanguageCode == "" {
return errors.New("language code cannot be empty") return errors.New("language code cannot be empty")

View File

@ -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"
) )

View File

@ -111,7 +111,7 @@ func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translatio
} }
return nil return nil
} }
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) { func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
if m.getByIDFunc != nil { if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id) return m.getByIDFunc(ctx, id)
} }
@ -165,7 +165,9 @@ func (m *mockCopyrightRepository) FindWithPreload(ctx context.Context, preloads
func (m *mockCopyrightRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Copyright, error) { func (m *mockCopyrightRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Copyright, error) {
return nil, nil return nil, nil
} }
func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) {
return false, nil
}
func (m *mockCopyrightRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockCopyrightRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil return nil
@ -182,40 +184,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
} }
return nil, nil return nil, nil
} }
type mockAuthorRepository struct { type mockAuthorRepository struct {
domain.AuthorRepository domain.AuthorRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
} }
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockBookRepository struct { type mockBookRepository struct {
domain.BookRepository domain.BookRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
} }
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockPublisherRepository struct { type mockPublisherRepository struct {
domain.PublisherRepository domain.PublisherRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
} }
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockSourceRepository struct { type mockSourceRepository struct {
domain.SourceRepository domain.SourceRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
} }
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// CopyrightQueries contains the query handlers for copyright. // CopyrightQueries contains the query handlers for copyright.
@ -23,8 +25,8 @@ func NewCopyrightQueries(repo domain.CopyrightRepository, workRepo domain.WorkRe
} }
// GetCopyrightByID retrieves a copyright by ID. // GetCopyrightByID retrieves a copyright by ID.
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
if id == 0 { if id == uuid.Nil {
return nil, errors.New("invalid copyright ID") return nil, errors.New("invalid copyright ID")
} }
log.FromContext(ctx).With("id", id).Debug("Getting copyright by ID") log.FromContext(ctx).With("id", id).Debug("Getting copyright by ID")
@ -40,7 +42,7 @@ func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyrig
} }
// GetCopyrightsForWork gets all copyrights for a specific work. // GetCopyrightsForWork gets all copyrights for a specific work.
func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uuid.UUID) ([]*domain.Copyright, error) {
log.FromContext(ctx).With("work_id", workID).Debug("Getting copyrights for work") log.FromContext(ctx).With("work_id", workID).Debug("Getting copyrights for work")
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil { if err != nil {
@ -50,7 +52,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint
} }
// GetCopyrightsForAuthor gets all copyrights for a specific author. // GetCopyrightsForAuthor gets all copyrights for a specific author.
func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uuid.UUID) ([]*domain.Copyright, error) {
log.FromContext(ctx).With("author_id", authorID).Debug("Getting copyrights for author") log.FromContext(ctx).With("author_id", authorID).Debug("Getting copyrights for author")
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil { if err != nil {
@ -60,7 +62,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID
} }
// GetCopyrightsForBook gets all copyrights for a specific book. // GetCopyrightsForBook gets all copyrights for a specific book.
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uuid.UUID) ([]*domain.Copyright, error) {
log.FromContext(ctx).With("book_id", bookID).Debug("Getting copyrights for book") log.FromContext(ctx).With("book_id", bookID).Debug("Getting copyrights for book")
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil { if err != nil {
@ -70,7 +72,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint
} }
// GetCopyrightsForPublisher gets all copyrights for a specific publisher. // GetCopyrightsForPublisher gets all copyrights for a specific publisher.
func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uuid.UUID) ([]*domain.Copyright, error) {
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting copyrights for publisher") log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting copyrights for publisher")
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil { if err != nil {
@ -80,7 +82,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis
} }
// GetCopyrightsForSource gets all copyrights for a specific source. // GetCopyrightsForSource gets all copyrights for a specific source.
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) { func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uuid.UUID) ([]*domain.Copyright, error) {
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting copyrights for source") log.FromContext(ctx).With("source_id", sourceID).Debug("Getting copyrights for source")
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil { if err != nil {
@ -90,8 +92,8 @@ func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID
} }
// GetTranslations gets all translations for a copyright. // GetTranslations gets all translations for a copyright.
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) { func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uuid.UUID) ([]domain.CopyrightTranslation, error) {
if copyrightID == 0 { if copyrightID == uuid.Nil {
return nil, errors.New("invalid copyright ID") return nil, errors.New("invalid copyright ID")
} }
log.FromContext(ctx).With("copyright_id", copyrightID).Debug("Getting translations for copyright") log.FromContext(ctx).With("copyright_id", copyrightID).Debug("Getting translations for copyright")
@ -99,8 +101,8 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint
} }
// GetTranslationByLanguage gets a specific translation by language code. // GetTranslationByLanguage gets a specific translation by language code.
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) { func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uuid.UUID, languageCode string) (*domain.CopyrightTranslation, error) {
if copyrightID == 0 { if copyrightID == uuid.Nil {
return nil, errors.New("invalid copyright ID") return nil, errors.New("invalid copyright ID")
} }
if languageCode == "" { if languageCode == "" {

View File

@ -6,6 +6,8 @@ import (
"tercul/internal/app/analytics" "tercul/internal/app/analytics"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// LikeCommands contains the command handlers for the like aggregate. // LikeCommands contains the command handlers for the like aggregate.
@ -24,10 +26,10 @@ func NewLikeCommands(repo domain.LikeRepository, analyticsSvc analytics.Service)
// CreateLikeInput represents the input for creating a new like. // CreateLikeInput represents the input for creating a new like.
type CreateLikeInput struct { type CreateLikeInput struct {
UserID uint UserID uuid.UUID
WorkID *uint WorkID *uuid.UUID
TranslationID *uint TranslationID *uuid.UUID
CommentID *uint CommentID *uuid.UUID
} }
// CreateLike creates a new like and increments the relevant counter. // CreateLike creates a new like and increments the relevant counter.
@ -69,7 +71,7 @@ func (c *LikeCommands) CreateLike(ctx context.Context, input CreateLikeInput) (*
} }
// DeleteLike deletes a like by ID and decrements the relevant counter. // DeleteLike deletes a like by ID and decrements the relevant counter.
func (c *LikeCommands) DeleteLike(ctx context.Context, id uint) error { func (c *LikeCommands) DeleteLike(ctx context.Context, id uuid.UUID) error {
// First, get the like to determine what it was attached to. // First, get the like to determine what it was attached to.
like, err := c.repo.GetByID(ctx, id) like, err := c.repo.GetByID(ctx, id)
if err != nil { if err != nil {

View File

@ -3,6 +3,8 @@ package like
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// LikeQueries contains the query handlers for the like aggregate. // LikeQueries contains the query handlers for the like aggregate.
@ -16,27 +18,27 @@ func NewLikeQueries(repo domain.LikeRepository) *LikeQueries {
} }
// Like returns a like by ID. // Like returns a like by ID.
func (q *LikeQueries) Like(ctx context.Context, id uint) (*domain.Like, error) { func (q *LikeQueries) Like(ctx context.Context, id uuid.UUID) (*domain.Like, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
// LikesByUserID returns all likes for a user. // LikesByUserID returns all likes for a user.
func (q *LikeQueries) LikesByUserID(ctx context.Context, userID uint) ([]domain.Like, error) { func (q *LikeQueries) LikesByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Like, error) {
return q.repo.ListByUserID(ctx, userID) return q.repo.ListByUserID(ctx, userID)
} }
// LikesByWorkID returns all likes for a work. // LikesByWorkID returns all likes for a work.
func (q *LikeQueries) LikesByWorkID(ctx context.Context, workID uint) ([]domain.Like, error) { func (q *LikeQueries) LikesByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Like, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }
// LikesByTranslationID returns all likes for a translation. // LikesByTranslationID returns all likes for a translation.
func (q *LikeQueries) LikesByTranslationID(ctx context.Context, translationID uint) ([]domain.Like, error) { func (q *LikeQueries) LikesByTranslationID(ctx context.Context, translationID uuid.UUID) ([]domain.Like, error) {
return q.repo.ListByTranslationID(ctx, translationID) return q.repo.ListByTranslationID(ctx, translationID)
} }
// LikesByCommentID returns all likes for a comment. // LikesByCommentID returns all likes for a comment.
func (q *LikeQueries) LikesByCommentID(ctx context.Context, commentID uint) ([]domain.Like, error) { func (q *LikeQueries) LikesByCommentID(ctx context.Context, commentID uuid.UUID) ([]domain.Like, error) {
return q.repo.ListByCommentID(ctx, commentID) return q.repo.ListByCommentID(ctx, commentID)
} }

View File

@ -3,6 +3,8 @@ package localization
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// LocalizationQueries contains the query handlers for the localization aggregate. // LocalizationQueries contains the query handlers for the localization aggregate.
@ -26,11 +28,11 @@ func (q *LocalizationQueries) GetTranslations(ctx context.Context, keys []string
} }
// GetAuthorBiography returns the biography of an author in a specific language. // GetAuthorBiography returns the biography of an author in a specific language.
func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error) { func (q *LocalizationQueries) GetAuthorBiography(ctx context.Context, authorID uuid.UUID, language string) (string, error) {
return q.repo.GetAuthorBiography(ctx, authorID, language) return q.repo.GetAuthorBiography(ctx, authorID, language)
} }
// GetWorkContent returns the content of a work in a specific language. // GetWorkContent returns the content of a work in a specific language.
func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uint, language string) (string, error) { func (q *LocalizationQueries) GetWorkContent(ctx context.Context, workID uuid.UUID, language string) (string, error) {
return q.repo.GetWorkContent(ctx, workID, language) return q.repo.GetWorkContent(ctx, workID, language)
} }

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// MonetizationCommands contains the command handlers for monetization. // MonetizationCommands contains the command handlers for monetization.
@ -18,8 +20,8 @@ func NewMonetizationCommands(repo domain.MonetizationRepository) *MonetizationCo
} }
// AddMonetizationToWork adds a monetization to a work. // AddMonetizationToWork adds a monetization to a work.
func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error { func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
if workID == 0 || monetizationID == 0 { if workID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid work ID or monetization ID") return errors.New("invalid work ID or monetization ID")
} }
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Adding monetization to work") log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Adding monetization to work")
@ -27,72 +29,72 @@ func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID
} }
// RemoveMonetizationFromWork removes a monetization from a work. // RemoveMonetizationFromWork removes a monetization from a work.
func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error { func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, workID uuid.UUID, monetizationID uuid.UUID) error {
if workID == 0 || monetizationID == 0 { if workID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid work ID or monetization ID") return errors.New("invalid work ID or monetization ID")
} }
log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Removing monetization from work") log.FromContext(ctx).With("work_id", workID).With("monetization_id", monetizationID).Debug("Removing monetization from work")
return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID) return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID)
} }
func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error { func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
if authorID == 0 || monetizationID == 0 { if authorID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid author ID or monetization ID") return errors.New("invalid author ID or monetization ID")
} }
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Adding monetization to author") log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Adding monetization to author")
return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID) return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID)
} }
func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error { func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, authorID uuid.UUID, monetizationID uuid.UUID) error {
if authorID == 0 || monetizationID == 0 { if authorID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid author ID or monetization ID") return errors.New("invalid author ID or monetization ID")
} }
log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Removing monetization from author") log.FromContext(ctx).With("author_id", authorID).With("monetization_id", monetizationID).Debug("Removing monetization from author")
return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID) return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID)
} }
func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error { func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
if bookID == 0 || monetizationID == 0 { if bookID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid book ID or monetization ID") return errors.New("invalid book ID or monetization ID")
} }
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Adding monetization to book") log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Adding monetization to book")
return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID) return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID)
} }
func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error { func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, bookID uuid.UUID, monetizationID uuid.UUID) error {
if bookID == 0 || monetizationID == 0 { if bookID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid book ID or monetization ID") return errors.New("invalid book ID or monetization ID")
} }
log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Removing monetization from book") log.FromContext(ctx).With("book_id", bookID).With("monetization_id", monetizationID).Debug("Removing monetization from book")
return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID) return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID)
} }
func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error { func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
if publisherID == 0 || monetizationID == 0 { if publisherID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid publisher ID or monetization ID") return errors.New("invalid publisher ID or monetization ID")
} }
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Adding monetization to publisher") log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Adding monetization to publisher")
return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID) return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID)
} }
func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error { func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uuid.UUID, monetizationID uuid.UUID) error {
if publisherID == 0 || monetizationID == 0 { if publisherID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid publisher ID or monetization ID") return errors.New("invalid publisher ID or monetization ID")
} }
log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Removing monetization from publisher") log.FromContext(ctx).With("publisher_id", publisherID).With("monetization_id", monetizationID).Debug("Removing monetization from publisher")
return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID) return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID)
} }
func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error { func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
if sourceID == 0 || monetizationID == 0 { if sourceID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid source ID or monetization ID") return errors.New("invalid source ID or monetization ID")
} }
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Adding monetization to source") log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Adding monetization to source")
return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID) return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID)
} }
func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error { func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, sourceID uuid.UUID, monetizationID uuid.UUID) error {
if sourceID == 0 || monetizationID == 0 { if sourceID == uuid.Nil || monetizationID == uuid.Nil {
return errors.New("invalid source ID or monetization ID") return errors.New("invalid source ID or monetization ID")
} }
log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Removing monetization from source") log.FromContext(ctx).With("source_id", sourceID).With("monetization_id", monetizationID).Debug("Removing monetization from source")

View File

@ -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"
) )

View File

@ -21,7 +21,7 @@ type mockMonetizationRepository struct {
listAllFunc func(ctx context.Context) ([]domain.Monetization, error) listAllFunc func(ctx context.Context) ([]domain.Monetization, error)
} }
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uint) (*domain.Monetization, error) { func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
if m.getByIDFunc != nil { if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id) return m.getByIDFunc(ctx, id)
} }
@ -107,40 +107,48 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
} }
return nil, nil return nil, nil
} }
type mockAuthorRepository struct { type mockAuthorRepository struct {
domain.AuthorRepository domain.AuthorRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
} }
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockBookRepository struct { type mockBookRepository struct {
domain.BookRepository domain.BookRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
} }
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockPublisherRepository struct { type mockPublisherRepository struct {
domain.PublisherRepository domain.PublisherRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
} }
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)
} }
return nil, nil return nil, nil
} }
type mockSourceRepository struct { type mockSourceRepository struct {
domain.SourceRepository domain.SourceRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
} }
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
if m.getByIDWithOptionsFunc != nil { if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options) return m.getByIDWithOptionsFunc(ctx, id, options)

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"github.com/google/uuid"
) )
// MonetizationQueries contains the query handlers for monetization. // MonetizationQueries contains the query handlers for monetization.
@ -23,8 +25,8 @@ func NewMonetizationQueries(repo domain.MonetizationRepository, workRepo domain.
} }
// GetMonetizationByID retrieves a monetization by ID. // GetMonetizationByID retrieves a monetization by ID.
func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) (*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
if id == 0 { if id == uuid.Nil {
return nil, errors.New("invalid monetization ID") return nil, errors.New("invalid monetization ID")
} }
log.FromContext(ctx).With("id", id).Debug("Getting monetization by ID") log.FromContext(ctx).With("id", id).Debug("Getting monetization by ID")
@ -37,7 +39,7 @@ func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.M
return q.repo.ListAll(ctx) return q.repo.ListAll(ctx)
} }
func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uuid.UUID) ([]*domain.Monetization, error) {
log.FromContext(ctx).With("work_id", workID).Debug("Getting monetizations for work") log.FromContext(ctx).With("work_id", workID).Debug("Getting monetizations for work")
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil { if err != nil {
@ -46,7 +48,7 @@ func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workI
return workRecord.Monetizations, nil return workRecord.Monetizations, nil
} }
func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uuid.UUID) ([]*domain.Monetization, error) {
log.FromContext(ctx).With("author_id", authorID).Debug("Getting monetizations for author") log.FromContext(ctx).With("author_id", authorID).Debug("Getting monetizations for author")
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil { if err != nil {
@ -55,7 +57,7 @@ func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, aut
return author.Monetizations, nil return author.Monetizations, nil
} }
func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uuid.UUID) ([]*domain.Monetization, error) {
log.FromContext(ctx).With("book_id", bookID).Debug("Getting monetizations for book") log.FromContext(ctx).With("book_id", bookID).Debug("Getting monetizations for book")
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil { if err != nil {
@ -64,7 +66,7 @@ func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookI
return book.Monetizations, nil return book.Monetizations, nil
} }
func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uuid.UUID) ([]*domain.Monetization, error) {
log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting monetizations for publisher") log.FromContext(ctx).With("publisher_id", publisherID).Debug("Getting monetizations for publisher")
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil { if err != nil {
@ -73,7 +75,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context,
return publisher.Monetizations, nil return publisher.Monetizations, nil
} }
func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) { func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uuid.UUID) ([]*domain.Monetization, error) {
log.FromContext(ctx).With("source_id", sourceID).Debug("Getting monetizations for source") log.FromContext(ctx).With("source_id", sourceID).Debug("Getting monetizations for source")
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}})
if err != nil { if err != nil {

View File

@ -3,6 +3,8 @@ package tag
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// TagCommands contains the command handlers for the tag aggregate. // TagCommands contains the command handlers for the tag aggregate.
@ -36,7 +38,7 @@ func (c *TagCommands) CreateTag(ctx context.Context, input CreateTagInput) (*dom
// UpdateTagInput represents the input for updating an existing tag. // UpdateTagInput represents the input for updating an existing tag.
type UpdateTagInput struct { type UpdateTagInput struct {
ID uint ID uuid.UUID
Name string Name string
Description string Description string
} }
@ -57,6 +59,6 @@ func (c *TagCommands) UpdateTag(ctx context.Context, input UpdateTagInput) (*dom
} }
// DeleteTag deletes a tag by ID. // DeleteTag deletes a tag by ID.
func (c *TagCommands) DeleteTag(ctx context.Context, id uint) error { func (c *TagCommands) DeleteTag(ctx context.Context, id uuid.UUID) error {
return c.repo.Delete(ctx, id) return c.repo.Delete(ctx, id)
} }

View File

@ -3,6 +3,8 @@ package tag
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// TagQueries contains the query handlers for the tag aggregate. // TagQueries contains the query handlers for the tag aggregate.
@ -16,7 +18,7 @@ func NewTagQueries(repo domain.TagRepository) *TagQueries {
} }
// Tag returns a tag by ID. // Tag returns a tag by ID.
func (q *TagQueries) Tag(ctx context.Context, id uint) (*domain.Tag, error) { func (q *TagQueries) Tag(ctx context.Context, id uuid.UUID) (*domain.Tag, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
@ -26,7 +28,7 @@ func (q *TagQueries) TagByName(ctx context.Context, name string) (*domain.Tag, e
} }
// TagsByWorkID returns all tags for a work. // TagsByWorkID returns all tags for a work.
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) { func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Tag, error) {
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }

View File

@ -7,6 +7,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -34,9 +35,9 @@ type CreateOrUpdateTranslationInput struct {
Description string Description string
Language string Language string
Status domain.TranslationStatus Status domain.TranslationStatus
TranslatableID uint TranslatableID uuid.UUID
TranslatableType string TranslatableType string
TranslatorID *uint TranslatorID *uuid.UUID
IsOriginalLanguage bool IsOriginalLanguage bool
} }
@ -49,8 +50,8 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
if input.Language == "" { if input.Language == "" {
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation) return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
} }
if input.TranslatableID == 0 { if input.TranslatableID == uuid.Nil {
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation) return nil, fmt.Errorf("%w: translatable ID cannot be nil", domain.ErrValidation)
} }
if input.TranslatableType == "" { if input.TranslatableType == "" {
return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation) return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation)
@ -70,7 +71,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
return nil, domain.ErrForbidden return nil, domain.ErrForbidden
} }
var translatorID uint var translatorID uuid.UUID
if input.TranslatorID != nil { if input.TranslatorID != nil {
translatorID = *input.TranslatorID translatorID = *input.TranslatorID
} else { } else {
@ -97,7 +98,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
} }
// DeleteTranslation deletes a translation by ID. // DeleteTranslation deletes a translation by ID.
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error { func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uuid.UUID) error {
ctx, span := c.tracer.Start(ctx, "DeleteTranslation") ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
defer span.End() defer span.End()

View File

@ -27,7 +27,7 @@ func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
args := m.Called(ctx, tx, entity) args := m.Called(ctx, tx, entity)
return args.Error(0) return args.Error(0)
} }
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -1,10 +1,12 @@
package translation package translation
import "github.com/google/uuid"
// TranslationDTO is a read model for a translation. // TranslationDTO is a read model for a translation.
type TranslationDTO struct { type TranslationDTO struct {
ID uint ID uuid.UUID
Title string Title string
Language string Language string
Content string Content string
TranslatableID uint TranslatableID uuid.UUID
} }

View File

@ -2,8 +2,8 @@ package translation
import ( import (
"context" "context"
"tercul/internal/domain"
"gorm.io/gorm" "gorm.io/gorm"
"tercul/internal/domain"
) )
type mockTranslationRepository struct { type mockTranslationRepository struct {
@ -12,7 +12,7 @@ type mockTranslationRepository struct {
listByWorkIDPaginatedFunc func(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) listByWorkIDPaginatedFunc func(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error)
} }
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { func (m *mockTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
if m.getByIDFunc != nil { if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id) return m.getByIDFunc(ctx, id)
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@ -23,7 +24,7 @@ func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQuerie
} }
// Translation returns a translation by ID. // Translation returns a translation by ID.
func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*TranslationDTO, error) { func (q *TranslationQueries) Translation(ctx context.Context, id uuid.UUID) (*TranslationDTO, error) {
ctx, span := q.tracer.Start(ctx, "Translation") ctx, span := q.tracer.Start(ctx, "Translation")
defer span.End() defer span.End()
@ -45,21 +46,21 @@ func (q *TranslationQueries) Translation(ctx context.Context, id uint) (*Transla
} }
// TranslationsByWorkID returns all translations for a work. // TranslationsByWorkID returns all translations for a work.
func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { func (q *TranslationQueries) TranslationsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
ctx, span := q.tracer.Start(ctx, "TranslationsByWorkID") ctx, span := q.tracer.Start(ctx, "TranslationsByWorkID")
defer span.End() defer span.End()
return q.repo.ListByWorkID(ctx, workID) return q.repo.ListByWorkID(ctx, workID)
} }
// TranslationsByEntity returns all translations for an entity. // TranslationsByEntity returns all translations for an entity.
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
ctx, span := q.tracer.Start(ctx, "TranslationsByEntity") ctx, span := q.tracer.Start(ctx, "TranslationsByEntity")
defer span.End() defer span.End()
return q.repo.ListByEntity(ctx, entityType, entityID) return q.repo.ListByEntity(ctx, entityType, entityID)
} }
// TranslationsByTranslatorID returns all translations for a translator. // TranslationsByTranslatorID returns all translations for a translator.
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID") ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID")
defer span.End() defer span.End()
return q.repo.ListByTranslatorID(ctx, translatorID) return q.repo.ListByTranslatorID(ctx, translatorID)
@ -80,7 +81,7 @@ func (q *TranslationQueries) Translations(ctx context.Context) ([]domain.Transla
} }
// ListTranslations returns a paginated list of translations for a work, with optional language filtering. // ListTranslations returns a paginated list of translations for a work, with optional language filtering.
func (q *TranslationQueries) ListTranslations(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[TranslationDTO], error) { func (q *TranslationQueries) ListTranslations(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[TranslationDTO], error) {
ctx, span := q.tracer.Start(ctx, "ListTranslations") ctx, span := q.tracer.Start(ctx, "ListTranslations")
defer span.End() defer span.End()

View File

@ -7,6 +7,8 @@ import (
"tercul/internal/app/authz" "tercul/internal/app/authz"
"tercul/internal/domain" "tercul/internal/domain"
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
) )
// UserCommands contains the command handlers for the user aggregate. // UserCommands contains the command handlers for the user aggregate.
@ -52,7 +54,7 @@ func (c *UserCommands) CreateUser(ctx context.Context, input CreateUserInput) (*
// UpdateUserInput represents the input for updating an existing user. // UpdateUserInput represents the input for updating an existing user.
type UpdateUserInput struct { type UpdateUserInput struct {
ID uint ID uuid.UUID
Username *string Username *string
Email *string Email *string
Password *string Password *string
@ -64,12 +66,13 @@ type UpdateUserInput struct {
Role *domain.UserRole Role *domain.UserRole
Verified *bool Verified *bool
Active *bool Active *bool
CountryID *uint CountryID *uuid.UUID
CityID *uint CityID *uuid.UUID
AddressID *uint AddressID *uuid.UUID
} }
// UpdateUser updates an existing user. // UpdateUser updates an existing user.
//
//nolint:gocyclo // Complex update logic //nolint:gocyclo // Complex update logic
func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) { func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*domain.User, error) {
actorID, ok := platform_auth.GetUserIDFromContext(ctx) actorID, ok := platform_auth.GetUserIDFromContext(ctx)
@ -147,7 +150,7 @@ func (c *UserCommands) UpdateUser(ctx context.Context, input UpdateUserInput) (*
} }
// DeleteUser deletes a user by ID. // DeleteUser deletes a user by ID.
func (c *UserCommands) DeleteUser(ctx context.Context, id uint) error { func (c *UserCommands) DeleteUser(ctx context.Context, id uuid.UUID) error {
actorID, ok := platform_auth.GetUserIDFromContext(ctx) actorID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok { if !ok {
return domain.ErrUnauthorized return domain.ErrUnauthorized

View File

@ -23,7 +23,7 @@ func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
return args.Error(0) return args.Error(0)
} }
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) { func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -3,6 +3,8 @@ package user
import ( import (
"context" "context"
"tercul/internal/domain" "tercul/internal/domain"
"github.com/google/uuid"
) )
// UserQueries contains the query handlers for the user aggregate. // UserQueries contains the query handlers for the user aggregate.
@ -17,7 +19,7 @@ func NewUserQueries(repo domain.UserRepository, profileRepo domain.UserProfileRe
} }
// User returns a user by ID. // User returns a user by ID.
func (q *UserQueries) User(ctx context.Context, id uint) (*domain.User, error) { func (q *UserQueries) User(ctx context.Context, id uuid.UUID) (*domain.User, error) {
return q.repo.GetByID(ctx, id) return q.repo.GetByID(ctx, id)
} }
@ -42,6 +44,6 @@ func (q *UserQueries) Users(ctx context.Context) ([]domain.User, error) {
} }
// UserProfile returns a user profile by user ID. // UserProfile returns a user profile by user ID.
func (q *UserQueries) UserProfile(ctx context.Context, userID uint) (*domain.UserProfile, error) { func (q *UserQueries) UserProfile(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
return q.profileRepo.GetByUserID(ctx, userID) return q.profileRepo.GetByUserID(ctx, userID)
} }

View File

@ -18,7 +18,7 @@ type mockUserProfileRepository struct {
mock.Mock mock.Mock
} }
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) { func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
args := m.Called(ctx, userID) args := m.Called(ctx, userID)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)
@ -36,7 +36,7 @@ func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB,
return args.Error(0) return args.Error(0)
} }
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) { func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserProfile, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -14,6 +14,8 @@ import (
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/google/uuid"
) )
// WorkCommands contains the command handlers for the work aggregate. // WorkCommands contains the command handlers for the work aggregate.
@ -108,8 +110,8 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
if work == nil { if work == nil {
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation) return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
} }
if work.ID == 0 { if work.ID == uuid.Nil {
return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation) return fmt.Errorf("%w: work ID cannot be nil", domain.ErrValidation)
} }
userID, ok := platform_auth.GetUserIDFromContext(ctx) userID, ok := platform_auth.GetUserIDFromContext(ctx)
@ -149,10 +151,10 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
} }
// DeleteWork deletes a work by ID after performing an authorization check. // DeleteWork deletes a work by ID after performing an authorization check.
func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error { func (c *WorkCommands) DeleteWork(ctx context.Context, id uuid.UUID) error {
ctx, span := c.tracer.Start(ctx, "DeleteWork") ctx, span := c.tracer.Start(ctx, "DeleteWork")
defer span.End() defer span.End()
if id == 0 { if id == uuid.Nil {
return fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -181,7 +183,7 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
} }
// AnalyzeWork performs linguistic analysis on a work and its translations. // AnalyzeWork performs linguistic analysis on a work and its translations.
func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error { func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uuid.UUID) error {
ctx, span := c.tracer.Start(ctx, "AnalyzeWork") ctx, span := c.tracer.Start(ctx, "AnalyzeWork")
defer span.End() defer span.End()
logger := log.FromContext(ctx).With("workID", workID) logger := log.FromContext(ctx).With("workID", workID)
@ -220,8 +222,9 @@ func (c *WorkCommands) AnalyzeWork(ctx context.Context, workID uint) error {
} }
// MergeWork merges two works, moving all associations from the source to the target and deleting the source. // MergeWork merges two works, moving all associations from the source to the target and deleting the source.
//
//nolint:gocyclo // Complex merge logic //nolint:gocyclo // Complex merge logic
func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) error { func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uuid.UUID) error {
ctx, span := c.tracer.Start(ctx, "MergeWork") ctx, span := c.tracer.Start(ctx, "MergeWork")
defer span.End() defer span.End()
if sourceID == targetID { if sourceID == targetID {
@ -331,7 +334,7 @@ func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uint) e
return nil return nil
} }
func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error { func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uuid.UUID) error {
var sourceStats domain.WorkStats var sourceStats domain.WorkStats
err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@ -352,7 +355,7 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uint) error {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
// If target has no stats, create a new stats record for it. // If target has no stats, create a new stats record for it.
newStats := sourceStats newStats := sourceStats
newStats.ID = 0 newStats.ID = uuid.Nil
newStats.WorkID = targetWorkID newStats.WorkID = targetWorkID
if err = tx.Create(&newStats).Error; err != nil { if err = tx.Create(&newStats).Error; err != nil {
return fmt.Errorf("failed to create new target stats: %w", err) return fmt.Errorf("failed to create new target stats: %w", err)

View File

@ -1,8 +1,10 @@
package work package work
import "github.com/google/uuid"
// WorkDTO is a read model for a work, containing only the data needed for API responses. // WorkDTO is a read model for a work, containing only the data needed for API responses.
type WorkDTO struct { type WorkDTO struct {
ID uint ID uuid.UUID
Title string Title string
Language string Language string
} }

View File

@ -21,7 +21,7 @@ func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity
args := m.Called(ctx, tx, entity) args := m.Called(ctx, tx, entity)
return args.Error(0) return args.Error(0)
} }
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { func (m *mockWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -7,6 +7,8 @@ import (
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/google/uuid"
) )
// WorkQueries contains the query handlers for the work aggregate. // WorkQueries contains the query handlers for the work aggregate.
@ -24,10 +26,10 @@ func NewWorkQueries(repo domain.WorkRepository) *WorkQueries {
} }
// GetWorkByID retrieves a work by ID. // GetWorkByID retrieves a work by ID.
func (q *WorkQueries) GetWorkByID(ctx context.Context, id uint) (*WorkDTO, error) { func (q *WorkQueries) GetWorkByID(ctx context.Context, id uuid.UUID) (*WorkDTO, error) {
ctx, span := q.tracer.Start(ctx, "GetWorkByID") ctx, span := q.tracer.Start(ctx, "GetWorkByID")
defer span.End() defer span.End()
if id == 0 { if id == uuid.Nil {
return nil, errors.New("invalid work ID") return nil, errors.New("invalid work ID")
} }
@ -75,10 +77,10 @@ func (q *WorkQueries) ListWorks(ctx context.Context, page, pageSize int) (*domai
} }
// GetWorkWithTranslations retrieves a work with its translations. // GetWorkWithTranslations retrieves a work with its translations.
func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uint) (*domain.Work, error) { func (q *WorkQueries) GetWorkWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
ctx, span := q.tracer.Start(ctx, "GetWorkWithTranslations") ctx, span := q.tracer.Start(ctx, "GetWorkWithTranslations")
defer span.End() defer span.End()
if id == 0 { if id == uuid.Nil {
return nil, errors.New("invalid work ID") return nil, errors.New("invalid work ID")
} }
return q.repo.GetWithTranslations(ctx, id) return q.repo.GetWithTranslations(ctx, id)
@ -95,20 +97,20 @@ func (q *WorkQueries) FindWorksByTitle(ctx context.Context, title string) ([]dom
} }
// FindWorksByAuthor finds works by author ID. // FindWorksByAuthor finds works by author ID.
func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { func (q *WorkQueries) FindWorksByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
ctx, span := q.tracer.Start(ctx, "FindWorksByAuthor") ctx, span := q.tracer.Start(ctx, "FindWorksByAuthor")
defer span.End() defer span.End()
if authorID == 0 { if authorID == uuid.Nil {
return nil, errors.New("invalid author ID") return nil, errors.New("invalid author ID")
} }
return q.repo.FindByAuthor(ctx, authorID) return q.repo.FindByAuthor(ctx, authorID)
} }
// FindWorksByCategory finds works by category ID. // FindWorksByCategory finds works by category ID.
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
ctx, span := q.tracer.Start(ctx, "FindWorksByCategory") ctx, span := q.tracer.Start(ctx, "FindWorksByCategory")
defer span.End() defer span.End()
if categoryID == 0 { if categoryID == uuid.Nil {
return nil, errors.New("invalid category ID") return nil, errors.New("invalid category ID")
} }
return q.repo.FindByCategory(ctx, categoryID) return q.repo.FindByCategory(ctx, categoryID)
@ -125,10 +127,10 @@ func (q *WorkQueries) FindWorksByLanguage(ctx context.Context, language string,
} }
// ListByCollectionID finds works by collection ID. // ListByCollectionID finds works by collection ID.
func (q *WorkQueries) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { func (q *WorkQueries) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
ctx, span := q.tracer.Start(ctx, "ListByCollectionID") ctx, span := q.tracer.Start(ctx, "ListByCollectionID")
defer span.End() defer span.End()
if collectionID == 0 { if collectionID == uuid.Nil {
return nil, errors.New("invalid collection ID") return nil, errors.New("invalid collection ID")
} }
return q.repo.ListByCollectionID(ctx, collectionID) return q.repo.ListByCollectionID(ctx, collectionID)

View File

@ -7,6 +7,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
platform_cache "tercul/internal/platform/cache" platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -52,7 +53,7 @@ func (r *CachedAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, en
return err return err
} }
func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Author var cached domain.Author
key := r.opt.Keys.EntityKey("author", id) key := r.opt.Keys.EntityKey("author", id)
@ -71,7 +72,7 @@ func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.
return author, nil return author, nil
} }
func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Author, error) {
return r.inner.GetByIDWithOptions(ctx, id, options) return r.inner.GetByIDWithOptions(ctx, id, options)
} }
@ -97,7 +98,7 @@ func (r *CachedAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, en
return err return err
} }
func (r *CachedAuthorRepository) Delete(ctx context.Context, id uint) error { func (r *CachedAuthorRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id) err := r.inner.Delete(ctx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -108,7 +109,7 @@ func (r *CachedAuthorRepository) Delete(ctx context.Context, id uint) error {
return err return err
} }
func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id) err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -154,7 +155,7 @@ func (r *CachedAuthorRepository) CountWithOptions(ctx context.Context, options *
return r.inner.CountWithOptions(ctx, options) return r.inner.CountWithOptions(ctx, options)
} }
func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Author, error) {
return r.inner.FindWithPreload(ctx, preloads, id) return r.inner.FindWithPreload(ctx, preloads, id)
} }
@ -162,7 +163,7 @@ func (r *CachedAuthorRepository) GetAllForSync(ctx context.Context, batchSize, o
return r.inner.GetAllForSync(ctx, batchSize, offset) return r.inner.GetAllForSync(ctx, batchSize, offset)
} }
func (r *CachedAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { func (r *CachedAuthorRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id) return r.inner.Exists(ctx, id)
} }
@ -178,19 +179,19 @@ func (r *CachedAuthorRepository) FindByName(ctx context.Context, name string) (*
return r.inner.FindByName(ctx, name) return r.inner.FindByName(ctx, name)
} }
func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByWorkID(ctx, workID) return r.inner.ListByWorkID(ctx, workID)
} }
func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByBookID(ctx, bookID) return r.inner.ListByBookID(ctx, bookID)
} }
func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByCountryID(ctx, countryID) return r.inner.ListByCountryID(ctx, countryID)
} }
func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Author var cached domain.Author
key := r.opt.Keys.QueryKey("author", "withTranslations", id) key := r.opt.Keys.QueryKey("author", "withTranslations", id)

View File

@ -0,0 +1,231 @@
package cache
import (
"context"
"encoding/json"
"errors"
"sync"
"testing"
"time"
"tercul/internal/domain"
"gorm.io/gorm"
"github.com/stretchr/testify/require"
)
// memCache (duplicated) - tiny in-memory Cache impl for tests
type memCacheA struct {
m map[string][]byte
mu sync.RWMutex
}
func newMemCacheA() *memCacheA { return &memCacheA{m: map[string][]byte{}} }
func (c *memCacheA) Get(_ context.Context, key string, value interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
b, ok := c.m[key]
if !ok {
return errors.New("cache miss")
}
return json.Unmarshal(b, value)
}
func (c *memCacheA) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
b, err := json.Marshal(value)
if err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
c.m[key] = b
return nil
}
func (c *memCacheA) Delete(_ context.Context, key string) error {
c.mu.Lock()
delete(c.m, key)
c.mu.Unlock()
return nil
}
func (c *memCacheA) Clear(_ context.Context) error {
c.mu.Lock()
c.m = map[string][]byte{}
c.mu.Unlock()
return nil
}
func (c *memCacheA) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
res := map[string][]byte{}
c.mu.RLock()
defer c.mu.RUnlock()
for _, k := range keys {
if v, ok := c.m[k]; ok {
res[k] = v
}
}
return res, nil
}
func (c *memCacheA) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
for k, v := range items {
b, _ := json.Marshal(v)
c.m[k] = b
}
return nil
}
// dummyAuthorRepo implements domain.AuthorRepository minimal methods for tests
type dummyAuthorRepo struct {
store map[uint]*domain.Author
calls int
mu sync.Mutex
}
func newDummyAuthorRepo() *dummyAuthorRepo { return &dummyAuthorRepo{store: map[uint]*domain.Author{}} }
func (d *dummyAuthorRepo) Create(_ context.Context, entity *domain.Author) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
d.store[entity.ID] = entity
return nil
}
func (d *dummyAuthorRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Author) error {
return errors.New("not implemented")
}
func (d *dummyAuthorRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Author, error) {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
a, ok := d.store[id]
if !ok {
return nil, errors.New("not found")
}
cp := *a
return &cp, nil
}
func (d *dummyAuthorRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyAuthorRepo) Update(_ context.Context, entity *domain.Author) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[entity.ID]; !ok {
return errors.New("not found")
}
d.store[entity.ID] = entity
return nil
}
func (d *dummyAuthorRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Author) error {
return errors.New("not implemented")
}
func (d *dummyAuthorRepo) Delete(_ context.Context, id uint) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[id]; !ok {
return errors.New("not found")
}
delete(d.store, id)
return nil
}
func (d *dummyAuthorRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
return errors.New("not implemented")
}
func (d *dummyAuthorRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
return &domain.PaginatedResult[domain.Author]{Items: []domain.Author{}}, nil
}
func (d *dummyAuthorRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) ListAll(_ context.Context) ([]domain.Author, error) { return nil, nil }
func (d *dummyAuthorRepo) Count(_ context.Context) (int64, error) { return 0, nil }
func (d *dummyAuthorRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
return 0, nil
}
func (d *dummyAuthorRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Author, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyAuthorRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) Exists(_ context.Context, id uint) (bool, error) {
_, ok := d.store[id]
return ok, nil
}
func (d *dummyAuthorRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
func (d *dummyAuthorRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error { return fn(nil) }
func (d *dummyAuthorRepo) FindByName(_ context.Context, name string) (*domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) ListByWorkID(_ context.Context, workID uint) ([]domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) ListByBookID(_ context.Context, bookID uint) ([]domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) ListByCountryID(_ context.Context, countryID uint) ([]domain.Author, error) {
return nil, nil
}
func (d *dummyAuthorRepo) GetWithTranslations(_ context.Context, id uint) (*domain.Author, error) {
return d.GetByID(context.Background(), id)
}
func TestCachedAuthor_GetByID_Caches(t *testing.T) {
ctx := context.Background()
d := newDummyAuthorRepo()
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
mc := newMemCacheA()
ca := NewCachedAuthorRepository(d, mc, nil)
a, err := ca.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "Tolstoy", a.Name)
require.Equal(t, 1, d.calls)
// second read should hit cache
a2, err := ca.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "Tolstoy", a2.Name)
require.Equal(t, 1, d.calls)
}
func TestCachedAuthor_Update_Invalidates(t *testing.T) {
ctx := context.Background()
d := newDummyAuthorRepo()
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
mc := newMemCacheA()
ca := NewCachedAuthorRepository(d, mc, nil)
_, err := ca.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, 1, d.calls)
// update underlying
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Leo"}
err = ca.Update(ctx, d.store[1])
require.NoError(t, err)
// next get should fetch again
a, err := ca.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "Leo", a.Name)
require.Equal(t, 3, d.calls)
}
func TestCachedAuthor_Delete_Invalidates(t *testing.T) {
ctx := context.Background()
d := newDummyAuthorRepo()
d.store[1] = &domain.Author{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: "Tolstoy"}
mc := newMemCacheA()
ca := NewCachedAuthorRepository(d, mc, nil)
_, err := ca.GetByID(ctx, 1)
require.NoError(t, err)
err = ca.Delete(ctx, 1)
require.NoError(t, err)
_, err = ca.GetByID(ctx, 1)
require.Error(t, err)
}

View File

@ -7,6 +7,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
platform_cache "tercul/internal/platform/cache" platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -52,7 +53,7 @@ func (r *CachedTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.D
return err return err
} }
func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Translation var cached domain.Translation
key := r.opt.Keys.EntityKey("translation", id) key := r.opt.Keys.EntityKey("translation", id)
@ -71,7 +72,7 @@ func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uint) (*do
return tr, nil return tr, nil
} }
func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Translation, error) {
return r.inner.GetByIDWithOptions(ctx, id, options) return r.inner.GetByIDWithOptions(ctx, id, options)
} }
@ -97,7 +98,7 @@ func (r *CachedTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.D
return err return err
} }
func (r *CachedTranslationRepository) Delete(ctx context.Context, id uint) error { func (r *CachedTranslationRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id) err := r.inner.Delete(ctx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -108,7 +109,7 @@ func (r *CachedTranslationRepository) Delete(ctx context.Context, id uint) error
return err return err
} }
func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id) err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -154,7 +155,7 @@ func (r *CachedTranslationRepository) CountWithOptions(ctx context.Context, opti
return r.inner.CountWithOptions(ctx, options) return r.inner.CountWithOptions(ctx, options)
} }
func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Translation, error) {
return r.inner.FindWithPreload(ctx, preloads, id) return r.inner.FindWithPreload(ctx, preloads, id)
} }
@ -162,7 +163,7 @@ func (r *CachedTranslationRepository) GetAllForSync(ctx context.Context, batchSi
return r.inner.GetAllForSync(ctx, batchSize, offset) return r.inner.GetAllForSync(ctx, batchSize, offset)
} }
func (r *CachedTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { func (r *CachedTranslationRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id) return r.inner.Exists(ctx, id)
} }
@ -174,7 +175,7 @@ func (r *CachedTranslationRepository) WithTx(ctx context.Context, fn func(tx *go
return r.inner.WithTx(ctx, fn) return r.inner.WithTx(ctx, fn)
} }
func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached []domain.Translation var cached []domain.Translation
key := r.opt.Keys.QueryKey("translation", "byWork", workID) key := r.opt.Keys.QueryKey("translation", "byWork", workID)
@ -193,7 +194,7 @@ func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID u
return res, nil return res, nil
} }
func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
lang := "" lang := ""
if language != nil { if language != nil {
lang = *language lang = *language
@ -217,11 +218,11 @@ func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context,
return res, nil return res, nil
} }
func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
return r.inner.ListByEntity(ctx, entityType, entityID) return r.inner.ListByEntity(ctx, entityType, entityID)
} }
func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
return r.inner.ListByTranslatorID(ctx, translatorID) return r.inner.ListByTranslatorID(ctx, translatorID)
} }

View File

@ -0,0 +1,256 @@
package cache
import (
"context"
"encoding/json"
"errors"
"sync"
"testing"
"time"
"tercul/internal/domain"
"gorm.io/gorm"
"github.com/stretchr/testify/require"
)
// memCacheT - small in-memory cache for translation tests
type memCacheT struct {
m map[string][]byte
mu sync.RWMutex
}
func newMemCacheT() *memCacheT { return &memCacheT{m: map[string][]byte{}} }
func (c *memCacheT) Get(_ context.Context, key string, value interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
b, ok := c.m[key]
if !ok {
return errors.New("cache miss")
}
return json.Unmarshal(b, value)
}
func (c *memCacheT) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
b, err := json.Marshal(value)
if err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
c.m[key] = b
return nil
}
func (c *memCacheT) Delete(_ context.Context, key string) error {
c.mu.Lock()
delete(c.m, key)
c.mu.Unlock()
return nil
}
func (c *memCacheT) Clear(_ context.Context) error {
c.mu.Lock()
c.m = map[string][]byte{}
c.mu.Unlock()
return nil
}
func (c *memCacheT) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
res := map[string][]byte{}
c.mu.RLock()
defer c.mu.RUnlock()
for _, k := range keys {
if v, ok := c.m[k]; ok {
res[k] = v
}
}
return res, nil
}
func (c *memCacheT) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
for k, v := range items {
b, _ := json.Marshal(v)
c.m[k] = b
}
return nil
}
// dummyTranslationRepo implements domain.TranslationRepository minimal functionality for tests
type dummyTranslationRepo struct {
store map[uint]*domain.Translation
calls int
mu sync.Mutex
}
func newDummyTranslationRepo() *dummyTranslationRepo {
return &dummyTranslationRepo{store: map[uint]*domain.Translation{}}
}
func (d *dummyTranslationRepo) Create(_ context.Context, entity *domain.Translation) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
d.store[entity.ID] = entity
return nil
}
func (d *dummyTranslationRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Translation) error {
return errors.New("not implemented")
}
func (d *dummyTranslationRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Translation, error) {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
tr, ok := d.store[id]
if !ok {
return nil, errors.New("not found")
}
cp := *tr
return &cp, nil
}
func (d *dummyTranslationRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyTranslationRepo) Update(_ context.Context, entity *domain.Translation) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[entity.ID]; !ok {
return errors.New("not found")
}
d.store[entity.ID] = entity
return nil
}
func (d *dummyTranslationRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Translation) error {
return errors.New("not implemented")
}
func (d *dummyTranslationRepo) Delete(_ context.Context, id uint) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[id]; !ok {
return errors.New("not found")
}
delete(d.store, id)
return nil
}
func (d *dummyTranslationRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
return errors.New("not implemented")
}
func (d *dummyTranslationRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
return &domain.PaginatedResult[domain.Translation]{Items: []domain.Translation{}}, nil
}
func (d *dummyTranslationRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) ListAll(_ context.Context) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) Count(_ context.Context) (int64, error) { return 0, nil }
func (d *dummyTranslationRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
return 0, nil
}
func (d *dummyTranslationRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Translation, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyTranslationRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) Exists(_ context.Context, id uint) (bool, error) {
_, ok := d.store[id]
return ok, nil
}
func (d *dummyTranslationRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
func (d *dummyTranslationRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error {
return fn(nil)
}
func (d *dummyTranslationRepo) ListByWorkID(_ context.Context, workID uint) ([]domain.Translation, error) {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
res := []domain.Translation{}
for _, v := range d.store {
if v.TranslatableID == workID {
res = append(res, *v)
}
}
return res, nil
}
func (d *dummyTranslationRepo) ListByWorkIDPaginated(_ context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
all, _ := d.ListByWorkID(context.Background(), workID)
return &domain.PaginatedResult[domain.Translation]{Items: all, TotalCount: int64(len(all)), Page: page, PageSize: pageSize}, nil
}
func (d *dummyTranslationRepo) ListByEntity(_ context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) ListByTranslatorID(_ context.Context, translatorID uint) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) ListByStatus(_ context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
return nil, nil
}
func (d *dummyTranslationRepo) Upsert(_ context.Context, translation *domain.Translation) error {
d.mu.Lock()
defer d.mu.Unlock()
if translation.ID == 0 {
translation.ID = uint(len(d.store) + 1)
}
d.store[translation.ID] = translation
d.calls++
return nil
}
func TestCachedTranslation_GetByID_Caches(t *testing.T) {
ctx := context.Background()
d := newDummyTranslationRepo()
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "T1"}
mc := newMemCacheT()
ct := NewCachedTranslationRepository(d, mc, nil)
tr, err := ct.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "T1", tr.Title)
require.Equal(t, 1, d.calls)
// second read should hit cache
tr2, err := ct.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "T1", tr2.Title)
require.Equal(t, 1, d.calls)
}
func TestCachedTranslation_ListByWorkID_Caches(t *testing.T) {
ctx := context.Background()
d := newDummyTranslationRepo()
d.store[11] = &domain.Translation{BaseModel: domain.BaseModel{ID: 11}, TranslatableID: 100, Language: "en", Title: "A"}
d.store[12] = &domain.Translation{BaseModel: domain.BaseModel{ID: 12}, TranslatableID: 100, Language: "fr", Title: "B"}
mc := newMemCacheT()
ct := NewCachedTranslationRepository(d, mc, nil)
list, err := ct.ListByWorkID(ctx, 100)
require.NoError(t, err)
require.Len(t, list, 2)
require.Equal(t, 1, d.calls)
// second call should hit cache
list2, err := ct.ListByWorkID(ctx, 100)
require.NoError(t, err)
require.Len(t, list2, 2)
require.Equal(t, 1, d.calls)
}
func TestCachedTranslation_Update_Invalidates(t *testing.T) {
ctx := context.Background()
d := newDummyTranslationRepo()
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "Old"}
mc := newMemCacheT()
ct := NewCachedTranslationRepository(d, mc, nil)
_, err := ct.GetByID(ctx, 1)
require.NoError(t, err)
// update underlying
d.store[1] = &domain.Translation{BaseModel: domain.BaseModel{ID: 1}, TranslatableID: 10, Language: "en", Title: "New"}
err = ct.Update(ctx, d.store[1])
require.NoError(t, err)
tr, err := ct.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "New", tr.Title)
require.Equal(t, 3, d.calls)
}

View File

@ -7,6 +7,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
platform_cache "tercul/internal/platform/cache" platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -52,7 +53,7 @@ func (r *CachedWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, enti
return err return err
} }
func (r *CachedWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { func (r *CachedWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work var cached domain.Work
key := r.opt.Keys.EntityKey("work", id) key := r.opt.Keys.EntityKey("work", id)
@ -72,7 +73,7 @@ func (r *CachedWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Wo
return work, nil return work, nil
} }
func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Work, error) {
// Options can include varying preloads/where/order; avoid caching to prevent incorrect results. // Options can include varying preloads/where/order; avoid caching to prevent incorrect results.
return r.inner.GetByIDWithOptions(ctx, id, options) return r.inner.GetByIDWithOptions(ctx, id, options)
} }
@ -99,7 +100,7 @@ func (r *CachedWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, enti
return err return err
} }
func (r *CachedWorkRepository) Delete(ctx context.Context, id uint) error { func (r *CachedWorkRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id) err := r.inner.Delete(ctx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -110,7 +111,7 @@ func (r *CachedWorkRepository) Delete(ctx context.Context, id uint) error {
return err return err
} }
func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id) err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil { if err == nil {
if r.opt.Cache != nil { if r.opt.Cache != nil {
@ -156,7 +157,7 @@ func (r *CachedWorkRepository) CountWithOptions(ctx context.Context, options *do
return r.inner.CountWithOptions(ctx, options) return r.inner.CountWithOptions(ctx, options)
} }
func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Work, error) {
return r.inner.FindWithPreload(ctx, preloads, id) return r.inner.FindWithPreload(ctx, preloads, id)
} }
@ -164,7 +165,7 @@ func (r *CachedWorkRepository) GetAllForSync(ctx context.Context, batchSize, off
return r.inner.GetAllForSync(ctx, batchSize, offset) return r.inner.GetAllForSync(ctx, batchSize, offset)
} }
func (r *CachedWorkRepository) Exists(ctx context.Context, id uint) (bool, error) { func (r *CachedWorkRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id) return r.inner.Exists(ctx, id)
} }
@ -180,11 +181,11 @@ func (r *CachedWorkRepository) FindByTitle(ctx context.Context, title string) ([
return r.inner.FindByTitle(ctx, title) return r.inner.FindByTitle(ctx, title)
} }
func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
return r.inner.FindByAuthor(ctx, authorID) return r.inner.FindByAuthor(ctx, authorID)
} }
func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
return r.inner.FindByCategory(ctx, categoryID) return r.inner.FindByCategory(ctx, categoryID)
} }
@ -207,7 +208,7 @@ func (r *CachedWorkRepository) FindByLanguage(ctx context.Context, language stri
return res, nil return res, nil
} }
func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) { func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work var cached domain.Work
key := r.opt.Keys.QueryKey("work", "withTranslations", id) key := r.opt.Keys.QueryKey("work", "withTranslations", id)
@ -226,7 +227,7 @@ func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uint)
return work, nil return work, nil
} }
func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) { func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil { if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work var cached domain.Work
key := r.opt.Keys.QueryKey("work", "withAssociations", id) key := r.opt.Keys.QueryKey("work", "withAssociations", id)
@ -245,7 +246,7 @@ func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uint)
return work, nil return work, nil
} }
func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) (*domain.Work, error) {
// Tx-scoped reads should bypass cache. // Tx-scoped reads should bypass cache.
return r.inner.GetWithAssociationsInTx(ctx, tx, id) return r.inner.GetWithAssociationsInTx(ctx, tx, id)
} }
@ -269,10 +270,10 @@ func (r *CachedWorkRepository) ListWithTranslations(ctx context.Context, page, p
return res, nil return res, nil
} }
func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) { func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uuid.UUID, authorID uuid.UUID) (bool, error) {
return r.inner.IsAuthor(ctx, workID, authorID) return r.inner.IsAuthor(ctx, workID, authorID)
} }
func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
return r.inner.ListByCollectionID(ctx, collectionID) return r.inner.ListByCollectionID(ctx, collectionID)
} }

View File

@ -0,0 +1,287 @@
package cache
import (
"context"
"encoding/json"
"errors"
"sync"
"testing"
"time"
"tercul/internal/domain"
"gorm.io/gorm"
"github.com/stretchr/testify/require"
)
// memCache is a tiny in-memory implementation of platform_cache.Cache for tests.
type memCache struct {
m map[string][]byte
mu sync.RWMutex
}
func newMemCache() *memCache { return &memCache{m: map[string][]byte{}} }
func (c *memCache) Get(_ context.Context, key string, value interface{}) error {
c.mu.RLock()
defer c.mu.RUnlock()
b, ok := c.m[key]
if !ok {
return errors.New("cache miss")
}
return json.Unmarshal(b, value)
}
func (c *memCache) Set(_ context.Context, key string, value interface{}, expiration time.Duration) error {
b, err := json.Marshal(value)
if err != nil {
return err
}
c.mu.Lock()
defer c.mu.Unlock()
c.m[key] = b
return nil
}
func (c *memCache) Delete(_ context.Context, key string) error {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.m, key)
return nil
}
func (c *memCache) Clear(_ context.Context) error {
c.mu.Lock()
c.m = map[string][]byte{}
c.mu.Unlock()
return nil
}
func (c *memCache) GetMulti(_ context.Context, keys []string) (map[string][]byte, error) {
res := map[string][]byte{}
c.mu.RLock()
defer c.mu.RUnlock()
for _, k := range keys {
if v, ok := c.m[k]; ok {
res[k] = v
}
}
return res, nil
}
func (c *memCache) SetMulti(_ context.Context, items map[string]interface{}, expiration time.Duration) error {
for k, v := range items {
b, _ := json.Marshal(v)
c.m[k] = b
}
return nil
}
// dummyWorkRepo implements the minimal parts of domain.WorkRepository we need for tests.
type dummyWorkRepo struct {
store map[uint]*domain.Work
calls int
mu sync.Mutex
}
func newDummyWorkRepo() *dummyWorkRepo { return &dummyWorkRepo{store: map[uint]*domain.Work{}} }
func (d *dummyWorkRepo) Create(_ context.Context, entity *domain.Work) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
d.store[entity.ID] = entity
return nil
}
func (d *dummyWorkRepo) CreateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
return errors.New("not implemented")
}
func (d *dummyWorkRepo) GetByID(_ context.Context, id uuid.UUID) (*domain.Work, error) {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
w, ok := d.store[id]
if !ok {
return nil, errors.New("not found")
}
// return a copy to ensure caching serializes
cp := *w
return &cp, nil
}
func (d *dummyWorkRepo) GetByIDWithOptions(_ context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyWorkRepo) Update(_ context.Context, entity *domain.Work) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[entity.ID]; !ok {
return errors.New("not found")
}
d.store[entity.ID] = entity
return nil
}
func (d *dummyWorkRepo) UpdateInTx(_ context.Context, tx *gorm.DB, entity *domain.Work) error {
return errors.New("not implemented")
}
func (d *dummyWorkRepo) Delete(_ context.Context, id uint) error {
d.mu.Lock()
defer d.mu.Unlock()
d.calls++
if _, ok := d.store[id]; !ok {
return errors.New("not found")
}
delete(d.store, id)
return nil
}
func (d *dummyWorkRepo) DeleteInTx(_ context.Context, tx *gorm.DB, id uint) error {
return errors.New("not implemented")
}
func (d *dummyWorkRepo) List(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
}
func (d *dummyWorkRepo) ListWithOptions(_ context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
return nil, nil
}
func (d *dummyWorkRepo) ListAll(_ context.Context) ([]domain.Work, error) { return nil, nil }
func (d *dummyWorkRepo) Count(_ context.Context) (int64, error) { return 0, nil }
func (d *dummyWorkRepo) CountWithOptions(_ context.Context, options *domain.QueryOptions) (int64, error) {
return 0, nil
}
func (d *dummyWorkRepo) FindWithPreload(_ context.Context, preloads []string, id uint) (*domain.Work, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyWorkRepo) GetAllForSync(_ context.Context, batchSize, offset int) ([]domain.Work, error) {
return nil, nil
}
func (d *dummyWorkRepo) Exists(_ context.Context, id uint) (bool, error) {
_, ok := d.store[id]
return ok, nil
}
func (d *dummyWorkRepo) BeginTx(_ context.Context) (*gorm.DB, error) { return nil, nil }
func (d *dummyWorkRepo) WithTx(_ context.Context, fn func(tx *gorm.DB) error) error {
return fn(nil)
}
func (d *dummyWorkRepo) FindByTitle(_ context.Context, title string) ([]domain.Work, error) {
return nil, nil
}
func (d *dummyWorkRepo) FindByAuthor(_ context.Context, authorID uint) ([]domain.Work, error) {
return nil, nil
}
func (d *dummyWorkRepo) FindByCategory(_ context.Context, categoryID uint) ([]domain.Work, error) {
return nil, nil
}
func (d *dummyWorkRepo) GetWithTranslations(_ context.Context, id uint) (*domain.Work, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyWorkRepo) GetWithAssociations(_ context.Context, id uint) (*domain.Work, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyWorkRepo) GetWithAssociationsInTx(_ context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
return d.GetByID(context.Background(), id)
}
func (d *dummyWorkRepo) ListWithTranslations(_ context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
// return a simple paginated result of all works
items := []domain.Work{}
for _, w := range d.store {
items = append(items, *w)
}
start := (page - 1) * pageSize
if start < 0 {
start = 0
}
end := start + pageSize
if start >= len(items) {
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
}
if end > len(items) {
end = len(items)
}
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
}
func (d *dummyWorkRepo) FindByLanguage(_ context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
items := []domain.Work{}
for _, w := range d.store {
if w.Language == language {
items = append(items, *w)
}
}
start := (page - 1) * pageSize
if start < 0 {
start = 0
}
end := start + pageSize
if start >= len(items) {
return &domain.PaginatedResult[domain.Work]{Items: []domain.Work{}}, nil
}
if end > len(items) {
end = len(items)
}
return &domain.PaginatedResult[domain.Work]{Items: items[start:end], TotalCount: int64(len(items)), Page: page, PageSize: pageSize}, nil
}
func (d *dummyWorkRepo) IsAuthor(_ context.Context, workID uint, authorID uint) (bool, error) {
return false, nil
}
func (d *dummyWorkRepo) ListByCollectionID(_ context.Context, collectionID uint) ([]domain.Work, error) {
return nil, nil
}
func TestCachedWork_GetByID_Caches(t *testing.T) {
ctx := context.Background()
d := newDummyWorkRepo()
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
mc := newMemCache()
cw := NewCachedWorkRepository(d, mc, nil)
w, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "initial", w.Title)
require.Equal(t, 1, d.calls)
// second read should hit cache and not increment inner calls
w2, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "initial", w2.Title)
require.Equal(t, 1, d.calls)
}
func TestCachedWork_Update_Invalidates(t *testing.T) {
ctx := context.Background()
d := newDummyWorkRepo()
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
mc := newMemCache()
cw := NewCachedWorkRepository(d, mc, nil)
_, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, 1, d.calls)
// Update underlying entity
d.store[1] = &domain.Work{Title: "updated", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
err = cw.Update(ctx, d.store[1])
require.NoError(t, err)
// Next GetByID should call inner again (cache invalidated)
w, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "updated", w.Title)
require.Equal(t, 3, d.calls, "expected inner to be called again for update invalidation")
}
func TestCachedWork_Delete_Invalidates(t *testing.T) {
ctx := context.Background()
d := newDummyWorkRepo()
d.store[1] = &domain.Work{Title: "initial", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}}
mc := newMemCache()
cw := NewCachedWorkRepository(d, mc, nil)
_, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
// delete
err = cw.Delete(ctx, 1)
require.NoError(t, err)
_, err = cw.GetByID(ctx, 1)
require.Error(t, err)
}

View File

@ -0,0 +1,78 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"time"
"tercul/internal/domain"
platform_cache "tercul/internal/platform/cache"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestRedisIntegration_CachedWorkRepository(t *testing.T) {
if os.Getenv("INTEGRATION_TESTS") != "true" {
t.Skip("skipping integration test; set INTEGRATION_TESTS=true to run")
}
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "redis:7.0",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForListeningPort("6379/tcp").WithStartupTimeout(30 * time.Second),
}
redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true})
require.NoError(t, err)
defer redisC.Terminate(ctx)
host, err := redisC.Host(ctx)
require.NoError(t, err)
port, err := redisC.MappedPort(ctx, "6379")
require.NoError(t, err)
addr := fmt.Sprintf("%s:%s", host, port.Port())
rcClient := redis.NewClient(&redis.Options{Addr: addr})
defer rcClient.Close()
require.NoError(t, rcClient.Ping(ctx).Err())
// create platform redis cache using key prefix that caches expect
keyGen := platform_cache.NewDefaultKeyGenerator("tercul:repo:")
rc := platform_cache.NewRedisCache(rcClient, keyGen, 0)
// create dummy repo and wrap
d := newDummyWorkRepo()
d.store[1] = &domain.Work{Title: "intwork", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}, Language: "en"}}
cw := NewCachedWorkRepository(d, rc, nil)
// call and assert Redis stored entity
w, err := cw.GetByID(ctx, 1)
require.NoError(t, err)
require.Equal(t, "intwork", w.Title)
key := keyGen.EntityKey("work", 1)
val, err := rcClient.Get(ctx, key).Bytes()
require.NoError(t, err)
var stored domain.Work
require.NoError(t, json.Unmarshal(val, &stored))
require.Equal(t, "intwork", stored.Title)
// list caching test
d.store[2] = &domain.Work{Title: "intwork2", TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 2}, Language: "en"}}
// call list
_, err = cw.List(ctx, 1, 10)
require.NoError(t, err)
listKey := keyGen.ListKey("work", 1, 10)
_, err = rcClient.Get(ctx, listKey).Bytes()
require.NoError(t, err)
}

View File

@ -1,96 +1,99 @@
-- +goose Up -- +goose Up
CREATE TABLE "countries" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"code" text NOT NULL,"phone_code" text,"currency" text,"continent" text); -- Enable UUID extension
CREATE TABLE "cities" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"country_id" bigint,CONSTRAINT "fk_countries_cities" FOREIGN KEY ("country_id") REFERENCES "countries"("id")); CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE "addresses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"street" text,"street_number" text,"postal_code" text,"country_id" bigint,"city_id" bigint,"latitude" real,"longitude" real,CONSTRAINT "fk_cities_addresses" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_addresses" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "users" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"username" text NOT NULL,"email" text NOT NULL,"password" text NOT NULL,"first_name" text,"last_name" text,"display_name" text,"bio" text,"avatar_url" text,"role" text DEFAULT 'reader',"last_login_at" timestamptz,"verified" boolean DEFAULT false,"active" boolean DEFAULT true,"country_id" bigint,"city_id" bigint,"address_id" bigint,CONSTRAINT "fk_users_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_users_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"),CONSTRAINT "fk_users_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "uni_users_username" UNIQUE ("username"),CONSTRAINT "uni_users_email" UNIQUE ("email")); CREATE TABLE "countries" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"code" text NOT NULL,"phone_code" text,"currency" text,"continent" text);
CREATE TABLE "user_sessions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"ip" text,"user_agent" text,"expires_at" timestamptz NOT NULL,CONSTRAINT "fk_user_sessions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "cities" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"country_id" UUID,CONSTRAINT "fk_countries_cities" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "password_resets" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_password_resets_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "addresses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"street" text,"street_number" text,"postal_code" text,"country_id" UUID,"city_id" UUID,"latitude" real,"longitude" real,CONSTRAINT "fk_cities_addresses" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_addresses" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "email_verifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_email_verifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "users" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"username" text NOT NULL,"email" text NOT NULL,"password" text NOT NULL,"first_name" text,"last_name" text,"display_name" text,"bio" text,"avatar_url" text,"role" text DEFAULT 'reader',"last_login_at" timestamptz,"verified" boolean DEFAULT false,"active" boolean DEFAULT true,"country_id" UUID,"city_id" UUID,"address_id" UUID,CONSTRAINT "fk_users_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_users_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"),CONSTRAINT "fk_users_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "uni_users_username" UNIQUE ("username"),CONSTRAINT "uni_users_email" UNIQUE ("email"));
CREATE TABLE "works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"type" text DEFAULT 'other',"status" text DEFAULT 'draft',"published_at" timestamptz); CREATE TABLE "user_sessions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"ip" text,"user_agent" text,"expires_at" timestamptz NOT NULL,CONSTRAINT "fk_user_sessions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "work_copyrights" ("work_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_id")); CREATE TABLE "password_resets" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_password_resets_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "categories" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"parent_id" bigint,"path" text,"slug" text,CONSTRAINT "fk_categories_children" FOREIGN KEY ("parent_id") REFERENCES "categories"("id")); CREATE TABLE "email_verifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"token" text NOT NULL,"expires_at" timestamptz NOT NULL,"used" boolean DEFAULT false,CONSTRAINT "fk_email_verifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "work_categories" ("category_id" bigint,"work_id" bigint,PRIMARY KEY ("category_id","work_id"),CONSTRAINT "fk_work_categories_category" FOREIGN KEY ("category_id") REFERENCES "categories"("id"),CONSTRAINT "fk_work_categories_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"type" text DEFAULT 'other',"status" text DEFAULT 'draft',"published_at" timestamptz);
CREATE TABLE "tags" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"slug" text); CREATE TABLE "work_copyrights" ("work_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","copyright_id"));
CREATE TABLE "work_tags" ("tag_id" bigint,"work_id" bigint,PRIMARY KEY ("tag_id","work_id"),CONSTRAINT "fk_work_tags_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_tags_tag" FOREIGN KEY ("tag_id") REFERENCES "tags"("id")); CREATE TABLE "categories" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"parent_id" UUID,"path" text,"slug" text,CONSTRAINT "fk_categories_children" FOREIGN KEY ("parent_id") REFERENCES "categories"("id"));
CREATE TABLE "places" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"latitude" real,"longitude" real,"country_id" bigint,"city_id" bigint,CONSTRAINT "fk_cities_places" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_places" FOREIGN KEY ("country_id") REFERENCES "countries"("id")); CREATE TABLE "work_categories" ("category_id" UUID,"work_id" UUID,PRIMARY KEY ("category_id","work_id"),CONSTRAINT "fk_work_categories_category" FOREIGN KEY ("category_id") REFERENCES "categories"("id"),CONSTRAINT "fk_work_categories_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"status" text DEFAULT 'active',"birth_date" timestamptz,"death_date" timestamptz,"country_id" bigint,"city_id" bigint,"place_id" bigint,"address_id" bigint,CONSTRAINT "fk_authors_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "fk_authors_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_authors_place" FOREIGN KEY ("place_id") REFERENCES "places"("id"),CONSTRAINT "fk_authors_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id")); CREATE TABLE "tags" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"slug" text);
CREATE TABLE "work_authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"author_id" bigint,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_work_authors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id")); CREATE TABLE "work_tags" ("tag_id" UUID,"work_id" UUID,PRIMARY KEY ("tag_id","work_id"),CONSTRAINT "fk_work_tags_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_tags_tag" FOREIGN KEY ("tag_id") REFERENCES "tags"("id"));
CREATE TABLE "work_monetizations" ("work_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_id")); CREATE TABLE "places" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"latitude" real,"longitude" real,"country_id" UUID,"city_id" UUID,CONSTRAINT "fk_cities_places" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_countries_places" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "publishers" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"status" text DEFAULT 'active',"country_id" bigint,CONSTRAINT "fk_publishers_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id")); CREATE TABLE "authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"status" text DEFAULT 'active',"birth_date" timestamptz,"death_date" timestamptz,"country_id" UUID,"city_id" UUID,"place_id" UUID,"address_id" UUID,CONSTRAINT "fk_authors_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"),CONSTRAINT "fk_authors_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_authors_place" FOREIGN KEY ("place_id") REFERENCES "places"("id"),CONSTRAINT "fk_authors_address" FOREIGN KEY ("address_id") REFERENCES "addresses"("id"));
CREATE TABLE "books" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"isbn" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"publisher_id" bigint,CONSTRAINT "fk_publishers_books" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id")); CREATE TABLE "work_authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"author_id" UUID,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_work_authors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
CREATE TABLE "book_authors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"book_id" bigint,"author_id" bigint,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_book_authors_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id")); CREATE TABLE "work_monetizations" ("work_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("work_id","monetization_id"));
CREATE TABLE "author_monetizations" ("author_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_id")); CREATE TABLE "publishers" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"status" text DEFAULT 'active',"country_id" UUID,CONSTRAINT "fk_publishers_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "author_copyrights" ("author_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_id")); CREATE TABLE "books" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"title" text NOT NULL,"description" text,"isbn" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"publisher_id" UUID,CONSTRAINT "fk_publishers_books" FOREIGN KEY ("publisher_id") REFERENCES "publishers"("id"));
CREATE TABLE "book_works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"book_id" bigint,"work_id" bigint,"order" integer DEFAULT 0,CONSTRAINT "fk_book_works_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "book_authors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"book_id" UUID,"author_id" UUID,"role" text DEFAULT 'author',"ordinal" integer DEFAULT 0,CONSTRAINT "fk_book_authors_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_authors_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"));
CREATE TABLE "book_monetizations" ("book_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id")); CREATE TABLE "author_monetizations" ("author_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","monetization_id"));
CREATE TABLE "book_copyrights" ("book_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_id")); CREATE TABLE "author_copyrights" ("author_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("author_id","copyright_id"));
CREATE TABLE "publisher_monetizations" ("publisher_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id")); CREATE TABLE "book_works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"book_id" UUID,"work_id" UUID,"order" integer DEFAULT 0,CONSTRAINT "fk_book_works_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"),CONSTRAINT "fk_book_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "publisher_copyrights" ("publisher_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id")); CREATE TABLE "book_monetizations" ("book_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","monetization_id"));
CREATE TABLE "sources" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"url" text,"status" text DEFAULT 'active'); CREATE TABLE "book_copyrights" ("book_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("book_id","copyright_id"));
CREATE TABLE "source_monetizations" ("source_id" bigint,"monetization_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id")); CREATE TABLE "publisher_monetizations" ("publisher_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","monetization_id"));
CREATE TABLE "source_copyrights" ("source_id" bigint,"copyright_id" bigint,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_id")); CREATE TABLE "publisher_copyrights" ("publisher_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("publisher_id","copyright_id"));
CREATE TABLE "work_sources" ("source_id" bigint,"work_id" bigint,PRIMARY KEY ("source_id","work_id"),CONSTRAINT "fk_work_sources_source" FOREIGN KEY ("source_id") REFERENCES "sources"("id"),CONSTRAINT "fk_work_sources_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "sources" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"url" text,"status" text DEFAULT 'active');
CREATE TABLE "editions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"description" text,"isbn" text,"version" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"book_id" bigint,CONSTRAINT "fk_editions_book" FOREIGN KEY ("book_id") REFERENCES "books"("id")); CREATE TABLE "source_monetizations" ("source_id" UUID,"monetization_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","monetization_id"));
CREATE TABLE "translations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"content" text,"description" text,"language" text NOT NULL,"status" text DEFAULT 'draft',"published_at" timestamptz,"translatable_id" bigint NOT NULL,"translatable_type" text NOT NULL,"translator_id" bigint,"is_original_language" boolean DEFAULT false,"audio_url" text,"date_translated" timestamptz,CONSTRAINT "fk_users_translations" FOREIGN KEY ("translator_id") REFERENCES "users"("id")); CREATE TABLE "source_copyrights" ("source_id" UUID,"copyright_id" UUID,"created_at" timestamptz,PRIMARY KEY ("source_id","copyright_id"));
CREATE TABLE "text_blocks" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"translation_id" bigint,"index" bigint,"type" text,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"text" text,CONSTRAINT "fk_text_blocks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_text_blocks_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "work_sources" ("source_id" UUID,"work_id" UUID,PRIMARY KEY ("source_id","work_id"),CONSTRAINT "fk_work_sources_source" FOREIGN KEY ("source_id") REFERENCES "sources"("id"),CONSTRAINT "fk_work_sources_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "comments" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"line_number" bigint,"text_block_id" bigint,"parent_id" bigint,CONSTRAINT "fk_comments_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_comments_children" FOREIGN KEY ("parent_id") REFERENCES "comments"("id"),CONSTRAINT "fk_users_comments" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_comments_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "editions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"description" text,"isbn" text,"version" text,"format" text DEFAULT 'paperback',"status" text DEFAULT 'draft',"published_at" timestamptz,"book_id" UUID,CONSTRAINT "fk_editions_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
CREATE TABLE "likes" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"comment_id" bigint,CONSTRAINT "fk_users_likes" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_likes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_likes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_likes" FOREIGN KEY ("comment_id") REFERENCES "comments"("id")); CREATE TABLE "translations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"title" text NOT NULL,"content" text,"description" text,"language" text NOT NULL,"status" text DEFAULT 'draft',"published_at" timestamptz,"translatable_id" UUID NOT NULL,"translatable_type" text NOT NULL,"translator_id" UUID,"is_original_language" boolean DEFAULT false,"audio_url" text,"date_translated" timestamptz,CONSTRAINT "fk_users_translations" FOREIGN KEY ("translator_id") REFERENCES "users"("id"));
CREATE TABLE "bookmarks" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text,"user_id" bigint,"work_id" bigint,"notes" text,"last_read_at" timestamptz,"progress" integer DEFAULT 0,CONSTRAINT "fk_bookmarks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_users_bookmarks" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "text_blocks" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"translation_id" UUID,"index" bigint,"type" text,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"text" text,CONSTRAINT "fk_text_blocks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_text_blocks_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
CREATE TABLE "collections" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"user_id" bigint,"is_public" boolean DEFAULT true,"cover_image_url" text,CONSTRAINT "fk_users_collections" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "comments" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"line_number" bigint,"text_block_id" UUID,"parent_id" UUID,CONSTRAINT "fk_comments_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_comments_children" FOREIGN KEY ("parent_id") REFERENCES "comments"("id"),CONSTRAINT "fk_users_comments" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_comments_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "collection_works" ("collection_id" bigint,"work_id" bigint,PRIMARY KEY ("collection_id","work_id"),CONSTRAINT "fk_collection_works_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"),CONSTRAINT "fk_collection_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "likes" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"comment_id" UUID,CONSTRAINT "fk_users_likes" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_likes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_likes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_comments_likes" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
CREATE TABLE "contributions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"status" text DEFAULT 'draft',"user_id" bigint,"work_id" bigint,"translation_id" bigint,"reviewer_id" bigint,"reviewed_at" timestamptz,"feedback" text,CONSTRAINT "fk_contributions_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"),CONSTRAINT "fk_users_contributions" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributions_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "bookmarks" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text,"user_id" UUID,"work_id" UUID,"notes" text,"last_read_at" timestamptz,"progress" integer DEFAULT 0,CONSTRAINT "fk_bookmarks_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_users_bookmarks" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "languages" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"code" text NOT NULL,"name" text NOT NULL,"script" text,"direction" text); CREATE TABLE "collections" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text DEFAULT 'multi',"slug" text,"name" text NOT NULL,"description" text,"user_id" UUID,"is_public" boolean DEFAULT true,"cover_image_url" text,CONSTRAINT "fk_users_collections" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "series" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text); CREATE TABLE "collection_works" ("collection_id" UUID,"work_id" UUID,PRIMARY KEY ("collection_id","work_id"),CONSTRAINT "fk_collection_works_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"),CONSTRAINT "fk_collection_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "work_series" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"work_id" bigint,"series_id" bigint,"number_in_series" integer DEFAULT 0,CONSTRAINT "fk_work_series_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_series_series" FOREIGN KEY ("series_id") REFERENCES "series"("id")); CREATE TABLE "contributions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"status" text DEFAULT 'draft',"user_id" UUID,"work_id" UUID,"translation_id" UUID,"reviewer_id" UUID,"reviewed_at" timestamptz,"feedback" text,CONSTRAINT "fk_contributions_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"),CONSTRAINT "fk_users_contributions" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributions_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
CREATE TABLE "translation_fields" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"translation_id" bigint,"field_name" text NOT NULL,"field_value" text NOT NULL,"language" text NOT NULL,CONSTRAINT "fk_translation_fields_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "languages" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"code" text NOT NULL,"name" text NOT NULL,"script" text,"direction" text);
CREATE TABLE "copyrights" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"identificator" text NOT NULL,"name" text NOT NULL,"description" text,"license" text,"start_date" timestamptz,"end_date" timestamptz); CREATE TABLE "series" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
CREATE TABLE "copyright_translations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"copyright_id" bigint,"language_code" text NOT NULL,"message" text NOT NULL,"description" text,CONSTRAINT "fk_copyrights_translations" FOREIGN KEY ("copyright_id") REFERENCES "copyrights"("id")); CREATE TABLE "work_series" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"work_id" UUID,"series_id" UUID,"number_in_series" integer DEFAULT 0,CONSTRAINT "fk_work_series_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_series_series" FOREIGN KEY ("series_id") REFERENCES "series"("id"));
CREATE TABLE "copyright_claims" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"details" text NOT NULL,"status" text DEFAULT 'pending',"claim_date" timestamptz NOT NULL,"resolution" text,"resolved_at" timestamptz,"user_id" bigint,CONSTRAINT "fk_copyright_claims_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "translation_fields" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"translation_id" UUID,"field_name" text NOT NULL,"field_value" text NOT NULL,"language" text NOT NULL,CONSTRAINT "fk_translation_fields_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
CREATE TABLE "monetizations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"amount" decimal(10,2) DEFAULT 0,"currency" text DEFAULT 'USD',"type" text,"status" text DEFAULT 'active',"start_date" timestamptz,"end_date" timestamptz,"language" text NOT NULL); CREATE TABLE "copyrights" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"identificator" text NOT NULL,"name" text NOT NULL,"description" text,"license" text,"start_date" timestamptz,"end_date" timestamptz);
CREATE TABLE "licenses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"spdx_identifier" text,"name" text NOT NULL,"url" text,"description" text); CREATE TABLE "copyright_translations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"copyright_id" UUID,"language_code" text NOT NULL,"message" text NOT NULL,"description" text,CONSTRAINT "fk_copyrights_translations" FOREIGN KEY ("copyright_id") REFERENCES "copyrights"("id"));
CREATE TABLE "moderation_flags" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"target_type" text NOT NULL,"target_id" bigint NOT NULL,"reason" text,"status" text DEFAULT 'open',"reviewer_id" bigint,"notes" text,CONSTRAINT "fk_moderation_flags_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id")); CREATE TABLE "copyright_claims" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"details" text NOT NULL,"status" text DEFAULT 'pending',"claim_date" timestamptz NOT NULL,"resolution" text,"resolved_at" timestamptz,"user_id" UUID,CONSTRAINT "fk_copyright_claims_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "audit_logs" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"actor_id" bigint,"action" text NOT NULL,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"before" jsonb DEFAULT '{}',"after" jsonb DEFAULT '{}',"at" timestamptz,CONSTRAINT "fk_audit_logs_actor" FOREIGN KEY ("actor_id") REFERENCES "users"("id")); CREATE TABLE "monetizations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"amount" decimal(10,2) DEFAULT 0,"currency" text DEFAULT 'USD',"type" text,"status" text DEFAULT 'active',"start_date" timestamptz,"end_date" timestamptz,"language" text NOT NULL);
CREATE TABLE "work_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"bookmarks" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"translation_count" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"complexity" decimal(5,2) DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"work_id" bigint,CONSTRAINT "fk_work_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id") ON DELETE CASCADE ON UPDATE CASCADE); CREATE TABLE "licenses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"spdx_identifier" text,"name" text NOT NULL,"url" text,"description" text);
CREATE TABLE "translation_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"translation_id" bigint,CONSTRAINT "fk_translation_stats_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id") ON DELETE CASCADE ON UPDATE CASCADE); CREATE TABLE "moderation_flags" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"target_type" text NOT NULL,"target_id" UUID NOT NULL,"reason" text,"status" text DEFAULT 'open',"reviewer_id" UUID,"notes" text,CONSTRAINT "fk_moderation_flags_reviewer" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id"));
CREATE TABLE "user_engagements" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"date" date,"works_read" integer DEFAULT 0,"comments_made" integer DEFAULT 0,"likes_given" integer DEFAULT 0,"bookmarks_made" integer DEFAULT 0,"translations_made" integer DEFAULT 0,CONSTRAINT "fk_user_engagements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "audit_logs" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"actor_id" UUID,"action" text NOT NULL,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"before" jsonb DEFAULT '{}',"after" jsonb DEFAULT '{}',"at" timestamptz,CONSTRAINT "fk_audit_logs_actor" FOREIGN KEY ("actor_id") REFERENCES "users"("id"));
CREATE TABLE "trendings" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"rank" integer NOT NULL,"score" decimal(10,2) DEFAULT 0,"time_period" text NOT NULL,"date" date); CREATE TABLE "work_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"bookmarks" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"translation_count" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"complexity" decimal(5,2) DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"work_id" UUID,CONSTRAINT "fk_work_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id") ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE "book_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"sales" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"book_id" bigint,CONSTRAINT "fk_book_stats_book" FOREIGN KEY ("book_id") REFERENCES "books"("id")); CREATE TABLE "translation_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"comments" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"reading_time" integer DEFAULT 0,"sentiment" decimal(5,2) DEFAULT 0,"translation_id" UUID,CONSTRAINT "fk_translation_stats_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id") ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE "collection_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"items" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"collection_id" bigint,CONSTRAINT "fk_collection_stats_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id")); CREATE TABLE "user_engagements" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"date" date,"works_read" integer DEFAULT 0,"comments_made" integer DEFAULT 0,"likes_given" integer DEFAULT 0,"bookmarks_made" integer DEFAULT 0,"translations_made" integer DEFAULT 0,CONSTRAINT "fk_user_engagements_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "media_stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"downloads" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"media_id" bigint); CREATE TABLE "trendings" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"rank" integer NOT NULL,"score" decimal(10,2) DEFAULT 0,"time_period" text NOT NULL,"date" date);
CREATE TABLE "author_countries" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"author_id" bigint,"country_id" bigint,CONSTRAINT "fk_author_countries_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_author_countries_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id")); CREATE TABLE "book_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"sales" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"book_id" UUID,CONSTRAINT "fk_book_stats_book" FOREIGN KEY ("book_id") REFERENCES "books"("id"));
CREATE TABLE "readability_scores" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"score" decimal(5,2),"language" text NOT NULL,"method" text,"work_id" bigint,CONSTRAINT "fk_readability_scores_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "collection_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"items" bigint DEFAULT 0,"views" bigint DEFAULT 0,"likes" bigint DEFAULT 0,"collection_id" UUID,CONSTRAINT "fk_collection_stats_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
CREATE TABLE "writing_styles" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"work_id" bigint,CONSTRAINT "fk_writing_styles_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "media_stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"views" bigint DEFAULT 0,"downloads" bigint DEFAULT 0,"shares" bigint DEFAULT 0,"media_id" UUID);
CREATE TABLE "linguistic_layers" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"type" text,"work_id" bigint,"data" jsonb DEFAULT '{}',CONSTRAINT "fk_linguistic_layers_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "author_countries" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"author_id" UUID,"country_id" UUID,CONSTRAINT "fk_author_countries_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_author_countries_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "text_metadata" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"analysis" text,"language" text NOT NULL,"word_count" integer DEFAULT 0,"sentence_count" integer DEFAULT 0,"paragraph_count" integer DEFAULT 0,"average_word_length" decimal(5,2),"average_sentence_length" decimal(5,2),"work_id" bigint,CONSTRAINT "fk_text_metadata_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "readability_scores" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"score" decimal(5,2),"language" text NOT NULL,"method" text,"work_id" UUID,CONSTRAINT "fk_readability_scores_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "poetic_analyses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"structure" text,"language" text NOT NULL,"rhyme_scheme" text,"meter_type" text,"stanza_count" integer DEFAULT 0,"line_count" integer DEFAULT 0,"work_id" bigint,CONSTRAINT "fk_poetic_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "writing_styles" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"work_id" UUID,CONSTRAINT "fk_writing_styles_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "concepts" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text); CREATE TABLE "linguistic_layers" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"type" text,"work_id" UUID,"data" jsonb DEFAULT '{}',CONSTRAINT "fk_linguistic_layers_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "words" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"language" text NOT NULL,"part_of_speech" text,"lemma" text,"concept_id" bigint,CONSTRAINT "fk_concepts_words" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id")); CREATE TABLE "text_metadata" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"analysis" text,"language" text NOT NULL,"word_count" integer DEFAULT 0,"sentence_count" integer DEFAULT 0,"paragraph_count" integer DEFAULT 0,"average_word_length" decimal(5,2),"average_sentence_length" decimal(5,2),"work_id" UUID,CONSTRAINT "fk_text_metadata_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "work_words" ("word_id" bigint,"work_id" bigint,PRIMARY KEY ("word_id","work_id"),CONSTRAINT "fk_work_words_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_words_word" FOREIGN KEY ("word_id") REFERENCES "words"("id")); CREATE TABLE "poetic_analyses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"structure" text,"language" text NOT NULL,"rhyme_scheme" text,"meter_type" text,"stanza_count" integer DEFAULT 0,"line_count" integer DEFAULT 0,"work_id" UUID,CONSTRAINT "fk_poetic_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "word_occurrences" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" bigint,"word_id" bigint,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"lemma" text,"part_of_speech" text,CONSTRAINT "fk_word_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_word_occurrences_word" FOREIGN KEY ("word_id") REFERENCES "words"("id")); CREATE TABLE "concepts" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text);
CREATE TABLE "work_concepts" ("concept_id" bigint,"work_id" bigint,PRIMARY KEY ("concept_id","work_id"),CONSTRAINT "fk_work_concepts_concept" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"),CONSTRAINT "fk_work_concepts_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "words" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text" text NOT NULL,"language" text NOT NULL,"part_of_speech" text,"lemma" text,"concept_id" UUID,CONSTRAINT "fk_concepts_words" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"));
CREATE TABLE "language_entities" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"language" text NOT NULL); CREATE TABLE "work_words" ("word_id" UUID,"work_id" UUID,PRIMARY KEY ("word_id","work_id"),CONSTRAINT "fk_work_words_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_work_words_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
CREATE TABLE "work_language_entities" ("language_entity_id" bigint,"work_id" bigint,PRIMARY KEY ("language_entity_id","work_id"),CONSTRAINT "fk_work_language_entities_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"),CONSTRAINT "fk_work_language_entities_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "word_occurrences" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" UUID,"word_id" UUID,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,"lemma" text,"part_of_speech" text,CONSTRAINT "fk_word_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_word_occurrences_word" FOREIGN KEY ("word_id") REFERENCES "words"("id"));
CREATE TABLE "entity_occurrences" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" bigint,"language_entity_id" bigint,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,CONSTRAINT "fk_entity_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_entity_occurrences_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id")); CREATE TABLE "work_concepts" ("concept_id" UUID,"work_id" UUID,PRIMARY KEY ("concept_id","work_id"),CONSTRAINT "fk_work_concepts_concept" FOREIGN KEY ("concept_id") REFERENCES "concepts"("id"),CONSTRAINT "fk_work_concepts_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "language_analyses" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"language" text NOT NULL,"analysis" jsonb DEFAULT '{}',"work_id" bigint,CONSTRAINT "fk_language_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "language_entities" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"language" text NOT NULL);
CREATE TABLE "gamifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"points" integer DEFAULT 0,"level" integer DEFAULT 1,"badges" jsonb DEFAULT '{}',"streaks" integer DEFAULT 0,"last_active" timestamptz,"user_id" bigint,CONSTRAINT "fk_gamifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "work_language_entities" ("language_entity_id" UUID,"work_id" UUID,PRIMARY KEY ("language_entity_id","work_id"),CONSTRAINT "fk_work_language_entities_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"),CONSTRAINT "fk_work_language_entities_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "stats" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"data" jsonb DEFAULT '{}',"period" text,"start_date" timestamptz,"end_date" timestamptz,"user_id" bigint,"work_id" bigint,CONSTRAINT "fk_stats_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "entity_occurrences" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"text_block_id" UUID,"language_entity_id" UUID,"start_offset" integer DEFAULT 0,"end_offset" integer DEFAULT 0,CONSTRAINT "fk_entity_occurrences_text_block" FOREIGN KEY ("text_block_id") REFERENCES "text_blocks"("id"),CONSTRAINT "fk_entity_occurrences_language_entity" FOREIGN KEY ("language_entity_id") REFERENCES "language_entities"("id"));
CREATE TABLE "search_documents" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text,"entity_id" bigint,"language_code" text,"title" text,"body" text,"keywords" text); CREATE TABLE "language_analyses" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"language" text NOT NULL,"analysis" jsonb DEFAULT '{}',"work_id" UUID,CONSTRAINT "fk_language_analyses_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "emotions" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"intensity" decimal(5,2) DEFAULT 0,"user_id" bigint,"work_id" bigint,"collection_id" bigint,CONSTRAINT "fk_emotions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_emotions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_emotions_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id")); CREATE TABLE "gamifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"points" integer DEFAULT 0,"level" integer DEFAULT 1,"badges" jsonb DEFAULT '{}',"streaks" integer DEFAULT 0,"last_active" timestamptz,"user_id" UUID,CONSTRAINT "fk_gamifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "moods" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL); CREATE TABLE "stats" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"data" jsonb DEFAULT '{}',"period" text,"start_date" timestamptz,"end_date" timestamptz,"user_id" UUID,"work_id" UUID,CONSTRAINT "fk_stats_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_stats_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "work_moods" ("mood_id" bigint,"work_id" bigint,PRIMARY KEY ("mood_id","work_id"),CONSTRAINT "fk_work_moods_mood" FOREIGN KEY ("mood_id") REFERENCES "moods"("id"),CONSTRAINT "fk_work_moods_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "search_documents" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"entity_type" text,"entity_id" UUID,"language_code" text,"title" text,"body" text,"keywords" text);
CREATE TABLE "topic_clusters" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"keywords" text); CREATE TABLE "emotions" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL,"intensity" decimal(5,2) DEFAULT 0,"user_id" UUID,"work_id" UUID,"collection_id" UUID,CONSTRAINT "fk_emotions_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_emotions_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_emotions_collection" FOREIGN KEY ("collection_id") REFERENCES "collections"("id"));
CREATE TABLE "work_topic_clusters" ("topic_cluster_id" bigint,"work_id" bigint,PRIMARY KEY ("topic_cluster_id","work_id"),CONSTRAINT "fk_work_topic_clusters_topic_cluster" FOREIGN KEY ("topic_cluster_id") REFERENCES "topic_clusters"("id"),CONSTRAINT "fk_work_topic_clusters_work" FOREIGN KEY ("work_id") REFERENCES "works"("id")); CREATE TABLE "moods" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"language" text NOT NULL);
CREATE TABLE "edges" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"source_table" text NOT NULL,"source_id" bigint NOT NULL,"target_table" text NOT NULL,"target_id" bigint NOT NULL,"relation" text NOT NULL DEFAULT 'ASSOCIATED_WITH',"language" text DEFAULT 'en',"extra" jsonb DEFAULT '{}'); CREATE TABLE "work_moods" ("mood_id" UUID,"work_id" UUID,PRIMARY KEY ("mood_id","work_id"),CONSTRAINT "fk_work_moods_mood" FOREIGN KEY ("mood_id") REFERENCES "moods"("id"),CONSTRAINT "fk_work_moods_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "embeddings" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"external_id" text,"entity_type" text NOT NULL,"entity_id" bigint NOT NULL,"model" text NOT NULL,"dim" integer DEFAULT 0,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_embeddings_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_embeddings_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "topic_clusters" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"description" text,"keywords" text);
CREATE TABLE "localizations" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"key" text NOT NULL,"value" text NOT NULL,"language" text NOT NULL); CREATE TABLE "work_topic_clusters" ("topic_cluster_id" UUID,"work_id" UUID,PRIMARY KEY ("topic_cluster_id","work_id"),CONSTRAINT "fk_work_topic_clusters_topic_cluster" FOREIGN KEY ("topic_cluster_id") REFERENCES "topic_clusters"("id"),CONSTRAINT "fk_work_topic_clusters_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"));
CREATE TABLE "media" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"url" text NOT NULL,"type" text NOT NULL,"mime_type" text,"size" bigint DEFAULT 0,"title" text,"description" text,"language" text NOT NULL,"author_id" bigint,"translation_id" bigint,"country_id" bigint,"city_id" bigint,CONSTRAINT "fk_media_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_media_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_media_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_media_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id")); CREATE TABLE "edges" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"source_table" text NOT NULL,"source_id" UUID NOT NULL,"target_table" text NOT NULL,"target_id" UUID NOT NULL,"relation" text NOT NULL DEFAULT 'ASSOCIATED_WITH',"language" text DEFAULT 'en',"extra" jsonb DEFAULT '{}');
CREATE TABLE "notifications" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"message" text NOT NULL,"type" text,"read" boolean DEFAULT false,"language" text NOT NULL,"user_id" bigint,"related_id" bigint,"related_type" text,CONSTRAINT "fk_notifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "embeddings" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"external_id" text,"entity_type" text NOT NULL,"entity_id" UUID NOT NULL,"model" text NOT NULL,"dim" integer DEFAULT 0,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_embeddings_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_embeddings_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
CREATE TABLE "editorial_workflows" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"stage" text NOT NULL,"notes" text,"language" text NOT NULL,"work_id" bigint,"translation_id" bigint,"user_id" bigint,"assigned_to_id" bigint,"due_date" timestamptz,"completed_at" timestamptz,CONSTRAINT "fk_editorial_workflows_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_editorial_workflows_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_editorial_workflows_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_editorial_workflows_assigned_to" FOREIGN KEY ("assigned_to_id") REFERENCES "users"("id")); CREATE TABLE "localizations" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"key" text NOT NULL,"value" text NOT NULL,"language" text NOT NULL);
CREATE TABLE "admins" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"role" text NOT NULL,"permissions" jsonb DEFAULT '{}',CONSTRAINT "fk_admins_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "media" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"url" text NOT NULL,"type" text NOT NULL,"mime_type" text,"size" bigint DEFAULT 0,"title" text,"description" text,"language" text NOT NULL,"author_id" UUID,"translation_id" UUID,"country_id" UUID,"city_id" UUID,CONSTRAINT "fk_media_city" FOREIGN KEY ("city_id") REFERENCES "cities"("id"),CONSTRAINT "fk_media_author" FOREIGN KEY ("author_id") REFERENCES "authors"("id"),CONSTRAINT "fk_media_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_media_country" FOREIGN KEY ("country_id") REFERENCES "countries"("id"));
CREATE TABLE "votes" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"value" integer DEFAULT 0,"user_id" bigint,"work_id" bigint,"translation_id" bigint,"comment_id" bigint,CONSTRAINT "fk_votes_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_votes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_votes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_votes_comment" FOREIGN KEY ("comment_id") REFERENCES "comments"("id")); CREATE TABLE "notifications" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"message" text NOT NULL,"type" text,"read" boolean DEFAULT false,"language" text NOT NULL,"user_id" UUID,"related_id" UUID,"related_type" text,CONSTRAINT "fk_notifications_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "contributors" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"role" text,"user_id" bigint,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_contributors_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributors_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "editorial_workflows" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"stage" text NOT NULL,"notes" text,"language" text NOT NULL,"work_id" UUID,"translation_id" UUID,"user_id" UUID,"assigned_to_id" UUID,"due_date" timestamptz,"completed_at" timestamptz,CONSTRAINT "fk_editorial_workflows_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_editorial_workflows_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_editorial_workflows_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_editorial_workflows_assigned_to" FOREIGN KEY ("assigned_to_id") REFERENCES "users"("id"));
CREATE TABLE "interaction_events" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"user_id" bigint,"target_type" text NOT NULL,"target_id" bigint NOT NULL,"kind" text NOT NULL,"occurred_at" timestamptz,CONSTRAINT "fk_interaction_events_user" FOREIGN KEY ("user_id") REFERENCES "users"("id")); CREATE TABLE "admins" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"role" text NOT NULL,"permissions" jsonb DEFAULT '{}',CONSTRAINT "fk_admins_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "hybrid_entity_works" ("id" SERIAL PRIMARY KEY,"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"work_id" bigint,"translation_id" bigint,CONSTRAINT "fk_hybrid_entity_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_hybrid_entity_works_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id")); CREATE TABLE "votes" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"value" integer DEFAULT 0,"user_id" UUID,"work_id" UUID,"translation_id" UUID,"comment_id" UUID,CONSTRAINT "fk_votes_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_votes_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_votes_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"),CONSTRAINT "fk_votes_comment" FOREIGN KEY ("comment_id") REFERENCES "comments"("id"));
CREATE TABLE "contributors" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"role" text,"user_id" UUID,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_contributors_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"),CONSTRAINT "fk_contributors_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_contributors_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
CREATE TABLE "interaction_events" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"user_id" UUID,"target_type" text NOT NULL,"target_id" UUID NOT NULL,"kind" text NOT NULL,"occurred_at" timestamptz,CONSTRAINT "fk_interaction_events_user" FOREIGN KEY ("user_id") REFERENCES "users"("id"));
CREATE TABLE "hybrid_entity_works" ("id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),"created_at" timestamptz,"updated_at" timestamptz,"name" text NOT NULL,"type" text,"work_id" UUID,"translation_id" UUID,CONSTRAINT "fk_hybrid_entity_works_work" FOREIGN KEY ("work_id") REFERENCES "works"("id"),CONSTRAINT "fk_hybrid_entity_works_translation" FOREIGN KEY ("translation_id") REFERENCES "translations"("id"));
-- +goose Down -- +goose Down
DROP TABLE IF EXISTS "hybrid_entity_works"; DROP TABLE IF EXISTS "hybrid_entity_works";

View File

@ -8,6 +8,7 @@ import (
"tercul/internal/platform/config" "tercul/internal/platform/config"
"time" "time"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -41,7 +42,7 @@ var allowedTranslationCounterFields = map[string]bool{
"shares": true, "shares": true,
} }
func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uint, field string, value int) error { func (r *analyticsRepository) IncrementWorkCounter(ctx context.Context, workID uuid.UUID, field string, value int) error {
ctx, span := r.tracer.Start(ctx, "IncrementWorkCounter") ctx, span := r.tracer.Start(ctx, "IncrementWorkCounter")
defer span.End() defer span.End()
if !allowedWorkCounterFields[field] { if !allowedWorkCounterFields[field] {
@ -83,7 +84,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
return []*domain.Work{}, nil return []*domain.Work{}, nil
} }
workIDs := make([]uint, len(trendingWorks)) workIDs := make([]uuid.UUID, len(trendingWorks))
for i, tw := range trendingWorks { for i, tw := range trendingWorks {
workIDs[i] = tw.EntityID workIDs[i] = tw.EntityID
} }
@ -95,7 +96,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
// This part is tricky because the order from the IN clause is not guaranteed. // This part is tricky because the order from the IN clause is not guaranteed.
// We need to re-order the works based on the trending rank. // We need to re-order the works based on the trending rank.
workMap := make(map[uint]*domain.Work) workMap := make(map[uuid.UUID]*domain.Work)
for _, w := range works { for _, w := range works {
workMap[w.ID] = w workMap[w.ID] = w
} }
@ -110,7 +111,7 @@ func (r *analyticsRepository) GetTrendingWorks(ctx context.Context, timePeriod s
return orderedWorks, err return orderedWorks, err
} }
func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uint, field string, value int) error { func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, translationID uuid.UUID, field string, value int) error {
ctx, span := r.tracer.Start(ctx, "IncrementTranslationCounter") ctx, span := r.tracer.Start(ctx, "IncrementTranslationCounter")
defer span.End() defer span.End()
if !allowedTranslationCounterFields[field] { if !allowedTranslationCounterFields[field] {
@ -132,19 +133,19 @@ func (r *analyticsRepository) IncrementTranslationCounter(ctx context.Context, t
}) })
} }
func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { func (r *analyticsRepository) UpdateWorkStats(ctx context.Context, workID uuid.UUID, stats domain.WorkStats) error {
ctx, span := r.tracer.Start(ctx, "UpdateWorkStats") ctx, span := r.tracer.Start(ctx, "UpdateWorkStats")
defer span.End() defer span.End()
return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error return r.db.WithContext(ctx).Model(&domain.WorkStats{}).Where("work_id = ?", workID).Updates(stats).Error
} }
func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uint, stats domain.TranslationStats) error { func (r *analyticsRepository) UpdateTranslationStats(ctx context.Context, translationID uuid.UUID, stats domain.TranslationStats) error {
ctx, span := r.tracer.Start(ctx, "UpdateTranslationStats") ctx, span := r.tracer.Start(ctx, "UpdateTranslationStats")
defer span.End() defer span.End()
return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error return r.db.WithContext(ctx).Model(&domain.TranslationStats{}).Where("translation_id = ?", translationID).Updates(stats).Error
} }
func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error) {
ctx, span := r.tracer.Start(ctx, "GetOrCreateWorkStats") ctx, span := r.tracer.Start(ctx, "GetOrCreateWorkStats")
defer span.End() defer span.End()
var stats domain.WorkStats var stats domain.WorkStats
@ -152,7 +153,7 @@ func (r *analyticsRepository) GetOrCreateWorkStats(ctx context.Context, workID u
return &stats, err return &stats, err
} }
func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error) {
ctx, span := r.tracer.Start(ctx, "GetOrCreateTranslationStats") ctx, span := r.tracer.Start(ctx, "GetOrCreateTranslationStats")
defer span.End() defer span.End()
var stats domain.TranslationStats var stats domain.TranslationStats
@ -160,7 +161,7 @@ func (r *analyticsRepository) GetOrCreateTranslationStats(ctx context.Context, t
return &stats, err return &stats, err
} }
func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uint, date time.Time) (*domain.UserEngagement, error) { func (r *analyticsRepository) GetOrCreateUserEngagement(ctx context.Context, userID uuid.UUID, date time.Time) (*domain.UserEngagement, error) {
ctx, span := r.tracer.Start(ctx, "GetOrCreateUserEngagement") ctx, span := r.tracer.Start(ctx, "GetOrCreateUserEngagement")
defer span.End() defer span.End()
var engagement domain.UserEngagement var engagement domain.UserEngagement

View File

@ -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"

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/platform/config" "tercul/internal/platform/config"
"time" "time"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -23,7 +24,7 @@ func NewAuthRepository(db *gorm.DB, cfg *config.Config) domain.AuthRepository {
} }
} }
func (r *authRepository) StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error { func (r *authRepository) StoreToken(ctx context.Context, userID uuid.UUID, token string, expiresAt time.Time) error {
ctx, span := r.tracer.Start(ctx, "StoreToken") ctx, span := r.tracer.Start(ctx, "StoreToken")
defer span.End() defer span.End()
session := &domain.UserSession{ session := &domain.UserSession{

View File

@ -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"

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -34,27 +36,27 @@ func (r *authorRepository) FindByName(ctx context.Context, name string) (*domain
return &author, nil return &author, nil
} }
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { func (r *authorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
var authors []domain.Author var authors []domain.Author
err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id"). err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
Where("work_authors.work_id = ?", workID).Find(&authors).Error Where("work_authors.work_id = ?", workID).Find(&authors).Error
return authors, err return authors, err
} }
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { func (r *authorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
var authors []domain.Author var authors []domain.Author
err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id"). err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
Where("book_authors.book_id = ?", bookID).Find(&authors).Error Where("book_authors.book_id = ?", bookID).Find(&authors).Error
return authors, err return authors, err
} }
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
var authors []domain.Author var authors []domain.Author
err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error
return authors, err return authors, err
} }
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { func (r *authorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
var author domain.Author var author domain.Author
err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error
if err != nil { if err != nil {

View File

@ -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"
) )

View File

@ -9,6 +9,7 @@ import (
"tercul/internal/platform/log" "tercul/internal/platform/log"
"time" "time"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -39,8 +40,8 @@ func (r *BaseRepositoryImpl[T]) validateContext(ctx context.Context) error {
} }
// validateID ensures ID is valid // validateID ensures ID is valid
func (r *BaseRepositoryImpl[T]) validateID(id uint) error { func (r *BaseRepositoryImpl[T]) validateID(id uuid.UUID) error {
if id == 0 { if id == uuid.Nil {
return domain.ErrValidation return domain.ErrValidation
} }
return nil return nil
@ -158,7 +159,7 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
} }
// GetByID retrieves an entity by its ID // GetByID retrieves an entity by its ID
func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error) { func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uuid.UUID) (*T, error) {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return nil, err return nil, err
} }
@ -187,7 +188,7 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
} }
// GetByIDWithOptions retrieves an entity by its ID with query options // GetByIDWithOptions retrieves an entity by its ID with query options
func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*T, error) { func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*T, error) {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return nil, err return nil, err
} }
@ -268,7 +269,7 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
} }
// Delete removes an entity by its ID // Delete removes an entity by its ID
func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error { func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uuid.UUID) error {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return err return err
} }
@ -298,7 +299,7 @@ func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
} }
// DeleteInTx removes an entity by its ID within a transaction // DeleteInTx removes an entity by its ID within a transaction
func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return err return err
} }
@ -475,7 +476,7 @@ func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *d
} }
// FindWithPreload retrieves an entity by its ID with preloaded relationships // FindWithPreload retrieves an entity by its ID with preloaded relationships
func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error) { func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*T, error) {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return nil, err return nil, err
} }
@ -541,7 +542,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
} }
// Exists checks if an entity exists by ID // Exists checks if an entity exists by ID
func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uint) (bool, error) { func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
if err := r.validateContext(ctx); err != nil { if err := r.validateContext(ctx); err != nil {
return false, err return false, err
} }

View File

@ -3,11 +3,11 @@ package sql_test
import ( import (
"context" "context"
"errors" "errors"
"testing"
"tercul/internal/data/sql" "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"tercul/internal/testutil" "tercul/internal/testutil"
"testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gorm.io/gorm" "gorm.io/gorm"

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -27,7 +28,7 @@ func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
} }
// ListByAuthorID finds books by author ID // ListByAuthorID finds books by author ID
func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]domain.Book, error) { func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uuid.UUID) ([]domain.Book, error) {
ctx, span := r.tracer.Start(ctx, "ListByAuthorID") ctx, span := r.tracer.Start(ctx, "ListByAuthorID")
defer span.End() defer span.End()
var books []domain.Book var books []domain.Book
@ -40,7 +41,7 @@ func (r *bookRepository) ListByAuthorID(ctx context.Context, authorID uint) ([]d
} }
// ListByPublisherID finds books by publisher ID // ListByPublisherID finds books by publisher ID
func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint) ([]domain.Book, error) { func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uuid.UUID) ([]domain.Book, error) {
ctx, span := r.tracer.Start(ctx, "ListByPublisherID") ctx, span := r.tracer.Start(ctx, "ListByPublisherID")
defer span.End() defer span.End()
var books []domain.Book var books []domain.Book
@ -51,7 +52,7 @@ func (r *bookRepository) ListByPublisherID(ctx context.Context, publisherID uint
} }
// ListByWorkID finds books by work ID // ListByWorkID finds books by work ID
func (r *bookRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Book, error) { func (r *bookRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Book, error) {
ctx, span := r.tracer.Start(ctx, "ListByWorkID") ctx, span := r.tracer.Start(ctx, "ListByWorkID")
defer span.End() defer span.End()
var books []domain.Book var books []domain.Book

View File

@ -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"
) )

View File

@ -5,6 +5,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -26,7 +27,7 @@ func NewBookmarkRepository(db *gorm.DB, cfg *config.Config) domain.BookmarkRepos
} }
// ListByUserID finds bookmarks by user ID // ListByUserID finds bookmarks by user ID
func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]domain.Bookmark, error) { func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uuid.UUID) ([]domain.Bookmark, error) {
ctx, span := r.tracer.Start(ctx, "ListByUserID") ctx, span := r.tracer.Start(ctx, "ListByUserID")
defer span.End() defer span.End()
var bookmarks []domain.Bookmark var bookmarks []domain.Bookmark
@ -37,7 +38,7 @@ func (r *bookmarkRepository) ListByUserID(ctx context.Context, userID uint) ([]d
} }
// ListByWorkID finds bookmarks by work ID // ListByWorkID finds bookmarks by work ID
func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Bookmark, error) { func (r *bookmarkRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Bookmark, error) {
ctx, span := r.tracer.Start(ctx, "ListByWorkID") ctx, span := r.tracer.Start(ctx, "ListByWorkID")
defer span.End() defer span.End()
var bookmarks []domain.Bookmark var bookmarks []domain.Bookmark

View File

@ -4,10 +4,10 @@ import (
"context" "context"
"database/sql" "database/sql"
"regexp" "regexp"
"testing"
repo "tercul/internal/data/sql" repo "tercul/internal/data/sql"
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"testing"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"

View File

@ -6,6 +6,7 @@ import (
"tercul/internal/domain" "tercul/internal/domain"
"tercul/internal/platform/config" "tercul/internal/platform/config"
"github.com/google/uuid"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"gorm.io/gorm" "gorm.io/gorm"
@ -41,7 +42,7 @@ func (r *categoryRepository) FindByName(ctx context.Context, name string) (*doma
} }
// ListByWorkID finds categories by work ID // ListByWorkID finds categories by work ID
func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) { func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Category, error) {
ctx, span := r.tracer.Start(ctx, "ListByWorkID") ctx, span := r.tracer.Start(ctx, "ListByWorkID")
defer span.End() defer span.End()
var categories []domain.Category var categories []domain.Category
@ -54,7 +55,7 @@ func (r *categoryRepository) ListByWorkID(ctx context.Context, workID uint) ([]d
} }
// ListByParentID finds categories by parent ID // ListByParentID finds categories by parent ID
func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint) ([]domain.Category, error) { func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error) {
ctx, span := r.tracer.Start(ctx, "ListByParentID") ctx, span := r.tracer.Start(ctx, "ListByParentID")
defer span.End() defer span.End()
var categories []domain.Category var categories []domain.Category

View File

@ -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