@vatvaghool/create-ipl-dashboard 0.1.13 → 0.1.15
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/README.md +10 -7
- package/package.json +1 -1
- package/src/index.mjs +13 -3
- package/src/prompts.mjs +34 -1
- package/src/scaffold.mjs +17 -7
- package/template/app/api/ipl/route.ts +32 -63
- package/template/app/api/ipl/transfers/route.ts +15 -55
- package/template/app/api/ops/status/route.ts +23 -57
- package/template/app/lib/config.ts +4 -0
- package/template/app/lib/storage/google-sheets-storage.ts +147 -0
- package/template/app/lib/storage/index.ts +20 -0
- package/template/app/lib/storage/mongo-storage.ts +53 -0
- package/template/app/lib/storage/types.ts +13 -0
- package/template/scripts/seed-league.mjs +55 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Scaffold a full-featured IPL fantasy cricket dashboard in seconds.
|
|
|
6
6
|
npx @vatvaghool/create-ipl-dashboard my-league
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Follow the prompts to enter your fantasy league URL, collection name, and team names —
|
|
9
|
+
Follow the prompts to enter your MongoDB URI, fantasy league URL, collection name, and team names — you get a ready-to-run Next.js dashboard with standings charts, performance trackers, AI roasts, and more.
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
@@ -27,12 +27,15 @@ npx @vatvaghool/create-ipl-dashboard [project-name] [options]
|
|
|
27
27
|
| Prompt | Description |
|
|
28
28
|
|--------|-------------|
|
|
29
29
|
| Project name | Directory to scaffold into |
|
|
30
|
+
| Storage backend | `mongodb` (default) or `google_sheets` |
|
|
31
|
+
| MongoDB URI | Your MongoDB connection string (only if MongoDB chosen, press Enter for default) |
|
|
32
|
+
| Google Sheet ID | Google Sheet ID (only if google_sheets chosen) |
|
|
33
|
+
| Service account email | Google service account email (only if google_sheets chosen) |
|
|
34
|
+
| Private key | Google service account private key (only if google_sheets chosen) |
|
|
30
35
|
| League URL | The fantasy.iplt20.com league page URL |
|
|
31
|
-
| Collection name | MongoDB collection
|
|
36
|
+
| Collection/sheet name | MongoDB collection or Google sheet for this league's data |
|
|
32
37
|
| League name | Display name for your league |
|
|
33
|
-
| Teams | Team names (and optional owners) in your league
|
|
34
|
-
|
|
35
|
-
The database connection (`MONGODB_URI`) is hardcoded — you only need to specify which collection each league should use.
|
|
38
|
+
| Teams | Team names (and optional owners) in your league
|
|
36
39
|
|
|
37
40
|
If `--scrape` is provided, the CLI attempts to extract team names from the league page HTML. If that fails, it falls back to manual entry.
|
|
38
41
|
|
|
@@ -97,7 +100,7 @@ Open http://localhost:3000 to see your dashboard.
|
|
|
97
100
|
The CLI:
|
|
98
101
|
|
|
99
102
|
1. Copies a pre-built Next.js app template
|
|
100
|
-
2. Writes your `.env` with the
|
|
103
|
+
2. Writes your `.env` with the `MONGODB_URI`, `COLLECTION_NAME`, league URL, and league name
|
|
101
104
|
3. Generates `app/data/teams.ts` with your team roster
|
|
102
105
|
4. Generates `app/data/league.ts` with league metadata
|
|
103
106
|
5. Creates a placeholder `app/data/match-points.ts` (auto-populates as you sync)
|
|
@@ -132,4 +135,4 @@ npm run build
|
|
|
132
135
|
npx vercel --prod
|
|
133
136
|
```
|
|
134
137
|
|
|
135
|
-
Set `IPL_POST_SECRET` in your Vercel dashboard. The `MONGODB_URI` and `COLLECTION_NAME` are already
|
|
138
|
+
Set `IPL_POST_SECRET` in your Vercel dashboard. The `MONGODB_URI` and `COLLECTION_NAME` are already populated in the scaffolded `.env`.
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { getProjectName, getLeagueUrl, getCollectionName, getLeagueName, getTeams, close } from "./prompts.mjs";
|
|
6
|
+
import { getProjectName, getMongoUri, getStorageBackend, getGoogleSheetsConfig, getLeagueUrl, getCollectionName, getLeagueName, getTeams, close } from "./prompts.mjs";
|
|
7
7
|
import { scaffoldProject } from "./scaffold.mjs";
|
|
8
8
|
import { scrapeTeamsFromUrl } from "./scraper.mjs";
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
|
|
11
11
|
|
|
12
12
|
function printHelp(exitCode) {
|
|
13
13
|
console.log(`
|
|
@@ -61,6 +61,16 @@ async function main() {
|
|
|
61
61
|
|
|
62
62
|
await mkdir(projectPath, { recursive: true });
|
|
63
63
|
|
|
64
|
+
const storageBackend = await getStorageBackend();
|
|
65
|
+
let mongoUri = "";
|
|
66
|
+
let googleSheets = {};
|
|
67
|
+
|
|
68
|
+
if (storageBackend === "google_sheets") {
|
|
69
|
+
googleSheets = await getGoogleSheetsConfig();
|
|
70
|
+
} else {
|
|
71
|
+
mongoUri = await getMongoUri(DEFAULT_MONGODB_URI);
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
const leagueUrl = await getLeagueUrl();
|
|
65
75
|
const collectionName = await getCollectionName();
|
|
66
76
|
const leagueName = await getLeagueName();
|
|
@@ -81,7 +91,7 @@ async function main() {
|
|
|
81
91
|
|
|
82
92
|
close();
|
|
83
93
|
|
|
84
|
-
await scaffoldProject(projectPath, { mongoUri
|
|
94
|
+
await scaffoldProject(projectPath, { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall: flags.skipInstall });
|
|
85
95
|
|
|
86
96
|
console.log("");
|
|
87
97
|
console.log(" Next steps:");
|
package/src/prompts.mjs
CHANGED
|
@@ -33,6 +33,15 @@ export async function getProjectName(args) {
|
|
|
33
33
|
return (await prompt("Project name: ")).trim() || "ipl-dashboard";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export async function getMongoUri(defaultUri) {
|
|
37
|
+
if (!isTTY) {
|
|
38
|
+
await consumeAllLines();
|
|
39
|
+
return (pipedLines[promptIndex++] || defaultUri || "").trim();
|
|
40
|
+
}
|
|
41
|
+
const answer = (await prompt(`MongoDB URI (press Enter for default): `)).trim();
|
|
42
|
+
return answer || defaultUri || "";
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
export async function getLeagueUrl() {
|
|
37
46
|
if (!isTTY) {
|
|
38
47
|
await consumeAllLines();
|
|
@@ -41,12 +50,36 @@ export async function getLeagueUrl() {
|
|
|
41
50
|
return (await prompt("IPL fantasy league URL (or press Enter to skip, can be set later): ")).trim();
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
export async function getStorageBackend() {
|
|
54
|
+
if (!isTTY) {
|
|
55
|
+
await consumeAllLines();
|
|
56
|
+
return (pipedLines[promptIndex++] || "mongodb").trim();
|
|
57
|
+
}
|
|
58
|
+
const answer = (await prompt("Storage backend (mongodb / google_sheets, default: mongodb): ")).trim().toLowerCase();
|
|
59
|
+
return answer === "google_sheets" ? "google_sheets" : "mongodb";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getGoogleSheetsConfig() {
|
|
63
|
+
if (!isTTY) {
|
|
64
|
+
await consumeAllLines();
|
|
65
|
+
const sheetId = (pipedLines[promptIndex++] || "").trim();
|
|
66
|
+
const email = (pipedLines[promptIndex++] || "").trim();
|
|
67
|
+
const key = (pipedLines[promptIndex++] || "").trim();
|
|
68
|
+
return { sheetId, serviceAccountEmail: email, privateKey: key };
|
|
69
|
+
}
|
|
70
|
+
console.log("\n Google Sheets configuration:\n");
|
|
71
|
+
const sheetId = (await prompt(" Google Sheet ID: ")).trim();
|
|
72
|
+
const serviceAccountEmail = (await prompt(" Service account email: ")).trim();
|
|
73
|
+
const privateKey = (await prompt(" Private key (paste the full key, including -----BEGIN/END-----): ")).trim();
|
|
74
|
+
return { sheetId, serviceAccountEmail, privateKey };
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
export async function getCollectionName() {
|
|
45
78
|
if (!isTTY) {
|
|
46
79
|
await consumeAllLines();
|
|
47
80
|
return (pipedLines[promptIndex++] || "league_data").trim();
|
|
48
81
|
}
|
|
49
|
-
return (await prompt("
|
|
82
|
+
return (await prompt("Collection/sheet name for this league's data (e.g. ipl_2025_office_league): ")).trim() || "league_data";
|
|
50
83
|
}
|
|
51
84
|
|
|
52
85
|
export async function getLeagueName() {
|
package/src/scaffold.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
|
|
|
10
10
|
|
|
11
11
|
export async function scaffoldProject(
|
|
12
12
|
projectPath,
|
|
13
|
-
{ mongoUri, leagueUrl, collectionName, leagueName, teams, skipInstall = false },
|
|
13
|
+
{ mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall = false },
|
|
14
14
|
) {
|
|
15
15
|
console.log(`\nCreating project at ${projectPath}...`);
|
|
16
16
|
|
|
@@ -35,6 +35,8 @@ export async function scaffoldProject(
|
|
|
35
35
|
|
|
36
36
|
await writeEnvFile(projectPath, {
|
|
37
37
|
mongoUri,
|
|
38
|
+
storageBackend,
|
|
39
|
+
googleSheets,
|
|
38
40
|
leagueUrl,
|
|
39
41
|
collectionName,
|
|
40
42
|
leagueName,
|
|
@@ -42,7 +44,7 @@ export async function scaffoldProject(
|
|
|
42
44
|
await writeTeamData(projectPath, teams);
|
|
43
45
|
await writeLeagueData(projectPath, { leagueName, leagueUrl, teams });
|
|
44
46
|
await writeMatchPointsPlaceholder(projectPath);
|
|
45
|
-
await updatePackageJson(projectPath);
|
|
47
|
+
await updatePackageJson(projectPath, storageBackend);
|
|
46
48
|
|
|
47
49
|
for (const f of [
|
|
48
50
|
"app/api/ipl/live-snapshot.json",
|
|
@@ -57,15 +59,15 @@ export async function scaffoldProject(
|
|
|
57
59
|
console.log(" Running npm install...");
|
|
58
60
|
execSync("npm install", { cwd: projectPath, stdio: "inherit" });
|
|
59
61
|
|
|
60
|
-
if (
|
|
61
|
-
console.log(" Seeding league metadata
|
|
62
|
+
if (leagueName) {
|
|
63
|
+
console.log(" Seeding league metadata...");
|
|
62
64
|
try {
|
|
63
65
|
execSync(`npm run seed:league -- "${leagueName}" "${collectionName}"`, {
|
|
64
66
|
cwd: projectPath,
|
|
65
67
|
stdio: "inherit",
|
|
66
68
|
});
|
|
67
69
|
} catch {
|
|
68
|
-
console.log(" League seed skipped (
|
|
70
|
+
console.log(" League seed skipped (storage may not be reachable yet)");
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
} else {
|
|
@@ -79,12 +81,16 @@ export async function scaffoldProject(
|
|
|
79
81
|
|
|
80
82
|
async function writeEnvFile(
|
|
81
83
|
projectPath,
|
|
82
|
-
{ mongoUri, leagueUrl, collectionName, leagueName },
|
|
84
|
+
{ mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName },
|
|
83
85
|
) {
|
|
84
86
|
const envPath = join(projectPath, ".env");
|
|
85
87
|
const lines = [
|
|
88
|
+
`STORAGE_BACKEND=${storageBackend || "mongodb"}`,
|
|
86
89
|
`MONGODB_URI=${mongoUri || ""}`,
|
|
87
90
|
`COLLECTION_NAME=${collectionName || "league_data"}`,
|
|
91
|
+
`GOOGLE_SHEET_ID=${(googleSheets && googleSheets.sheetId) || ""}`,
|
|
92
|
+
`GOOGLE_SERVICE_ACCOUNT_EMAIL=${(googleSheets && googleSheets.serviceAccountEmail) || ""}`,
|
|
93
|
+
`GOOGLE_PRIVATE_KEY=${(googleSheets && googleSheets.privateKey) || ""}`,
|
|
88
94
|
`IPL_LEAGUE_URL=${leagueUrl || ""}`,
|
|
89
95
|
`IPL_LEAGUE_NAME=${leagueName || ""}`,
|
|
90
96
|
`IPL_POST_SECRET=`,
|
|
@@ -180,7 +186,7 @@ async function writeMatchPointsPlaceholder(projectPath) {
|
|
|
180
186
|
await writeFile(join(dir, "match-points.ts"), content);
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
async function updatePackageJson(projectPath) {
|
|
189
|
+
async function updatePackageJson(projectPath, storageBackend) {
|
|
184
190
|
const pkgPath = join(projectPath, "package.json");
|
|
185
191
|
try {
|
|
186
192
|
const raw = await readFile(pkgPath, "utf8");
|
|
@@ -189,6 +195,10 @@ async function updatePackageJson(projectPath) {
|
|
|
189
195
|
pkg.private = false;
|
|
190
196
|
delete pkg.scripts?.["generate:template"];
|
|
191
197
|
pkg.scripts["seed:league"] = "node scripts/seed-league.mjs";
|
|
198
|
+
if (storageBackend === "google_sheets" && !pkg.dependencies.googleapis) {
|
|
199
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
200
|
+
pkg.dependencies.googleapis = "^140.0.0";
|
|
201
|
+
}
|
|
192
202
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
193
203
|
} catch {}
|
|
194
204
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getTransformOptions,
|
|
13
13
|
} from "./transform";
|
|
14
14
|
import { rawApiUsers } from "./data";
|
|
15
|
-
import
|
|
15
|
+
import { getStorage } from "../../lib/storage";
|
|
16
16
|
import { config } from "../../lib/config.ts";
|
|
17
17
|
|
|
18
18
|
export const dynamic = "force-dynamic";
|
|
@@ -54,39 +54,24 @@ const log = (message: string, details?: unknown) => {
|
|
|
54
54
|
|
|
55
55
|
const opts = getTransformOptions();
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
try {
|
|
59
|
-
return await getMongoDb();
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.error("MongoDB connection failed:", error);
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
57
|
+
const storage = getStorage();
|
|
65
58
|
|
|
66
59
|
const readMongoSnapshot = async () => {
|
|
67
|
-
const db = await getDb();
|
|
68
|
-
if (!db) return null;
|
|
69
60
|
try {
|
|
70
|
-
const document = await
|
|
71
|
-
.collection(DASHBOARD_COLLECTION)
|
|
72
|
-
.findOne({ type: DASHBOARD_DOCUMENT_TYPE });
|
|
61
|
+
const document = await storage.readDocument(DASHBOARD_DOCUMENT_TYPE);
|
|
73
62
|
return normalizePayload(document);
|
|
74
63
|
} catch (error) {
|
|
75
|
-
console.error("Failed to read IPL snapshot from
|
|
64
|
+
console.error("Failed to read IPL snapshot from storage:", error);
|
|
76
65
|
return null;
|
|
77
66
|
}
|
|
78
67
|
};
|
|
79
68
|
|
|
80
69
|
const readMongoRawUsers = async () => {
|
|
81
|
-
const db = await getDb();
|
|
82
|
-
if (!db) return null;
|
|
83
70
|
try {
|
|
84
|
-
const document = await
|
|
85
|
-
|
|
86
|
-
.findOne({ type: RAW_USERS_DOCUMENT_TYPE });
|
|
87
|
-
return normalizeRawApiUsers(document?.users);
|
|
71
|
+
const document = await storage.readDocument(RAW_USERS_DOCUMENT_TYPE);
|
|
72
|
+
return normalizeRawApiUsers((document?.users as any[]) ?? null);
|
|
88
73
|
} catch (error) {
|
|
89
|
-
console.error("Failed to read IPL raw users from
|
|
74
|
+
console.error("Failed to read IPL raw users from storage:", error);
|
|
90
75
|
return null;
|
|
91
76
|
}
|
|
92
77
|
};
|
|
@@ -192,60 +177,44 @@ const snapshotSummary = (payload: ScrapedDashboardPayload | null) => {
|
|
|
192
177
|
};
|
|
193
178
|
|
|
194
179
|
const writeMongoSnapshot = async (payload: ScrapedDashboardPayload) => {
|
|
195
|
-
|
|
196
|
-
if (!db) return { configured: false, stored: false };
|
|
180
|
+
if (!storage.isConfigured()) return { configured: false, stored: false };
|
|
197
181
|
try {
|
|
198
182
|
const current = await readMongoSnapshot();
|
|
199
183
|
if (snapshotsAreEqual(current, payload)) {
|
|
200
184
|
return { configured: true, stored: true, status: "unchanged" as const };
|
|
201
185
|
}
|
|
202
|
-
await
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
teamId: resolveTeamId(l.name),
|
|
213
|
-
})),
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
{ upsert: true },
|
|
217
|
-
);
|
|
218
|
-
return { configured: true, stored: true, status: "updated" as const };
|
|
186
|
+
const result = await storage.upsertDocument(DASHBOARD_DOCUMENT_TYPE, {
|
|
187
|
+
updatedAt: payload.updatedAt,
|
|
188
|
+
dailyTransferUpdatedAt: payload.dailyTransferUpdatedAt,
|
|
189
|
+
completedMatches: payload.completedMatches,
|
|
190
|
+
leaders: payload.leaders.map((l) => ({
|
|
191
|
+
...l,
|
|
192
|
+
teamId: resolveTeamId(l.name),
|
|
193
|
+
})),
|
|
194
|
+
});
|
|
195
|
+
return { configured: result.configured, stored: result.stored, status: result.status || "updated" };
|
|
219
196
|
} catch (error) {
|
|
220
|
-
console.error("Failed to write IPL snapshot to
|
|
197
|
+
console.error("Failed to write IPL snapshot to storage:", error);
|
|
221
198
|
return { configured: true, stored: false, status: "failed" as const };
|
|
222
199
|
}
|
|
223
200
|
};
|
|
224
201
|
|
|
225
202
|
const writeMongoRawUsers = async (users: RawApiUser[]) => {
|
|
226
|
-
|
|
227
|
-
if (!db) return { configured: false, stored: false };
|
|
203
|
+
if (!storage.isConfigured()) return { configured: false, stored: false };
|
|
228
204
|
try {
|
|
229
|
-
await
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
matches: [...user.matches].sort((a, b) => a.matchId - b.matchId),
|
|
241
|
-
})),
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
{ upsert: true },
|
|
245
|
-
);
|
|
246
|
-
return { configured: true, stored: true };
|
|
205
|
+
const result = await storage.upsertDocument(RAW_USERS_DOCUMENT_TYPE, {
|
|
206
|
+
updatedAt: new Date().toISOString(),
|
|
207
|
+
users: users.map((user) => ({
|
|
208
|
+
rno: user.rno,
|
|
209
|
+
temname: user.temname,
|
|
210
|
+
points: user.points,
|
|
211
|
+
teamId: resolveTeamId(user.temname),
|
|
212
|
+
matches: [...user.matches].sort((a, b) => a.matchId - b.matchId),
|
|
213
|
+
})),
|
|
214
|
+
});
|
|
215
|
+
return { configured: result.configured, stored: result.stored };
|
|
247
216
|
} catch (error) {
|
|
248
|
-
console.error("Failed to write IPL raw users to
|
|
217
|
+
console.error("Failed to write IPL raw users to storage:", error);
|
|
249
218
|
return { configured: true, stored: false };
|
|
250
219
|
}
|
|
251
220
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import
|
|
3
|
+
import { getStorage } from "../../../lib/storage";
|
|
4
4
|
import { config } from "../../../lib/config.ts";
|
|
5
5
|
import type {
|
|
6
6
|
ScrapedTransferItem,
|
|
@@ -49,35 +49,17 @@ const log = (message: string, details?: unknown) => {
|
|
|
49
49
|
console.log(LOG_TAG, message, details);
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
try {
|
|
54
|
-
return await getMongoDb();
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error("MongoDB connection failed:", error);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
52
|
+
const storage = getStorage();
|
|
60
53
|
|
|
61
54
|
const readMongoTransfers = async () => {
|
|
62
|
-
const db = await getDb();
|
|
63
|
-
|
|
64
|
-
if (!db) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
55
|
try {
|
|
69
|
-
const documents = await
|
|
70
|
-
.collection(DASHBOARD_COLLECTION)
|
|
71
|
-
.find({ type: TRANSFER_DOCUMENT_TYPE })
|
|
72
|
-
.toArray();
|
|
73
|
-
|
|
56
|
+
const documents = await storage.readDocuments(TRANSFER_DOCUMENT_TYPE);
|
|
74
57
|
const teams = documents
|
|
75
58
|
.map(normalizeTransferItem)
|
|
76
59
|
.filter(Boolean) as ScrapedTransferItem[];
|
|
77
|
-
|
|
78
60
|
return teams.length > 0 ? teams : null;
|
|
79
61
|
} catch (error) {
|
|
80
|
-
console.error("Failed to read IPL transfer stats from
|
|
62
|
+
console.error("Failed to read IPL transfer stats from storage:", error);
|
|
81
63
|
return null;
|
|
82
64
|
}
|
|
83
65
|
};
|
|
@@ -98,45 +80,23 @@ const readSnapshotFile = async () => {
|
|
|
98
80
|
};
|
|
99
81
|
|
|
100
82
|
const writeMongoTransfer = async (record: ScrapedTransferItem) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!db) {
|
|
104
|
-
return {
|
|
105
|
-
configured: false,
|
|
106
|
-
stored: false,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
83
|
+
if (!storage.isConfigured()) return { configured: false, stored: false };
|
|
110
84
|
try {
|
|
111
|
-
await
|
|
85
|
+
const result = await storage.upsertDocument(
|
|
86
|
+
TRANSFER_DOCUMENT_TYPE,
|
|
112
87
|
{
|
|
113
|
-
type: TRANSFER_DOCUMENT_TYPE,
|
|
114
88
|
team: record.team,
|
|
89
|
+
matchesPlayed: record.matchesPlayed,
|
|
90
|
+
boostersUsed: record.boostersUsed,
|
|
91
|
+
transfersLeft: record.transfersLeft,
|
|
92
|
+
updatedAt: record.updatedAt,
|
|
115
93
|
},
|
|
116
|
-
{
|
|
117
|
-
$set: {
|
|
118
|
-
type: TRANSFER_DOCUMENT_TYPE,
|
|
119
|
-
team: record.team,
|
|
120
|
-
matchesPlayed: record.matchesPlayed,
|
|
121
|
-
boostersUsed: record.boostersUsed,
|
|
122
|
-
transfersLeft: record.transfersLeft,
|
|
123
|
-
updatedAt: record.updatedAt,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
{ upsert: true },
|
|
94
|
+
{ team: record.team },
|
|
127
95
|
);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
configured: true,
|
|
131
|
-
stored: true,
|
|
132
|
-
};
|
|
96
|
+
return { configured: result.configured, stored: result.stored };
|
|
133
97
|
} catch (error) {
|
|
134
|
-
console.error("Failed to write IPL transfer stats to
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
configured: true,
|
|
138
|
-
stored: false,
|
|
139
|
-
};
|
|
98
|
+
console.error("Failed to write IPL transfer stats to storage:", error);
|
|
99
|
+
return { configured: true, stored: false };
|
|
140
100
|
}
|
|
141
101
|
};
|
|
142
102
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import
|
|
2
|
+
import { getStorage } from "../../../lib/storage";
|
|
3
3
|
import { config } from "../../../lib/config.ts";
|
|
4
4
|
import { normalizePayload } from "../../ipl/transform";
|
|
5
|
-
import { summarizeTransferSnapshot } from "../../ipl/transfers/transform";
|
|
5
|
+
import { normalizeTransferItem, summarizeTransferSnapshot } from "../../ipl/transfers/transform";
|
|
6
6
|
|
|
7
7
|
export const dynamic = "force-dynamic";
|
|
8
8
|
export const revalidate = 0;
|
|
@@ -64,73 +64,38 @@ const getFreshness = (
|
|
|
64
64
|
};
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const
|
|
68
|
-
try {
|
|
69
|
-
return await getMongoDb();
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error("MongoDB connection failed:", error);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
};
|
|
67
|
+
const storage = getStorage();
|
|
75
68
|
|
|
76
69
|
export async function GET() {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
70
|
+
const storageConfigured = storage.isConfigured();
|
|
71
|
+
const storageConnected = await storage.isConnected();
|
|
79
72
|
|
|
80
|
-
if (!
|
|
73
|
+
if (!storageConnected) {
|
|
81
74
|
return NextResponse.json(
|
|
82
75
|
{
|
|
83
76
|
ok: !isProduction,
|
|
84
77
|
mode: isProduction ? "cloud" : "local",
|
|
85
|
-
mongoConfigured,
|
|
78
|
+
mongoConfigured: storageConfigured,
|
|
86
79
|
mongoConnected: false,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
storageBackend: process.env.STORAGE_BACKEND || "mongodb",
|
|
81
|
+
reason: storageConfigured
|
|
82
|
+
? "Storage connection failed."
|
|
83
|
+
: "Storage is not configured.",
|
|
90
84
|
},
|
|
91
|
-
{ status:
|
|
85
|
+
{ status: storageConfigured ? 503 : 200 },
|
|
92
86
|
);
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
const [dashboardDocument, rawUsersDocument, transferDocuments] = await Promise.all([
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.collection(DASHBOARD_COLLECTION)
|
|
100
|
-
.find({ type: TRANSFER_DOCUMENT_TYPE })
|
|
101
|
-
.toArray(),
|
|
90
|
+
storage.readDocument(DASHBOARD_DOCUMENT_TYPE),
|
|
91
|
+
storage.readDocument(RAW_USERS_DOCUMENT_TYPE),
|
|
92
|
+
storage.readDocuments(TRANSFER_DOCUMENT_TYPE),
|
|
102
93
|
]);
|
|
103
94
|
const snapshot = normalizePayload(dashboardDocument);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
typeof document.team === "string" ? document.team : "",
|
|
109
|
-
matchesPlayed:
|
|
110
|
-
typeof document.matchesPlayed === "number"
|
|
111
|
-
? document.matchesPlayed
|
|
112
|
-
: Number.NaN,
|
|
113
|
-
boostersUsed:
|
|
114
|
-
typeof document.boostersUsed === "number"
|
|
115
|
-
? document.boostersUsed
|
|
116
|
-
: Number.NaN,
|
|
117
|
-
transfersLeft:
|
|
118
|
-
typeof document.transfersLeft === "string"
|
|
119
|
-
? document.transfersLeft
|
|
120
|
-
: String(document.transfersLeft ?? "").trim(),
|
|
121
|
-
updatedAt:
|
|
122
|
-
typeof document.updatedAt === "string"
|
|
123
|
-
? document.updatedAt
|
|
124
|
-
: new Date(0).toISOString(),
|
|
125
|
-
}))
|
|
126
|
-
.filter(
|
|
127
|
-
(item) =>
|
|
128
|
-
item.team &&
|
|
129
|
-
Number.isFinite(item.matchesPlayed) &&
|
|
130
|
-
Number.isFinite(item.boostersUsed) &&
|
|
131
|
-
item.transfersLeft,
|
|
132
|
-
),
|
|
133
|
-
);
|
|
95
|
+
const transferItems = transferDocuments
|
|
96
|
+
.map((doc) => normalizeTransferItem(doc as any))
|
|
97
|
+
.filter(Boolean) as any[];
|
|
98
|
+
const transfers = summarizeTransferSnapshot(transferItems);
|
|
134
99
|
const transfersUpdatedAt =
|
|
135
100
|
transfers.teams.length > 0 ? transfers.updatedAt : undefined;
|
|
136
101
|
const dashboardFreshness = getFreshness(
|
|
@@ -190,8 +155,9 @@ export async function GET() {
|
|
|
190
155
|
status,
|
|
191
156
|
mode: isProduction ? "cloud" : "local",
|
|
192
157
|
checkedAt: new Date().toISOString(),
|
|
193
|
-
mongoConfigured,
|
|
194
|
-
mongoConnected:
|
|
158
|
+
mongoConfigured: storageConfigured,
|
|
159
|
+
mongoConnected: storageConnected,
|
|
160
|
+
storageBackend: process.env.STORAGE_BACKEND || "mongodb",
|
|
195
161
|
localSnapshotFallbackEnabled: !isProduction,
|
|
196
162
|
thresholds: {
|
|
197
163
|
dashboardStaleMinutes,
|
|
@@ -207,7 +173,7 @@ export async function GET() {
|
|
|
207
173
|
}
|
|
208
174
|
: null,
|
|
209
175
|
rawUsers: {
|
|
210
|
-
count: Array.isArray(rawUsersDocument?.users) ? rawUsersDocument.users.length : 0,
|
|
176
|
+
count: Array.isArray((rawUsersDocument as any)?.users) ? (rawUsersDocument as any).users.length : 0,
|
|
211
177
|
updatedAt:
|
|
212
178
|
typeof rawUsersDocument?.updatedAt === "string"
|
|
213
179
|
? rawUsersDocument.updatedAt
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { IStorage, StorageResult } from "./types";
|
|
2
|
+
|
|
3
|
+
const SHEET_ID = () => process.env.GOOGLE_SHEET_ID || "";
|
|
4
|
+
const SHEET_NAMES: Record<string, string> = {
|
|
5
|
+
dashboard: "dashboard",
|
|
6
|
+
"raw-users": "raw_users",
|
|
7
|
+
"transfer-stats": "transfers",
|
|
8
|
+
league: "league",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function sheetNameFor(type: string): string {
|
|
12
|
+
return SHEET_NAMES[type] || type.replace(/[^a-z0-9_]/g, "_");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let sheetsClient: any = null;
|
|
16
|
+
|
|
17
|
+
async function getSheetsClient() {
|
|
18
|
+
if (sheetsClient) return sheetsClient;
|
|
19
|
+
try {
|
|
20
|
+
const { google } = await import("googleapis");
|
|
21
|
+
const auth = new google.auth.GoogleAuth({
|
|
22
|
+
credentials: {
|
|
23
|
+
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
|
|
24
|
+
private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
|
25
|
+
},
|
|
26
|
+
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
|
|
27
|
+
});
|
|
28
|
+
sheetsClient = google.sheets({ version: "v4", auth });
|
|
29
|
+
return sheetsClient;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("Failed to create Google Sheets client:", error);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class GoogleSheetsStorage implements IStorage {
|
|
37
|
+
isConfigured(): boolean {
|
|
38
|
+
return Boolean(process.env.GOOGLE_SHEET_ID && process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_PRIVATE_KEY);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async isConnected(): Promise<boolean> {
|
|
42
|
+
const client = await getSheetsClient();
|
|
43
|
+
return client !== null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private async ensureSheet(sheetName: string): Promise<void> {
|
|
47
|
+
const client = await getSheetsClient();
|
|
48
|
+
if (!client) return;
|
|
49
|
+
try {
|
|
50
|
+
const res = await client.spreadsheets.get({ spreadsheetId: SHEET_ID() });
|
|
51
|
+
const sheets: any[] = res.data.sheets || [];
|
|
52
|
+
if (sheets.some((s: any) => s.properties?.title === sheetName)) return;
|
|
53
|
+
await client.spreadsheets.batchUpdate({
|
|
54
|
+
spreadsheetId: SHEET_ID(),
|
|
55
|
+
requestBody: {
|
|
56
|
+
requests: [{ addSheet: { properties: { title: sheetName } } }],
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// Sheet already exists or can't be created
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async readDocument(type: string): Promise<Record<string, unknown> | null> {
|
|
65
|
+
const client = await getSheetsClient();
|
|
66
|
+
if (!client) return null;
|
|
67
|
+
const sheetName = sheetNameFor(type);
|
|
68
|
+
try {
|
|
69
|
+
const res = await client.spreadsheets.values.get({
|
|
70
|
+
spreadsheetId: SHEET_ID(),
|
|
71
|
+
range: `${sheetName}!A:C`,
|
|
72
|
+
});
|
|
73
|
+
const rows: string[][] = res.data.values || [];
|
|
74
|
+
const dataRow = rows.find((r) => r[0] === type);
|
|
75
|
+
if (dataRow?.[1]) {
|
|
76
|
+
return { type, ...JSON.parse(dataRow[1]), updatedAt: dataRow[2] || new Date().toISOString() };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async upsertDocument(
|
|
85
|
+
type: string,
|
|
86
|
+
data: Record<string, unknown>,
|
|
87
|
+
_filter?: Record<string, unknown>,
|
|
88
|
+
): Promise<StorageResult> {
|
|
89
|
+
const client = await getSheetsClient();
|
|
90
|
+
if (!client) return { configured: true, stored: false, status: "no_connection" };
|
|
91
|
+
const sheetName = sheetNameFor(type);
|
|
92
|
+
await this.ensureSheet(sheetName);
|
|
93
|
+
try {
|
|
94
|
+
const res = await client.spreadsheets.values.get({
|
|
95
|
+
spreadsheetId: SHEET_ID(),
|
|
96
|
+
range: `${sheetName}!A:C`,
|
|
97
|
+
});
|
|
98
|
+
const rows: string[][] = res.data.values || [];
|
|
99
|
+
const rowIndex = rows.findIndex((r) => r[0] === type);
|
|
100
|
+
const jsonData = JSON.stringify(data);
|
|
101
|
+
const updatedAt = new Date().toISOString();
|
|
102
|
+
if (rowIndex >= 0) {
|
|
103
|
+
await client.spreadsheets.values.update({
|
|
104
|
+
spreadsheetId: SHEET_ID(),
|
|
105
|
+
range: `${sheetName}!A${rowIndex + 1}:C${rowIndex + 1}`,
|
|
106
|
+
valueInputOption: "RAW",
|
|
107
|
+
requestBody: { values: [[type, jsonData, updatedAt]] },
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
await client.spreadsheets.values.append({
|
|
111
|
+
spreadsheetId: SHEET_ID(),
|
|
112
|
+
range: `${sheetName}!A:C`,
|
|
113
|
+
valueInputOption: "RAW",
|
|
114
|
+
requestBody: { values: [[type, jsonData, updatedAt]] },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { configured: true, stored: true, status: "updated" };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Failed to write document type "${type}" to Google Sheets:`, error);
|
|
120
|
+
return { configured: true, stored: false, status: "failed" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async readDocuments(type: string): Promise<Record<string, unknown>[]> {
|
|
125
|
+
const client = await getSheetsClient();
|
|
126
|
+
if (!client) return [];
|
|
127
|
+
const sheetName = sheetNameFor(type);
|
|
128
|
+
try {
|
|
129
|
+
const res = await client.spreadsheets.values.get({
|
|
130
|
+
spreadsheetId: SHEET_ID(),
|
|
131
|
+
range: `${sheetName}!A:C`,
|
|
132
|
+
});
|
|
133
|
+
const rows: string[][] = res.data.values || [];
|
|
134
|
+
return rows
|
|
135
|
+
.filter((r) => r[0] && r[0] !== type)
|
|
136
|
+
.map((r) => {
|
|
137
|
+
try {
|
|
138
|
+
return { team: r[0], ...JSON.parse(r[1] || "{}"), updatedAt: r[2] || new Date().toISOString() };
|
|
139
|
+
} catch {
|
|
140
|
+
return { team: r[0], updatedAt: r[2] || new Date().toISOString() };
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IStorage } from "./types";
|
|
2
|
+
import { MongoStorage } from "./mongo-storage";
|
|
3
|
+
import { GoogleSheetsStorage } from "./google-sheets-storage";
|
|
4
|
+
|
|
5
|
+
let _instance: IStorage | null = null;
|
|
6
|
+
|
|
7
|
+
function detectBackend(): "mongodb" | "google_sheets" {
|
|
8
|
+
if (process.env.STORAGE_BACKEND === "google_sheets") return "google_sheets";
|
|
9
|
+
if (process.env.GOOGLE_SHEET_ID && process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_PRIVATE_KEY) return "google_sheets";
|
|
10
|
+
return "mongodb";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getStorage(): IStorage {
|
|
14
|
+
if (_instance) return _instance;
|
|
15
|
+
const backend = detectBackend();
|
|
16
|
+
_instance = backend === "google_sheets" ? new GoogleSheetsStorage() : new MongoStorage();
|
|
17
|
+
return _instance;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { IStorage } from "./types";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import getMongoDb from "../useDb";
|
|
2
|
+
import { config } from "../config";
|
|
3
|
+
import type { IStorage, StorageResult } from "./types";
|
|
4
|
+
|
|
5
|
+
const COLLECTION = config.mongodb.collectionName;
|
|
6
|
+
|
|
7
|
+
export class MongoStorage implements IStorage {
|
|
8
|
+
isConfigured(): boolean {
|
|
9
|
+
return Boolean(process.env.MONGODB_URI);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async isConnected(): Promise<boolean> {
|
|
13
|
+
const db = await getMongoDb();
|
|
14
|
+
return db !== null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async readDocument(type: string): Promise<Record<string, unknown> | null> {
|
|
18
|
+
const db = await getMongoDb();
|
|
19
|
+
if (!db) return null;
|
|
20
|
+
try {
|
|
21
|
+
return (await db.collection(COLLECTION).findOne({ type })) as Record<string, unknown> | null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async upsertDocument(
|
|
28
|
+
type: string,
|
|
29
|
+
data: Record<string, unknown>,
|
|
30
|
+
filter?: Record<string, unknown>,
|
|
31
|
+
): Promise<StorageResult> {
|
|
32
|
+
const db = await getMongoDb();
|
|
33
|
+
if (!db) return { configured: true, stored: false, status: "no_connection" };
|
|
34
|
+
try {
|
|
35
|
+
const query = filter ? { type, ...filter } : { type };
|
|
36
|
+
await db.collection(COLLECTION).updateOne(query, { $set: { type, ...data } }, { upsert: true });
|
|
37
|
+
return { configured: true, stored: true, status: "updated" };
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Failed to upsert document type "${type}" in MongoDB:`, error);
|
|
40
|
+
return { configured: true, stored: false, status: "failed" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async readDocuments(type: string): Promise<Record<string, unknown>[]> {
|
|
45
|
+
const db = await getMongoDb();
|
|
46
|
+
if (!db) return [];
|
|
47
|
+
try {
|
|
48
|
+
return (await db.collection(COLLECTION).find({ type }).toArray()) as Record<string, unknown>[];
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface StorageResult {
|
|
2
|
+
configured: boolean;
|
|
3
|
+
stored: boolean;
|
|
4
|
+
status?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface IStorage {
|
|
8
|
+
readDocument(type: string): Promise<Record<string, unknown> | null>;
|
|
9
|
+
upsertDocument(type: string, data: Record<string, unknown>, filter?: Record<string, unknown>): Promise<StorageResult>;
|
|
10
|
+
readDocuments(type: string): Promise<Record<string, unknown>[]>;
|
|
11
|
+
isConfigured(): boolean;
|
|
12
|
+
isConnected(): Promise<boolean>;
|
|
13
|
+
}
|
|
@@ -29,6 +29,61 @@ async function main() {
|
|
|
29
29
|
|
|
30
30
|
const collectionName = process.argv[3] || process.env.COLLECTION_NAME?.trim() || sanitizeCollectionName(leagueName);
|
|
31
31
|
|
|
32
|
+
const isGoogleSheets = process.env.STORAGE_BACKEND === "google_sheets";
|
|
33
|
+
|
|
34
|
+
if (isGoogleSheets) {
|
|
35
|
+
const sheetId = process.env.GOOGLE_SHEET_ID;
|
|
36
|
+
const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
|
37
|
+
const key = process.env.GOOGLE_PRIVATE_KEY;
|
|
38
|
+
|
|
39
|
+
if (!sheetId || !email || !key) {
|
|
40
|
+
console.log("Google Sheets not configured. Skipping league seed.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const leagueUrl = process.env.IPL_LEAGUE_URL?.trim();
|
|
45
|
+
|
|
46
|
+
let teams = [];
|
|
47
|
+
const leagueDataPath = join(ROOT, "app/data/league.ts");
|
|
48
|
+
if (existsSync(leagueDataPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(leagueDataPath, "utf8");
|
|
51
|
+
const match = content.match(/teams:\s*\[([\s\S]*?)\],/);
|
|
52
|
+
if (match) {
|
|
53
|
+
const entries = [...match[1].matchAll(/\{\s*id:\s*(\d+),\s*name:\s*"([^"]+)",\s*owner:\s*"([^"]+)"\s*\}/g)];
|
|
54
|
+
teams = entries.map(([, id, name, owner]) => ({
|
|
55
|
+
id: Number(id),
|
|
56
|
+
name,
|
|
57
|
+
owner,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { google } = await import("googleapis");
|
|
64
|
+
const auth = new google.auth.GoogleAuth({
|
|
65
|
+
credentials: { client_email: email, private_key: key.replace(/\\n/g, "\n") },
|
|
66
|
+
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
|
|
67
|
+
});
|
|
68
|
+
const sheets = google.sheets({ version: "v4", auth });
|
|
69
|
+
const sheetName = collectionName.replace(/[^a-z0-9_]/g, "_");
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await sheets.spreadsheets.values.append({
|
|
73
|
+
spreadsheetId: sheetId,
|
|
74
|
+
range: `${sheetName}!A:C`,
|
|
75
|
+
valueInputOption: "RAW",
|
|
76
|
+
requestBody: {
|
|
77
|
+
values: [["league", JSON.stringify({ name: leagueName, leagueUrl, teams }), new Date().toISOString()]],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
console.log(`League "${leagueName}" seeded in sheet "${sheetName}" with ${teams.length} team(s).`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Failed to seed league in Google Sheets:", error.message);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
const uri = process.env.MONGODB_URI;
|
|
33
88
|
if (!uri) {
|
|
34
89
|
console.log("MONGODB_URI not set. Skipping league seed.");
|