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]
# 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`.
bin = "tmp/tercul"
# Customize binary.
@ -43,4 +43,4 @@ runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
clean_on_exit = true

16
.github/dependabot.yml vendored 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
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)
RUN go install github.com/air-verse/air@v1.52.3
RUN go install github.com/air-verse/air@latest
# Set working directory
WORKDIR /app
@ -19,4 +19,4 @@ COPY . .
EXPOSE 8080
# Command to run the application with Air for hot reloading
CMD ["air"]
CMD ["air"]

View File

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

View File

@ -29,7 +29,6 @@ import (
"tercul/internal/app/translation"
"tercul/internal/app/user"
"tercul/internal/app/work"
datacache "tercul/internal/data/cache"
dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics"
"tercul/internal/observability"
@ -126,36 +125,13 @@ func main() {
// Create repositories
repos := dbsql.NewRepositories(database, cfg)
// Initialize Redis cache once (shared by APQ, optional repo caching, and linguistics)
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
// Create linguistics dependencies
analysisRepo := linguistics.NewGORMAnalysisRepository(database)
sentimentProvider, sErr := linguistics.NewGoVADERSentimentProvider()
if sErr != nil {
app_log.Fatal(sErr, "Failed to create sentiment provider")
}
workAnalyticsDeps := linguistics.WorkAnalyticsDeps{
StatsRepo: repos.Analytics,
LikeCounter: repos.Like,
CommentCounter: repos.Comment,
BookmarkCounter: repos.Bookmark,
TranslationCount: repos.Translation,
TranslationList: repos.Translation,
}
linguisticsFactory := linguistics.NewLinguisticsFactory(
cfg,
database,
redisCache,
2,
true,
sentimentProvider,
workAnalyticsDeps,
)
analysisRepo := linguisticsFactory.GetAnalysisRepository()
// Create platform components
jwtManager := platform_auth.NewJWTManager(cfg)
@ -205,18 +181,12 @@ func main() {
App: application,
}
// Initialize Redis Cache for APQ
redisCache, cacheErr := cache.NewDefaultRedisCache(cfg)
var queryCache gql.Cache[string]
if cacheErr != nil {
app_log.Warn("Redis cache initialization failed, APQ disabled: " + cacheErr.Error())
} 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}
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"
"os"
"path/filepath"
"strconv"
"time"
"tercul/internal/data/sql"
@ -29,7 +30,7 @@ const (
)
type checkpoint struct {
LastProcessedID string `json:"last_processed_id"`
LastProcessedID uint `json:"last_processed_id"`
TotalProcessed int `json:"total_processed"`
LastUpdated time.Time `json:"last_updated"`
}
@ -103,7 +104,7 @@ Example:
if resume {
cp = loadCheckpoint()
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))
// Filter translations if resuming from checkpoint
if cp != nil && cp.LastProcessedID != "" {
if cp != nil && cp.LastProcessedID > 0 {
filtered := make([]domain.Translation, 0, len(translations))
for _, t := range translations {
if t.ID.String() > cp.LastProcessedID {
if t.ID > cp.LastProcessedID {
filtered = append(filtered, t)
}
}
@ -281,11 +282,11 @@ func migrateTranslations(
// Process translations in batches
batch := make([]domain.Translation, 0, batchSize)
lastProcessedID := ""
lastProcessedID := uint(0)
for i, translation := range translations {
batch = append(batch, translation)
lastProcessedID = translation.ID.String()
lastProcessedID = translation.ID
// Process batch when it reaches the batch size or at the end
if len(batch) >= batchSize || i == len(translations)-1 {
@ -325,7 +326,7 @@ func indexBatch(index bleve.Index, translations []domain.Translation) error {
batch := index.NewBatch()
for _, t := range translations {
doc := map[string]interface{}{
"id": t.ID.String(),
"id": strconv.FormatUint(uint64(t.ID), 10),
"title": t.Title,
"content": t.Content,
"description": t.Description,

View File

@ -38,11 +38,11 @@ func TestMigrateTranslations_LargeBatch(t *testing.T) {
translations := make([]domain.Translation, 100)
for i := 0; i < 100; i++ {
translations[i] = domain.Translation{
BaseModel: domain.BaseModel{ID: uint(i + 1)},
Title: "Test Translation",
Content: "Content",
Language: "en",
Status: domain.TranslationStatusPublished,
BaseModel: domain.BaseModel{ID: uint(i + 1)},
Title: "Test Translation",
Content: "Content",
Language: "en",
Status: domain.TranslationStatusPublished,
TranslatableID: uint(i + 1),
TranslatableType: "works",
}
@ -104,11 +104,11 @@ func TestIndexBatch_WithTranslatorID(t *testing.T) {
translatorID := uint(123)
translations := []domain.Translation{
{
BaseModel: domain.BaseModel{ID: 1},
Title: "Test",
Content: "Content",
Language: "en",
Status: domain.TranslationStatusPublished,
BaseModel: domain.BaseModel{ID: 1},
Title: "Test",
Content: "Content",
Language: "en",
Status: domain.TranslationStatusPublished,
TranslatableID: 100,
TranslatableType: "works",
TranslatorID: &translatorID,
@ -129,3 +129,4 @@ func TestCheckpoint_InvalidJSON(t *testing.T) {
// This would require mocking file system, but for now we test the happy path
// Invalid JSON handling is tested implicitly through file operations
}

View File

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

View File

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

View File

@ -6,10 +6,7 @@ import (
"syscall"
"tercul/cmd/cli/internal/bootstrap"
dbsql "tercul/internal/data/sql"
"tercul/internal/jobs/linguistics"
"tercul/internal/jobs/sync"
"tercul/internal/platform/cache"
"tercul/internal/platform/config"
"tercul/internal/platform/db"
app_log "tercul/internal/platform/log"
@ -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
syncJob := sync.NewSyncJob(database, asynqClient, cfg, weaviateClient)
linguisticJob := linguistics.NewLinguisticSyncJob(database, linguisticsFactory.GetAnalyzer(), asynqClient)
// Create a new ServeMux for routing jobs
mux := asynq.NewServeMux()
// Register all job handlers
sync.RegisterQueueHandlers(mux, syncJob)
linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// Placeholder for other job handlers that might be added in the future
// linguistics.RegisterLinguisticHandlers(mux, linguisticJob)
// trending.RegisterTrendingHandlers(mux, analyticsService)
// Start the server in a goroutine

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

View File

@ -73,4 +73,4 @@ func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod
return nil, args.Error(1)
}
return args.Get(0).([]*domain.Work), args.Error(1)
}
}

View File

@ -46,4 +46,4 @@ func NewErrorPresenter() graphql.ErrorPresenterFunc {
return gqlErr
}
}
}

View File

@ -1,13 +1,9 @@
package graphql
import (
"context"
"github.com/google/uuid"
)
import "context"
// 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 {
return nil
}

View File

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

View File

@ -116,4 +116,4 @@ func (s *LikeResolversUnitSuite) TestDeleteLike() {
// Verify that the repository's Delete method was called
s.mockLikeRepo.AssertCalled(s.T(), "Delete", mock.Anything, uint(likeIDUint))
}
}

View File

@ -8,7 +8,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"strconv"
"tercul/internal/adapters/graphql/model"
"tercul/internal/app/auth"
"tercul/internal/app/author"
@ -25,27 +25,8 @@ import (
platform_auth "tercul/internal/platform/auth"
"tercul/internal/platform/log"
"time"
"github.com/google/uuid"
)
func toModelUserRole(role domain.UserRole) model.UserRole {
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.
func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInput) (*model.AuthPayload, error) {
// Convert GraphQL input to service input
@ -73,7 +54,7 @@ func (r *mutationResolver) Register(ctx context.Context, input model.RegisterInp
FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role),
Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified,
Active: authResponse.User.Active,
},
@ -104,7 +85,7 @@ func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*
FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role),
Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified,
Active: authResponse.User.Active,
},
@ -145,7 +126,7 @@ func (r *mutationResolver) CreateWork(ctx context.Context, input model.WorkInput
// Convert to GraphQL model
return &model.Work{
ID: createdWork.ID.String(),
ID: fmt.Sprintf("%d", createdWork.ID),
Name: createdWork.Title,
Language: createdWork.Language,
Content: input.Content,
@ -158,7 +139,7 @@ func (r *mutationResolver) UpdateWork(ctx context.Context, id string, input mode
return nil, err
}
workID, err := uuid.Parse(id)
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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
workModel := &domain.Work{
TranslatableModel: domain.TranslatableModel{
BaseModel: domain.BaseModel{ID: workID},
BaseModel: domain.BaseModel{ID: uint(workID)},
Language: input.Language,
},
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.
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 {
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 {
return false, err
}
@ -208,7 +189,7 @@ func (r *mutationResolver) CreateTranslation(ctx context.Context, input model.Tr
return nil, err
}
workID, err := uuid.Parse(input.WorkID)
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
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,
Content: content,
Language: input.Language,
TranslatableID: workID,
TranslatableID: uint(workID),
TranslatableType: "works",
}
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() {
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")
}
}()
@ -251,7 +232,7 @@ func (r *mutationResolver) UpdateTranslation(ctx context.Context, id string, inp
return nil, err
}
workID, err := uuid.Parse(input.WorkID)
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
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,
Content: content,
Language: input.Language,
TranslatableID: workID,
TranslatableID: uint(workID),
TranslatableType: "works",
}
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.
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 {
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 {
return false, err
}
@ -330,13 +311,13 @@ func (r *mutationResolver) UpdateBook(ctx context.Context, id string, input mode
return nil, err
}
bookID, err := uuid.Parse(id)
bookID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid book ID", domain.ErrValidation)
}
updateInput := book.UpdateBookInput{
ID: bookID,
ID: uint(bookID),
Title: &input.Name,
Description: input.Description,
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.
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 {
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 {
return false, err
}
@ -397,13 +378,13 @@ func (r *mutationResolver) UpdateAuthor(ctx context.Context, id string, input mo
if err := Validate(input); err != nil {
return nil, err
}
authorID, err := uuid.Parse(id)
authorID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid author ID: %v", err)
}
updateInput := author.UpdateAuthorInput{
ID: authorID,
ID: uint(authorID),
Name: input.Name,
}
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.
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 {
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 {
return false, err
}
@ -439,13 +420,13 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
return nil, err
}
userID, err := uuid.Parse(id)
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid user ID: %v", err)
}
updateInput := user.UpdateUserInput{
ID: userID,
ID: uint(userID),
Username: input.Username,
Email: input.Email,
Password: input.Password,
@ -464,25 +445,28 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
}
if input.CountryID != nil {
countryID, err := uuid.Parse(*input.CountryID)
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err)
}
updateInput.CountryID = &countryID
uid := uint(countryID)
updateInput.CountryID = &uid
}
if input.CityID != nil {
cityID, err := uuid.Parse(*input.CityID)
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err)
}
updateInput.CityID = &cityID
uid := uint(cityID)
updateInput.CityID = &uid
}
if input.AddressID != nil {
addressID, err := uuid.Parse(*input.AddressID)
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil {
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)
@ -499,7 +483,7 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL,
Role: toModelUserRole(updatedUser.Role),
Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified,
Active: updatedUser.Active,
}, nil
@ -507,12 +491,12 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, id string, input mode
// DeleteUser is the resolver for the deleteUser field.
func (r *mutationResolver) DeleteUser(ctx context.Context, id string) (bool, error) {
userID, err := uuid.Parse(id)
userID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return false, err
}
@ -556,13 +540,13 @@ func (r *mutationResolver) UpdateCollection(ctx context.Context, id string, inpu
return nil, fmt.Errorf("unauthorized")
}
collectionID, err := uuid.Parse(id)
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
updateInput := collection.UpdateCollectionInput{
ID: collectionID,
ID: uint(collectionID),
Name: input.Name,
UserID: userID,
}
@ -591,12 +575,12 @@ func (r *mutationResolver) DeleteCollection(ctx context.Context, id string) (boo
return false, fmt.Errorf("unauthorized")
}
collectionID, err := uuid.Parse(id)
collectionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return false, err
}
@ -611,18 +595,18 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
return nil, fmt.Errorf("unauthorized")
}
collID, err := uuid.Parse(collectionID)
collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
wID, err := uuid.Parse(workID)
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
addInput := collection.AddWorkToCollectionInput{
CollectionID: collID,
WorkID: wID,
CollectionID: uint(collID),
WorkID: uint(wID),
UserID: userID,
}
err = r.App.Collection.Commands.AddWorkToCollection(ctx, addInput)
@ -630,7 +614,7 @@ func (r *mutationResolver) AddWorkToCollection(ctx context.Context, collectionID
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 {
return nil, err
}
@ -649,18 +633,18 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
return nil, fmt.Errorf("unauthorized")
}
collID, err := uuid.Parse(collectionID)
collID, err := strconv.ParseUint(collectionID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid collection ID: %v", err)
}
wID, err := uuid.Parse(workID)
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
removeInput := collection.RemoveWorkFromCollectionInput{
CollectionID: collID,
WorkID: wID,
CollectionID: uint(collID),
WorkID: uint(wID),
UserID: userID,
}
err = r.App.Collection.Commands.RemoveWorkFromCollection(ctx, removeInput)
@ -668,7 +652,7 @@ func (r *mutationResolver) RemoveWorkFromCollection(ctx context.Context, collect
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 {
return nil, err
}
@ -696,25 +680,28 @@ func (r *mutationResolver) CreateComment(ctx context.Context, input model.Commen
UserID: userID,
}
if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID)
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
createInput.WorkID = &workID
wID := uint(workID)
createInput.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID)
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
createInput.TranslationID = &translationID
tID := uint(translationID)
createInput.TranslationID = &tID
}
if input.ParentCommentID != nil {
parentCommentID, err := uuid.Parse(*input.ParentCommentID)
parentCommentID, err := strconv.ParseUint(*input.ParentCommentID, 10, 32)
if err != nil {
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)
@ -749,12 +736,12 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
return nil, fmt.Errorf("unauthorized")
}
commentID, err := uuid.Parse(id)
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return nil, err
}
@ -767,7 +754,7 @@ func (r *mutationResolver) UpdateComment(ctx context.Context, id string, input m
}
updateInput := comment.UpdateCommentInput{
ID: commentID,
ID: uint(commentID),
Text: input.Text,
}
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")
}
commentID, err := uuid.Parse(id)
commentID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return false, err
}
@ -808,7 +795,7 @@ func (r *mutationResolver) DeleteComment(ctx context.Context, id string) (bool,
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 {
return false, err
}
@ -834,25 +821,28 @@ func (r *mutationResolver) CreateLike(ctx context.Context, input model.LikeInput
UserID: userID,
}
if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID)
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
createInput.WorkID = &workID
wID := uint(workID)
createInput.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID)
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
createInput.TranslationID = &translationID
tID := uint(translationID)
createInput.TranslationID = &tID
}
if input.CommentID != nil {
commentID, err := uuid.Parse(*input.CommentID)
commentID, err := strconv.ParseUint(*input.CommentID, 10, 32)
if err != nil {
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)
@ -884,12 +874,12 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
return false, fmt.Errorf("unauthorized")
}
likeID, err := uuid.Parse(id)
likeID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return false, err
}
@ -901,7 +891,7 @@ func (r *mutationResolver) DeleteLike(ctx context.Context, id string) (bool, err
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 {
return false, err
}
@ -916,14 +906,14 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
return nil, fmt.Errorf("unauthorized")
}
workID, err := uuid.Parse(input.WorkID)
workID, err := strconv.ParseUint(input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
createInput := bookmark.CreateBookmarkInput{
UserID: userID,
WorkID: workID,
WorkID: uint(workID),
}
if input.Name != nil {
createInput.Name = *input.Name
@ -934,7 +924,7 @@ func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.Bookm
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")
}
@ -953,12 +943,12 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
return false, fmt.Errorf("unauthorized")
}
bookmarkID, err := uuid.Parse(id)
bookmarkID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return false, err
}
@ -970,7 +960,7 @@ func (r *mutationResolver) DeleteBookmark(ctx context.Context, id string) (bool,
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 {
return false, err
}
@ -990,19 +980,21 @@ func (r *mutationResolver) CreateContribution(ctx context.Context, input model.C
}
if input.WorkID != nil {
workID, err := uuid.Parse(*input.WorkID)
workID, err := strconv.ParseUint(*input.WorkID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid work ID: %v", err)
}
createInput.WorkID = &workID
wID := uint(workID)
createInput.WorkID = &wID
}
if input.TranslationID != nil {
translationID, err := uuid.Parse(*input.TranslationID)
translationID, err := strconv.ParseUint(*input.TranslationID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid translation ID: %v", err)
}
createInput.TranslationID = &translationID
tID := uint(translationID)
createInput.TranslationID = &tID
}
if input.Status != nil {
@ -1033,13 +1025,13 @@ func (r *mutationResolver) UpdateContribution(ctx context.Context, id string, in
return nil, domain.ErrUnauthorized
}
contributionID, err := uuid.Parse(id)
contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
}
updateInput := contribution.UpdateContributionInput{
ID: contributionID,
ID: uint(contributionID),
UserID: userID,
Name: &input.Name,
}
@ -1071,12 +1063,12 @@ func (r *mutationResolver) DeleteContribution(ctx context.Context, id string) (b
return false, domain.ErrUnauthorized
}
contributionID, err := uuid.Parse(id)
contributionID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
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.
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 {
return nil, fmt.Errorf("%w: invalid contribution ID", domain.ErrValidation)
}
reviewInput := contribution.ReviewContributionInput{
ID: contributionID,
ID: uint(contributionID),
Status: status.String(),
Feedback: feedback,
}
@ -1137,7 +1129,7 @@ func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload
FirstName: &authResponse.User.FirstName,
LastName: &authResponse.User.LastName,
DisplayName: &authResponse.User.DisplayName,
Role: toModelUserRole(authResponse.User.Role),
Role: model.UserRole(authResponse.User.Role),
Verified: authResponse.User.Verified,
Active: authResponse.User.Active,
},
@ -1201,25 +1193,28 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
}
if input.CountryID != nil {
countryID, err := uuid.Parse(*input.CountryID)
countryID, err := strconv.ParseUint(*input.CountryID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid country ID: %v", err)
}
updateInput.CountryID = &countryID
uid := uint(countryID)
updateInput.CountryID = &uid
}
if input.CityID != nil {
cityID, err := uuid.Parse(*input.CityID)
cityID, err := strconv.ParseUint(*input.CityID, 10, 32)
if err != nil {
return nil, fmt.Errorf("invalid city ID: %v", err)
}
updateInput.CityID = &cityID
uid := uint(cityID)
updateInput.CityID = &uid
}
if input.AddressID != nil {
addressID, err := uuid.Parse(*input.AddressID)
addressID, err := strconv.ParseUint(*input.AddressID, 10, 32)
if err != nil {
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)
@ -1236,7 +1231,7 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, input model.UserIn
DisplayName: &updatedUser.DisplayName,
Bio: &updatedUser.Bio,
AvatarURL: &updatedUser.AvatarURL,
Role: toModelUserRole(updatedUser.Role),
Role: model.UserRole(updatedUser.Role),
Verified: updatedUser.Verified,
Active: updatedUser.Active,
}, nil
@ -1265,12 +1260,12 @@ func (r *mutationResolver) ChangePassword(ctx context.Context, currentPassword s
// Work is the resolver for the work field.
func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error) {
workID, err := uuid.Parse(id)
workID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 errors.Is(err, domain.ErrEntityNotFound) {
return nil, nil
@ -1279,7 +1274,7 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
}
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")
}
}()
@ -1325,12 +1320,12 @@ func (r *queryResolver) Works(ctx context.Context, limit *int32, offset *int32,
// Translation is the resolver for the translation field.
func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Translation, error) {
translationID, err := uuid.Parse(id)
translationID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return nil, err
}
@ -1339,7 +1334,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
}
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")
}
}()
@ -1355,7 +1350,7 @@ func (r *queryResolver) Translation(ctx context.Context, id string) (*model.Tran
// Translations is the resolver for the translations field.
func (r *queryResolver) Translations(ctx context.Context, workID string, language *string, limit *int32, offset *int32) ([]*model.Translation, error) {
wID, err := uuid.Parse(workID)
wID, err := strconv.ParseUint(workID, 10, 32)
if err != nil {
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
}
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 {
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.
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 {
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 {
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.
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 {
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 {
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) {
var authors []*domain.Author
var err error
var countryIDUUID *uuid.UUID
var countryIDUint *uint
if countryID != nil {
parsedID, err := uuid.Parse(*countryID)
parsedID, err := strconv.ParseUint(*countryID, 10, 32)
if err != nil {
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 {
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.
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 {
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 {
return nil, err
}
@ -1529,7 +1525,7 @@ func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role),
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
@ -1554,7 +1550,7 @@ func (r *queryResolver) UserByEmail(ctx context.Context, email string) (*model.U
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role),
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
@ -1579,7 +1575,7 @@ func (r *queryResolver) UserByUsername(ctx context.Context, username string) (*m
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role),
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
@ -1668,7 +1664,7 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
DisplayName: &userRecord.DisplayName,
Bio: &userRecord.Bio,
AvatarURL: &userRecord.AvatarURL,
Role: toModelUserRole(userRecord.Role),
Role: model.UserRole(userRecord.Role),
Verified: userRecord.Verified,
Active: userRecord.Active,
}, nil
@ -1676,12 +1672,12 @@ func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
// UserProfile is the resolver for the userProfile field.
func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.UserProfile, error) {
uID, err := uuid.Parse(userID)
uID, err := strconv.ParseUint(userID, 10, 32)
if err != nil {
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 {
return nil, err
}
@ -1689,12 +1685,12 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
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 {
return nil, err
}
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{
@ -1709,7 +1705,7 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
DisplayName: &user.DisplayName,
Bio: &user.Bio,
AvatarURL: &user.AvatarURL,
Role: toModelUserRole(user.Role),
Role: model.UserRole(user.Role),
Verified: user.Verified,
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.
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 {
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 {
return nil, err
}
@ -1737,7 +1733,7 @@ func (r *queryResolver) Collection(ctx context.Context, id string) (*model.Colle
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 {
return nil, err
}
@ -1770,11 +1766,11 @@ func (r *queryResolver) Collections(ctx context.Context, userID *string, limit *
var err error
if userID != nil {
uID, idErr := uuid.Parse(*userID)
uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil {
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 {
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.
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 {
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 {
return nil, err
}
return &model.Tag{
ID: tag.ID.String(),
ID: fmt.Sprintf("%d", tag.ID),
Name: tag.Name,
}, nil
}
@ -1851,12 +1847,12 @@ func (r *queryResolver) Tags(ctx context.Context, limit *int32, offset *int32) (
// Category is the resolver for the category field.
func (r *queryResolver) Category(ctx context.Context, id string) (*model.Category, error) {
categoryID, err := uuid.Parse(id)
categoryID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return nil, err
}
@ -1865,7 +1861,7 @@ func (r *queryResolver) Category(ctx context.Context, id string) (*model.Categor
}
return &model.Category{
ID: category.ID.String(),
ID: fmt.Sprintf("%d", category.ID),
Name: category.Name,
}, nil
}
@ -1890,12 +1886,12 @@ func (r *queryResolver) Categories(ctx context.Context, limit *int32, offset *in
// Comment is the resolver for the comment field.
func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment, error) {
cID, err := uuid.Parse(id)
cID, err := strconv.ParseUint(id, 10, 32)
if err != nil {
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 {
return nil, err
}
@ -1904,10 +1900,10 @@ func (r *queryResolver) Comment(ctx context.Context, id string) (*model.Comment,
}
return &model.Comment{
ID: commentRecord.ID.String(),
ID: fmt.Sprintf("%d", commentRecord.ID),
Text: commentRecord.Text,
User: &model.User{
ID: commentRecord.UserID.String(),
ID: fmt.Sprintf("%d", commentRecord.UserID),
},
}, nil
}
@ -1918,23 +1914,23 @@ func (r *queryResolver) Comments(ctx context.Context, workID *string, translatio
var err error
if workID != nil {
wID, idErr := uuid.Parse(*workID)
wID, idErr := strconv.ParseUint(*workID, 10, 32)
if idErr != nil {
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 {
tID, idErr := uuid.Parse(*translationID)
tID, idErr := strconv.ParseUint(*translationID, 10, 32)
if idErr != nil {
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 {
uID, idErr := uuid.Parse(*userID)
uID, idErr := strconv.ParseUint(*userID, 10, 32)
if idErr != nil {
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 {
commentRecords, err = r.App.Comment.Queries.Comments(ctx)
}
@ -2007,7 +2003,7 @@ func (r *queryResolver) Search(ctx context.Context, query string, limit *int32,
}
params := domainsearch.SearchParams{
Query: query,
Query: query,
Filters: domainsearch.SearchFilters{
Languages: searchFilters.Languages,
Tags: searchFilters.Tags,

View File

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

View File

@ -30,4 +30,4 @@ func Validate(s interface{}) error {
// For other unexpected errors, like invalid validation input.
return fmt.Errorf("unexpected error during validation: %w", err)
}
}

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 {
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)
if args.Get(0) == nil {
return nil, args.Error(1)
@ -153,4 +153,4 @@ func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionI
return nil, args.Error(1)
}
return args.Get(0).([]domain.Work), args.Error(1)
}
}

View File

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

View File

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

View File

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

View File

@ -13,36 +13,34 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"github.com/google/uuid"
)
type Service interface {
IncrementWorkViews(ctx context.Context, workID uuid.UUID) error
IncrementWorkLikes(ctx context.Context, workID uuid.UUID) error
IncrementWorkComments(ctx context.Context, workID uuid.UUID) error
IncrementWorkBookmarks(ctx context.Context, workID uuid.UUID) error
IncrementWorkShares(ctx context.Context, workID uuid.UUID) error
IncrementWorkTranslationCount(ctx context.Context, workID uuid.UUID) error
IncrementTranslationViews(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
DecrementWorkLikes(ctx context.Context, workID uuid.UUID) error
DecrementTranslationLikes(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationComments(ctx context.Context, translationID uuid.UUID) error
IncrementTranslationShares(ctx context.Context, translationID uuid.UUID) error
GetOrCreateWorkStats(ctx context.Context, workID uuid.UUID) (*domain.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uuid.UUID) (*domain.TranslationStats, error)
IncrementWorkViews(ctx context.Context, workID uint) error
IncrementWorkLikes(ctx context.Context, workID uint) error
IncrementWorkComments(ctx context.Context, workID uint) error
IncrementWorkBookmarks(ctx context.Context, workID uint) error
IncrementWorkShares(ctx context.Context, workID uint) error
IncrementWorkTranslationCount(ctx context.Context, workID uint) error
IncrementTranslationViews(ctx context.Context, translationID uint) error
IncrementTranslationLikes(ctx context.Context, translationID uint) error
DecrementWorkLikes(ctx context.Context, workID uint) error
DecrementTranslationLikes(ctx context.Context, translationID uint) error
IncrementTranslationComments(ctx context.Context, translationID uint) error
IncrementTranslationShares(ctx context.Context, translationID uint) error
GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error)
GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error)
UpdateWorkReadingTime(ctx context.Context, workID uuid.UUID) error
UpdateWorkComplexity(ctx context.Context, workID uuid.UUID) error
UpdateWorkSentiment(ctx context.Context, workID uuid.UUID) error
UpdateTranslationReadingTime(ctx context.Context, translationID uuid.UUID) error
UpdateTranslationSentiment(ctx context.Context, translationID uuid.UUID) error
UpdateWorkReadingTime(ctx context.Context, workID uint) error
UpdateWorkComplexity(ctx context.Context, workID uint) error
UpdateWorkSentiment(ctx context.Context, workID uint) error
UpdateTranslationReadingTime(ctx context.Context, translationID uint) error
UpdateTranslationSentiment(ctx context.Context, translationID uint) error
UpdateUserEngagement(ctx context.Context, userID uuid.UUID, eventType string) error
UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error
UpdateTrending(ctx context.Context) 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 {
@ -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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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")
defer span.End()
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)
}
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")
defer span.End()
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)
}
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")
defer span.End()
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)
}
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")
defer span.End()
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)
}
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")
defer span.End()
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)
}
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")
defer span.End()
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)
}
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")
defer span.End()
return s.repo.UpdateWorkStats(ctx, workID, stats)

View File

@ -3,13 +3,13 @@ package analytics_test
import (
"context"
"strings"
"testing"
"tercul/internal/app/analytics"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/jobs/linguistics"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"testing"
"github.com/stretchr/testify/suite"
)
@ -271,4 +271,4 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
// TestAnalyticsService runs the full test suite.
func TestAnalyticsService(t *testing.T) {
suite.Run(t, new(AnalyticsServiceTestSuite))
}
}

View File

@ -80,4 +80,4 @@ func NewApplication(
Search: searchSvc,
Analytics: analyticsSvc,
}
}
}

View File

@ -11,7 +11,6 @@ import (
"time"
"github.com/asaskevich/govalidator"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
@ -39,9 +38,9 @@ type RegisterInput struct {
// AuthResponse represents authentication response
type AuthResponse struct {
Token string `json:"token"`
Token string `json:"token"`
User *domain.User `json:"user"`
ExpiresAt time.Time `json:"expires_at"`
ExpiresAt time.Time `json:"expires_at"`
}
// AuthCommands contains the command handlers for authentication.
@ -179,7 +178,7 @@ func (c *AuthCommands) ResendVerificationEmail(ctx context.Context, email string
// ChangePasswordInput represents the input for changing a password.
type ChangePasswordInput struct {
UserID uuid.UUID
UserID uint
CurrentPassword string
NewPassword string
}

View File

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

View File

@ -10,12 +10,12 @@ import (
// mockUserRepository is a local mock for the UserRepository interface.
type mockUserRepository struct {
users map[uint]domain.User
users map[uint]domain.User
findByEmailFunc func(ctx context.Context, email string) (*domain.User, error)
findByUsernameFunc func(ctx context.Context, username string) (*domain.User, error)
createFunc func(ctx context.Context, user *domain.User) error
updateFunc func(ctx context.Context, user *domain.User) error
getByIDFunc func(ctx context.Context, id uint) (*domain.User, error)
createFunc func(ctx context.Context, user *domain.User) error
updateFunc func(ctx context.Context, user *domain.User) error
getByIDFunc func(ctx context.Context, id uint) (*domain.User, error)
}
func newMockUserRepository() *mockUserRepository {
@ -71,7 +71,7 @@ func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) erro
return errors.New("user not found")
}
func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id)
}
@ -85,8 +85,8 @@ func (m *mockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
return nil, nil
}
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil }
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil }
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
return nil, nil
}
@ -114,7 +114,7 @@ func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offse
return nil, nil
}
func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
}

View File

@ -3,8 +3,6 @@ package author
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
type UpdateAuthorInput struct {
ID uuid.UUID
ID uint
Name string
}
@ -55,6 +53,6 @@ func (c *AuthorCommands) UpdateAuthor(ctx context.Context, input UpdateAuthorInp
}
// 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)
}

View File

@ -3,8 +3,6 @@ package author
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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 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.
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)
}

View File

@ -4,8 +4,6 @@ import (
"context"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
)
// 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.
// 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)
if !ok {
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.
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)
if !ok {
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.
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 {
case "works":
// 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.
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)
if !ok {
return false, domain.ErrUnauthorized
@ -159,7 +157,7 @@ func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
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)
if !ok {
return false, domain.ErrUnauthorized
@ -213,7 +211,7 @@ func (s *Service) CanDeleteBook(ctx context.Context) (bool, error) {
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)
if !ok {
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.
// 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)
if !ok {
return false, domain.ErrUnauthorized
@ -251,4 +249,4 @@ func (s *Service) CanDeleteComment(ctx context.Context, userID uuid.UUID, commen
}
return false, domain.ErrForbidden
}
}

View File

@ -4,8 +4,6 @@ import (
"context"
"tercul/internal/app/authz"
"tercul/internal/domain"
"github.com/google/uuid"
)
// BookCommands contains the command handlers for the book aggregate.
@ -28,7 +26,7 @@ type CreateBookInput struct {
Description string
Language string
ISBN *string
AuthorIDs []uuid.UUID
AuthorIDs []uint
}
// 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.
type UpdateBookInput struct {
ID uuid.UUID
ID uint
Title *string
Description *string
Language *string
ISBN *string
AuthorIDs []uuid.UUID
AuthorIDs []uint
}
// UpdateBook updates an existing book.
@ -108,7 +106,7 @@ func (c *BookCommands) UpdateBook(ctx context.Context, input UpdateBookInput) (*
}
// 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)
if err != nil {
return err
@ -117,4 +115,4 @@ func (c *BookCommands) DeleteBook(ctx context.Context, id uuid.UUID) error {
return domain.ErrForbidden
}
return c.repo.Delete(ctx, id)
}
}

View File

@ -3,8 +3,6 @@ package book
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// BookQueries contains the query handlers for the book aggregate.
@ -18,11 +16,11 @@ func NewBookQueries(repo domain.BookRepository) *BookQueries {
}
// 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)
}
// Books retrieves a list of all books.
func (q *BookQueries) Books(ctx context.Context) ([]domain.Book, error) {
return q.repo.ListAll(ctx)
}
}

View File

@ -17,4 +17,4 @@ func NewService(repo domain.BookRepository, authzSvc *authz.Service) *Service {
Commands: NewBookCommands(repo, authzSvc),
Queries: NewBookQueries(repo),
}
}
}

