@opprs/db-prisma 0.5.2-canary.8e722bf
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/index.cjs +573 -0
- package/dist/index.d.cts +608 -0
- package/dist/index.d.ts +608 -0
- package/dist/index.js +500 -0
- package/package.json +85 -0
- package/prisma/schema.prisma +123 -0
- package/prisma/seed.ts +349 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { PrismaClient } from "@prisma/client";
|
|
3
|
+
var globalForPrisma = globalThis;
|
|
4
|
+
var prisma = globalForPrisma.prisma ?? new PrismaClient({
|
|
5
|
+
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"]
|
|
6
|
+
});
|
|
7
|
+
if (process.env.NODE_ENV !== "production") {
|
|
8
|
+
globalForPrisma.prisma = prisma;
|
|
9
|
+
}
|
|
10
|
+
async function disconnect() {
|
|
11
|
+
await prisma.$disconnect();
|
|
12
|
+
}
|
|
13
|
+
async function connect() {
|
|
14
|
+
await prisma.$connect();
|
|
15
|
+
}
|
|
16
|
+
async function testConnection() {
|
|
17
|
+
try {
|
|
18
|
+
await prisma.$queryRaw`SELECT 1`;
|
|
19
|
+
return true;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("Database connection test failed:", error);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/players.ts
|
|
27
|
+
async function createPlayer(data) {
|
|
28
|
+
return prisma.player.create({
|
|
29
|
+
data
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function findPlayerById(id, include) {
|
|
33
|
+
return prisma.player.findUnique({
|
|
34
|
+
where: { id },
|
|
35
|
+
include
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function findPlayerByExternalId(externalId, include) {
|
|
39
|
+
return prisma.player.findUnique({
|
|
40
|
+
where: { externalId },
|
|
41
|
+
include
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function findPlayerByEmail(email, include) {
|
|
45
|
+
return prisma.player.findUnique({
|
|
46
|
+
where: { email },
|
|
47
|
+
include
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function findPlayers(options = {}) {
|
|
51
|
+
return prisma.player.findMany({
|
|
52
|
+
take: options.take,
|
|
53
|
+
skip: options.skip,
|
|
54
|
+
where: options.where,
|
|
55
|
+
orderBy: options.orderBy,
|
|
56
|
+
include: options.include
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function getRatedPlayers(options = {}) {
|
|
60
|
+
return findPlayers({
|
|
61
|
+
...options,
|
|
62
|
+
where: { isRated: true }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function getTopPlayersByRating(limit = 50) {
|
|
66
|
+
return findPlayers({
|
|
67
|
+
take: limit,
|
|
68
|
+
orderBy: { rating: "desc" },
|
|
69
|
+
where: { isRated: true }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function getTopPlayersByRanking(limit = 50) {
|
|
73
|
+
return findPlayers({
|
|
74
|
+
take: limit,
|
|
75
|
+
orderBy: { ranking: "asc" },
|
|
76
|
+
where: {
|
|
77
|
+
isRated: true,
|
|
78
|
+
ranking: { not: null }
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async function updatePlayer(id, data) {
|
|
83
|
+
return prisma.player.update({
|
|
84
|
+
where: { id },
|
|
85
|
+
data
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function updatePlayerRating(id, rating, ratingDeviation, eventCount) {
|
|
89
|
+
const updateData = {
|
|
90
|
+
rating,
|
|
91
|
+
ratingDeviation,
|
|
92
|
+
lastRatingUpdate: /* @__PURE__ */ new Date(),
|
|
93
|
+
lastEventDate: /* @__PURE__ */ new Date()
|
|
94
|
+
};
|
|
95
|
+
if (eventCount !== void 0) {
|
|
96
|
+
updateData.eventCount = eventCount;
|
|
97
|
+
updateData.isRated = eventCount >= 5;
|
|
98
|
+
}
|
|
99
|
+
return updatePlayer(id, updateData);
|
|
100
|
+
}
|
|
101
|
+
async function deletePlayer(id) {
|
|
102
|
+
return prisma.player.delete({
|
|
103
|
+
where: { id }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function countPlayers(where) {
|
|
107
|
+
return prisma.player.count({ where });
|
|
108
|
+
}
|
|
109
|
+
async function getPlayerWithResults(id) {
|
|
110
|
+
const player = await prisma.player.findUnique({
|
|
111
|
+
where: { id },
|
|
112
|
+
include: {
|
|
113
|
+
tournamentResults: {
|
|
114
|
+
include: {
|
|
115
|
+
tournament: true
|
|
116
|
+
},
|
|
117
|
+
orderBy: {
|
|
118
|
+
tournament: {
|
|
119
|
+
date: "desc"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
if (!player) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...player,
|
|
130
|
+
results: player.tournamentResults
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function searchPlayers(query, limit = 20) {
|
|
134
|
+
return findPlayers({
|
|
135
|
+
take: limit,
|
|
136
|
+
where: {
|
|
137
|
+
OR: [
|
|
138
|
+
{ name: { contains: query, mode: "insensitive" } },
|
|
139
|
+
{ email: { contains: query, mode: "insensitive" } }
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/tournaments.ts
|
|
146
|
+
async function createTournament(data) {
|
|
147
|
+
return prisma.tournament.create({
|
|
148
|
+
data: {
|
|
149
|
+
...data,
|
|
150
|
+
eventBooster: data.eventBooster ?? "NONE"
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async function findTournamentById(id, include) {
|
|
155
|
+
return prisma.tournament.findUnique({
|
|
156
|
+
where: { id },
|
|
157
|
+
include
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function findTournamentByExternalId(externalId, include) {
|
|
161
|
+
return prisma.tournament.findUnique({
|
|
162
|
+
where: { externalId },
|
|
163
|
+
include
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function findTournaments(options = {}) {
|
|
167
|
+
return prisma.tournament.findMany({
|
|
168
|
+
take: options.take,
|
|
169
|
+
skip: options.skip,
|
|
170
|
+
where: options.where,
|
|
171
|
+
orderBy: options.orderBy,
|
|
172
|
+
include: options.include
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function getRecentTournaments(limit = 20, include) {
|
|
176
|
+
return findTournaments({
|
|
177
|
+
take: limit,
|
|
178
|
+
orderBy: { date: "desc" },
|
|
179
|
+
include
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function getTournamentsByDateRange(startDate, endDate, options = {}) {
|
|
183
|
+
return findTournaments({
|
|
184
|
+
...options,
|
|
185
|
+
where: {
|
|
186
|
+
date: {
|
|
187
|
+
gte: startDate,
|
|
188
|
+
lte: endDate
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function getTournamentsByBoosterType(boosterType, options = {}) {
|
|
194
|
+
return findTournaments({
|
|
195
|
+
...options,
|
|
196
|
+
where: { eventBooster: boosterType }
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function getMajorTournaments(limit) {
|
|
200
|
+
return findTournaments({
|
|
201
|
+
take: limit,
|
|
202
|
+
where: { eventBooster: "MAJOR" },
|
|
203
|
+
orderBy: { date: "desc" }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function updateTournament(id, data) {
|
|
207
|
+
return prisma.tournament.update({
|
|
208
|
+
where: { id },
|
|
209
|
+
data
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function deleteTournament(id) {
|
|
213
|
+
return prisma.tournament.delete({
|
|
214
|
+
where: { id }
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function countTournaments(where) {
|
|
218
|
+
return prisma.tournament.count({ where });
|
|
219
|
+
}
|
|
220
|
+
async function getTournamentWithResults(id) {
|
|
221
|
+
return prisma.tournament.findUnique({
|
|
222
|
+
where: { id },
|
|
223
|
+
include: {
|
|
224
|
+
results: {
|
|
225
|
+
include: {
|
|
226
|
+
player: true
|
|
227
|
+
},
|
|
228
|
+
orderBy: {
|
|
229
|
+
position: "asc"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function searchTournaments(query, limit = 20) {
|
|
236
|
+
return findTournaments({
|
|
237
|
+
take: limit,
|
|
238
|
+
where: {
|
|
239
|
+
OR: [
|
|
240
|
+
{ name: { contains: query, mode: "insensitive" } },
|
|
241
|
+
{ location: { contains: query, mode: "insensitive" } }
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
orderBy: { date: "desc" }
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function getTournamentStats(id) {
|
|
248
|
+
const tournament = await getTournamentWithResults(id);
|
|
249
|
+
if (!tournament) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const playerCount = tournament.results.length;
|
|
253
|
+
if (playerCount === 0) {
|
|
254
|
+
return {
|
|
255
|
+
tournament,
|
|
256
|
+
playerCount: 0,
|
|
257
|
+
averagePoints: 0,
|
|
258
|
+
averageEfficiency: 0,
|
|
259
|
+
highestPoints: 0,
|
|
260
|
+
lowestPoints: 0
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const totalPoints = tournament.results.reduce((sum, r) => sum + (r.totalPoints || 0), 0);
|
|
264
|
+
const totalEfficiency = tournament.results.reduce((sum, r) => sum + (r.efficiency || 0), 0);
|
|
265
|
+
const allPoints = tournament.results.map((r) => r.totalPoints || 0);
|
|
266
|
+
return {
|
|
267
|
+
tournament,
|
|
268
|
+
playerCount,
|
|
269
|
+
averagePoints: totalPoints / playerCount,
|
|
270
|
+
averageEfficiency: totalEfficiency / playerCount,
|
|
271
|
+
highestPoints: Math.max(...allPoints),
|
|
272
|
+
lowestPoints: Math.min(...allPoints)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/results.ts
|
|
277
|
+
async function createResult(data) {
|
|
278
|
+
const resultData = {
|
|
279
|
+
...data,
|
|
280
|
+
decayedPoints: data.decayedPoints ?? data.totalPoints ?? 0
|
|
281
|
+
};
|
|
282
|
+
return prisma.tournamentResult.create({
|
|
283
|
+
data: resultData
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async function createManyResults(data) {
|
|
287
|
+
const resultsData = data.map((item) => ({
|
|
288
|
+
...item,
|
|
289
|
+
decayedPoints: item.decayedPoints ?? item.totalPoints ?? 0
|
|
290
|
+
}));
|
|
291
|
+
return prisma.tournamentResult.createMany({
|
|
292
|
+
data: resultsData
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function findResultById(id, include) {
|
|
296
|
+
return prisma.tournamentResult.findUnique({
|
|
297
|
+
where: { id },
|
|
298
|
+
include
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function findResultByPlayerAndTournament(playerId, tournamentId, include) {
|
|
302
|
+
return prisma.tournamentResult.findUnique({
|
|
303
|
+
where: {
|
|
304
|
+
playerId_tournamentId: {
|
|
305
|
+
playerId,
|
|
306
|
+
tournamentId
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
include
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function findResults(options = {}) {
|
|
313
|
+
return prisma.tournamentResult.findMany({
|
|
314
|
+
take: options.take,
|
|
315
|
+
skip: options.skip,
|
|
316
|
+
where: options.where,
|
|
317
|
+
orderBy: options.orderBy,
|
|
318
|
+
include: options.include
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async function getPlayerResults(playerId, options = {}) {
|
|
322
|
+
return findResults({
|
|
323
|
+
...options,
|
|
324
|
+
where: { playerId },
|
|
325
|
+
include: { tournament: true, ...options.include },
|
|
326
|
+
orderBy: { tournament: { date: "desc" } }
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
async function getTournamentResults(tournamentId, options = {}) {
|
|
330
|
+
return findResults({
|
|
331
|
+
...options,
|
|
332
|
+
where: { tournamentId },
|
|
333
|
+
include: { player: true, ...options.include },
|
|
334
|
+
orderBy: { position: "asc" }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async function getPlayerTopFinishes(playerId, limit = 15) {
|
|
338
|
+
return findResults({
|
|
339
|
+
where: { playerId },
|
|
340
|
+
take: limit,
|
|
341
|
+
include: { tournament: true },
|
|
342
|
+
orderBy: { decayedPoints: "desc" }
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async function updateResult(id, data) {
|
|
346
|
+
return prisma.tournamentResult.update({
|
|
347
|
+
where: { id },
|
|
348
|
+
data
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async function updateResultPoints(id, linearPoints, dynamicPoints, totalPoints) {
|
|
352
|
+
const result = await findResultById(id, {
|
|
353
|
+
tournament: true
|
|
354
|
+
});
|
|
355
|
+
if (!result) {
|
|
356
|
+
throw new Error(`Result with id ${id} not found`);
|
|
357
|
+
}
|
|
358
|
+
const now = /* @__PURE__ */ new Date();
|
|
359
|
+
const tournamentDate = result.tournament.date;
|
|
360
|
+
const ageInDays = Math.floor((now.getTime() - tournamentDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
361
|
+
const ageInYears = ageInDays / 365;
|
|
362
|
+
let decayMultiplier = 0;
|
|
363
|
+
if (ageInYears < 1) {
|
|
364
|
+
decayMultiplier = 1;
|
|
365
|
+
} else if (ageInYears < 2) {
|
|
366
|
+
decayMultiplier = 0.75;
|
|
367
|
+
} else if (ageInYears < 3) {
|
|
368
|
+
decayMultiplier = 0.5;
|
|
369
|
+
} else {
|
|
370
|
+
decayMultiplier = 0;
|
|
371
|
+
}
|
|
372
|
+
const decayedPoints = totalPoints * decayMultiplier;
|
|
373
|
+
return updateResult(id, {
|
|
374
|
+
linearPoints,
|
|
375
|
+
dynamicPoints,
|
|
376
|
+
totalPoints,
|
|
377
|
+
ageInDays,
|
|
378
|
+
decayMultiplier,
|
|
379
|
+
decayedPoints
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function deleteResult(id) {
|
|
383
|
+
return prisma.tournamentResult.delete({
|
|
384
|
+
where: { id }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function deleteResultsByTournament(tournamentId) {
|
|
388
|
+
return prisma.tournamentResult.deleteMany({
|
|
389
|
+
where: { tournamentId }
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async function countResults(where) {
|
|
393
|
+
return prisma.tournamentResult.count({ where });
|
|
394
|
+
}
|
|
395
|
+
async function getPlayerStats(playerId) {
|
|
396
|
+
const results = await getPlayerResults(playerId);
|
|
397
|
+
if (results.length === 0) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const totalPoints = results.reduce((sum, r) => sum + (r.totalPoints || 0), 0);
|
|
401
|
+
const totalDecayedPoints = results.reduce((sum, r) => sum + (r.decayedPoints || 0), 0);
|
|
402
|
+
const averagePosition = results.reduce((sum, r) => sum + r.position, 0) / results.length;
|
|
403
|
+
const averageEfficiency = results.reduce((sum, r) => sum + (r.efficiency || 0), 0) / results.length;
|
|
404
|
+
const firstPlaceFinishes = results.filter((r) => r.position === 1).length;
|
|
405
|
+
const topThreeFinishes = results.filter((r) => r.position <= 3).length;
|
|
406
|
+
return {
|
|
407
|
+
totalEvents: results.length,
|
|
408
|
+
totalPoints,
|
|
409
|
+
totalDecayedPoints,
|
|
410
|
+
averagePoints: totalPoints / results.length,
|
|
411
|
+
averagePosition,
|
|
412
|
+
averageFinish: averagePosition,
|
|
413
|
+
averageEfficiency,
|
|
414
|
+
firstPlaceFinishes,
|
|
415
|
+
topThreeFinishes,
|
|
416
|
+
bestFinish: Math.min(...results.map((r) => r.position)),
|
|
417
|
+
highestPoints: Math.max(...results.map((r) => r.totalPoints || 0))
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async function recalculateTimeDecay(referenceDate = /* @__PURE__ */ new Date()) {
|
|
421
|
+
const results = await findResults({
|
|
422
|
+
include: { tournament: true }
|
|
423
|
+
});
|
|
424
|
+
const updates = results.map((result) => {
|
|
425
|
+
const tournamentDate = result.tournament.date;
|
|
426
|
+
const ageInDays = Math.floor(
|
|
427
|
+
(referenceDate.getTime() - tournamentDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
428
|
+
);
|
|
429
|
+
const ageInYears = ageInDays / 365;
|
|
430
|
+
let decayMultiplier = 0;
|
|
431
|
+
if (ageInYears < 1) {
|
|
432
|
+
decayMultiplier = 1;
|
|
433
|
+
} else if (ageInYears < 2) {
|
|
434
|
+
decayMultiplier = 0.75;
|
|
435
|
+
} else if (ageInYears < 3) {
|
|
436
|
+
decayMultiplier = 0.5;
|
|
437
|
+
} else {
|
|
438
|
+
decayMultiplier = 0;
|
|
439
|
+
}
|
|
440
|
+
const decayedPoints = (result.totalPoints || 0) * decayMultiplier;
|
|
441
|
+
return prisma.tournamentResult.update({
|
|
442
|
+
where: { id: result.id },
|
|
443
|
+
data: {
|
|
444
|
+
ageInDays,
|
|
445
|
+
decayMultiplier,
|
|
446
|
+
decayedPoints
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
return Promise.all(updates);
|
|
451
|
+
}
|
|
452
|
+
export {
|
|
453
|
+
connect,
|
|
454
|
+
countPlayers,
|
|
455
|
+
countResults,
|
|
456
|
+
countTournaments,
|
|
457
|
+
createManyResults,
|
|
458
|
+
createPlayer,
|
|
459
|
+
createResult,
|
|
460
|
+
createTournament,
|
|
461
|
+
deletePlayer,
|
|
462
|
+
deleteResult,
|
|
463
|
+
deleteResultsByTournament,
|
|
464
|
+
deleteTournament,
|
|
465
|
+
disconnect,
|
|
466
|
+
findPlayerByEmail,
|
|
467
|
+
findPlayerByExternalId,
|
|
468
|
+
findPlayerById,
|
|
469
|
+
findPlayers,
|
|
470
|
+
findResultById,
|
|
471
|
+
findResultByPlayerAndTournament,
|
|
472
|
+
findResults,
|
|
473
|
+
findTournamentByExternalId,
|
|
474
|
+
findTournamentById,
|
|
475
|
+
findTournaments,
|
|
476
|
+
getMajorTournaments,
|
|
477
|
+
getPlayerResults,
|
|
478
|
+
getPlayerStats,
|
|
479
|
+
getPlayerTopFinishes,
|
|
480
|
+
getPlayerWithResults,
|
|
481
|
+
getRatedPlayers,
|
|
482
|
+
getRecentTournaments,
|
|
483
|
+
getTopPlayersByRanking,
|
|
484
|
+
getTopPlayersByRating,
|
|
485
|
+
getTournamentResults,
|
|
486
|
+
getTournamentStats,
|
|
487
|
+
getTournamentWithResults,
|
|
488
|
+
getTournamentsByBoosterType,
|
|
489
|
+
getTournamentsByDateRange,
|
|
490
|
+
prisma,
|
|
491
|
+
recalculateTimeDecay,
|
|
492
|
+
searchPlayers,
|
|
493
|
+
searchTournaments,
|
|
494
|
+
testConnection,
|
|
495
|
+
updatePlayer,
|
|
496
|
+
updatePlayerRating,
|
|
497
|
+
updateResult,
|
|
498
|
+
updateResultPoints,
|
|
499
|
+
updateTournament
|
|
500
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opprs/db-prisma",
|
|
3
|
+
"version": "0.5.2-canary.8e722bf",
|
|
4
|
+
"description": "Database backend for OPPR (Open Pinball Player Ranking System) using Prisma and PostgreSQL",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"oppr",
|
|
7
|
+
"pinball",
|
|
8
|
+
"ranking",
|
|
9
|
+
"database",
|
|
10
|
+
"prisma",
|
|
11
|
+
"postgresql",
|
|
12
|
+
"tournament"
|
|
13
|
+
],
|
|
14
|
+
"author": "Mitch McAffee",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/themcaffee/OPPR",
|
|
19
|
+
"directory": "packages/db-prisma"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"prisma",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@prisma/client": "^6.2.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@testcontainers/postgresql": "^11.11.0",
|
|
43
|
+
"@types/node": "^22.10.5",
|
|
44
|
+
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
|
45
|
+
"@typescript-eslint/parser": "^8.19.1",
|
|
46
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
47
|
+
"@vitest/ui": "^4.0.16",
|
|
48
|
+
"eslint": "^9.17.0",
|
|
49
|
+
"prettier": "^3.4.2",
|
|
50
|
+
"prisma": "^6.2.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"tsx": "^4.19.2",
|
|
53
|
+
"typescript": "^5.7.2",
|
|
54
|
+
"vitest": "^4.0.16"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@opprs/core": "^0.5.2-canary.8e722bf"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
},
|
|
62
|
+
"prisma": {
|
|
63
|
+
"seed": "tsx prisma/seed.ts"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
67
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"lint": "eslint src --ext .ts",
|
|
70
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
71
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
72
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
73
|
+
"test": "vitest run",
|
|
74
|
+
"test:watch": "vitest watch",
|
|
75
|
+
"test:ui": "vitest --ui",
|
|
76
|
+
"test:coverage": "vitest run --coverage",
|
|
77
|
+
"db:generate": "prisma generate",
|
|
78
|
+
"db:push": "prisma db push",
|
|
79
|
+
"db:migrate": "prisma migrate dev",
|
|
80
|
+
"db:migrate:prod": "prisma migrate deploy",
|
|
81
|
+
"db:studio": "prisma studio",
|
|
82
|
+
"db:seed": "tsx prisma/seed.ts",
|
|
83
|
+
"db:reset": "prisma migrate reset"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Prisma schema for OPPR Database
|
|
2
|
+
// Documentation: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
generator client {
|
|
5
|
+
provider = "prisma-client-js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
url = env("DATABASE_URL")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Player model - represents a pinball player
|
|
14
|
+
model Player {
|
|
15
|
+
id String @id @default(cuid())
|
|
16
|
+
createdAt DateTime @default(now())
|
|
17
|
+
updatedAt DateTime @updatedAt
|
|
18
|
+
|
|
19
|
+
// Player identification
|
|
20
|
+
externalId String? @unique // External ID from OPPR or other systems
|
|
21
|
+
name String?
|
|
22
|
+
email String? @unique
|
|
23
|
+
|
|
24
|
+
// OPPR Rating fields
|
|
25
|
+
rating Float @default(1500) // Glicko rating
|
|
26
|
+
ratingDeviation Float @default(200) // Rating uncertainty (RD)
|
|
27
|
+
ranking Int? // World ranking position
|
|
28
|
+
isRated Boolean @default(false) // Has 5+ events
|
|
29
|
+
eventCount Int @default(0) // Number of events participated
|
|
30
|
+
|
|
31
|
+
// Timestamps for rating calculations
|
|
32
|
+
lastRatingUpdate DateTime @default(now())
|
|
33
|
+
lastEventDate DateTime?
|
|
34
|
+
|
|
35
|
+
// Relations
|
|
36
|
+
tournamentResults TournamentResult[]
|
|
37
|
+
|
|
38
|
+
@@index([email])
|
|
39
|
+
@@index([externalId])
|
|
40
|
+
@@index([rating])
|
|
41
|
+
@@index([ranking])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Tournament model - represents a pinball tournament event
|
|
45
|
+
model Tournament {
|
|
46
|
+
id String @id @default(cuid())
|
|
47
|
+
createdAt DateTime @default(now())
|
|
48
|
+
updatedAt DateTime @updatedAt
|
|
49
|
+
|
|
50
|
+
// Tournament identification
|
|
51
|
+
externalId String? @unique // External ID from OPPR or other systems
|
|
52
|
+
name String
|
|
53
|
+
location String?
|
|
54
|
+
date DateTime
|
|
55
|
+
|
|
56
|
+
// Tournament configuration (stored as JSON)
|
|
57
|
+
// Contains TGPConfig structure from OPPR
|
|
58
|
+
tgpConfig Json?
|
|
59
|
+
|
|
60
|
+
// Event classification
|
|
61
|
+
eventBooster EventBoosterType @default(NONE)
|
|
62
|
+
allowsOptOut Boolean @default(false)
|
|
63
|
+
|
|
64
|
+
// Tournament value calculations (can be calculated or cached)
|
|
65
|
+
baseValue Float?
|
|
66
|
+
tvaRating Float?
|
|
67
|
+
tvaRanking Float?
|
|
68
|
+
totalTVA Float?
|
|
69
|
+
tgp Float?
|
|
70
|
+
eventBoosterMultiplier Float?
|
|
71
|
+
firstPlaceValue Float?
|
|
72
|
+
|
|
73
|
+
// Relations
|
|
74
|
+
results TournamentResult[]
|
|
75
|
+
|
|
76
|
+
@@index([date])
|
|
77
|
+
@@index([eventBooster])
|
|
78
|
+
@@index([externalId])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Tournament Result - junction table linking players to tournaments
|
|
82
|
+
model TournamentResult {
|
|
83
|
+
id String @id @default(cuid())
|
|
84
|
+
createdAt DateTime @default(now())
|
|
85
|
+
updatedAt DateTime @updatedAt
|
|
86
|
+
|
|
87
|
+
// Relations
|
|
88
|
+
playerId String
|
|
89
|
+
player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
|
|
90
|
+
tournamentId String
|
|
91
|
+
tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade)
|
|
92
|
+
|
|
93
|
+
// Result data
|
|
94
|
+
position Int // Finishing position (1 = first place)
|
|
95
|
+
optedOut Boolean @default(false)
|
|
96
|
+
|
|
97
|
+
// Points awarded
|
|
98
|
+
linearPoints Float? @default(0) // Linear distribution points
|
|
99
|
+
dynamicPoints Float? @default(0) // Dynamic distribution points
|
|
100
|
+
totalPoints Float? // Total points (linear + dynamic)
|
|
101
|
+
|
|
102
|
+
// Time decay tracking
|
|
103
|
+
ageInDays Int? @default(0) // Age of event in days at calculation time
|
|
104
|
+
decayMultiplier Float? @default(1.0) // Time decay multiplier (1.0, 0.75, 0.5, or 0.0)
|
|
105
|
+
decayedPoints Float? // Points after applying decay (defaults to totalPoints via application logic)
|
|
106
|
+
|
|
107
|
+
// Efficiency tracking
|
|
108
|
+
efficiency Float? // Performance efficiency percentage
|
|
109
|
+
|
|
110
|
+
@@unique([playerId, tournamentId])
|
|
111
|
+
@@index([playerId])
|
|
112
|
+
@@index([tournamentId])
|
|
113
|
+
@@index([position])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Enum for event booster types
|
|
117
|
+
enum EventBoosterType {
|
|
118
|
+
NONE
|
|
119
|
+
CERTIFIED
|
|
120
|
+
CERTIFIED_PLUS
|
|
121
|
+
CHAMPIONSHIP_SERIES
|
|
122
|
+
MAJOR
|
|
123
|
+
}
|