Compare commits

..

No commits in common. "d50722dad54a136d93ab965e2a8a07b5657bbd6c" and "ad749d9184621ebdcb08e9a7c55dfc27a6761ca7" have entirely different histories.

222 changed files with 2998 additions and 4934 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/api" cmd = "go build -o ./tmp/tercul ."
# Binary file yields from `cmd`. # Binary file yields from `cmd`.
bin = "tmp/tercul" bin = "tmp/tercul"
# Customize binary. # Customize binary.

16
.github/dependabot.yml vendored Normal file
View File

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

769
.github/workflows/README.md vendored Normal file
View File

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

46
.github/workflows/build.yml vendored Normal file
View File

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

60
.github/workflows/deploy.yml vendored Normal file
View File

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

65
.github/workflows/docker-build.yml vendored Normal file
View File

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

33
.github/workflows/lint.yml vendored Normal file
View File

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

70
.github/workflows/security.yml vendored Normal file
View File

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

116
.github/workflows/test.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,6 @@ import (
"tercul/internal/app/translation" "tercul/internal/app/translation"
"tercul/internal/app/user" "tercul/internal/app/user"
"tercul/internal/app/work" "tercul/internal/app/work"
datacache "tercul/internal/data/cache"
dbsql "tercul/internal/data/sql" dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics" "tercul/internal/jobs/linguistics"
"tercul/internal/observability" "tercul/internal/observability"
@ -126,36 +125,13 @@ func main() {
// Create repositories // Create repositories
repos := dbsql.NewRepositories(database, cfg) repos := dbsql.NewRepositories(database, cfg)
// Initialize Redis cache once (shared by APQ, optional repo caching, and linguistics)
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
// Create linguistics dependencies // Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider() sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
if sErr != nil { if sErr != nil {
app_log.Fatal(sErr, "Failed to create sentiment provider") app_log.Fatal(sErr, "Failed to create sentiment provider")
} }
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
redisCache,
2,
true,
sentimentProvider,
workAnalyticsDeps,
)
analysisRepo := linguisticsFactory.GetAnalysisRepository()
// Create platform components // Create platform components
jwtManager := platform_auth.NewJWTManager(cfg) jwtManager := platform_auth.NewJWTManager(cfg)
@ -205,18 +181,12 @@ func main() {
App: application, App: application,
} }
// Initialize Redis Cache for APQ
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
var queryCache gql.Cache[string] var queryCache gql.Cache[string]
if cacheErr != nil { if cacheErr != nil {
app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error()) app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error())
} else { } else {
// Optional repository caching (opt-in)
if os.Getenv("REPO_CACHE_ENABLED") == "true" {
repos.Work = datacache.NewCachedWorkRepository(repos.Work, redisCache, nil)
repos.Author = datacache.NewCachedAuthorRepository(repos.Author, redisCache, nil)
repos.Translation = datacache.NewCachedTranslationRepository(repos.Translation, redisCache, nil)
app_log.Info("Repository caching enabled")
}
queryCache = &cache.GraphQLCacheAdapter{RedisCache: redisCache} queryCache = &cache.GraphQLCacheAdapter{RedisCache: redisCache}
app_log.Info("Redis cache initialized for APQ") app_log.Info("Redis cache initialized for APQ")
} }

View File

@ -1 +1 @@
{"last_processed_id":3,"total_processed":3,"last_updated":"2025-12-26T15:37:36.561092+01:00"} {"last_processed_id":3,"total_processed":3,"last_updated":"2025-11-30T21:59:16.811419372Z"}

View File

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

View File

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

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 uuid.UUID) (*domain.Translation, error) { func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
return nil, nil return nil, nil
} }
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

43
content/blog/post1.json Normal file
View File

@ -0,0 +1,43 @@
{
"contentTypeSlug": "blog",
"title": "The Future of Artificial Intelligence",
"slug": "future-of-ai",
"status": "published",
"content": {
"excerpt": "A deep dive into the future of artificial intelligence, exploring its potential impact on society, industry, and our daily lives.",
"content": "<p>Artificial intelligence (AI) is no longer a concept confined to science fiction. It's a powerful force that's reshaping our world in countless ways. From the algorithms that power our social media feeds to the sophisticated systems that drive autonomous vehicles, AI is already here. But what does the future hold for this transformative technology?</p><p>In this post, we'll explore some of the most exciting advancements on the horizon, including the rise of general AI, the potential for AI-driven scientific discovery, and the ethical considerations that we must address as we move forward.</p>",
"publishDate": "2024-09-15",
"author": "Dr. Evelyn Reed",
"tags": ["AI", "Machine Learning", "Technology"],
"meta_title": "The Future of AI: A Comprehensive Overview",
"meta_description": "Learn about the future of artificial intelligence and its potential impact on our world."
},
"languageCode": "en-US",
"isDefault": true,
"id": "post-1",
"translation_group_id": "tg-future-of-ai",
"lifecycle": {
"state": "published",
"published_at": "2024-09-15T10:00:00Z",
"timezone": "UTC"
},
"seo": {
"canonical": "https://example.com/blog/future-of-ai",
"og_title": "The Future of Artificial Intelligence",
"og_description": "A deep dive into the future of AI.",
"twitter_card": "summary_large_image"
},
"taxonomy": {
"categories": ["Technology", "Science"],
"featured": true
},
"relations": {
"related_posts": ["post-2"]
},
"assets": {
"hero_image": {
"url": "https://example.com/images/ai-future.jpg",
"alt": "An abstract image representing artificial intelligence."
}
}
}

43
content/blog/post2.json Normal file
View File

@ -0,0 +1,43 @@
{
"contentTypeSlug": "blog",
"title": "A Guide to Sustainable Living",
"slug": "guide-to-sustainable-living",
"status": "published",
"content": {
"excerpt": "Discover practical tips and simple changes you can make to live a more sustainable and eco-friendly lifestyle.",
"content": "<p>Living sustainably doesn't have to be complicated. It's about making conscious choices that reduce your environmental impact. In this guide, we'll cover everything from reducing your plastic consumption to creating a more energy-efficient home.</p><p>We'll also explore the benefits of a plant-based diet and how you can support local, sustainable businesses in your community.</p>",
"publishDate": "2024-09-18",
"author": "Liam Carter",
"tags": ["Sustainability", "Eco-Friendly", "Lifestyle"],
"meta_title": "Your Ultimate Guide to Sustainable Living",
"meta_description": "Learn how to live a more sustainable lifestyle with our comprehensive guide."
},
"languageCode": "en-US",
"isDefault": true,
"id": "post-2",
"translation_group_id": "tg-sustainable-living",
"lifecycle": {
"state": "published",
"published_at": "2024-09-18T10:00:00Z",
"timezone": "UTC"
},
"seo": {
"canonical": "https://example.com/blog/guide-to-sustainable-living",
"og_title": "A Guide to Sustainable Living",
"og_description": "Discover practical tips for a more sustainable lifestyle.",
"twitter_card": "summary"
},
"taxonomy": {
"categories": ["Lifestyle", "Environment"],
"featured": false
},
"relations": {
"related_posts": ["post-1", "post-3"]
},
"assets": {
"hero_image": {
"url": "https://example.com/images/sustainable-living.jpg",
"alt": "A person holding a reusable water bottle in a lush green environment."
}
}
}