View File

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

View File

@ -3,8 +3,6 @@ package bookmark
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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)
}
// 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)
}

View File

@ -3,8 +3,6 @@ package category
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// CategoryCommands contains the command handlers for the category aggregate.
@ -21,7 +19,7 @@ func NewCategoryCommands(repo domain.CategoryRepository) *CategoryCommands {
type CreateCategoryInput struct {
Name string
Description string
ParentID *uuid.UUID
ParentID *uint
}
// 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.
type UpdateCategoryInput struct {
ID uuid.UUID
ID uint
Name string
Description string
ParentID *uuid.UUID
ParentID *uint
}
// UpdateCategory updates an existing category.
@ -63,6 +61,6 @@ func (c *CategoryCommands) UpdateCategory(ctx context.Context, input UpdateCateg
}
// 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)
}

View File

@ -3,8 +3,6 @@ package category
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
@ -28,12 +26,12 @@ func (q *CategoryQueries) CategoryByName(ctx context.Context, name string) (*dom
}
// CategoriesByWorkID returns all categories for a work.
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Category, error) {
func (q *CategoryQueries) CategoriesByWorkID(ctx context.Context, workID uint) ([]domain.Category, error) {
return q.repo.ListByWorkID(ctx, workID)
}
// 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)
}

View File

