From 3bb8b202b00f8b7c52c700373ed09a92714528be Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Fri, 20 Feb 2026 21:45:15 +0100 Subject: [PATCH] fix(dev): We did it funky bun dev workarounds are no more --- PERFORMANCE_IDEAS.md | 589 ++++++++++++++++++ bun.lock | 45 +- ormconfig.js | 10 +- package.json | 3 +- src/app.ts | 47 +- .../RunnerSelfServiceController.ts | 2 +- src/controllers/ScanController.ts | 2 +- src/loaders/database.ts | 6 + src/models/entities/DistanceDonation.ts | 6 +- src/models/entities/Donation.ts | 6 +- src/models/entities/Donor.ts | 6 +- src/models/entities/GroupContact.ts | 42 +- src/models/entities/Permission.ts | 6 +- src/models/entities/Principal.ts | 6 +- src/models/entities/Runner.ts | 18 +- src/models/entities/RunnerCard.ts | 12 +- src/models/entities/RunnerGroup.ts | 6 +- src/models/entities/RunnerOrganization.ts | 6 +- src/models/entities/RunnerTeam.ts | 4 +- src/models/entities/Scan.ts | 6 +- src/models/entities/ScanStation.ts | 12 +- src/models/entities/Track.ts | 12 +- src/models/entities/TrackScan.ts | 18 +- src/models/entities/User.ts | 28 +- src/models/entities/UserAction.ts | 6 +- src/models/responses/ResponseDonation.ts | 4 +- src/models/responses/ResponseDonor.ts | 5 +- .../responses/ResponseRunnerOrganization.ts | 5 +- src/models/responses/ResponseRunnerTeam.ts | 4 +- 29 files changed, 797 insertions(+), 125 deletions(-) create mode 100644 PERFORMANCE_IDEAS.md diff --git a/PERFORMANCE_IDEAS.md b/PERFORMANCE_IDEAS.md new file mode 100644 index 0000000..a9096ca --- /dev/null +++ b/PERFORMANCE_IDEAS.md @@ -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 { + 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 { + 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(key: string): T | undefined { + return cache.get(key); +} + +export function setCached(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(); + +async function getTotalCount(repo: Repository): Promise { + 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 diff --git a/bun.lock b/bun.lock index 99cb024..99f3a9f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/ormconfig.js b/ormconfig.js index d924241..3e24beb 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -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"] }; diff --git a/package.json b/package.json index 823130a..031b129 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app.ts b/src/app.ts index b9d36b4..4ed6452 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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() { diff --git a/src/controllers/RunnerSelfServiceController.ts b/src/controllers/RunnerSelfServiceController.ts index f02f8f3..7b2d5ef 100644 --- a/src/controllers/RunnerSelfServiceController.ts +++ b/src/controllers/RunnerSelfServiceController.ts @@ -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'; diff --git a/src/controllers/ScanController.ts b/src/controllers/ScanController.ts index 79e1017..8ffe039 100644 --- a/src/controllers/ScanController.ts +++ b/src/controllers/ScanController.ts @@ -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'; diff --git a/src/loaders/database.ts b/src/loaders/database.ts index 0b4a16a..92dd06c 100644 --- a/src/loaders/database.ts +++ b/src/loaders/database.ts @@ -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 diff --git a/src/models/entities/DistanceDonation.ts b/src/models/entities/DistanceDonation.ts index 848fba3..61cede1 100644 --- a/src/models/entities/DistanceDonation.ts +++ b/src/models/entities/DistanceDonation.ts @@ -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. diff --git a/src/models/entities/Donation.ts b/src/models/entities/Donation.ts index f09cfa1..c1757af 100644 --- a/src/models/entities/Donation.ts +++ b/src/models/entities/Donation.ts @@ -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.). diff --git a/src/models/entities/Donor.ts b/src/models/entities/Donor.ts index 4d42769..24d211e 100644 --- a/src/models/entities/Donor.ts +++ b/src/models/entities/Donor.ts @@ -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. diff --git a/src/models/entities/GroupContact.ts b/src/models/entities/GroupContact.ts index 0660100..7783652 100644 --- a/src/models/entities/GroupContact.ts +++ b/src/models/entities/GroupContact.ts @@ -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() diff --git a/src/models/entities/Permission.ts b/src/models/entities/Permission.ts index 077dd8c..205f39b 100644 --- a/src/models/entities/Permission.ts +++ b/src/models/entities/Permission.ts @@ -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. diff --git a/src/models/entities/Principal.ts b/src/models/entities/Principal.ts index 476af1b..14e3dec 100644 --- a/src/models/entities/Principal.ts +++ b/src/models/entities/Principal.ts @@ -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() diff --git a/src/models/entities/Runner.ts b/src/models/entities/Runner.ts index 0616006..4da32e7 100644 --- a/src/models/entities/Runner.ts +++ b/src/models/entities/Runner.ts @@ -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. diff --git a/src/models/entities/RunnerCard.ts b/src/models/entities/RunnerCard.ts index e4adc1a..847a201 100644 --- a/src/models/entities/RunnerCard.ts +++ b/src/models/entities/RunnerCard.ts @@ -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() diff --git a/src/models/entities/RunnerGroup.ts b/src/models/entities/RunnerGroup.ts index d17df88..fff7355 100644 --- a/src/models/entities/RunnerGroup.ts +++ b/src/models/entities/RunnerGroup.ts @@ -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() diff --git a/src/models/entities/RunnerOrganization.ts b/src/models/entities/RunnerOrganization.ts index 1988dca..7373151 100644 --- a/src/models/entities/RunnerOrganization.ts +++ b/src/models/entities/RunnerOrganization.ts @@ -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. diff --git a/src/models/entities/RunnerTeam.ts b/src/models/entities/RunnerTeam.ts index db9624e..a3d2076 100644 --- a/src/models/entities/RunnerTeam.ts +++ b/src/models/entities/RunnerTeam.ts @@ -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; /** diff --git a/src/models/entities/Scan.ts b/src/models/entities/Scan.ts index 0a92bad..ced2345 100644 --- a/src/models/entities/Scan.ts +++ b/src/models/entities/Scan.ts @@ -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). diff --git a/src/models/entities/ScanStation.ts b/src/models/entities/ScanStation.ts index db23220..141437d 100644 --- a/src/models/entities/ScanStation.ts +++ b/src/models/entities/ScanStation.ts @@ -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? diff --git a/src/models/entities/Track.ts b/src/models/entities/Track.ts index 843abb7..00943cf 100644 --- a/src/models/entities/Track.ts +++ b/src/models/entities/Track.ts @@ -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() diff --git a/src/models/entities/TrackScan.ts b/src/models/entities/TrackScan.ts index e6af00a..1c98ab9 100644 --- a/src/models/entities/TrackScan.ts +++ b/src/models/entities/TrackScan.ts @@ -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. diff --git a/src/models/entities/User.ts b/src/models/entities/User.ts index 3408799..51cc56e 100644 --- a/src/models/entities/User.ts +++ b/src/models/entities/User.ts @@ -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. diff --git a/src/models/entities/UserAction.ts b/src/models/entities/UserAction.ts index 0d17912..804cf26 100644 --- a/src/models/entities/UserAction.ts +++ b/src/models/entities/UserAction.ts @@ -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) diff --git a/src/models/responses/ResponseDonation.ts b/src/models/responses/ResponseDonation.ts index 5d7534a..a871582 100644 --- a/src/models/responses/ResponseDonation.ts +++ b/src/models/responses/ResponseDonation.ts @@ -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). diff --git a/src/models/responses/ResponseDonor.ts b/src/models/responses/ResponseDonor.ts index ee5a491..b1dbe25 100644 --- a/src/models/responses/ResponseDonor.ts +++ b/src/models/responses/ResponseDonor.ts @@ -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; + donations?: Array; /** * 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(); if (donor.donations?.length > 0) { for (const donation of donor.donations) { diff --git a/src/models/responses/ResponseRunnerOrganization.ts b/src/models/responses/ResponseRunnerOrganization.ts index 152bb63..ce68368 100644 --- a/src/models/responses/ResponseRunnerOrganization.ts +++ b/src/models/responses/ResponseRunnerOrganization.ts @@ -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(); if (org.teams) { for (let team of org.teams) { diff --git a/src/models/responses/ResponseRunnerTeam.ts b/src/models/responses/ResponseRunnerTeam.ts index 0434789..9c1e9f7 100644 --- a/src/models/responses/ResponseRunnerTeam.ts +++ b/src/models/responses/ResponseRunnerTeam.ts @@ -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.