@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Multi-market hit-rate backtest.
|
||||
|
||||
Runs the orchestrator against historical finished matches and measures raw V25
|
||||
pick accuracy per market — independent of the "playable" gate. This isolates
|
||||
model quality from the value-detection thresholds.
|
||||
|
||||
Usage:
|
||||
python scripts/backtest_hitrate.py --start 2026-05-01 --end 2026-05-09 [--limit 500]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from data.db import get_clean_dsn
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
|
||||
def fetch_matches(cur, start_date: str, end_date: str, limit: Optional[int]) -> List[Dict[str, Any]]:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT m.id, m.score_home, m.score_away, m.ht_score_home, m.ht_score_away,
|
||||
m.mst_utc, t1.name as home_name, t2.name as away_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.status IN ('FT', 'AET', 'PEN')
|
||||
AND m.sport = 'football'
|
||||
AND to_timestamp(m.mst_utc / 1000.0)::date BETWEEN %s::date AND %s::date
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc ASC
|
||||
""" + (f" LIMIT {int(limit)}" if limit else ""),
|
||||
(start_date, end_date),
|
||||
)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
def actual_ms(h: int, a: int) -> str:
|
||||
return "1" if h > a else ("X" if h == a else "2")
|
||||
|
||||
|
||||
def actual_ht(hh: Optional[int], ha: Optional[int]) -> Optional[str]:
|
||||
if hh is None or ha is None:
|
||||
return None
|
||||
return "1" if hh > ha else ("X" if hh == ha else "2")
|
||||
|
||||
|
||||
OVER_TOKENS = {"over", "üst", "ust"}
|
||||
UNDER_TOKENS = {"under", "alt"}
|
||||
YES_TOKENS = {"yes", "var", "kg var"}
|
||||
NO_TOKENS = {"no", "yok", "kg yok"}
|
||||
ODD_TOKENS = {"odd", "tek"}
|
||||
EVEN_TOKENS = {"even", "çift", "cift"}
|
||||
|
||||
|
||||
def _norm(s: str) -> str:
|
||||
return str(s or "").strip().lower()
|
||||
|
||||
|
||||
def score_pick(market: str, predicted: str, h: int, a: int, hh: Optional[int], ha: Optional[int]) -> Optional[bool]:
|
||||
"""Return True/False for hit, or None if cannot evaluate."""
|
||||
total = h + a
|
||||
ht_total = (hh + ha) if hh is not None and ha is not None else None
|
||||
p = _norm(predicted)
|
||||
|
||||
if market == "MS":
|
||||
return p.upper() == actual_ms(h, a)
|
||||
if market in ("OU15", "OU25", "OU35"):
|
||||
line = {"OU15": 1.5, "OU25": 2.5, "OU35": 3.5}[market]
|
||||
if p in OVER_TOKENS:
|
||||
return total > line
|
||||
if p in UNDER_TOKENS:
|
||||
return total < line
|
||||
return None
|
||||
if market == "BTTS":
|
||||
btts = h > 0 and a > 0
|
||||
if p in YES_TOKENS:
|
||||
return btts
|
||||
if p in NO_TOKENS:
|
||||
return not btts
|
||||
return None
|
||||
if market == "HT":
|
||||
ht = actual_ht(hh, ha)
|
||||
return None if ht is None else p.upper() == ht
|
||||
if market in ("HT_OU05", "HT_OU15"):
|
||||
if ht_total is None:
|
||||
return None
|
||||
line = 0.5 if market == "HT_OU05" else 1.5
|
||||
if p in OVER_TOKENS:
|
||||
return ht_total > line
|
||||
if p in UNDER_TOKENS:
|
||||
return ht_total < line
|
||||
return None
|
||||
if market == "HTFT":
|
||||
ht = actual_ht(hh, ha)
|
||||
if ht is None:
|
||||
return None
|
||||
full = actual_ms(h, a)
|
||||
norm = p.replace(" ", "").upper().replace("0", "X")
|
||||
return norm == f"{ht}/{full}"
|
||||
if market == "OE":
|
||||
odd = total % 2 == 1
|
||||
if p in ODD_TOKENS:
|
||||
return odd
|
||||
if p in EVEN_TOKENS:
|
||||
return not odd
|
||||
return None
|
||||
if market == "DC":
|
||||
ms = actual_ms(h, a)
|
||||
compact = p.replace("-", "").upper()
|
||||
if compact == "1X":
|
||||
return ms in ("1", "X")
|
||||
if compact == "X2":
|
||||
return ms in ("X", "2")
|
||||
if compact == "12":
|
||||
return ms in ("1", "2")
|
||||
return None
|
||||
# CARDS / HCAP cannot be scored without extra data
|
||||
return None
|
||||
|
||||
|
||||
def top_pick(probs: Dict[str, float]) -> Tuple[Optional[str], float]:
|
||||
if not probs:
|
||||
return None, 0.0
|
||||
key = max(probs, key=lambda k: float(probs.get(k, 0) or 0))
|
||||
return key, float(probs.get(key, 0) or 0)
|
||||
|
||||
|
||||
def run(start_date: str, end_date: str, limit: Optional[int], out_path: Optional[str]) -> None:
|
||||
dsn = get_clean_dsn()
|
||||
print(f"DSN host={dsn.split('@')[-1].split('/')[0]}")
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
matches = fetch_matches(cur, start_date, end_date, limit)
|
||||
print(f"Found {len(matches)} matches between {start_date} and {end_date}")
|
||||
if not matches:
|
||||
return
|
||||
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
|
||||
market_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
|
||||
"total": 0, "hits": 0, "skipped": 0,
|
||||
"playable_total": 0, "playable_hits": 0,
|
||||
"conf_sum": 0.0,
|
||||
})
|
||||
detailed_rows: List[Dict[str, Any]] = []
|
||||
errors = 0
|
||||
started = time.time()
|
||||
|
||||
for idx, m in enumerate(matches, 1):
|
||||
try:
|
||||
pkg = orchestrator.analyze_match(m["id"])
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
if errors <= 5:
|
||||
print(f"[ERR] {m['id']}: {e}")
|
||||
continue
|
||||
if not pkg:
|
||||
continue
|
||||
|
||||
board = pkg.get("market_board", {}) or {}
|
||||
h = int(m["score_home"])
|
||||
a = int(m["score_away"])
|
||||
hh = m.get("ht_score_home")
|
||||
ha = m.get("ht_score_away")
|
||||
|
||||
for market, entry in board.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
probs = entry.get("probs") or {}
|
||||
pick, prob = top_pick(probs)
|
||||
if pick is None:
|
||||
continue
|
||||
hit = score_pick(market, pick, h, a, hh, ha)
|
||||
stats = market_stats[market]
|
||||
if hit is None:
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
stats["total"] += 1
|
||||
stats["conf_sum"] += prob
|
||||
if hit:
|
||||
stats["hits"] += 1
|
||||
if entry.get("playable") is True:
|
||||
stats["playable_total"] += 1
|
||||
if hit:
|
||||
stats["playable_hits"] += 1
|
||||
detailed_rows.append({
|
||||
"match_id": m["id"],
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"prob": round(prob, 4),
|
||||
"hit": hit,
|
||||
"playable": bool(entry.get("playable")),
|
||||
"score": f"{h}-{a}",
|
||||
"ht_score": f"{hh}-{ha}" if hh is not None else None,
|
||||
})
|
||||
|
||||
if idx % 25 == 0:
|
||||
elapsed = time.time() - started
|
||||
print(f" ... processed {idx}/{len(matches)} ({elapsed:.1f}s)")
|
||||
|
||||
elapsed = time.time() - started
|
||||
print("\n" + "=" * 72)
|
||||
print(f"BACKTEST {start_date} .. {end_date} | matches={len(matches)} errors={errors} elapsed={elapsed:.1f}s")
|
||||
print("=" * 72)
|
||||
header = f"{'Market':<10} {'N':>5} {'Hit':>5} {'Rate':>7} {'AvgConf':>8} | {'PlayN':>6} {'PlayHit':>7} {'PlayRate':>8}"
|
||||
print(header)
|
||||
print("-" * 72)
|
||||
for market in sorted(market_stats.keys()):
|
||||
s = market_stats[market]
|
||||
n = s["total"]
|
||||
rate = (s["hits"] / n * 100) if n else 0.0
|
||||
avg_conf = (s["conf_sum"] / n * 100) if n else 0.0
|
||||
pn = s["playable_total"]
|
||||
prate = (s["playable_hits"] / pn * 100) if pn else 0.0
|
||||
print(f"{market:<10} {n:>5} {s['hits']:>5} {rate:>6.1f}% {avg_conf:>7.1f}% | {pn:>6} {s['playable_hits']:>7} {prate:>7.1f}%")
|
||||
|
||||
if out_path:
|
||||
payload = {
|
||||
"range": {"start": start_date, "end": end_date},
|
||||
"match_count": len(matches),
|
||||
"errors": errors,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"market_stats": {k: dict(v) for k, v in market_stats.items()},
|
||||
"rows": detailed_rows,
|
||||
}
|
||||
with open(out_path, "w") as f:
|
||||
json.dump(payload, f, indent=2, ensure_ascii=False)
|
||||
print(f"\nSaved details to {out_path}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--start", required=True, help="YYYY-MM-DD")
|
||||
p.add_argument("--end", required=True, help="YYYY-MM-DD")
|
||||
p.add_argument("--limit", type=int, default=None)
|
||||
p.add_argument("--out", default=None, help="Optional JSON output path")
|
||||
args = p.parse_args()
|
||||
run(args.start, args.end, args.limit, args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+283
-40
@@ -26,7 +26,7 @@
|
||||
"@nestjs/swagger": "^11.2.4",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/client": "5.22.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"axios": "^1.13.6",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.66.4",
|
||||
@@ -46,7 +46,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pino": "^10.1.0",
|
||||
"pino-http": "^11.0.0",
|
||||
"prisma": "5.22.0",
|
||||
"prisma": "^6.19.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"twitter-api-v2": "^1.29.0",
|
||||
@@ -3773,60 +3773,75 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
|
||||
"integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz",
|
||||
"integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
"prisma": "*",
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz",
|
||||
"integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==",
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
"deepmerge-ts": "7.1.5",
|
||||
"effect": "3.21.0",
|
||||
"empathic": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
|
||||
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ=="
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz",
|
||||
"integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw=="
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
|
||||
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz",
|
||||
"integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/fetch-engine": "5.22.0",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
"@prisma/debug": "6.19.3",
|
||||
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"@prisma/fetch-engine": "6.19.3",
|
||||
"@prisma/get-platform": "6.19.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
|
||||
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ=="
|
||||
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA=="
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
|
||||
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz",
|
||||
"integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0",
|
||||
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
|
||||
"@prisma/get-platform": "5.22.0"
|
||||
"@prisma/debug": "6.19.3",
|
||||
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"@prisma/get-platform": "6.19.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
|
||||
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz",
|
||||
"integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.22.0"
|
||||
"@prisma/debug": "6.19.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
@@ -4603,6 +4618,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
|
||||
},
|
||||
"node_modules/@tokenizer/inflate": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
||||
@@ -6366,6 +6386,44 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/c12": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"confbox": "^0.2.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.6.1",
|
||||
"exsolve": "^1.0.7",
|
||||
"giget": "^2.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^2.2.0",
|
||||
"rc9": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"magicast": "^0.3.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"magicast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/c12/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager": {
|
||||
"version": "7.2.7",
|
||||
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.7.tgz",
|
||||
@@ -6582,7 +6640,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -6623,6 +6680,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/citty": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
|
||||
@@ -6852,6 +6917,11 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||
@@ -7109,6 +7179,14 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge-ts": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defaults": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
|
||||
@@ -7121,6 +7199,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -7145,6 +7228,11 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -7292,6 +7380,15 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/effect": {
|
||||
"version": "3.21.0",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz",
|
||||
"integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"fast-check": "^3.23.1"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
@@ -7315,6 +7412,14 @@
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/empathic": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
@@ -7862,11 +7967,52 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"pure-rand": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check/node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
|
||||
@@ -8388,6 +8534,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.0",
|
||||
"defu": "^6.1.4",
|
||||
"node-fetch-native": "^1.6.6",
|
||||
"nypm": "^0.6.0",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
@@ -9727,6 +9889,14 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||
@@ -10575,6 +10745,11 @@
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
@@ -10651,6 +10826,27 @@
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz",
|
||||
"integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==",
|
||||
"dependencies": {
|
||||
"citty": "^0.2.2",
|
||||
"pathe": "^2.0.3",
|
||||
"tinyexec": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"nypm": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm/node_modules/citty": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz",
|
||||
"integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -10679,6 +10875,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
@@ -10995,11 +11196,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -11181,6 +11392,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
|
||||
"integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.4",
|
||||
"exsolve": "^1.0.8",
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
@@ -11308,21 +11529,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
|
||||
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
|
||||
"version": "6.19.3",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz",
|
||||
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.22.0"
|
||||
"@prisma/config": "6.19.3",
|
||||
"@prisma/engines": "6.19.3"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.3"
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
@@ -11474,6 +11701,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rc9": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -11497,7 +11733,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
@@ -12593,6 +12828,14 @@
|
||||
"real-require": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||
"integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -12918,7 +13161,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
+5
-5
@@ -55,7 +55,7 @@
|
||||
"@nestjs/swagger": "^11.2.4",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/throttler": "^6.5.0",
|
||||
"@prisma/client": "5.22.0",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"axios": "^1.13.6",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.66.4",
|
||||
@@ -75,7 +75,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pino": "^10.1.0",
|
||||
"pino-http": "^11.0.0",
|
||||
"prisma": "5.22.0",
|
||||
"prisma": "^6.19.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"twitter-api-v2": "^1.29.0",
|
||||
@@ -110,9 +110,6 @@
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
@@ -129,5 +126,8 @@
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -1,7 +1,15 @@
|
||||
module.exports = {
|
||||
datasource: {
|
||||
url:
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db?schema=public',
|
||||
import path from 'node:path';
|
||||
import { defineConfig, env } from 'prisma/config';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config({ path: '.env.local' });
|
||||
|
||||
export default defineConfig({
|
||||
schema: path.join('prisma', 'schema.prisma'),
|
||||
migrations: {
|
||||
path: path.join('prisma', 'migrations'),
|
||||
},
|
||||
};
|
||||
datasource: {
|
||||
url: env('DATABASE_URL'),
|
||||
},
|
||||
});
|
||||
|
||||
+122
-98
@@ -42,22 +42,22 @@ model League {
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id
|
||||
name String
|
||||
slug String?
|
||||
sport Sport
|
||||
logoUrl String? @map("logo_url")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
awayMatchesLive LiveMatch[] @relation("AwayTeamLive")
|
||||
homeMatchesLive LiveMatch[] @relation("HomeTeamLive")
|
||||
playerEvents MatchPlayerEvents[]
|
||||
playerParticipations MatchPlayerParticipation[]
|
||||
id String @id
|
||||
name String
|
||||
slug String?
|
||||
sport Sport
|
||||
logoUrl String? @map("logo_url")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
awayMatchesLive LiveMatch[] @relation("AwayTeamLive")
|
||||
homeMatchesLive LiveMatch[] @relation("HomeTeamLive")
|
||||
playerEvents MatchPlayerEvents[]
|
||||
playerParticipations MatchPlayerParticipation[]
|
||||
basketballPlayerStats BasketballPlayerStats[]
|
||||
footballTeamStats FootballTeamStats[]
|
||||
basketballTeamStats BasketballTeamStats[]
|
||||
awayMatches Match[] @relation("AwayTeam")
|
||||
homeMatches Match[] @relation("HomeTeam")
|
||||
eloRating TeamEloRating?
|
||||
footballTeamStats FootballTeamStats[]
|
||||
basketballTeamStats BasketballTeamStats[]
|
||||
awayMatches Match[] @relation("AwayTeam")
|
||||
homeMatches Match[] @relation("HomeTeam")
|
||||
eloRating TeamEloRating?
|
||||
|
||||
@@index([name])
|
||||
@@index([sport])
|
||||
@@ -80,24 +80,24 @@ model Player {
|
||||
}
|
||||
|
||||
model Match {
|
||||
id String @id
|
||||
leagueId String? @map("league_id")
|
||||
homeTeamId String? @map("home_team_id")
|
||||
awayTeamId String? @map("away_team_id")
|
||||
id String @id
|
||||
leagueId String? @map("league_id")
|
||||
homeTeamId String? @map("home_team_id")
|
||||
awayTeamId String? @map("away_team_id")
|
||||
sport Sport
|
||||
matchName String? @map("match_name")
|
||||
matchSlug String? @map("match_slug")
|
||||
mstUtc BigInt @map("mst_utc")
|
||||
matchName String? @map("match_name")
|
||||
matchSlug String? @map("match_slug")
|
||||
mstUtc BigInt @map("mst_utc")
|
||||
status String?
|
||||
state String?
|
||||
scoreHome Int? @map("score_home")
|
||||
scoreAway Int? @map("score_away")
|
||||
htScoreHome Int? @map("ht_score_home")
|
||||
htScoreAway Int? @map("ht_score_away")
|
||||
scoreHome Int? @map("score_home")
|
||||
scoreAway Int? @map("score_away")
|
||||
htScoreHome Int? @map("ht_score_home")
|
||||
htScoreAway Int? @map("ht_score_away")
|
||||
winner String?
|
||||
iddaaCode String? @map("iddaa_code")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
iddaaCode String? @map("iddaa_code")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
footballAiFeatures FootballAiFeature?
|
||||
basketballAiFeatures BasketballAiFeature?
|
||||
officials MatchOfficial[]
|
||||
@@ -106,11 +106,12 @@ model Match {
|
||||
basketballPlayerStats BasketballPlayerStats[]
|
||||
footballTeamStats FootballTeamStats[]
|
||||
basketballTeamStats BasketballTeamStats[]
|
||||
awayTeam Team? @relation("AwayTeam", fields: [awayTeamId], references: [id])
|
||||
homeTeam Team? @relation("HomeTeam", fields: [homeTeamId], references: [id])
|
||||
league League? @relation(fields: [leagueId], references: [id])
|
||||
awayTeam Team? @relation("AwayTeam", fields: [awayTeamId], references: [id])
|
||||
homeTeam Team? @relation("HomeTeam", fields: [homeTeamId], references: [id])
|
||||
league League? @relation(fields: [leagueId], references: [id])
|
||||
oddCategories OddCategory[]
|
||||
prediction Prediction?
|
||||
predictionOutcomes PredictionOutcome[]
|
||||
couponItems UserCouponItem[]
|
||||
|
||||
@@index([awayTeamId])
|
||||
@@ -270,25 +271,25 @@ model TeamEloRating {
|
||||
}
|
||||
|
||||
model MatchPlayerEvents {
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
playerId String @map("player_id")
|
||||
teamId String @map("team_id")
|
||||
eventType EventType @map("event_type")
|
||||
eventSubtype String? @map("event_subtype")
|
||||
timeMinute String @map("time_minute")
|
||||
timeSeconds Int? @map("time_seconds")
|
||||
periodId Int? @map("period_id")
|
||||
assistPlayerId String? @map("assist_player_id")
|
||||
scoreAfter String? @map("score_after")
|
||||
playerOutId String? @map("player_out_id")
|
||||
position MatchPosition?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
assistPlayer Player? @relation("AssistPlayer", fields: [assistPlayerId], references: [id])
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
player Player @relation("EventPlayer", fields: [playerId], references: [id], onDelete: Cascade)
|
||||
substitutedOut Player? @relation("SubstitutedOut", fields: [playerOutId], references: [id])
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
playerId String @map("player_id")
|
||||
teamId String @map("team_id")
|
||||
eventType EventType @map("event_type")
|
||||
eventSubtype String? @map("event_subtype")
|
||||
timeMinute String @map("time_minute")
|
||||
timeSeconds Int? @map("time_seconds")
|
||||
periodId Int? @map("period_id")
|
||||
assistPlayerId String? @map("assist_player_id")
|
||||
scoreAfter String? @map("score_after")
|
||||
playerOutId String? @map("player_out_id")
|
||||
position MatchPosition?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
assistPlayer Player? @relation("AssistPlayer", fields: [assistPlayerId], references: [id])
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
player Player @relation("EventPlayer", fields: [playerId], references: [id], onDelete: Cascade)
|
||||
substitutedOut Player? @relation("SubstitutedOut", fields: [playerOutId], references: [id])
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([assistPlayerId])
|
||||
@@index([eventType])
|
||||
@@ -319,28 +320,28 @@ model MatchPlayerParticipation {
|
||||
}
|
||||
|
||||
model BasketballPlayerStats {
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
playerId String @map("player_id")
|
||||
teamId String @map("team_id")
|
||||
minutes String?
|
||||
points Int?
|
||||
rebounds Int?
|
||||
assists Int?
|
||||
steals Int?
|
||||
blocks Int?
|
||||
turnovers Int?
|
||||
fgMade Int? @map("fg_made")
|
||||
fgAttempted Int? @map("fg_attempted")
|
||||
threePtMade Int? @map("three_pt_made")
|
||||
threePtAttempted Int? @map("three_pt_attempted")
|
||||
ftMade Int? @map("ft_made")
|
||||
ftAttempted Int? @map("ft_attempted")
|
||||
fouls Int?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
playerId String @map("player_id")
|
||||
teamId String @map("team_id")
|
||||
minutes String?
|
||||
points Int?
|
||||
rebounds Int?
|
||||
assists Int?
|
||||
steals Int?
|
||||
blocks Int?
|
||||
turnovers Int?
|
||||
fgMade Int? @map("fg_made")
|
||||
fgAttempted Int? @map("fg_attempted")
|
||||
threePtMade Int? @map("three_pt_made")
|
||||
threePtAttempted Int? @map("three_pt_attempted")
|
||||
ftMade Int? @map("ft_made")
|
||||
ftAttempted Int? @map("ft_attempted")
|
||||
fouls Int?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([matchId, playerId, teamId])
|
||||
@@index([matchId])
|
||||
@@ -429,13 +430,13 @@ model OfficialRole {
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
model OddCategory {
|
||||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||||
matchId String @map("match_id")
|
||||
categoryJsonId Int? @map("category_json_id")
|
||||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||||
matchId String @map("match_id")
|
||||
categoryJsonId Int? @map("category_json_id")
|
||||
name String?
|
||||
sport Sport?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
selections OddSelection[]
|
||||
|
||||
@@unique([matchId, name])
|
||||
@@ -445,17 +446,17 @@ model OddCategory {
|
||||
}
|
||||
|
||||
model OddSelection {
|
||||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||||
categoryId Int @map("odd_category_db_id")
|
||||
dbId Int @id @default(autoincrement()) @map("db_id")
|
||||
categoryId Int @map("odd_category_db_id")
|
||||
name String?
|
||||
oddValue String? @map("odd_value")
|
||||
oddValue String? @map("odd_value")
|
||||
position String?
|
||||
sov Float?
|
||||
state String?
|
||||
sport Sport?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @map("updated_at")
|
||||
category OddCategory @relation(fields: [categoryId], references: [dbId], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @map("updated_at")
|
||||
category OddCategory @relation(fields: [categoryId], references: [dbId], onDelete: Cascade)
|
||||
history OddsHistory[]
|
||||
|
||||
@@unique([categoryId, name])
|
||||
@@ -505,6 +506,29 @@ model PredictionRun {
|
||||
@@map("prediction_runs")
|
||||
}
|
||||
|
||||
model PredictionOutcome {
|
||||
id BigInt @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
market String
|
||||
pick String
|
||||
probability Float
|
||||
confidence Float
|
||||
odds Float?
|
||||
playable Boolean @default(false)
|
||||
engineVersion String @map("engine_version")
|
||||
generatedAt DateTime @default(now()) @map("generated_at")
|
||||
resolved Boolean @default(false)
|
||||
hit Boolean?
|
||||
resolvedAt DateTime? @map("resolved_at")
|
||||
match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([matchId, market, engineVersion])
|
||||
@@index([market, resolvedAt(sort: Desc)])
|
||||
@@index([resolved, generatedAt])
|
||||
@@index([playable, resolved])
|
||||
@@map("prediction_outcomes")
|
||||
}
|
||||
|
||||
model AiPredictionsLog {
|
||||
id Int @id @default(autoincrement())
|
||||
matchId String @map("match_id")
|
||||
@@ -553,20 +577,20 @@ model User {
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique @map("user_id")
|
||||
paddleSubscriptionId String? @unique @map("paddle_subscription_id")
|
||||
paddleCustomerId String? @map("paddle_customer_id")
|
||||
plan SubscriptionStatus @default(free)
|
||||
billingInterval BillingInterval? @map("billing_interval")
|
||||
currentPeriodStart DateTime? @map("current_period_start")
|
||||
currentPeriodEnd DateTime? @map("current_period_end")
|
||||
cancelledAt DateTime? @map("cancelled_at")
|
||||
cancelEffectiveDate DateTime? @map("cancel_effective_date")
|
||||
paddlePriceId String? @map("paddle_price_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(uuid())
|
||||
userId String @unique @map("user_id")
|
||||
paddleSubscriptionId String? @unique @map("paddle_subscription_id")
|
||||
paddleCustomerId String? @map("paddle_customer_id")
|
||||
plan SubscriptionStatus @default(free)
|
||||
billingInterval BillingInterval? @map("billing_interval")
|
||||
currentPeriodStart DateTime? @map("current_period_start")
|
||||
currentPeriodEnd DateTime? @map("current_period_end")
|
||||
cancelledAt DateTime? @map("cancelled_at")
|
||||
cancelEffectiveDate DateTime? @map("cancel_effective_date")
|
||||
paddlePriceId String? @map("paddle_price_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([paddleSubscriptionId])
|
||||
@@index([paddleCustomerId])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller, Post, Body, HttpCode } from "@nestjs/common";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { I18n, I18nContext } from "nestjs-i18n";
|
||||
import { ApiTags, ApiOperation, ApiOkResponse } from "@nestjs/swagger";
|
||||
import { AuthService } from "./auth.service";
|
||||
@@ -21,6 +22,7 @@ export class AuthController {
|
||||
|
||||
@Post("register")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Register a new user" })
|
||||
@ApiOkResponse({
|
||||
@@ -37,6 +39,7 @@ export class AuthController {
|
||||
|
||||
@Post("login")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Login with email and password" })
|
||||
@ApiOkResponse({ description: "Login successful", type: TokenResponseDto })
|
||||
@@ -50,6 +53,7 @@ export class AuthController {
|
||||
|
||||
@Post("refresh")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Refresh access token" })
|
||||
@ApiOkResponse({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "../../../database/prisma.service";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -108,16 +109,21 @@ export class FrequencyEngineService {
|
||||
venue: "home" | "away",
|
||||
oddsBand: string,
|
||||
): Promise<TeamFrequencyRow | null> {
|
||||
const venueColumn = venue === "home" ? "m.home_team_id" : "m.away_team_id";
|
||||
const oddsSelection = venue === "home" ? "'1'" : "'2'";
|
||||
// venue is a typed literal ("home"|"away") — safe to use with Prisma.raw()
|
||||
const venueColumnRaw = Prisma.raw(
|
||||
venue === "home" ? "m.home_team_id" : "m.away_team_id",
|
||||
);
|
||||
const oddsSelectionValue = venue === "home" ? "1" : "2";
|
||||
const winConditionRaw = Prisma.raw(
|
||||
venue === "home" ? "score_home > score_away" : "score_away > score_home",
|
||||
);
|
||||
const bandRange = this.parseBandRange(oddsBand);
|
||||
|
||||
if (!bandRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = await this.prisma.$queryRawUnsafe<TeamFrequencyRow[]>(
|
||||
`
|
||||
const rows = await this.prisma.$queryRaw<TeamFrequencyRow[]>(Prisma.sql`
|
||||
WITH team_matches AS (
|
||||
SELECT
|
||||
m.id AS match_id,
|
||||
@@ -127,32 +133,26 @@ export class FrequencyEngineService {
|
||||
CAST(os.odd_value AS DECIMAL) AS team_odds
|
||||
FROM matches m
|
||||
JOIN odd_categories oc ON oc.match_id = m.id AND oc.name = 'Maç Sonucu'
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id AND os.name = ${oddsSelection}
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id AND os.name = ${oddsSelectionValue}
|
||||
WHERE m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND ${venueColumn} = $1
|
||||
AND CAST(os.odd_value AS DECIMAL) >= $2
|
||||
AND CAST(os.odd_value AS DECIMAL) < $3
|
||||
AND ${venueColumnRaw} = ${teamId}
|
||||
AND CAST(os.odd_value AS DECIMAL) >= ${bandRange.min}
|
||||
AND CAST(os.odd_value AS DECIMAL) < ${bandRange.max}
|
||||
)
|
||||
SELECT
|
||||
$1::text AS team_id,
|
||||
$4::text AS venue,
|
||||
$5::text AS odds_band,
|
||||
${teamId}::text AS team_id,
|
||||
${venue}::text AS venue,
|
||||
${oddsBand}::text AS odds_band,
|
||||
COUNT(*)::int AS total_matches,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 1 THEN 1.0 ELSE 0.0 END), 0)::float AS ou15_rate,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 2 THEN 1.0 ELSE 0.0 END), 0)::float AS ou25_rate,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 3 THEN 1.0 ELSE 0.0 END), 0)::float AS ou35_rate,
|
||||
COALESCE(AVG(CASE WHEN score_home > 0 AND score_away > 0 THEN 1.0 ELSE 0.0 END), 0)::float AS btts_rate,
|
||||
COALESCE(AVG(CASE WHEN ${venue === "home" ? "score_home > score_away" : "score_away > score_home"} THEN 1.0 ELSE 0.0 END), 0)::float AS win_rate,
|
||||
COALESCE(AVG(CASE WHEN ${winConditionRaw} THEN 1.0 ELSE 0.0 END), 0)::float AS win_rate,
|
||||
COALESCE(AVG(total_goals), 0)::float AS avg_goals
|
||||
FROM team_matches
|
||||
`,
|
||||
teamId,
|
||||
bandRange.min,
|
||||
bandRange.max,
|
||||
venue,
|
||||
oddsBand,
|
||||
);
|
||||
`);
|
||||
|
||||
if (!rows.length || rows[0].total_matches < MIN_MATCHES) {
|
||||
return null;
|
||||
@@ -335,8 +335,7 @@ export class FrequencyEngineService {
|
||||
|
||||
if (matchIds && matchIds.length > 0) {
|
||||
// Belirli maçlar istendi
|
||||
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||
`
|
||||
return this.prisma.$queryRaw<UpcomingMatchRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
lm.id AS match_id,
|
||||
lm.home_team_id,
|
||||
@@ -359,18 +358,15 @@ export class FrequencyEngineService {
|
||||
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||
WHERE lm.id = ANY($1)
|
||||
WHERE lm.id = ANY(${matchIds})
|
||||
AND lm.odds IS NOT NULL
|
||||
AND lm.odds != 'null'::jsonb
|
||||
ORDER BY lm.mst_utc ASC
|
||||
`,
|
||||
matchIds,
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
// Otomatik: yaklaşan tüm maçlar
|
||||
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||
`
|
||||
return this.prisma.$queryRaw<UpcomingMatchRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
lm.id AS match_id,
|
||||
lm.home_team_id,
|
||||
@@ -393,26 +389,22 @@ export class FrequencyEngineService {
|
||||
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||
WHERE lm.mst_utc >= $1
|
||||
WHERE lm.mst_utc >= ${BigInt(nowMs)}
|
||||
AND lm.sport = 'football'
|
||||
AND lm.odds IS NOT NULL
|
||||
AND lm.odds != 'null'::jsonb
|
||||
AND (lm.status IS NULL OR lm.status NOT IN ('FT', 'AET', 'PEN', 'ABD', 'CANC', 'PST', 'SUSP', 'INT', 'AWD', 'WO'))
|
||||
AND (lm.state IS NULL OR lm.state NOT IN ('after', 'postponed', 'cancelled', 'abandoned'))
|
||||
ORDER BY lm.mst_utc ASC
|
||||
LIMIT $2
|
||||
`,
|
||||
BigInt(nowMs),
|
||||
limit,
|
||||
);
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lig bazlı gol profili.
|
||||
*/
|
||||
async getLeagueProfile(leagueId: string): Promise<LeagueProfileRow | null> {
|
||||
const rows = await this.prisma.$queryRawUnsafe<LeagueProfileRow[]>(
|
||||
`
|
||||
const rows = await this.prisma.$queryRaw<LeagueProfileRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
m.league_id,
|
||||
l.name AS league_name,
|
||||
@@ -424,12 +416,10 @@ export class FrequencyEngineService {
|
||||
JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.league_id = $1
|
||||
AND m.league_id = ${leagueId}
|
||||
GROUP BY m.league_id, l.name
|
||||
HAVING COUNT(*) >= 20
|
||||
`,
|
||||
leagueId,
|
||||
);
|
||||
`);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import {
|
||||
Sport,
|
||||
@@ -858,11 +859,11 @@ export class FeederPersistenceService {
|
||||
// Use raw SQL for performance — Prisma's { some: {} } relation filters
|
||||
// generate heavy correlated subqueries that hang on Raspberry Pi with
|
||||
// large tables (15M+ odd_selections, 3M+ participations).
|
||||
const result = await this.prisma.$queryRawUnsafe<Array<{ id: string }>>(
|
||||
`
|
||||
const result = await this.prisma.$queryRaw<Array<{ id: string }>>(
|
||||
Prisma.sql`
|
||||
SELECT m.id
|
||||
FROM matches m
|
||||
WHERE m.id = ANY($1::text[])
|
||||
WHERE m.id = ANY(${matchIds}::text[])
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
AND (
|
||||
(m.sport = 'football'
|
||||
@@ -875,7 +876,6 @@ export class FeederPersistenceService {
|
||||
AND EXISTS (SELECT 1 FROM basketball_player_stats bps WHERE bps.match_id = m.id))
|
||||
)
|
||||
`,
|
||||
matchIds,
|
||||
);
|
||||
|
||||
return result.map((r) => r.id);
|
||||
|
||||
Reference in New Issue
Block a user