@ -4,8 +4,6 @@ import (
"context"
"fmt"
"tercul/internal/domain"
"github.com/google/uuid"
)
// CollectionCommands contains the command handlers for the collection aggregate.
@ -22,7 +20,7 @@ func NewCollectionCommands(repo domain.CollectionRepository) *CollectionCommands
type CreateCollectionInput struct {
Name string
Description string
UserID uuid.UUID
UserID uint
IsPublic bool
CoverImageURL string
}
@ -45,12 +43,12 @@ func (c *CollectionCommands) CreateCollection(ctx context.Context, input CreateC
// UpdateCollectionInput represents the input for updating an existing collection.
type UpdateCollectionInput struct {
ID uuid.UUID
ID uint
Name string
Description string
IsPublic bool
CoverImageURL string
UserID uuid.UUID
UserID uint
}
// UpdateCollection updates an existing collection.
@ -60,7 +58,7 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
return nil, err
}
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.Description = input.Description
@ -74,22 +72,22 @@ func (c *CollectionCommands) UpdateCollection(ctx context.Context, input UpdateC
}
// 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)
if err != nil {
return err
}
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)
}
// AddWorkToCollectionInput represents the input for adding a work to a collection.
type AddWorkToCollectionInput struct {
CollectionID uuid.UUID
WorkID uuid.UUID
UserID uuid.UUID
CollectionID uint
WorkID uint
UserID uint
}
// AddWorkToCollection adds a work to a collection.
@ -99,16 +97,16 @@ func (c *CollectionCommands) AddWorkToCollection(ctx context.Context, input AddW
return err
}
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)
}
// RemoveWorkFromCollectionInput represents the input for removing a work from a collection.
type RemoveWorkFromCollectionInput struct {
CollectionID uuid.UUID
WorkID uuid.UUID
UserID uuid.UUID
CollectionID uint
WorkID uint
UserID uint
}
// RemoveWorkFromCollection removes a work from a collection.
@ -118,7 +116,7 @@ func (c *CollectionCommands) RemoveWorkFromCollection(ctx context.Context, input
return err
}
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)
}