43
content/blog/post3.json Normal file
View File

@ -0,0 +1,43 @@
{
"contentTypeSlug": "blog",
"title": "The Art of Mindful Meditation",
"slug": "art-of-mindful-meditation",
"status": "published",
"content": {
"excerpt": "Learn the basics of mindful meditation and how it can help you reduce stress, improve focus, and cultivate a sense of inner peace.",
"content": "<p>In our fast-paced world, it's easy to get caught up in the chaos. Mindful meditation offers a powerful tool to ground yourself in the present moment and find a sense of calm amidst the noise.</p><p>This post will guide you through the fundamental principles of mindfulness and provide simple exercises to help you start your meditation practice.</p>",
"publishDate": "2024-09-22",
"author": "Isabella Rossi",
"tags": ["Mindfulness", "Meditation", "Wellness"],
"meta_title": "A Beginner's Guide to Mindful Meditation",
"meta_description": "Start your journey with mindful meditation and discover its many benefits."
},
"languageCode": "en-US",
"isDefault": true,
"id": "post-3",
"translation_group_id": "tg-mindful-meditation",
"lifecycle": {
"state": "published",
"published_at": "2024-09-22T10:00:00Z",
"timezone": "UTC"
},
"seo": {
"canonical": "https://example.com/blog/art-of-mindful-meditation",
"og_title": "The Art of Mindful Meditation",
"og_description": "Learn the basics of mindful meditation.",
"twitter_card": "summary_large_image"
},
"taxonomy": {
"categories": ["Wellness", "Lifestyle"],
"featured": true
},
"relations": {
"related_posts": ["post-2", "post-4"]
},
"assets": {
"hero_image": {
"url": "https://example.com/images/meditation.jpg",
"alt": "A person meditating peacefully in a serene setting."
}
}
}

43
content/blog/post4.json Normal file
View File

@ -0,0 +1,43 @@
{
"contentTypeSlug": "blog",
"title": "Exploring the Wonders of the Cosmos",
"slug": "exploring-the-cosmos",
"status": "published",
"content": {
"excerpt": "Join us on a journey through the cosmos as we explore distant galaxies, mysterious black holes, and the search for extraterrestrial life.",
"content": "<p>The universe is a vast and mysterious place, filled with wonders that we are only just beginning to understand. From the birth of stars to the formation of galaxies, the cosmos is a story of epic proportions.</p><p>In this post, we'll take a look at some of the most awe-inspiring discoveries in modern astronomy and consider the big questions that continue to drive our exploration of space.</p>",
"publishDate": "2024-09-25",
"author": "Dr. Kenji Tanaka",
"tags": ["Astronomy", "Space", "Science"],
"meta_title": "A Journey Through the Cosmos",
"meta_description": "Explore the wonders of the universe with our guide to modern astronomy."
},
"languageCode": "en-US",
"isDefault": true,
"id": "post-4",
"translation_group_id": "tg-exploring-the-cosmos",
"lifecycle": {
"state": "published",
"published_at": "2024-09-25T10:00:00Z",
"timezone": "UTC"
},
"seo": {
"canonical": "https://example.com/blog/exploring-the-cosmos",
"og_title": "Exploring the Wonders of the Cosmos",
"og_description": "A journey through the cosmos.",
"twitter_card": "summary"
},
"taxonomy": {
"categories": ["Science", "Astronomy"],
"featured": false
},
"relations": {
"related_posts": ["post-1", "post-5"]
},
"assets": {
"hero_image": {
"url": "https://example.com/images/cosmos.jpg",
"alt": "A stunning image of a spiral galaxy."
}
}
}

43
content/blog/post5.json Normal file
View File

@ -0,0 +1,43 @@
{
"contentTypeSlug": "blog",
"title": "The Rise of Remote Work",
"slug": "rise-of-remote-work",
"status": "published",
"content": {
"excerpt": "Remote work is here to stay. In this post, we'll explore the benefits and challenges of working from home and how to create a productive and healthy remote work environment.",
"content": "<p>The way we work has been fundamentally transformed in recent years. Remote work has gone from a niche perk to a mainstream reality for millions of people around the world.</p><p>This shift has brought with it a host of new opportunities and challenges. We'll discuss how to stay focused and motivated while working from home, how to maintain a healthy work-life balance, and how companies can build strong remote teams.</p>",
"publishDate": "2024-09-28",
"author": "Chloe Davis",
"tags": ["Remote Work", "Productivity", "Future of Work"],
"meta_title": "Navigating the World of Remote Work",
"meta_description": "Learn how to thrive in a remote work environment."
},
"languageCode": "en-US",
"isDefault": true,
"id": "post-5",
"translation_group_id": "tg-remote-work",
"lifecycle": {
"state": "published",
"published_at": "2024-09-28T10:00:00Z",
"timezone": "UTC"
},
"seo": {
"canonical": "https://example.com/blog/rise-of-remote-work",
"og_title": "The Rise of Remote Work",
"og_description": "The benefits and challenges of working from home.",
"twitter_card": "summary_large_image"
},
"taxonomy": {
"categories": ["Work", "Productivity"],
"featured": true
},
"relations": {
"related_posts": ["post-2", "post-4"]
},
"assets": {
"hero_image": {
"url": "https://example.com/images/remote-work.jpg",
"alt": "A person working on a laptop in a comfortable home office setting."
}
}
}

View File

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

2
go.mod
View File

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

