fix(dev): We did it funky bun dev workarounds are no more

This commit is contained in:
2026-02-20 21:45:15 +01:00
parent d1c4744231
commit 3bb8b202b0
29 changed files with 797 additions and 125 deletions

589
PERFORMANCE_IDEAS.md Normal file
View File

@@ -0,0 +1,589 @@
# Performance Optimization Ideas for LfK Backend
This document outlines potential performance improvements for the LfK backend API, organized by impact and complexity.
---
## ✅ Already Implemented
### 1. Bun Runtime Migration
**Status**: Complete
**Impact**: 8-15% latency improvement
**Details**: Migrated from Node.js to Bun runtime, achieving:
- Parallel throughput: +8.3% (306 → 331 scans/sec)
- Parallel p50 latency: -9.5% (21ms → 19ms)
### 2. NATS KV Cache for Scan Intake
**Status**: Complete (based on code analysis)
**Impact**: Significant reduction in DB reads for hot path
**Details**: `ScanController.stationIntake()` uses NATS JetStream KV store to cache:
- Station tokens (1-hour TTL)
- Card→Runner mappings (1-hour TTL)
- Runner state (no TTL, CAS-based updates)
- Eliminates DB reads on cache hits
- Prevents race conditions via compare-and-swap (CAS)
---
## 🚀 High Impact, Low-Medium Complexity
### 3. Add Database Indexes
**Priority**: HIGH
**Complexity**: Low
**Estimated Impact**: 30-70% query time reduction
**Problem**: TypeORM synchronize() doesn't automatically create indexes on foreign keys or commonly queried fields.
**Observations**:
- Heavy use of `find()` with complex nested relations (e.g., `['runner', 'track', 'runner.scans', 'runner.group', 'runner.scans.track']`)
- No explicit `@Index()` decorators found in entity files
- Frequent filtering by foreign keys (runner_id, track_id, station_id, card_id)
**Recommended Indexes**:
```typescript
// src/models/entities/Scan.ts
@Index(['runner', 'timestamp']) // For runner scan history queries
@Index(['station', 'timestamp']) // For station-based queries
@Index(['card']) // For card lookup
// src/models/entities/Runner.ts
@Index(['email']) // For authentication/lookup
@Index(['group']) // For group-based queries
// src/models/entities/RunnerCard.ts
@Index(['runner']) // For card→runner lookups
@Index(['code']) // For barcode scans
// src/models/entities/Donation.ts
@Index(['runner']) // For runner donations
@Index(['donor']) // For donor contributions
```
**Implementation Steps**:
1. Audit all entities and add `@Index()` decorators
2. Test query performance with `EXPLAIN` before/after
3. Monitor index usage with database tools
4. Consider composite indexes for frequently combined filters
**Expected Results**:
- 50-70% faster JOIN operations
- 30-50% faster foreign key lookups
- Reduced database CPU usage
---
### 4. Implement Query Result Caching
**Priority**: HIGH
**Complexity**: Medium
**Estimated Impact**: 50-90% latency reduction for repeated queries
**Problem**: Stats endpoints and frequently accessed data (org totals, team rankings, runner lists) are recalculated on every request.
**Observations**:
- `StatsController` methods load entire datasets with deep relations:
- `getRunnerStats()`: loads all runners with scans, groups, donations
- `getTeamStats()`: loads all teams with nested runner data
- `getOrgStats()`: loads all orgs with teams, runners, scans
- Many `find()` calls without any caching layer
- Data changes infrequently (only during scan intake)
**Solution Options**:
**Option A: NATS KV Cache (Recommended)**
```typescript
// src/nats/StatsKV.ts
export async function getOrgStatsCache(): Promise<ResponseOrgStats[] | null> {
const kv = await NatsClient.getKV('stats_cache', { ttl: 60 * 1000 }); // 60s TTL
const entry = await kv.get('org_stats');
return entry ? JSON.parse(entry.string()) : null;
}
export async function setOrgStatsCache(stats: ResponseOrgStats[]): Promise<void> {
const kv = await NatsClient.getKV('stats_cache', { ttl: 60 * 1000 });
await kv.put('org_stats', JSON.stringify(stats));
}
// Invalidate on scan creation
// src/controllers/ScanController.ts (after line 173)
await invalidateStatsCache(); // Clear stats on new scan
```
**Option B: In-Memory Cache with TTL**
```typescript
// src/cache/MemoryCache.ts
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 60 }); // 60s TTL
export function getCached<T>(key: string): T | undefined {
return cache.get<T>(key);
}
export function setCached<T>(key: string, value: T, ttl?: number): void {
cache.set(key, value, ttl);
}
export function invalidatePattern(pattern: string): void {
const keys = cache.keys().filter(k => k.includes(pattern));
cache.del(keys);
}
```
**Option C: Redis Cache** (if Redis is already in stack)
**Recommended Cache Strategy**:
- **TTL**: 30-60 seconds for stats endpoints
- **Invalidation**: On scan creation, runner updates, donation changes
- **Keys**: `stats:org`, `stats:team:${id}`, `stats:runner:${id}`
- **Warm on startup**: Pre-populate cache for critical endpoints
**Expected Results**:
- 80-90% latency reduction for stats endpoints (from ~500ms to ~50ms)
- 70-80% reduction in database load
- Improved user experience for dashboards and leaderboards
---
### 5. Lazy Load Relations & DTOs
**Priority**: HIGH
**Complexity**: Medium
**Estimated Impact**: 40-60% query time reduction
**Problem**: Many queries eagerly load deeply nested relations that aren't always needed.
**Observations**:
```typescript
// Current: Loads everything
scan = await this.scanRepository.findOne(
{ id: scan.id },
{ relations: ['runner', 'track', 'runner.scans', 'runner.group',
'runner.scans.track', 'card', 'station'] }
);
```
**Solutions**:
**A. Create Lightweight Response DTOs**
```typescript
// src/models/responses/ResponseScanLight.ts
export class ResponseScanLight {
@IsInt() id: number;
@IsInt() distance: number;
@IsInt() timestamp: number;
@IsBoolean() valid: boolean;
// Omit nested runner.scans, runner.group, etc.
}
// Use for list views
@Get()
@ResponseSchema(ResponseScanLight, { isArray: true })
async getAll() {
const scans = await this.scanRepository.find({
relations: ['runner', 'track'] // Minimal relations
});
return scans.map(s => new ResponseScanLight(s));
}
// Keep detailed DTO for single-item views
@Get('/:id')
@ResponseSchema(ResponseScan) // Full details
async getOne(@Param('id') id: number) { ... }
```
**B. Use Query Builder for Selective Loading**
```typescript
// Instead of loading all scans with runner relations:
const scans = await this.scanRepository
.createQueryBuilder('scan')
.leftJoinAndSelect('scan.runner', 'runner')
.leftJoinAndSelect('scan.track', 'track')
.select([
'scan.id', 'scan.distance', 'scan.timestamp', 'scan.valid',
'runner.id', 'runner.firstname', 'runner.lastname',
'track.id', 'track.name'
])
.where('scan.id = :id', { id })
.getOne();
```
**C. Implement GraphQL-style Field Selection**
```typescript
@Get()
async getAll(@QueryParam('fields') fields?: string) {
const relations = [];
if (fields?.includes('runner')) relations.push('runner');
if (fields?.includes('track')) relations.push('track');
return this.scanRepository.find({ relations });
}
```
**Expected Results**:
- 40-60% faster list queries
- 50-70% reduction in data transfer size
- Reduced JOIN complexity and memory usage
---
### 6. Pagination Optimization
**Priority**: MEDIUM
**Complexity**: Low
**Estimated Impact**: 20-40% improvement for large result sets
**Problem**: Current pagination uses `skip/take` which becomes slow with large offsets.
**Current Implementation**:
```typescript
// Inefficient for large page numbers (e.g., page=1000)
scans = await this.scanRepository.find({
skip: page * page_size, // Scans 100,000 rows to skip them
take: page_size
});
```
**Solutions**:
**A. Cursor-Based Pagination (Recommended)**
```typescript
@Get()
async getAll(
@QueryParam('cursor') cursor?: number, // Last ID from previous page
@QueryParam('page_size') page_size: number = 100
) {
const query = this.scanRepository.createQueryBuilder('scan')
.orderBy('scan.id', 'ASC')
.take(page_size + 1); // Get 1 extra to determine if more pages exist
if (cursor) {
query.where('scan.id > :cursor', { cursor });
}
const scans = await query.getMany();
const hasMore = scans.length > page_size;
const results = scans.slice(0, page_size);
const nextCursor = hasMore ? results[results.length - 1].id : null;
return {
data: results.map(s => s.toResponse()),
pagination: { nextCursor, hasMore }
};
}
```
**B. Add Total Count Caching**
```typescript
// Cache total counts to avoid expensive COUNT(*) queries
const totalCache = new Map<string, { count: number, expires: number }>();
async function getTotalCount(repo: Repository<any>): Promise<number> {
const cacheKey = repo.metadata.tableName;
const cached = totalCache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.count;
}
const count = await repo.count();
totalCache.set(cacheKey, { count, expires: Date.now() + 60000 }); // 60s TTL
return count;
}
```
**Expected Results**:
- 60-80% faster pagination for large page numbers
- Consistent query performance regardless of offset
- Better mobile app experience with cursor-based loading
---
## 🔧 Medium Impact, Medium Complexity
### 7. Database Connection Pooling Optimization
**Priority**: MEDIUM
**Complexity**: Medium
**Estimated Impact**: 10-20% improvement under load
**Current**: Default TypeORM connection pooling (likely 10 connections)
**Recommendations**:
```typescript
// ormconfig.js
module.exports = {
// ... existing config
extra: {
// PostgreSQL specific
max: 20, // Max pool size (adjust based on load)
min: 5, // Min pool size
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000,
// MySQL specific
connectionLimit: 20,
waitForConnections: true,
queueLimit: 0
},
// Enable query logging in dev to identify slow queries
logging: process.env.NODE_ENV !== 'production' ? ['query', 'error'] : ['error'],
maxQueryExecutionTime: 1000, // Log queries taking >1s
};
```
**Monitor**:
- Connection pool exhaustion
- Query execution times
- Active connection count
---
### 8. Bulk Operations for Import
**Priority**: MEDIUM
**Complexity**: Medium
**Estimated Impact**: 50-80% faster imports
**Problem**: Import endpoints likely save entities one-by-one in loops.
**Solution**:
```typescript
// Instead of:
for (const runnerData of importData) {
const runner = await createRunner.toEntity();
await this.runnerRepository.save(runner); // N queries
}
// Use bulk insert:
const runners = await Promise.all(
importData.map(data => createRunner.toEntity())
);
await this.runnerRepository.save(runners); // 1 query
// Or use raw query for massive imports:
await getConnection()
.createQueryBuilder()
.insert()
.into(Runner)
.values(runners)
.execute();
```
---
### 9. Response Compression
**Priority**: MEDIUM
**Complexity**: Low
**Estimated Impact**: 60-80% reduction in response size
**Implementation**:
```typescript
// src/app.ts
import compression from 'compression';
const app = createExpressServer({ ... });
app.use(compression({
level: 6, // Compression level (1-9)
threshold: 1024, // Only compress responses >1KB
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
}
}));
```
**Benefits**:
- 70-80% smaller JSON responses
- Faster transfer times on slow networks
- Reduced bandwidth costs
**Dependencies**: `bun add compression @types/compression`
---
## 🎯 Lower Priority / High Complexity
### 10. Implement Read Replicas
**Priority**: LOW (requires infrastructure)
**Complexity**: High
**Estimated Impact**: 30-50% read query improvement
**When to Consider**:
- Database CPU consistently >70%
- Read-heavy workload (already true for stats endpoints)
- Running PostgreSQL/MySQL in production
**Implementation**:
```typescript
// ormconfig.js
module.exports = {
type: 'postgres',
replication: {
master: {
host: process.env.DB_WRITE_HOST,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
slaves: [
{
host: process.env.DB_READ_REPLICA_1,
port: 5432,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
}
]
}
};
```
---
### 11. Move to Serverless/Edge Functions
**Priority**: LOW (architectural change)
**Complexity**: Very High
**Estimated Impact**: Variable (depends on workload)
**Considerations**:
- Good for: Infrequent workloads, global distribution
- Bad for: High-frequency scan intake (cold starts)
- May conflict with TypeORM's connection model
---
### 12. GraphQL API Layer
**Priority**: LOW (major refactor)
**Complexity**: Very High
**Estimated Impact**: 30-50% for complex queries
**Benefits**:
- Clients request only needed fields
- Single request for complex nested data
- Better mobile app performance
**Trade-offs**:
- Complete rewrite of controller layer
- Learning curve for frontend teams
- More complex caching strategy
---
## 📊 Recommended Implementation Order
**Phase 1: Quick Wins** (1-2 weeks)
1. Add database indexes → Controllers still work, immediate improvement
2. Enable response compression → One-line change in `app.ts`
3. Implement cursor-based pagination → Better mobile UX
**Phase 2: Caching Layer** (2-3 weeks)
4. Add NATS KV cache for stats endpoints
5. Create lightweight response DTOs for list views
6. Cache total counts for pagination
**Phase 3: Query Optimization** (2-3 weeks)
7. Refactor controllers to use query builder with selective loading
8. Optimize database connection pooling
9. Implement bulk operations for imports
**Phase 4: Infrastructure** (ongoing)
10. Monitor query performance and add more indexes as needed
11. Consider read replicas when database becomes bottleneck
---
## 🔍 Performance Monitoring Recommendations
### Add Metrics Endpoint
```typescript
// src/controllers/MetricsController.ts
import { performance } from 'perf_hooks';
const requestMetrics = {
totalRequests: 0,
avgLatency: 0,
p95Latency: 0,
dbQueryCount: 0,
cacheHitRate: 0,
};
@JsonController('/metrics')
export class MetricsController {
@Get()
@Authorized('ADMIN') // Restrict to admins
async getMetrics() {
return requestMetrics;
}
}
```
### Enable Query Logging
```typescript
// ormconfig.js
logging: ['query', 'error'],
maxQueryExecutionTime: 1000, // Warn on queries >1s
```
### Add Request Timing Middleware
```typescript
// src/middlewares/TimingMiddleware.ts
export function timingMiddleware(req: Request, res: Response, next: NextFunction) {
const start = performance.now();
res.on('finish', () => {
const duration = performance.now() - start;
if (duration > 1000) {
consola.warn(`Slow request: ${req.method} ${req.path} took ${duration}ms`);
}
});
next();
}
```
---
## 📝 Performance Testing Commands
```bash
# Run baseline benchmark
bun run benchmark > baseline.txt
# After implementing changes, compare
bun run benchmark > optimized.txt
diff baseline.txt optimized.txt
# Load testing with artillery (if added)
artillery quick --count 100 --num 10 http://localhost:4010/api/runners
# Database query profiling (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM scan WHERE runner_id = 1;
# Check database indexes
SELECT * FROM pg_indexes WHERE tablename = 'scan';
# Monitor NATS cache hit rate
# (Add custom logging in NATS KV functions)
```
---
## 🎓 Key Principles
1. **Measure first**: Always benchmark before and after changes
2. **Start with indexes**: Biggest impact, lowest risk
3. **Cache strategically**: Stats endpoints benefit most
4. **Lazy load by default**: Only eager load when absolutely needed
5. **Monitor in production**: Use APM tools (New Relic, DataDog, etc.)
---
## 📚 Additional Resources
- [TypeORM Performance Tips](https://typeorm.io/performance)
- [PostgreSQL Index Best Practices](https://www.postgresql.org/docs/current/indexes.html)
- [Bun Performance Benchmarks](https://bun.sh/docs/runtime/performance)
- [NATS JetStream KV Guide](https://docs.nats.io/nats-concepts/jetstream/key-value-store)
---
**Last Updated**: 2026-02-20
**Status**: Ready for review and prioritization

View File

@@ -19,6 +19,7 @@
"csvtojson": "2.0.10",
"dotenv": "8.2.0",
"express": "4.17.1",
"glob": "^13.0.6",
"jsonwebtoken": "8.5.1",
"libphonenumber-js": "1.9.9",
"mysql": "2.18.1",
@@ -816,7 +817,7 @@
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@5.1.1", "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ=="],
@@ -1146,7 +1147,7 @@
"lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"lunr": ["lunr@2.3.9", "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="],
@@ -1198,7 +1199,7 @@
"minimist": ["minimist@1.2.5", "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz", {}, "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
@@ -1358,6 +1359,8 @@
"path-parse": ["path-parse@1.0.6", "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz", {}, "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"path-to-regexp": ["path-to-regexp@0.1.7", "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz", {}, "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
@@ -1898,6 +1901,8 @@
"@jest/pattern/jest-regex-util": ["jest-regex-util@30.0.1", "", {}, "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA=="],
"@jest/reporters/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"@jest/types/@types/node": ["@types/node@14.14.14", "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz", {}, "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@5.5.0", "", { "dependencies": { "@types/node": ">= 8" } }, "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ=="],
@@ -1946,6 +1951,8 @@
"busboy/readable-stream": ["readable-stream@1.1.14", "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha1-fPTFTvZI44EwhMY23SB54WbAgdk="],
"cacache/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"cacache/lru-cache": ["lru-cache@6.0.0", "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -2014,6 +2021,8 @@
"gauge/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"glob/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
"global-dirs/ini": ["ini@1.3.7", "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
"has-ansi/ansi-regex": ["ansi-regex@2.1.1", "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz", {}, "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="],
@@ -2034,6 +2043,8 @@
"jest-cli/yargs": ["yargs@15.4.1", "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"jest-config/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"jest-config/pretty-format": ["pretty-format@26.6.2", "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="],
"jest-diff/pretty-format": ["pretty-format@26.6.2", "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="],
@@ -2084,6 +2095,8 @@
"jest-runner/jest-message-util": ["jest-message-util@26.6.2", "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "micromatch": "^4.0.2", "pretty-format": "^26.6.2", "slash": "^3.0.0", "stack-utils": "^2.0.2" } }, "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA=="],
"jest-runtime/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"jest-runtime/jest-message-util": ["jest-message-util@26.6.2", "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "@jest/types": "^26.6.2", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", "micromatch": "^4.0.2", "pretty-format": "^26.6.2", "slash": "^3.0.0", "stack-utils": "^2.0.2" } }, "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA=="],
"jest-runtime/jest-mock": ["jest-mock@26.6.2", "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz", { "dependencies": { "@jest/types": "^26.6.2", "@types/node": "*" } }, "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew=="],
@@ -2138,8 +2151,6 @@
"koa-router/path-to-regexp": ["path-to-regexp@1.8.0", "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz", { "dependencies": { "isarray": "0.0.1" } }, "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA=="],
"lru-cache/yallist": ["yallist@3.1.1", "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"make-dir/semver": ["semver@6.3.0", "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz", { "bin": "bin/semver.js" }, "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="],
"make-fetch-happen/lru-cache": ["lru-cache@6.0.0", "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
@@ -2162,6 +2173,8 @@
"multer/mkdirp": ["mkdirp@0.5.5", "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz", { "dependencies": { "minimist": "^1.2.5" }, "bin": "bin/cmd.js" }, "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ=="],
"node-gyp/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"node-gyp/graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"node-notifier/semver": ["semver@7.3.4", "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": "bin/semver.js" }, "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw=="],
@@ -2172,6 +2185,8 @@
"object-copy/kind-of": ["kind-of@3.2.2", "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ="],
"onigasm/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"openapi3-ts/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"os-locale/execa": ["execa@1.0.0", "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="],
@@ -2210,6 +2225,10 @@
"request-promise-native/tough-cookie": ["tough-cookie@2.5.0", "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
"rimraf/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"routing-controllers/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"routing-controllers-openapi/openapi3-ts": ["openapi3-ts@1.4.0", "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-1.4.0.tgz", {}, "sha512-8DmE2oKayvSkIR3XSZ4+pRliBsx19bSNeIzkTPswY8r4wvjX86bMxsORdqwAwMxE8PefOcSAT2auvi/0TZe9yA=="],
"routing-controllers-openapi/path-to-regexp": ["path-to-regexp@2.4.0", "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz", {}, "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w=="],
@@ -2240,6 +2259,8 @@
"sha.js/safe-buffer": ["safe-buffer@5.2.1", "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"shelljs/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"snapdragon/debug": ["debug@2.6.9", "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"snapdragon/define-property": ["define-property@0.2.5", "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY="],
@@ -2266,10 +2287,14 @@
"static-extend/define-property": ["define-property@0.2.5", "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz", { "dependencies": { "is-descriptor": "^0.1.0" } }, "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY="],
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"tar-fs/chownr": ["chownr@1.1.4", "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"tar-stream/readable-stream": ["readable-stream@3.6.0", "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA=="],
"test-exclude/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"to-object-path/kind-of": ["kind-of@3.2.2", "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ="],
"ts-jest/@types/jest": ["@types/jest@26.0.19", "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz", { "dependencies": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" } }, "sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ=="],
@@ -2278,8 +2303,12 @@
"typedoc/fs-extra": ["fs-extra@9.0.1", "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^1.0.0" } }, "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ=="],
"typeorm/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"typeorm/tslib": ["tslib@1.14.1", "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"typeorm-seeding/glob": ["glob@7.1.6", "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA=="],
"typeorm-seeding/ora": ["ora@4.0.3", "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz", { "dependencies": { "chalk": "^3.0.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.2.0", "is-interactive": "^1.0.0", "log-symbols": "^3.0.0", "mute-stream": "0.0.8", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg=="],
"typeorm-seeding/yargs": ["yargs@15.3.1", "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.1" } }, "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA=="],
@@ -2394,6 +2423,8 @@
"gauge/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"has-values/is-number/kind-of": ["kind-of@3.2.2", "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ="],
"jest-cli/yargs/cliui": ["cliui@6.0.0", "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
@@ -2518,6 +2549,8 @@
"object-copy/define-property/is-descriptor": ["is-descriptor@0.1.6", "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz", { "dependencies": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", "kind-of": "^5.0.0" } }, "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg=="],
"onigasm/lru-cache/yallist": ["yallist@3.1.1", "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"os-locale/execa/cross-spawn": ["cross-spawn@6.0.5", "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ=="],
"os-locale/execa/get-stream": ["get-stream@4.1.0", "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
@@ -2676,6 +2709,8 @@
"expect/jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
"jest-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"jest-cli/yargs/find-up/locate-path": ["locate-path@5.0.0", "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],

View File

@@ -1,8 +1,6 @@
const dotenv = require('dotenv');
dotenv.config();
//
// Bun workflow: always compile first, then run from dist/
const SOURCE_PATH = 'dist';
module.exports = {
type: process.env.DB_TYPE,
host: process.env.DB_HOST,
@@ -10,7 +8,7 @@ module.exports = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// Always load compiled .js files from dist/ (TypeORM entities have circular deps)
entities: [ `${SOURCE_PATH}/**/entities/*.js` ],
seeds: [ `${SOURCE_PATH}/**/seeds/*.js` ]
// Run directly from TypeScript source (Bun workflow)
entities: ["src/models/entities/**/*.ts"],
seeds: ["src/seeds/**/*.ts"]
};

View File

@@ -36,6 +36,7 @@
"csvtojson": "2.0.10",
"dotenv": "8.2.0",
"express": "4.17.1",
"glob": "^13.0.6",
"jsonwebtoken": "8.5.1",
"libphonenumber-js": "1.9.9",
"mysql": "2.18.1",
@@ -72,7 +73,7 @@
},
"scripts": {
"dev": "bun scripts/dev_watch.ts",
"start": "bun dist/app.js",
"start": "bun src/app.ts",
"build": "rimraf ./dist && tsc && cp-cli ./src/static ./dist/static",
"docs": "typedoc --out docs src",
"test": "jest",

View File

@@ -7,8 +7,28 @@ import authchecker from "./middlewares/authchecker";
import { ErrorHandler } from './middlewares/ErrorHandler';
import UserChecker from './middlewares/UserChecker';
// Always use .js when running from dist/ (Bun workflow: always build first)
const CONTROLLERS_FILE_EXTENSION = 'js';
// Import all controllers directly to avoid Bun + routing-controllers glob/require issues
import { AuthController } from './controllers/AuthController';
import { DonationController } from './controllers/DonationController';
import { DonorController } from './controllers/DonorController';
import { GroupContactController } from './controllers/GroupContactController';
import { ImportController } from './controllers/ImportController';
import { MeController } from './controllers/MeController';
import { PermissionController } from './controllers/PermissionController';
import { RunnerCardController } from './controllers/RunnerCardController';
import { RunnerController } from './controllers/RunnerController';
import { RunnerOrganizationController } from './controllers/RunnerOrganizationController';
import { RunnerSelfServiceController } from './controllers/RunnerSelfServiceController';
import { RunnerTeamController } from './controllers/RunnerTeamController';
import { ScanController } from './controllers/ScanController';
import { ScanStationController } from './controllers/ScanStationController';
import { StatsClientController } from './controllers/StatsClientController';
import { StatsController } from './controllers/StatsController';
import { StatusController } from './controllers/StatusController';
import { TrackController } from './controllers/TrackController';
import { UserController } from './controllers/UserController';
import { UserGroupController } from './controllers/UserGroupController';
const app = createExpressServer({
authorizationChecker: authchecker,
currentUserChecker: UserChecker,
@@ -16,7 +36,28 @@ const app = createExpressServer({
development: config.development,
cors: true,
routePrefix: "/api",
controllers: [`${__dirname}/controllers/*.${CONTROLLERS_FILE_EXTENSION}`],
controllers: [
AuthController,
DonationController,
DonorController,
GroupContactController,
ImportController,
MeController,
PermissionController,
RunnerCardController,
RunnerController,
RunnerOrganizationController,
RunnerSelfServiceController,
RunnerTeamController,
ScanController,
ScanStationController,
StatsClientController,
StatsController,
StatusController,
TrackController,
UserController,
UserGroupController,
],
});
async function main() {

View File

@@ -1,4 +1,4 @@
import { Request } from "express";
import type { Request } from "express";
import * as jwt from "jsonwebtoken";
import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';

View File

@@ -1,4 +1,4 @@
import { Request } from "express";
import type { Request } from "express";
import { Authorized, Body, Delete, Get, HttpError, JsonController, OnUndefined, Param, Post, Put, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnection, getConnectionManager } from 'typeorm';

View File

@@ -1,5 +1,6 @@
import { createConnection } from "typeorm";
import { runSeeder } from 'typeorm-seeding';
import consola from 'consola';
import { config } from '../config';
import { ConfigFlag } from '../models/entities/ConfigFlags';
import SeedPublicOrg from '../seeds/SeedPublicOrg';
@@ -11,6 +12,11 @@ import SeedUsers from '../seeds/SeedUsers';
*/
export default async () => {
const connection = await createConnection();
// Log discovered entities for debugging
consola.info(`TypeORM discovered ${connection.entityMetadatas.length} entities:`);
consola.info(connection.entityMetadatas.map(m => m.name).sort().join(', '));
await connection.synchronize();
//The data seeding part

View File

@@ -2,7 +2,7 @@ import { IsInt, IsNotEmpty, IsPositive } from "class-validator";
import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
import { ResponseDistanceDonation } from '../responses/ResponseDistanceDonation';
import { Donation } from "./Donation";
import { Runner } from "./Runner";
import type { Runner } from "./Runner";
/**
* Defines the DistanceDonation entity.
@@ -16,8 +16,8 @@ export class DistanceDonation extends Donation {
* Used as the source of the donation's distance.
*/
@IsNotEmpty()
@ManyToOne(() => Runner, runner => runner.distanceDonations)
runner: Runner;
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.distanceDonations)
runner!: Runner;
/**
* The donation's amount donated per distance.

View File

@@ -4,7 +4,7 @@ import {
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseDonation } from '../responses/ResponseDonation';
import { Donor } from './Donor';
import type { Donor } from './Donor';
/**
* Defines the Donation entity.
@@ -25,8 +25,8 @@ export abstract class Donation {
/**
* The donations's donor.
*/
@ManyToOne(() => Donor, donor => donor.donations)
donor: Donor;
@ManyToOne(() => require("./Donor").Donor, (donor: Donor) => donor.donations)
donor!: Donor;
/**
* The donation's amount in cents (or whatever your currency's smallest unit is.).

View File

@@ -1,7 +1,7 @@
import { IsBoolean, IsInt } from "class-validator";
import { ChildEntity, Column, OneToMany } from "typeorm";
import { ResponseDonor } from '../responses/ResponseDonor';
import { Donation } from './Donation';
import type { Donation } from './Donation';
import { Participant } from "./Participant";
/**
@@ -21,8 +21,8 @@ export class Donor extends Participant {
* Used to link the participant as the donor of a donation.
* Attention: Only runner's can be associated as a distanceDonations distance source.
*/
@OneToMany(() => Donation, donation => donation.donor, { nullable: true })
donations: Donation[];
@OneToMany(() => require("./Donation").Donation, (donation: Donation) => donation.donor, { nullable: true })
donations!: Donation[];
/**
* Returns the total donations of a donor based on his linked donations.

View File

@@ -1,19 +1,19 @@
import {
IsEmail,
IsInt,
IsNotEmpty,
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
import { Address } from "./Address";
import { RunnerGroup } from "./RunnerGroup";
import {
IsEmail,
IsInt,
IsNotEmpty,
IsOptional,
IsPhoneNumber,
IsPositive,
IsString
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { config } from '../../config';
import { ResponseGroupContact } from '../responses/ResponseGroupContact';
import { Address } from "./Address";
import type { RunnerGroup } from "./RunnerGroup";
/**
* Defines the GroupContact entity.
@@ -77,11 +77,11 @@ export class GroupContact {
@IsEmail()
email?: string;
/**
* Used to link contacts to groups.
*/
@OneToMany(() => RunnerGroup, group => group.contact, { nullable: true })
groups: RunnerGroup[];
/**
* Used to link contacts to groups.
*/
@OneToMany(() => require("./RunnerGroup").RunnerGroup, (group: RunnerGroup) => group.contact, { nullable: true })
groups!: RunnerGroup[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()

View File

@@ -8,7 +8,7 @@ import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGenerated
import { PermissionAction } from '../enums/PermissionAction';
import { PermissionTarget } from '../enums/PermissionTargets';
import { ResponsePermission } from '../responses/ResponsePermission';
import { Principal } from './Principal';
import type { Principal } from './Principal';
/**
* Defines the Permission entity.
* Permissions can be granted to principals.
@@ -26,8 +26,8 @@ export class Permission {
/**
* The permission's principal.
*/
@ManyToOne(() => Principal, principal => principal.permissions)
principal: Principal;
@ManyToOne(() => require("./Principal").Principal, (principal: Principal) => principal.permissions)
principal!: Principal;
/**
* The permission's target.

View File

@@ -1,7 +1,7 @@
import { IsInt, IsPositive } from 'class-validator';
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn, TableInheritance } from 'typeorm';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { Permission } from './Permission';
import type { Permission } from './Permission';
/**
* Defines the principal entity.
@@ -20,8 +20,8 @@ export abstract class Principal {
/**
* The participant's permissions.
*/
@OneToMany(() => Permission, permission => permission.principal, { nullable: true })
permissions: Permission[];
@OneToMany(() => require("./Permission").Permission, (permission: Permission) => permission.principal, { nullable: true })
permissions!: Permission[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()

View File

@@ -1,11 +1,11 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
import { ChildEntity, Column, Index, ManyToOne, OneToMany } from "typeorm";
import { ResponseRunner } from '../responses/ResponseRunner';
import { DistanceDonation } from "./DistanceDonation";
import type { DistanceDonation } from "./DistanceDonation";
import { Participant } from "./Participant";
import { RunnerCard } from "./RunnerCard";
import type { RunnerCard } from "./RunnerCard";
import { RunnerGroup } from "./RunnerGroup";
import { Scan } from "./Scan";
import type { Scan } from "./Scan";
/**
* Defines the runner entity.
@@ -27,22 +27,22 @@ export class Runner extends Participant {
* The runner's associated distanceDonations.
* Used to link runners to distanceDonations in order to calculate the donation's amount based on the distance the runner ran.
*/
@OneToMany(() => DistanceDonation, distanceDonation => distanceDonation.runner, { nullable: true })
distanceDonations: DistanceDonation[];
@OneToMany(() => require("./DistanceDonation").DistanceDonation, (distanceDonation: DistanceDonation) => distanceDonation.runner, { nullable: true })
distanceDonations!: DistanceDonation[];
/**
* The runner's associated cards.
* Used to link runners to cards - yes a runner be associated with multiple cards this came in handy in the past.
*/
@OneToMany(() => RunnerCard, card => card.runner, { nullable: true })
cards: RunnerCard[];
@OneToMany(() => require("./RunnerCard").RunnerCard, (card: RunnerCard) => card.runner, { nullable: true })
cards!: RunnerCard[];
/**
* The runner's associated scans.
* Used to link runners to scans (valid and fraudulant).
*/
@OneToMany(() => Scan, scan => scan.runner, { nullable: true })
scans: Scan[];
@OneToMany(() => require("./Scan").Scan, (scan: Scan) => scan.runner, { nullable: true })
scans!: Scan[];
/**
* The last time the runner requested a selfservice link.

View File

@@ -9,8 +9,8 @@ import {
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { RunnerCardIdOutOfRangeError } from '../../errors/RunnerCardErrors';
import { ResponseRunnerCard } from '../responses/ResponseRunnerCard';
import { Runner } from "./Runner";
import { TrackScan } from "./TrackScan";
import type { Runner } from "./Runner";
import type { TrackScan } from "./TrackScan";
/**
* Defines the RunnerCard entity.
@@ -33,8 +33,8 @@ export class RunnerCard {
* To increase reusability a card can be reassigned.
*/
@IsOptional()
@ManyToOne(() => Runner, runner => runner.cards, { nullable: true })
runner: Runner;
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.cards, { nullable: true })
runner!: Runner;
/**
* Is the card enabled (for fraud reasons)?
@@ -48,8 +48,8 @@ export class RunnerCard {
* The card's associated scans.
* Used to link cards to track scans.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => scan.card, { nullable: true })
scans!: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()

View File

@@ -8,7 +8,7 @@ import {
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseRunnerGroup } from '../responses/ResponseRunnerGroup';
import { GroupContact } from "./GroupContact";
import { Runner } from "./Runner";
import type { Runner } from "./Runner";
/**
* Defines the RunnerGroup entity.
@@ -44,8 +44,8 @@ export abstract class RunnerGroup {
* The group's associated runners.
* Used to link runners to a runner group.
*/
@OneToMany(() => Runner, runner => runner.group, { nullable: true })
runners: Runner[];
@OneToMany(() => require("./Runner").Runner, (runner: Runner) => runner.group, { nullable: true })
runners!: Runner[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()

View File

@@ -4,7 +4,7 @@ import { ResponseRunnerOrganization } from '../responses/ResponseRunnerOrganizat
import { Address } from './Address';
import { Runner } from './Runner';
import { RunnerGroup } from "./RunnerGroup";
import { RunnerTeam } from "./RunnerTeam";
import type { RunnerTeam } from "./RunnerTeam";
/**
* Defines the RunnerOrganization entity.
@@ -24,8 +24,8 @@ export class RunnerOrganization extends RunnerGroup {
* The organization's teams.
* Used to link teams to a organization.
*/
@OneToMany(() => RunnerTeam, team => team.parentGroup, { nullable: true })
teams: RunnerTeam[];
@OneToMany(() => require("./RunnerTeam").RunnerTeam, (team: RunnerTeam) => team.parentGroup, { nullable: true })
teams!: RunnerTeam[];
/**
* The organization's api key for self-service registration.

View File

@@ -2,7 +2,7 @@ import { IsNotEmpty } from "class-validator";
import { ChildEntity, Index, ManyToOne } from "typeorm";
import { ResponseRunnerTeam } from '../responses/ResponseRunnerTeam';
import { RunnerGroup } from "./RunnerGroup";
import { RunnerOrganization } from "./RunnerOrganization";
import type { RunnerOrganization } from "./RunnerOrganization";
/**
* Defines the RunnerTeam entity.
@@ -17,7 +17,7 @@ export class RunnerTeam extends RunnerGroup {
* Every team has to be part of a runnerOrganization - this get's checked on creation and update.
*/
@IsNotEmpty()
@ManyToOne(() => RunnerOrganization, org => org.teams, { nullable: true })
@ManyToOne(() => require("./RunnerOrganization").RunnerOrganization, (org: RunnerOrganization) => org.teams, { nullable: true })
parentGroup?: RunnerOrganization;
/**

View File

@@ -7,7 +7,7 @@ import {
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, TableInheritance } from "typeorm";
import { ResponseScan } from '../responses/ResponseScan';
import { Runner } from "./Runner";
import type { Runner } from "./Runner";
/**
* Defines the Scan entity.
@@ -31,8 +31,8 @@ export class Scan {
* This is important to link ran distances to runners.
*/
@IsNotEmpty()
@ManyToOne(() => Runner, runner => runner.scans, { nullable: false })
runner: Runner;
@ManyToOne(() => require("./Runner").Runner, (runner: Runner) => runner.scans, { nullable: false })
runner!: Runner;
/**
* Is the scan valid (for fraud reasons).

View File

@@ -8,8 +8,8 @@ import {
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, Index, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseScanStation } from '../responses/ResponseScanStation';
import { Track } from "./Track";
import { TrackScan } from "./TrackScan";
import type { Track } from "./Track";
import type { TrackScan } from "./TrackScan";
/**
* Defines the ScanStation entity.
@@ -41,8 +41,8 @@ export class ScanStation {
* All scans created by this station will also be associated with this track.
*/
@IsNotEmpty()
@ManyToOne(() => Track, track => track.stations, { nullable: false })
track: Track;
@ManyToOne(() => require("./Track").Track, (track: Track) => track.stations, { nullable: false })
track!: Track;
/**
* The client's api key prefix.
@@ -72,8 +72,8 @@ export class ScanStation {
/**
* Used to link track scans to a scan station.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => scan.station, { nullable: true })
scans!: TrackScan[];
/**
* Is this station enabled?

View File

@@ -7,8 +7,8 @@ import {
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ResponseTrack } from '../responses/ResponseTrack';
import { ScanStation } from "./ScanStation";
import { TrackScan } from "./TrackScan";
import type { ScanStation } from "./ScanStation";
import type { TrackScan } from "./TrackScan";
/**
* Defines the Track entity.
@@ -53,15 +53,15 @@ export class Track {
* Used to link scan stations to a certain track.
* This makes the configuration of the scan stations easier.
*/
@OneToMany(() => ScanStation, station => station.track, { nullable: true })
stations: ScanStation[];
@OneToMany(() => require("./ScanStation").ScanStation, (station: ScanStation) => station.track, { nullable: true })
stations!: ScanStation[];
/**
* Used to link track scans to a track.
* The scan will derive it's distance from the track's distance.
*/
@OneToMany(() => TrackScan, scan => scan.track, { nullable: true })
scans: TrackScan[];
@OneToMany(() => require("./TrackScan").TrackScan, (scan: TrackScan) => scan.track, { nullable: true })
scans!: TrackScan[];
@Column({ type: 'bigint', nullable: true, readonly: true })
@IsInt()

View File

@@ -8,10 +8,10 @@ import {
} from "class-validator";
import { ChildEntity, Column, Index, ManyToOne } from "typeorm";
import { ResponseTrackScan } from '../responses/ResponseTrackScan';
import { RunnerCard } from "./RunnerCard";
import type { RunnerCard } from "./RunnerCard";
import { Scan } from "./Scan";
import { ScanStation } from "./ScanStation";
import { Track } from "./Track";
import type { ScanStation } from "./ScanStation";
import type { Track } from "./Track";
/**
* Defines the TrackScan entity.
@@ -29,24 +29,24 @@ export class TrackScan extends Scan {
* This is used to determine the scan's distance.
*/
@IsNotEmpty()
@ManyToOne(() => Track, track => track.scans, { nullable: true })
track: Track;
@ManyToOne(() => require("./Track").Track, (track: Track) => track.scans, { nullable: true })
track!: Track;
/**
* The runnerCard associated with the scan.
* This get's saved for documentation and management purposes.
*/
@IsNotEmpty()
@ManyToOne(() => RunnerCard, card => card.scans, { nullable: true })
card: RunnerCard;
@ManyToOne(() => require("./RunnerCard").RunnerCard, (card: RunnerCard) => card.scans, { nullable: true })
card!: RunnerCard;
/**
* The scanning station that created the scan.
* Mainly used for logging and traceing back scans (or errors)
*/
@IsNotEmpty()
@ManyToOne(() => ScanStation, station => station.scans, { nullable: true })
station: ScanStation;
@ManyToOne(() => require("./ScanStation").ScanStation, (station: ScanStation) => station.scans, { nullable: true })
station!: ScanStation;
/**
* The scan's distance in meters.

View File

@@ -1,12 +1,12 @@
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
import { ChildEntity, Column, Index, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUser } from '../responses/ResponseUser';
import { Permission } from './Permission';
import { Principal } from './Principal';
import { UserAction } from './UserAction';
import { UserGroup } from './UserGroup';
import { IsBoolean, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUrl, IsUUID } from "class-validator";
import { ChildEntity, Column, Index, JoinTable, ManyToMany, OneToMany } from "typeorm";
import { config } from '../../config';
import { ResponsePrincipal } from '../responses/ResponsePrincipal';
import { ResponseUser } from '../responses/ResponseUser';
import { Permission } from './Permission';
import { Principal } from './Principal';
import type { UserAction } from './UserAction';
import { UserGroup } from './UserGroup';
/**
* Defines the User entity.
@@ -125,11 +125,11 @@ export class User extends Principal {
/**
* The actions performed by this user.
* For documentation purposes only, will be implemented later.
*/
@IsOptional()
@OneToMany(() => UserAction, action => action.user, { nullable: true })
actions: UserAction[]
* For documentation purposes only, will be implemented later.
*/
@IsOptional()
@OneToMany(() => require("./UserAction").UserAction, (action: UserAction) => action.user, { nullable: true })
actions!: UserAction[]
/**
* Resolves all permissions granted to this user through groups.

View File

@@ -8,7 +8,7 @@ import {
} from "class-validator";
import { BeforeInsert, BeforeUpdate, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { PermissionAction } from '../enums/PermissionAction';
import { User } from './User';
import type { User } from './User';
/**
* Defines the UserAction entity.
@@ -26,8 +26,8 @@ export class UserAction {
/**
* The user that performed the action.
*/
@ManyToOne(() => User, user => user.actions)
user: User
@ManyToOne(() => require("./User").User, (user: User) => user.actions)
user!: User
/**
* The actions's target (e.g. Track#2)

View File

@@ -3,7 +3,7 @@ import { Donation } from '../entities/Donation';
import { DonationStatus } from '../enums/DonationStatus';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
import { ResponseDonor } from './ResponseDonor';
import type { ResponseDonor } from './ResponseDonor';
/**
* Defines the donation response.
@@ -33,7 +33,7 @@ export class ResponseDonation implements IResponse {
* The donation's donor.
*/
@IsNotEmpty()
donor: ResponseDonor;
donor?: ResponseDonor;
/**
* The donation's amount in the smalles unit of your currency (default: euro cent).

View File

@@ -4,7 +4,7 @@ import {
import { Donor } from '../entities/Donor';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
import { ResponseDonation } from './ResponseDonation';
import type { ResponseDonation } from './ResponseDonation';
import { ResponseParticipant } from './ResponseParticipant';
/**
@@ -35,7 +35,7 @@ export class ResponseDonor extends ResponseParticipant implements IResponse {
@IsInt()
paidDonationAmount: number;
donations: Array<ResponseDonation>;
donations?: Array<ResponseDonation>;
/**
* Creates a ResponseRunner object from a runner.
@@ -46,6 +46,7 @@ export class ResponseDonor extends ResponseParticipant implements IResponse {
this.receiptNeeded = donor.receiptNeeded;
this.donationAmount = donor.donationAmount;
this.paidDonationAmount = donor.paidDonationAmount;
const ResponseDonation = require('./ResponseDonation').ResponseDonation;
this.donations = new Array<ResponseDonation>();
if (donor.donations?.length > 0) {
for (const donation of donor.donations) {

View File

@@ -14,7 +14,7 @@ import { RunnerOrganization } from '../entities/RunnerOrganization';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
import { ResponseRunnerGroup } from './ResponseRunnerGroup';
import { ResponseRunnerTeam } from './ResponseRunnerTeam';
import type { ResponseRunnerTeam } from './ResponseRunnerTeam';
/**
* Defines the runnerOrganization response.
@@ -37,7 +37,7 @@ export class ResponseRunnerOrganization extends ResponseRunnerGroup implements I
* The runnerOrganization associated teams.
*/
@IsArray()
teams: ResponseRunnerTeam[];
teams?: ResponseRunnerTeam[];
/**
* The organization's registration key.
@@ -62,6 +62,7 @@ export class ResponseRunnerOrganization extends ResponseRunnerGroup implements I
public constructor(org: RunnerOrganization) {
super(org);
this.address = org.address;
const ResponseRunnerTeam = require('./ResponseRunnerTeam').ResponseRunnerTeam;
this.teams = new Array<ResponseRunnerTeam>();
if (org.teams) {
for (let team of org.teams) {

View File

@@ -3,7 +3,7 @@ import { RunnerTeam } from '../entities/RunnerTeam';
import { ResponseObjectType } from '../enums/ResponseObjectType';
import { IResponse } from './IResponse';
import { ResponseRunnerGroup } from './ResponseRunnerGroup';
import { ResponseRunnerOrganization } from './ResponseRunnerOrganization';
import type { ResponseRunnerOrganization } from './ResponseRunnerOrganization';
/**
* Defines the runnerTeam response.
@@ -20,7 +20,7 @@ export class ResponseRunnerTeam extends ResponseRunnerGroup implements IResponse
*/
@IsObject()
@IsNotEmpty()
parentGroup: ResponseRunnerOrganization;
parentGroup?: ResponseRunnerOrganization;
/**
* Creates a ResponseRunnerTeam object from a runnerTeam.