View File

@ -3,8 +3,6 @@ package collection
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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)
}
@ -33,7 +31,7 @@ func (q *CollectionQueries) PublicCollections(ctx context.Context) ([]domain.Col
}
// CollectionsByWorkID returns all collections for a work.
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Collection, error) {
func (q *CollectionQueries) CollectionsByWorkID(ctx context.Context, workID uint) ([]domain.Collection, error) {
return q.repo.ListByWorkID(ctx, workID)
}

View File

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

View File

@ -3,8 +3,6 @@ package comment
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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)
}
// 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)
}
// 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)
}
// 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)
}

View File

@ -5,8 +5,6 @@ import (
"tercul/internal/app/authz"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
)
// Commands contains the command handlers for the contribution aggregate.
@ -27,8 +25,8 @@ func NewCommands(repo domain.ContributionRepository, authzSvc *authz.Service) *C
type CreateContributionInput struct {
Name string
Status string
WorkID *uuid.UUID
TranslationID *uuid.UUID
WorkID *uint
TranslationID *uint
}
// 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.
type UpdateContributionInput struct {
ID uuid.UUID
UserID uuid.UUID
ID uint
UserID uint
Name *string
Status *string
}
@ -91,7 +89,7 @@ func (c *Commands) UpdateContribution(ctx context.Context, input UpdateContribut
}
// 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)
if err != nil {
return err
@ -107,7 +105,7 @@ func (c *Commands) DeleteContribution(ctx context.Context, contributionID uuid.U
// ReviewContributionInput represents the input for reviewing a contribution.
type ReviewContributionInput struct {
ID uuid.UUID
ID uint
Status string
Feedback *string
}
@ -137,4 +135,4 @@ func (c *Commands) ReviewContribution(ctx context.Context, input ReviewContribut
}
return contribution, nil
}
}