View File

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

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 uuid.UUID) (*domain.Like, error) { func (m *mockLikeRepository) GetByID(ctx context.Context, id uint) (*domain.Like, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

@ -8,7 +8,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings" "strconv"
"tercul/internal/adapters/graphql/model" "tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth" "tercul/internal/app/auth"
"tercul/internal/app/author" "tercul/internal/app/author"
@ -25,27 +25,8 @@ import (
platform_auth "tercul/internal/platform/auth" platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/log" "tercul/internal/platform/log"
"time" "time"
"github.com/google/uuid"
) )
func toModelUserRole(role domain.UserRole) model.UserRole {
switch strings.ToLower(string(role)) {
case "reader":
return model.UserRoleReader
case "contributor":
return model.UserRoleContributor
case "reviewer":
return model.UserRoleReviewer
case "editor":
return model.UserRoleEditor
case "admin":
return model.UserRoleAdmin
default:
return model.UserRoleReader
}
}
// Register is the resolver for the register field. // Register is the resolver for the register field.
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) { func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
// Convert GraphQL input to service input // Convert GraphQL input to service input
@ -73,7 +54,7 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
FirstName: &authResponse.User.FirstName, FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName, LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName, DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role), Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified, Verified: authResponse.User.Verified,
Active: authResponse.User.Active, Active: authResponse.User.Active,
}, },
@ -104,7 +85,7 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
FirstName: &authResponse.User.FirstName, FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName, LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName, DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role), Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified, Verified: authResponse.User.Verified,
Active: authResponse.User.Active, Active: authResponse.User.Active,
}, },
@ -145,7 +126,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
// Convert to GraphQL model // Convert to GraphQL model
return &model.Work{ return &model.Work{
ID: createdWork.ID.String(), ID: fmt.Sprintf("%d", createdWork.ID),
Name: createdWork.Title, Name: createdWork.Title,
Language: createdWork.Language, Language: createdWork.Language,
Content: input.Content, Content: input.Content,
@ -158,7 +139,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
workID, err := uuid.Parse(id) workID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -166,7 +147,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
// Create domain model // Create domain model
workModel := &domain.Work{ workModel := &domain.Work{
TranslatableModel: domain.TranslatableModel{ TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: workID}, BaseModel: domain.BaseModel{ID: uint(workID)},
Language: input.Language, Language: input.Language,
}, },
Title: input.Name, Title: input.Name,
@ -189,12 +170,12 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
// DeleteWork is the resolver for the deleteWork field. // DeleteWork is the resolver for the deleteWork field.
func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteWork(ctx context.Context, id string) (bool, error) {
workID, err := uuid.Parse(id) workID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
err = r.App.Work.Commands.DeleteWork(ctx, workID) err = r.App.Work.Commands.DeleteWork(ctx, uint(workID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -208,7 +189,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
return nil, err return nil, err
} }
workID, err := uuid.Parse(input.WorkID) workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -222,7 +203,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
Title: input.Name, Title: input.Name,
Content: content, Content: content,
Language: input.Language, Language: input.Language,
TranslatableID: workID, TranslatableID: uint(workID),
TranslatableType: "works", TranslatableType: "works",
} }
createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput) createdTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, createInput)
@ -231,7 +212,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
} }
go func() { go func() {
if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), workID); err != nil { if err := r.App.Analytics.IncrementWorkTranslationCount(context.Background(), uint(workID)); err != nil {
log.Error(err, "failed to increment work translation count") log.Error(err, "failed to increment work translation count")
} }
}() }()
@ -251,7 +232,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
return nil, err return nil, err
} }
workID, err := uuid.Parse(input.WorkID) workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -265,7 +246,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
Title: input.Name, Title: input.Name,
Content: content, Content: content,
Language: input.Language, Language: input.Language,
TranslatableID: workID, TranslatableID: uint(workID),
TranslatableType: "works", TranslatableType: "works",
} }
updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput) updatedTranslation, err := r.App.Translation.Commands.CreateOrUpdateTranslation(ctx, updateInput)
@ -284,12 +265,12 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
// DeleteTranslation is the resolver for the deleteTranslation field. // DeleteTranslation is the resolver for the deleteTranslation field.
func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteTranslation(ctx context.Context, id string) (bool, error) {
translationID, err := uuid.Parse(id) translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid translation ID: %v", err) return false, fmt.Errorf("invalid translation ID: %v", err)
} }
err = r.App.Translation.Commands.DeleteTranslation(ctx, translationID) err = r.App.Translation.Commands.DeleteTranslation(ctx, uint(translationID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -330,13 +311,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
bookID, err := uuid.Parse(id) bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
updateInput := book.UpdateBookInput{ updateInput := book.UpdateBookInput{
ID: bookID, ID: uint(bookID),
Title: &input.Name, Title: &input.Name,
Description: input.Description, Description: input.Description,
Language: &input.Language, Language: &input.Language,
@ -359,12 +340,12 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
// DeleteBook is the resolver for the deleteBook field. // DeleteBook is the resolver for the deleteBook field.
func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteBook(ctx context.Context, id string) (bool, error) {
bookID, err := uuid.Parse(id) bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
err = r.App.Book.Commands.DeleteBook(ctx, bookID) err = r.App.Book.Commands.DeleteBook(ctx, uint(bookID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -397,13 +378,13 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
if err := Validate(input); err != nil { if err := Validate(input); err != nil {
return nil, err return nil, err
} }
authorID, err := uuid.Parse(id) authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid author ID: %v", err) return nil, fmt.Errorf("invalid author ID: %v", err)
} }
updateInput := author.UpdateAuthorInput{ updateInput := author.UpdateAuthorInput{
ID: authorID, ID: uint(authorID),
Name: input.Name, Name: input.Name,
} }
updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput) updatedAuthor, err := r.App.Author.Commands.UpdateAuthor(ctx, updateInput)
@ -420,12 +401,12 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
// DeleteAuthor is the resolver for the deleteAuthor field. // DeleteAuthor is the resolver for the deleteAuthor field.
func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteAuthor(ctx context.Context, id string) (bool, error) {
authorID, err := uuid.Parse(id) authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid author ID: %v", err) return false, fmt.Errorf("invalid author ID: %v", err)
} }
err = r.App.Author.Commands.DeleteAuthor(ctx, authorID) err = r.App.Author.Commands.DeleteAuthor(ctx, uint(authorID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -439,13 +420,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
return nil, err return nil, err
} }
userID, err := uuid.Parse(id) userID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid user ID: %v", err) return nil, fmt.Errorf("invalid user ID: %v", err)
} }
updateInput := user.UpdateUserInput{ updateInput := user.UpdateUserInput{
ID: userID, ID: uint(userID),
Username: input.Username, Username: input.Username,
Email: input.Email, Email: input.Email,
Password: input.Password, Password: input.Password,
@ -464,25 +445,28 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
} }
if input.CountryID != nil { if input.CountryID != nil {
countryID, err := uuid.Parse(*input.CountryID) countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err) return nil, fmt.Errorf("invalid country ID: %v", err)
} }
updateInput.CountryID = &countryID uid := uint(countryID)
updateInput.CountryID = &uid
} }
if input.CityID != nil { if input.CityID != nil {
cityID, err := uuid.Parse(*input.CityID) cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err) return nil, fmt.Errorf("invalid city ID: %v", err)
} }
updateInput.CityID = &cityID uid := uint(cityID)
updateInput.CityID = &uid
} }
if input.AddressID != nil { if input.AddressID != nil {
addressID, err := uuid.Parse(*input.AddressID) addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err) return nil, fmt.Errorf("invalid address ID: %v", err)
} }
updateInput.AddressID = &addressID uid := uint(addressID)
updateInput.AddressID = &uid
} }
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput) updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
@ -499,7 +483,7 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
DisplayName: &updatedUser.DisplayName, DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio, Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL, AvatarURL: &updatedUser.AvatarURL,
Role: toModelUserRole(updatedUser.Role), Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified, Verified: updatedUser.Verified,
Active: updatedUser.Active, Active: updatedUser.Active,
}, nil }, nil
@ -507,12 +491,12 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
// DeleteUser is the resolver for the deleteUser field. // DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) { func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
userID, err := uuid.Parse(id) userID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
err = r.App.User.Commands.DeleteUser(ctx, userID) err = r.App.User.Commands.DeleteUser(ctx, uint(userID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -556,13 +540,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collectionID, err := uuid.Parse(id) collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
updateInput := collection.UpdateCollectionInput{ updateInput := collection.UpdateCollectionInput{
ID: collectionID, ID: uint(collectionID),
Name: input.Name, Name: input.Name,
UserID: userID, UserID: userID,
} }
@ -591,12 +575,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
collectionID, err := uuid.Parse(id) collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid collection ID: %v", err) return false, fmt.Errorf("invalid collection ID: %v", err)
} }
err = r.App.Collection.Commands.DeleteCollection(ctx, collectionID, userID) err = r.App.Collection.Commands.DeleteCollection(ctx, uint(collectionID), userID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -611,18 +595,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collID, err := uuid.Parse(collectionID) collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
wID, err := uuid.Parse(workID) wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
addInput := collection.AddWorkToCollectionInput{ addInput := collection.AddWorkToCollectionInput{
CollectionID: collID, CollectionID: uint(collID),
WorkID: wID, WorkID: uint(wID),
UserID: userID, UserID: userID,
} }
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput) err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
@ -630,7 +614,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
return nil, err return nil, err
} }
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID) updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -649,18 +633,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
collID, err := uuid.Parse(collectionID) collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err) return nil, fmt.Errorf("invalid collection ID: %v", err)
} }
wID, err := uuid.Parse(workID) wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
removeInput := collection.RemoveWorkFromCollectionInput{ removeInput := collection.RemoveWorkFromCollectionInput{
CollectionID: collID, CollectionID: uint(collID),
WorkID: wID, WorkID: uint(wID),
UserID: userID, UserID: userID,
} }
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput) err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
@ -668,7 +652,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
return nil, err return nil, err
} }
updatedCollection, err := r.App.Collection.Queries.Collection(ctx, collID) updatedCollection, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -696,25 +680,28 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
UserID: userID, UserID: userID,
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID) workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
createInput.WorkID = &workID wID := uint(workID)
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID) translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
createInput.TranslationID = &translationID tID := uint(translationID)
createInput.TranslationID = &tID
} }
if input.ParentCommentID != nil { if input.ParentCommentID != nil {
parentCommentID, err := uuid.Parse(*input.ParentCommentID) parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid parent comment ID: %v", err) return nil, fmt.Errorf("invalid parent comment ID: %v", err)
} }
createInput.ParentID = &parentCommentID pID := uint(parentCommentID)
createInput.ParentID = &pID
} }
createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput) createdComment, err := r.App.Comment.Commands.CreateComment(ctx, createInput)
@ -749,12 +736,12 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
commentID, err := uuid.Parse(id) commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err) return nil, fmt.Errorf("invalid comment ID: %v", err)
} }
commentModel, err := r.App.Comment.Queries.Comment(ctx, commentID) commentModel, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -767,7 +754,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
} }
updateInput := comment.UpdateCommentInput{ updateInput := comment.UpdateCommentInput{
ID: commentID, ID: uint(commentID),
Text: input.Text, Text: input.Text,
} }
updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput) updatedComment, err := r.App.Comment.Commands.UpdateComment(ctx, updateInput)
@ -791,12 +778,12 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
commentID, err := uuid.Parse(id) commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid comment ID: %v", err) return false, fmt.Errorf("invalid comment ID: %v", err)
} }
comment, err := r.App.Comment.Queries.Comment(ctx, commentID) comment, err := r.App.Comment.Queries.Comment(ctx, uint(commentID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -808,7 +795,7 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Comment.Commands.DeleteComment(ctx, commentID) err = r.App.Comment.Commands.DeleteComment(ctx, uint(commentID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -834,25 +821,28 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
UserID: userID, UserID: userID,
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID) workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
createInput.WorkID = &workID wID := uint(workID)
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID) translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
createInput.TranslationID = &translationID tID := uint(translationID)
createInput.TranslationID = &tID
} }
if input.CommentID != nil { if input.CommentID != nil {
commentID, err := uuid.Parse(*input.CommentID) commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid comment ID: %v", err) return nil, fmt.Errorf("invalid comment ID: %v", err)
} }
createInput.CommentID = &commentID cID := uint(commentID)
createInput.CommentID = &cID
} }
createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput) createdLike, err := r.App.Like.Commands.CreateLike(ctx, createInput)
@ -884,12 +874,12 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
likeID, err := uuid.Parse(id) likeID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid like ID: %v", err) return false, fmt.Errorf("invalid like ID: %v", err)
} }
like, err := r.App.Like.Queries.Like(ctx, likeID) like, err := r.App.Like.Queries.Like(ctx, uint(likeID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -901,7 +891,7 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Like.Commands.DeleteLike(ctx, likeID) err = r.App.Like.Commands.DeleteLike(ctx, uint(likeID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -916,14 +906,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
return nil, fmt.Errorf("unauthorized") return nil, fmt.Errorf("unauthorized")
} }
workID, err := uuid.Parse(input.WorkID) workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
createInput := bookmark.CreateBookmarkInput{ createInput := bookmark.CreateBookmarkInput{
UserID: userID, UserID: userID,
WorkID: workID, WorkID: uint(workID),
} }
if input.Name != nil { if input.Name != nil {
createInput.Name = *input.Name createInput.Name = *input.Name
@ -934,7 +924,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
return nil, err return nil, err
} }
if err := r.App.Analytics.IncrementWorkBookmarks(ctx, workID); err != nil { if err := r.App.Analytics.IncrementWorkBookmarks(ctx, uint(workID)); err != nil {
log.FromContext(ctx).Error(err, "failed to increment work bookmarks") log.FromContext(ctx).Error(err, "failed to increment work bookmarks")
} }
@ -953,12 +943,12 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
bookmarkID, err := uuid.Parse(id) bookmarkID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("invalid bookmark ID: %v", err) return false, fmt.Errorf("invalid bookmark ID: %v", err)
} }
bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, bookmarkID) bookmark, err := r.App.Bookmark.Queries.Bookmark(ctx, uint(bookmarkID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -970,7 +960,7 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized") return false, fmt.Errorf("unauthorized")
} }
err = r.App.Bookmark.Commands.DeleteBookmark(ctx, bookmarkID) err = r.App.Bookmark.Commands.DeleteBookmark(ctx, uint(bookmarkID))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -990,19 +980,21 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
} }
if input.WorkID != nil { if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID) workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
createInput.WorkID = &workID wID := uint(workID)
createInput.WorkID = &wID
} }
if input.TranslationID != nil { if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID) translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
createInput.TranslationID = &translationID tID := uint(translationID)
createInput.TranslationID = &tID
} }
if input.Status != nil { if input.Status != nil {
@ -1033,13 +1025,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
return nil, domain.ErrUnauthorized return nil, domain.ErrUnauthorized
} }
contributionID, err := uuid.Parse(id) contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
updateInput := contribution.UpdateContributionInput{ updateInput := contribution.UpdateContributionInput{
ID: contributionID, ID: uint(contributionID),
UserID: userID, UserID: userID,
Name: &input.Name, Name: &input.Name,
} }
@ -1071,12 +1063,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
return false, domain.ErrUnauthorized return false, domain.ErrUnauthorized
} }
contributionID, err := uuid.Parse(id) contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return false, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
err = r.App.Contribution.Commands.DeleteContribution(ctx, contributionID, userID) err = r.App.Contribution.Commands.DeleteContribution(ctx, uint(contributionID), userID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1086,13 +1078,13 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
// ReviewContribution is the resolver for the reviewContribution field. // ReviewContribution is the resolver for the reviewContribution field.
func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) { func (r *mutationResolver) ReviewContribution(ctx context.Context, id string, status model.ContributionStatus, feedback *string) (*model.Contribution, error) {
contributionID, err := uuid.Parse(id) contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
} }
reviewInput := contribution.ReviewContributionInput{ reviewInput := contribution.ReviewContributionInput{
ID: contributionID, ID: uint(contributionID),
Status: status.String(), Status: status.String(),
Feedback: feedback, Feedback: feedback,
} }
@ -1137,7 +1129,7 @@ func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload
FirstName: &authResponse.User.FirstName, FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName, LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName, DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role), Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified, Verified: authResponse.User.Verified,
Active: authResponse.User.Active, Active: authResponse.User.Active,
}, },
@ -1201,25 +1193,28 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
} }
if input.CountryID != nil { if input.CountryID != nil {
countryID, err := uuid.Parse(*input.CountryID) countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err) return nil, fmt.Errorf("invalid country ID: %v", err)
} }
updateInput.CountryID = &countryID uid := uint(countryID)
updateInput.CountryID = &uid
} }
if input.CityID != nil { if input.CityID != nil {
cityID, err := uuid.Parse(*input.CityID) cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err) return nil, fmt.Errorf("invalid city ID: %v", err)
} }
updateInput.CityID = &cityID uid := uint(cityID)
updateInput.CityID = &uid
} }
if input.AddressID != nil { if input.AddressID != nil {
addressID, err := uuid.Parse(*input.AddressID) addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid address ID: %v", err) return nil, fmt.Errorf("invalid address ID: %v", err)
} }
updateInput.AddressID = &addressID uid := uint(addressID)
updateInput.AddressID = &uid
} }
updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput) updatedUser, err := r.App.User.Commands.UpdateUser(ctx, updateInput)
@ -1236,7 +1231,7 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
DisplayName: &updatedUser.DisplayName, DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio, Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL, AvatarURL: &updatedUser.AvatarURL,
Role: toModelUserRole(updatedUser.Role), Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified, Verified: updatedUser.Verified,
Active: updatedUser.Active, Active: updatedUser.Active,
}, nil }, nil
@ -1265,12 +1260,12 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword s
// Work is the resolver for the work field. // Work is the resolver for the work field.
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) { func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
workID, err := uuid.Parse(id) workID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err) return nil, fmt.Errorf("invalid work ID: %v", err)
} }
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, workID) workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
if err != nil { if err != nil {
if errors.Is(err, domain.ErrEntityNotFound) { if errors.Is(err, domain.ErrEntityNotFound) {
return nil, nil return nil, nil
@ -1279,7 +1274,7 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
} }
go func() { go func() {
if err := r.App.Analytics.IncrementWorkViews(context.Background(), workID); err != nil { if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil {
log.Error(err, "failed to increment work views") log.Error(err, "failed to increment work views")
} }
}() }()
@ -1325,12 +1320,12 @@ func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32,
// Translation is the resolver for the translation field. // Translation is the resolver for the translation field.
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) { func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
translationID, err := uuid.Parse(id) translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err) return nil, fmt.Errorf("invalid translation ID: %v", err)
} }
translationDTO, err := r.App.Translation.Queries.Translation(ctx, translationID) translationDTO, err := r.App.Translation.Queries.Translation(ctx, uint(translationID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1339,7 +1334,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
} }
go func() { go func() {
if err := r.App.Analytics.IncrementTranslationViews(context.Background(), translationID); err != nil { if err := r.App.Analytics.IncrementTranslationViews(context.Background(), uint(translationID)); err != nil {
log.Error(err, "failed to increment translation views") log.Error(err, "failed to increment translation views")
} }
}() }()
@ -1355,7 +1350,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
// Translations is the resolver for the translations field. // Translations is the resolver for the translations field.
func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) { func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) {
wID, err := uuid.Parse(workID) wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
@ -1369,7 +1364,7 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
page = int(*offset)/pageSize + 1 page = int(*offset)/pageSize + 1
} }
paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, wID, language, page, pageSize) paginatedResult, err := r.App.Translation.Queries.ListTranslations(ctx, uint(wID), language, page, pageSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1389,12 +1384,12 @@ func (r *queryResolver) Translations(ctx context.Context, workID string, languag
// Book is the resolver for the book field. // Book is the resolver for the book field.
func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) { func (r *queryResolver) Book(ctx context.Context, id string) (*model.Book, error) {
bookID, err := uuid.Parse(id) bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
} }
bookRecord, err := r.App.Book.Queries.Book(ctx, bookID) bookRecord, err := r.App.Book.Queries.Book(ctx, uint(bookID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1434,12 +1429,12 @@ func (r *queryResolver) Books(ctx context.Context, limit *int32, offset *int32)
// Author is the resolver for the author field. // Author is the resolver for the author field.
func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) { func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, error) {
authorID, err := uuid.Parse(id) authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid author ID", domain.ErrValidation)
} }
authorRecord, err := r.App.Author.Queries.Author(ctx, authorID) authorRecord, err := r.App.Author.Queries.Author(ctx, uint(authorID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1468,17 +1463,18 @@ func (r *queryResolver) Author(ctx context.Context, id string) (*model.Author, e
func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) { func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32, search *string, countryID *string) ([]*model.Author, error) {
var authors []*domain.Author var authors []*domain.Author
var err error var err error
var countryIDUUID *uuid.UUID var countryIDUint *uint
if countryID != nil { if countryID != nil {
parsedID, err := uuid.Parse(*countryID) parsedID, err := strconv.ParseUint(*countryID, 10, 32)
if err != nil { if err != nil {
return nil, err return nil, err
} }
countryIDUUID = &parsedID uid := uint(parsedID)
countryIDUint = &uid
} }
authors, err = r.App.Author.Queries.Authors(ctx, countryIDUUID) authors, err = r.App.Author.Queries.Authors(ctx, countryIDUint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1507,12 +1503,12 @@ func (r *queryResolver) Authors(ctx context.Context, limit *int32, offset *int32
// User is the resolver for the user field. // User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) { func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
userID, err := uuid.Parse(id) userID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
userRecord, err := r.App.User.Queries.User(ctx, userID) userRecord, err := r.App.User.Queries.User(ctx, uint(userID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1529,7 +1525,7 @@ func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error
DisplayName: &userRecord.DisplayName, DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio, Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL, AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role), Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified, Verified: userRecord.Verified,
Active: userRecord.Active, Active: userRecord.Active,
}, nil }, nil
@ -1554,7 +1550,7 @@ func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.U
DisplayName: &userRecord.DisplayName, DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio, Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL, AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role), Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified, Verified: userRecord.Verified,
Active: userRecord.Active, Active: userRecord.Active,
}, nil }, nil
@ -1579,7 +1575,7 @@ func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*m
DisplayName: &userRecord.DisplayName, DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio, Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL, AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role), Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified, Verified: userRecord.Verified,
Active: userRecord.Active, Active: userRecord.Active,
}, nil }, nil
@ -1668,7 +1664,7 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
DisplayName: &userRecord.DisplayName, DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio, Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL, AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role), Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified, Verified: userRecord.Verified,
Active: userRecord.Active, Active: userRecord.Active,
}, nil }, nil
@ -1676,12 +1672,12 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
// UserProfile is the resolver for the userProfile field. // UserProfile is the resolver for the userProfile field.
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) { func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
uID, err := uuid.Parse(userID) uID, err := strconv.ParseUint(userID, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
profile, err := r.App.User.Queries.UserProfile(ctx, uID) profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1689,12 +1685,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
return nil, nil return nil, nil
} }
user, err := r.App.User.Queries.User(ctx, uID) user, err := r.App.User.Queries.User(ctx, uint(uID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil { if user == nil {
return nil, fmt.Errorf("user not found for profile %s", profile.ID.String()) return nil, fmt.Errorf("user not found for profile %d", profile.ID)
} }
return &model.UserProfile{ return &model.UserProfile{
@ -1709,7 +1705,7 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
DisplayName: &user.DisplayName, DisplayName: &user.DisplayName,
Bio: &user.Bio, Bio: &user.Bio,
AvatarURL: &user.AvatarURL, AvatarURL: &user.AvatarURL,
Role: toModelUserRole(user.Role), Role: model.UserRole(user.Role),
Verified: user.Verified, Verified: user.Verified,
Active: user.Active, Active: user.Active,
}, },
@ -1724,12 +1720,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
// Collection is the resolver for the collection field. // Collection is the resolver for the collection field.
func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) { func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Collection, error) {
collID, err := uuid.Parse(id) collID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid collection ID", domain.ErrValidation)
} }
collectionRecord, err := r.App.Collection.Queries.Collection(ctx, collID) collectionRecord, err := r.App.Collection.Queries.Collection(ctx, uint(collID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1737,7 +1733,7 @@ func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Colle
return nil, nil return nil, nil
} }
workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, collID) workRecords, err := r.App.Work.Queries.ListByCollectionID(ctx, uint(collID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1770,11 +1766,11 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
var err error var err error
if userID != nil { if userID != nil {
uID, idErr := uuid.Parse(*userID) uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uID) collectionRecords, err = r.App.Collection.Queries.CollectionsByUserID(ctx, uint(uID))
} else { } else {
collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx) collectionRecords, err = r.App.Collection.Queries.PublicCollections(ctx)
} }
@ -1815,18 +1811,18 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
// Tag is the resolver for the tag field. // Tag is the resolver for the tag field.
func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) { func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
tagID, err := uuid.Parse(id) tagID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag, err := r.App.Tag.Queries.Tag(ctx, tagID) tag, err := r.App.Tag.Queries.Tag(ctx, uint(tagID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &model.Tag{ return &model.Tag{
ID: tag.ID.String(), ID: fmt.Sprintf("%d", tag.ID),
Name: tag.Name, Name: tag.Name,
}, nil }, nil
} }
@ -1851,12 +1847,12 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) (
// Category is the resolver for the category field. // Category is the resolver for the category field.
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) { func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
categoryID, err := uuid.Parse(id) categoryID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid category ID: %v", err) return nil, fmt.Errorf("invalid category ID: %v", err)
} }
category, err := r.App.Category.Queries.Category(ctx, categoryID) category, err := r.App.Category.Queries.Category(ctx, uint(categoryID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1865,7 +1861,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
} }
return &model.Category{ return &model.Category{
ID: category.ID.String(), ID: fmt.Sprintf("%d", category.ID),
Name: category.Name, Name: category.Name,
}, nil }, nil
} }
@ -1890,12 +1886,12 @@ func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *in
// Comment is the resolver for the comment field. // Comment is the resolver for the comment field.
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) { func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
cID, err := uuid.Parse(id) cID, err := strconv.ParseUint(id, 10, 32)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid comment ID", domain.ErrValidation)
} }
commentRecord, err := r.App.Comment.Queries.Comment(ctx, cID) commentRecord, err := r.App.Comment.Queries.Comment(ctx, uint(cID))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1904,10 +1900,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
} }
return &model.Comment{ return &model.Comment{
ID: commentRecord.ID.String(), ID: fmt.Sprintf("%d", commentRecord.ID),
Text: commentRecord.Text, Text: commentRecord.Text,
User: &model.User{ User: &model.User{
ID: commentRecord.UserID.String(), ID: fmt.Sprintf("%d", commentRecord.UserID),
}, },
}, nil }, nil
} }
@ -1918,23 +1914,23 @@ func (r *queryResolver) Comments(ctx context.Context, workID *string, translatio
var err error var err error
if workID != nil { if workID != nil {
wID, idErr := uuid.Parse(*workID) wID, idErr := strconv.ParseUint(*workID, 10, 32)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid work ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, wID) commentRecords, err = r.App.Comment.Queries.CommentsByWorkID(ctx, uint(wID))
} else if translationID != nil { } else if translationID != nil {
tID, idErr := uuid.Parse(*translationID) tID, idErr := strconv.ParseUint(*translationID, 10, 32)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid translation ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, tID) commentRecords, err = r.App.Comment.Queries.CommentsByTranslationID(ctx, uint(tID))
} else if userID != nil { } else if userID != nil {
uID, idErr := uuid.Parse(*userID) uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil { if idErr != nil {
return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation) return nil, fmt.Errorf("%w: invalid user ID", domain.ErrValidation)
} }
commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uID) commentRecords, err = r.App.Comment.Queries.CommentsByUserID(ctx, uint(uID))
} else { } else {
commentRecords, err = r.App.Comment.Queries.Comments(ctx) commentRecords, err = r.App.Comment.Queries.Comments(ctx)
} }

