@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/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
+ }