View File

@ -11,4 +11,4 @@ func NewService(commands *Commands) *Service {
return &Service{
Commands: commands,
}
}
}

View File

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

View File

@ -4,10 +4,10 @@ package copyright_test
import (
"context"
"testing"
"tercul/internal/app/copyright"
"tercul/internal/domain"
"tercul/internal/testutil"
"testing"
"github.com/stretchr/testify/suite"
)

View File

@ -7,24 +7,24 @@ import (
)
type mockCopyrightRepository struct {
createFunc func(ctx context.Context, copyright *domain.Copyright) error
updateFunc func(ctx context.Context, copyright *domain.Copyright) error
deleteFunc func(ctx context.Context, id uint) error
addCopyrightToWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
removeCopyrightFromWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
addCopyrightToAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
removeCopyrightFromAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
addCopyrightToBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
removeCopyrightFromBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
addCopyrightToPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error
createFunc func(ctx context.Context, copyright *domain.Copyright) error
updateFunc func(ctx context.Context, copyright *domain.Copyright) error
deleteFunc func(ctx context.Context, id uint) error
addCopyrightToWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
removeCopyrightFromWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error
addCopyrightToAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
removeCopyrightFromAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error
addCopyrightToBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
removeCopyrightFromBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error
addCopyrightToPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error
removeCopyrightFromPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error
addCopyrightToSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
removeCopyrightFromSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
addTranslationFunc func(ctx context.Context, translation *domain.CopyrightTranslation) error
getByIDFunc func(ctx context.Context, id uint) (*domain.Copyright, error)
listAllFunc func(ctx context.Context) ([]domain.Copyright, error)
getTranslationsFunc func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error)
getTranslationByLanguageFunc func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error)
addCopyrightToSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
removeCopyrightFromSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error
addTranslationFunc func(ctx context.Context, translation *domain.CopyrightTranslation) error
getByIDFunc func(ctx context.Context, id uint) (*domain.Copyright, error)
listAllFunc func(ctx context.Context) ([]domain.Copyright, error)
getTranslationsFunc func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error)
getTranslationByLanguageFunc func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error)
}
func (m *mockCopyrightRepository) Create(ctx context.Context, copyright *domain.Copyright) error {
@ -111,7 +111,7 @@ func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translatio
}
return nil
}
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) {
if m.getByIDFunc != nil {
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) {
return nil, nil
}
func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) {
return false, nil
}
func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
func (m *mockCopyrightRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
return nil
@ -184,48 +182,40 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
}
return nil, nil
}
type mockAuthorRepository struct {
domain.AuthorRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
}
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockBookRepository struct {
domain.BookRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
}
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockPublisherRepository struct {
domain.PublisherRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
}
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockSourceRepository struct {
domain.SourceRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
}
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)