View File

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

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 uuid.UUID) (*domain.Work, error) { func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

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

View File

@ -0,0 +1 @@
# This file is created to ensure the directory structure is in place.

View File

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

View File

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

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

View File

@ -9,6 +9,7 @@ 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 uuid.UUID) (*domain.User, error) { func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
if m.getByIDFunc != nil { if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id) return m.getByIDFunc(ctx, id)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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 uuid.UUID) (*domain.Author, error) { func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

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

View File

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

View File

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

View File

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

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 uuid.UUID) (*domain.User, error) { func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

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

View File

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

View File

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

View File

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

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 uuid.UUID) (*domain.Work, error) { func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
args := m.Called(ctx, id) args := m.Called(ctx, id)
if args.Get(0) == nil { if args.Get(0) == nil {
return nil, args.Error(1) return nil, args.Error(1)

View File

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

View File

@ -1,211 +0,0 @@
package cache
import (
"context"
"time"
"tercul/internal/domain"
platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CachedAuthorRepository struct {
inner domain.AuthorRepository
opt Options
}
func NewCachedAuthorRepository(inner domain.AuthorRepository, c platform_cache.Cache, opt *Options) *CachedAuthorRepository {
resolved := DefaultOptions(c)
if opt != nil {
resolved = *opt
if resolved.Cache == nil {
resolved.Cache = c
}
if resolved.Keys == nil {
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
}
if resolved.EntityTTL == 0 {
resolved.EntityTTL = 1 * time.Hour
}
if resolved.ListTTL == 0 {
resolved.ListTTL = 5 * time.Minute
}
}
return &CachedAuthorRepository{inner: inner, opt: resolved}
}
func (r *CachedAuthorRepository) Create(ctx context.Context, entity *domain.Author) error {
err := r.inner.Create(ctx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
err := r.inner.CreateInTx(ctx, tx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Author
key := r.opt.Keys.EntityKey("author", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
author, err := r.inner.GetByID(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && author != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("author", id), author, r.opt.EntityTTL)
}
return author, nil
}
func (r *CachedAuthorRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Author, error) {
return r.inner.GetByIDWithOptions(ctx, id, options)
}
func (r *CachedAuthorRepository) Update(ctx context.Context, entity *domain.Author) error {
err := r.inner.Update(ctx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
err := r.inner.UpdateInTx(ctx, tx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", id))
}
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("author", id))
}
invalidateEntityType(ctx, r.opt.Cache, "author")
}
return err
}
func (r *CachedAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Author]
key := r.opt.Keys.ListKey("author", page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.List(ctx, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("author", page, pageSize), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
return r.inner.ListWithOptions(ctx, options)
}
func (r *CachedAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) {
return r.inner.ListAll(ctx)
}
func (r *CachedAuthorRepository) Count(ctx context.Context) (int64, error) {
return r.inner.Count(ctx)
}
func (r *CachedAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
return r.inner.CountWithOptions(ctx, options)
}
func (r *CachedAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Author, error) {
return r.inner.FindWithPreload(ctx, preloads, id)
}
func (r *CachedAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
return r.inner.GetAllForSync(ctx, batchSize, offset)
}
func (r *CachedAuthorRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id)
}
func (r *CachedAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
return r.inner.BeginTx(ctx)
}
func (r *CachedAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return r.inner.WithTx(ctx, fn)
}
func (r *CachedAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
return r.inner.FindByName(ctx, name)
}
func (r *CachedAuthorRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByWorkID(ctx, workID)
}
func (r *CachedAuthorRepository) ListByBookID(ctx context.Context, bookID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByBookID(ctx, bookID)
}
func (r *CachedAuthorRepository) ListByCountryID(ctx context.Context, countryID uuid.UUID) ([]domain.Author, error) {
return r.inner.ListByCountryID(ctx, countryID)
}
func (r *CachedAuthorRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Author, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Author
key := r.opt.Keys.QueryKey("author", "withTranslations", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
author, err := r.inner.GetWithTranslations(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && author != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("author", "withTranslations", id), author, r.opt.EntityTTL)
}
return author, nil
}

View File

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

View File

@ -1,242 +0,0 @@
package cache
import (
"context"
"time"
"tercul/internal/domain"
platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CachedTranslationRepository struct {
inner domain.TranslationRepository
opt Options
}
func NewCachedTranslationRepository(inner domain.TranslationRepository, c platform_cache.Cache, opt *Options) *CachedTranslationRepository {
resolved := DefaultOptions(c)
if opt != nil {
resolved = *opt
if resolved.Cache == nil {
resolved.Cache = c
}
if resolved.Keys == nil {
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
}
if resolved.EntityTTL == 0 {
resolved.EntityTTL = 1 * time.Hour
}
if resolved.ListTTL == 0 {
resolved.ListTTL = 5 * time.Minute
}
}
return &CachedTranslationRepository{inner: inner, opt: resolved}
}
func (r *CachedTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
err := r.inner.Create(ctx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
err := r.inner.CreateInTx(ctx, tx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Translation, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Translation
key := r.opt.Keys.EntityKey("translation", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
tr, err := r.inner.GetByID(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && tr != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("translation", id), tr, r.opt.EntityTTL)
}
return tr, nil
}
func (r *CachedTranslationRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Translation, error) {
return r.inner.GetByIDWithOptions(ctx, id, options)
}
func (r *CachedTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error {
err := r.inner.Update(ctx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
err := r.inner.UpdateInTx(ctx, tx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", id))
}
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", id))
}
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}
func (r *CachedTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Translation]
key := r.opt.Keys.ListKey("translation", page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.List(ctx, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("translation", page, pageSize), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
return r.inner.ListWithOptions(ctx, options)
}
func (r *CachedTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) {
return r.inner.ListAll(ctx)
}
func (r *CachedTranslationRepository) Count(ctx context.Context) (int64, error) {
return r.inner.Count(ctx)
}
func (r *CachedTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
return r.inner.CountWithOptions(ctx, options)
}
func (r *CachedTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Translation, error) {
return r.inner.FindWithPreload(ctx, preloads, id)
}
func (r *CachedTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) {
return r.inner.GetAllForSync(ctx, batchSize, offset)
}
func (r *CachedTranslationRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id)
}
func (r *CachedTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
return r.inner.BeginTx(ctx)
}
func (r *CachedTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return r.inner.WithTx(ctx, fn)
}
func (r *CachedTranslationRepository) ListByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Translation, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached []domain.Translation
key := r.opt.Keys.QueryKey("translation", "byWork", workID)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return cached, nil
}
}
res, err := r.inner.ListByWorkID(ctx, workID)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("translation", "byWork", workID), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uuid.UUID, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
lang := ""
if language != nil {
lang = *language
}
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Translation]
key := r.opt.Keys.QueryKey("translation", "byWorkPaged", workID, lang, page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.ListByWorkIDPaginated(ctx, workID, language, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
key := r.opt.Keys.QueryKey("translation", "byWorkPaged", workID, lang, page, pageSize)
_ = r.opt.Cache.Set(ctx, key, res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uuid.UUID) ([]domain.Translation, error) {
return r.inner.ListByEntity(ctx, entityType, entityID)
}
func (r *CachedTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
return r.inner.ListByTranslatorID(ctx, translatorID)
}
func (r *CachedTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
return r.inner.ListByStatus(ctx, status)
}
func (r *CachedTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error {
err := r.inner.Upsert(ctx, translation)
if err == nil {
if r.opt.Cache != nil && translation != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("translation", translation.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "translation")
}
return err
}

View File

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

View File

@ -1,279 +0,0 @@
package cache
import (
"context"
"time"
"tercul/internal/domain"
platform_cache "tercul/internal/platform/cache"
"github.com/google/uuid"
"gorm.io/gorm"
)
type CachedWorkRepository struct {
inner domain.WorkRepository
opt Options
}
func NewCachedWorkRepository(inner domain.WorkRepository, c platform_cache.Cache, opt *Options) *CachedWorkRepository {
resolved := DefaultOptions(c)
if opt != nil {
resolved = *opt
if resolved.Cache == nil {
resolved.Cache = c
}
if resolved.Keys == nil {
resolved.Keys = platform_cache.NewDefaultKeyGenerator("tercul:repo:")
}
if resolved.EntityTTL == 0 {
resolved.EntityTTL = 1 * time.Hour
}
if resolved.ListTTL == 0 {
resolved.ListTTL = 5 * time.Minute
}
}
return &CachedWorkRepository{inner: inner, opt: resolved}
}
func (r *CachedWorkRepository) Create(ctx context.Context, entity *domain.Work) error {
err := r.inner.Create(ctx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
err := r.inner.CreateInTx(ctx, tx, entity)
if err == nil {
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work
key := r.opt.Keys.EntityKey("work", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
work, err := r.inner.GetByID(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.EntityKey("work", id), work, r.opt.EntityTTL)
}
return work, nil
}
func (r *CachedWorkRepository) GetByIDWithOptions(ctx context.Context, id uuid.UUID, options *domain.QueryOptions) (*domain.Work, error) {
// Options can include varying preloads/where/order; avoid caching to prevent incorrect results.
return r.inner.GetByIDWithOptions(ctx, id, options)
}
func (r *CachedWorkRepository) Update(ctx context.Context, entity *domain.Work) error {
err := r.inner.Update(ctx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
err := r.inner.UpdateInTx(ctx, tx, entity)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", entity.ID))
}
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := r.inner.Delete(ctx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", id))
}
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) error {
err := r.inner.DeleteInTx(ctx, tx, id)
if err == nil {
if r.opt.Cache != nil {
_ = r.opt.Cache.Delete(ctx, r.opt.Keys.EntityKey("work", id))
}
invalidateEntityType(ctx, r.opt.Cache, "work")
}
return err
}
func (r *CachedWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Work]
key := r.opt.Keys.ListKey("work", page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.List(ctx, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.ListKey("work", page, pageSize), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
return r.inner.ListWithOptions(ctx, options)
}
func (r *CachedWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) {
return r.inner.ListAll(ctx)
}
func (r *CachedWorkRepository) Count(ctx context.Context) (int64, error) {
return r.inner.Count(ctx)
}
func (r *CachedWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
return r.inner.CountWithOptions(ctx, options)
}
func (r *CachedWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uuid.UUID) (*domain.Work, error) {
return r.inner.FindWithPreload(ctx, preloads, id)
}
func (r *CachedWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
return r.inner.GetAllForSync(ctx, batchSize, offset)
}
func (r *CachedWorkRepository) Exists(ctx context.Context, id uuid.UUID) (bool, error) {
return r.inner.Exists(ctx, id)
}
func (r *CachedWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
return r.inner.BeginTx(ctx)
}
func (r *CachedWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return r.inner.WithTx(ctx, fn)
}
func (r *CachedWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) {
return r.inner.FindByTitle(ctx, title)
}
func (r *CachedWorkRepository) FindByAuthor(ctx context.Context, authorID uuid.UUID) ([]domain.Work, error) {
return r.inner.FindByAuthor(ctx, authorID)
}
func (r *CachedWorkRepository) FindByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
return r.inner.FindByCategory(ctx, categoryID)
}
func (r *CachedWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Work]
key := r.opt.Keys.QueryKey("work", "lang", language, page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.FindByLanguage(ctx, language, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "lang", language, page, pageSize), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedWorkRepository) GetWithTranslations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work
key := r.opt.Keys.QueryKey("work", "withTranslations", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
work, err := r.inner.GetWithTranslations(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "withTranslations", id), work, r.opt.EntityTTL)
}
return work, nil
}
func (r *CachedWorkRepository) GetWithAssociations(ctx context.Context, id uuid.UUID) (*domain.Work, error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.Work
key := r.opt.Keys.QueryKey("work", "withAssociations", id)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
work, err := r.inner.GetWithAssociations(ctx, id)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && work != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "withAssociations", id), work, r.opt.EntityTTL)
}
return work, nil
}
func (r *CachedWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uuid.UUID) (*domain.Work, error) {
// Tx-scoped reads should bypass cache.
return r.inner.GetWithAssociationsInTx(ctx, tx, id)
}
func (r *CachedWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
if r.opt.Enabled && r.opt.Cache != nil {
var cached domain.PaginatedResult[domain.Work]
key := r.opt.Keys.QueryKey("work", "listWithTranslations", page, pageSize)
if err := r.opt.Cache.Get(ctx, key, &cached); err == nil {
return &cached, nil
}
}
res, err := r.inner.ListWithTranslations(ctx, page, pageSize)
if err != nil {
return nil, err
}
if r.opt.Enabled && r.opt.Cache != nil && res != nil {
_ = r.opt.Cache.Set(ctx, r.opt.Keys.QueryKey("work", "listWithTranslations", page, pageSize), res, r.opt.ListTTL)
}
return res, nil
}
func (r *CachedWorkRepository) IsAuthor(ctx context.Context, workID uuid.UUID, authorID uuid.UUID) (bool, error) {
return r.inner.IsAuthor(ctx, workID, authorID)
}
func (r *CachedWorkRepository) ListByCollectionID(ctx context.Context, collectionID uuid.UUID) ([]domain.Work, error) {
return r.inner.ListByCollectionID(ctx, collectionID)
}

View File

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

View File

@ -1,20 +0,0 @@
package cache
import (
"context"
platform_cache "tercul/internal/platform/cache"
)
type entityTypeInvalidator interface {
InvalidateEntityType(ctx context.Context, entityType string) error
}
func invalidateEntityType(ctx context.Context, c platform_cache.Cache, entityType string) {
if c == nil {
return
}
if inv, ok := c.(entityTypeInvalidator); ok {
_ = inv.InvalidateEntityType(ctx, entityType)
}
}

View File

@ -1,25 +0,0 @@
package cache
import (
"time"
platform_cache "tercul/internal/platform/cache"
)
type Options struct {
Enabled bool
Cache platform_cache.Cache
Keys platform_cache.KeyGenerator
EntityTTL time.Duration
ListTTL time.Duration
}
func DefaultOptions(c platform_cache.Cache) Options {
return Options{
Enabled: c != nil,
Cache: c,
Keys: platform_cache.NewDefaultKeyGenerator("tercul:repo:"),
EntityTTL: 1 * time.Hour,
ListTTL: 5 * time.Minute,
}
}

View File

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

View File

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

View File

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

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

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

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