View File

@ -5,8 +5,6 @@ import (
"errors"
"tercul/internal/domain"
"tercul/internal/platform/log"
"github.com/google/uuid"
)
// 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.
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uuid.UUID) (*domain.Copyright, error) {
if id == uuid.Nil {
func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*domain.Copyright, error) {
if id == 0 {
return nil, errors.New("invalid copyright 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.
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")
workRecord, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
@ -52,7 +50,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uuid
}
// 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")
author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
@ -62,7 +60,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID
}
// GetCopyrightsForBook gets all copyrights for a specific book.
func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID 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")
book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
@ -72,7 +70,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uuid
}
// 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")
publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
@ -82,7 +80,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis
}
// GetCopyrightsForSource gets all copyrights for a specific source.
func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID 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")
source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}})
if err != nil {
@ -92,8 +90,8 @@ func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID
}
// GetTranslations gets all translations for a copyright.
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uuid.UUID) ([]domain.CopyrightTranslation, error) {
if copyrightID == uuid.Nil {
func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) {
if copyrightID == 0 {
return nil, errors.New("invalid copyright ID")
}
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.
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uuid.UUID, languageCode string) (*domain.CopyrightTranslation, error) {
if copyrightID == uuid.Nil {
func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) {
if copyrightID == 0 {
return nil, errors.New("invalid copyright ID")
}
if languageCode == "" {

View File

@ -6,8 +6,6 @@ import (
"tercul/internal/app/analytics"
"tercul/internal/domain"
"tercul/internal/platform/log"
"github.com/google/uuid"
)
// LikeCommands contains the command handlers for the like aggregate.
@ -26,10 +24,10 @@ func NewLikeCommands(repo domain.LikeRepository, analyticsSvc analytics.Service)
// CreateLikeInput represents the input for creating a new like.
type CreateLikeInput struct {
UserID uuid.UUID
WorkID *uuid.UUID
TranslationID *uuid.UUID
CommentID *uuid.UUID
UserID uint
WorkID *uint
TranslationID *uint
CommentID *uint
}
// 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.
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.
like, err := c.repo.GetByID(ctx, id)
if err != nil {

View File

@ -3,8 +3,6 @@ package like
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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)
}
// 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)
}
// 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)
}
// 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)
}

View File

@ -10,4 +10,4 @@ type LocalizationCommands struct {
// NewLocalizationCommands creates a new LocalizationCommands handler.
func NewLocalizationCommands(repo domain.LocalizationRepository) *LocalizationCommands {
return &LocalizationCommands{repo: repo}
}
}

View File

@ -3,8 +3,6 @@ package localization
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
// 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)
}
}

View File

@ -14,4 +14,4 @@ func NewService(repo domain.LocalizationRepository) *Service {
Commands: NewLocalizationCommands(repo),
Queries: NewLocalizationQueries(repo),
}
}
}

View File

@ -90,4 +90,4 @@ func TestLocalizationService_GetAuthorBiography(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, expectedBiography, biography)
repo.AssertExpectations(t)
}
}

View File

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

View File

@ -4,10 +4,10 @@ package monetization_test
import (
"context"
"testing"
"tercul/internal/app/monetization"
"tercul/internal/domain"
"tercul/internal/testutil"
"testing"
"github.com/stretchr/testify/suite"
)

View File

@ -7,21 +7,21 @@ import (
type mockMonetizationRepository struct {
domain.MonetizationRepository
addMonetizationToWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
removeMonetizationFromWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
addMonetizationToAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
removeMonetizationFromAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
addMonetizationToBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
removeMonetizationFromBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
addMonetizationToPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error
addMonetizationToWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
removeMonetizationFromWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error
addMonetizationToAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
removeMonetizationFromAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error
addMonetizationToBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
removeMonetizationFromBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error
addMonetizationToPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error
removeMonetizationFromPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error
addMonetizationToSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
removeMonetizationFromSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
getByIDFunc func(ctx context.Context, id uint) (*domain.Monetization, error)
listAllFunc func(ctx context.Context) ([]domain.Monetization, error)
addMonetizationToSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
removeMonetizationFromSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error
getByIDFunc func(ctx context.Context, id uint) (*domain.Monetization, error)
listAllFunc func(ctx context.Context) ([]domain.Monetization, error)
}
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Monetization, error) {
func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uint) (*domain.Monetization, error) {
if m.getByIDFunc != nil {
return m.getByIDFunc(ctx, id)
}
@ -107,48 +107,40 @@ func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, op
}
return nil, nil
}
type mockAuthorRepository struct {
domain.AuthorRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error)
}
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockBookRepository struct {
domain.BookRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error)
}
func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockPublisherRepository struct {
domain.PublisherRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error)
}
func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)
}
return nil, nil
}
type mockSourceRepository struct {
domain.SourceRepository
getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error)
}
func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) {
if m.getByIDWithOptionsFunc != nil {
return m.getByIDWithOptionsFunc(ctx, id, options)

View File

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

View File

@ -52,4 +52,4 @@ func (s *service) IndexWork(ctx context.Context, work domain.Work) error {
}
logger.Info("Successfully indexed work")
return nil
}
}

View File

@ -83,7 +83,7 @@ func TestSearchService_Search(t *testing.T) {
Query: testQuery,
Filters: testFilters,
Limit: 10,
Offset: 0,
Offset: 0,
}
weaviateWrapper.On("Search", ctx, params).Return(expectedResults, nil)

View File

@ -3,8 +3,6 @@ package tag
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
type UpdateTagInput struct {
ID uuid.UUID
ID uint
Name string
Description string
}
@ -59,6 +57,6 @@ func (c *TagCommands) UpdateTag(ctx context.Context, input UpdateTagInput) (*dom
}
// DeleteTag deletes a tag by ID.
func (c *TagCommands) DeleteTag(ctx context.Context, id uuid.UUID) error {
func (c *TagCommands) DeleteTag(ctx context.Context, id uint) error {
return c.repo.Delete(ctx, id)
}

View File

@ -3,8 +3,6 @@ package tag
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
@ -28,7 +26,7 @@ func (q *TagQueries) TagByName(ctx context.Context, name string) (*domain.Tag, e
}
// TagsByWorkID returns all tags for a work.
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uuid.UUID) ([]domain.Tag, error) {
func (q *TagQueries) TagsByWorkID(ctx context.Context, workID uint) ([]domain.Tag, error) {
return q.repo.ListByWorkID(ctx, workID)
}

View File

@ -7,7 +7,6 @@ import (
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
@ -35,9 +34,9 @@ type CreateOrUpdateTranslationInput struct {
Description string
Language string
Status domain.TranslationStatus
TranslatableID uuid.UUID
TranslatableID uint
TranslatableType string
TranslatorID *uuid.UUID
TranslatorID *uint
IsOriginalLanguage bool
}
@ -50,8 +49,8 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
if input.Language == "" {
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
}
if input.TranslatableID == uuid.Nil {
return nil, fmt.Errorf("%w: translatable ID cannot be nil", domain.ErrValidation)
if input.TranslatableID == 0 {
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation)
}
if input.TranslatableType == "" {
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
}
var translatorID uuid.UUID
var translatorID uint
if input.TranslatorID != nil {
translatorID = *input.TranslatorID
} else {
@ -98,7 +97,7 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
}
// 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")
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)
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)
if args.Get(0) == nil {
return nil, args.Error(1)
@ -281,4 +281,4 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
func TestTranslationCommands(t *testing.T) {
suite.Run(t, new(TranslationCommandsTestSuite))
}
}

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
@ -24,7 +23,7 @@ func NewTranslationQueries(repo domain.TranslationRepository) *TranslationQuerie
}
// 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")
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.
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")
defer span.End()
return q.repo.ListByWorkID(ctx, workID)
}
// TranslationsByEntity returns all translations for an entity.
func (q *TranslationQueries) TranslationsByEntity(ctx context.Context, entityType string, entityID 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")
defer span.End()
return q.repo.ListByEntity(ctx, entityType, entityID)
}
// TranslationsByTranslatorID returns all translations for a translator.
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uuid.UUID) ([]domain.Translation, error) {
func (q *TranslationQueries) TranslationsByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
ctx, span := q.tracer.Start(ctx, "TranslationsByTranslatorID")
defer span.End()
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.
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")
defer span.End()

View File

@ -76,4 +76,4 @@ func (s *TranslationQueriesSuite) TestListTranslations_Success() {
TotalPages: 1,
}
assert.Equal(s.T(), expectedDTOs, paginatedDTOs)
}
}

View File

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

View File

@ -311,4 +311,4 @@ func (s *UserCommandsSuite) TestDeleteUser_NotFound() {
assert.Error(s.T(), err)
assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
s.repo.AssertExpectations(s.T())
}
}

View File

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

View File

@ -3,8 +3,6 @@ package user
import (
"context"
"tercul/internal/domain"
"github.com/google/uuid"
)
// 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.
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)
}
@ -44,6 +42,6 @@ func (q *UserQueries) Users(ctx context.Context) ([]domain.User, error) {
}
// UserProfile returns a user profile by user ID.
func (q *UserQueries) UserProfile(ctx context.Context, userID uuid.UUID) (*domain.UserProfile, error) {
func (q *UserQueries) UserProfile(ctx context.Context, userID uint) (*domain.UserProfile, error) {
return q.profileRepo.GetByUserID(ctx, userID)
}

View File

@ -18,7 +18,7 @@ type mockUserProfileRepository struct {
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)
if args.Get(0) == nil {
return nil, args.Error(1)
@ -36,7 +36,7 @@ func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB,
return args.Error(0)
}
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.UserProfile, error) {
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
@ -275,4 +275,4 @@ func TestNewService(t *testing.T) {
assert.NotNil(t, service)
assert.NotNil(t, service.Commands)
assert.NotNil(t, service.Queries)
}
}

View File

@ -14,8 +14,6 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"gorm.io/gorm"
"github.com/google/uuid"
)
// WorkCommands contains the command handlers for the work aggregate.
@ -110,8 +108,8 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
if work == nil {
return fmt.Errorf("%w: work cannot be nil", domain.ErrValidation)
}
if work.ID == uuid.Nil {
return fmt.Errorf("%w: work ID cannot be nil", domain.ErrValidation)
if work.ID == 0 {
return fmt.Errorf("%w: work ID cannot be zero", domain.ErrValidation)
}
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.
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")
defer span.End()
if id == uuid.Nil {
if id == 0 {
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.
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")
defer span.End()
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.
//
//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")
defer span.End()
if sourceID == targetID {
@ -334,7 +331,7 @@ func (c *WorkCommands) MergeWork(ctx context.Context, sourceID, targetID uuid.UU
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
err := tx.Where("work_id = ?", sourceWorkID).First(&sourceStats).Error
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 target has no stats, create a new stats record for it.
newStats := sourceStats
newStats.ID = uuid.Nil
newStats.ID = 0
newStats.WorkID = targetWorkID
if err = tx.Create(&newStats).Error; err != nil {
return fmt.Errorf("failed to create new target stats: %w", err)
@ -376,4 +373,4 @@ func mergeWorkStats(tx *gorm.DB, sourceWorkID, targetWorkID uuid.UUID) error {
}
return nil
}
}

View File

@ -1,10 +1,8 @@
package work
import "github.com/google/uuid"
// WorkDTO is a read model for a work, containing only the data needed for API responses.
type WorkDTO struct {
ID uuid.UUID
ID uint
Title 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)
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)
if args.Get(0) == nil {
return nil, args.Error(1)

View File

@ -1,4 +1,4 @@
package work
// This file is intentionally left empty.
// Mocks are defined in main_test.go to avoid redeclaration errors.
// Mocks are defined in main_test.go to avoid redeclaration errors.

View File

@ -7,8 +7,6 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"github.com/google/uuid"
)
// 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.
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")
defer span.End()
if id == uuid.Nil {
if id == 0 {
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.
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")
defer span.End()
if id == uuid.Nil {
if id == 0 {
return nil, errors.New("invalid work 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.
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")
defer span.End()
if authorID == uuid.Nil {
if authorID == 0 {
return nil, errors.New("invalid author ID")
}
return q.repo.FindByAuthor(ctx, authorID)
}
// FindWorksByCategory finds works by category ID.
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uuid.UUID) ([]domain.Work, error) {
func (q *WorkQueries) FindWorksByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
ctx, span := q.tracer.Start(ctx, "FindWorksByCategory")
defer span.End()
if categoryID == uuid.Nil {
if categoryID == 0 {
return nil, errors.New("invalid category ID")
}
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.
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")
defer span.End()
if collectionID == uuid.Nil {
if collectionID == 0 {
return nil, errors.New("invalid collection ID")
}
return q.repo.ListByCollectionID(ctx, collectionID)

View File

@ -159,4 +159,4 @@ func (s *WorkQueriesSuite) TestFindWorksByLanguage_Empty() {
w, err := s.queries.FindWorksByLanguage(context.Background(), "", 1, 10)
assert.Error(s.T(), err)
assert.Nil(s.T(), w)
}
}

Some files were not shown because too many files have changed in this diff Show More