Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3ad2c09
feat(schema-007): Create player_identities linking table
jackwatters45 Jan 21, 2026
205bd15
feat(schema-008): Create player_stats table for per-game statistics
jackwatters45 Jan 21, 2026
b55a56f
feat(schema-009): Create games table for schedule/results
jackwatters45 Jan 21, 2026
ed858ba
feat(schema-010): Create standings table
jackwatters45 Jan 21, 2026
46bc99c
feat(pipeline): add selective test running for Ralph loop
jackwatters45 Jan 21, 2026
f165e67
feat(backend-001): Create StatsRepo with Effect pattern
jackwatters45 Jan 21, 2026
2fd949b
feat(backend-002): Create PlayersRepo with identity resolution
jackwatters45 Jan 21, 2026
9d13aae
feat(backend-003): Create TeamsRepo
jackwatters45 Jan 21, 2026
ead141c
feat(backend-004): Create StatsService business logic layer
jackwatters45 Jan 21, 2026
5229f25
feat(backend-005): Create PlayersService with identity linking
jackwatters45 Jan 21, 2026
7c1b57f
feat(backend-006): Create IdentityService for player matching
jackwatters45 Jan 21, 2026
ceefb95
feat(backend-007): Create data loader to populate DB from extracted JSON
jackwatters45 Jan 21, 2026
2f27102
feat(backend-008): Add CacheService with ETags/conditional requests
jackwatters45 Jan 21, 2026
c45f6d1
chore: migrate to drizzle-kit v1.0 format and update pipeline drizzle…
jackwatters45 Jan 22, 2026
43e026b
chore: add drizzle-kit to pipeline package
jackwatters45 Jan 22, 2026
592cfcb
feat(core): add pipeline RPC schemas and contracts
jackwatters45 Jan 22, 2026
f97c178
feat(pipeline): add RPC repositories and services
jackwatters45 Jan 22, 2026
cadadb7
feat(api): add pipeline RPC handlers
jackwatters45 Jan 22, 2026
46eda5c
feat(api): add stats HTTP API endpoints
jackwatters45 Jan 22, 2026
0c743f1
feat(web): add public /stats page with filtering and pagination
jackwatters45 Jan 22, 2026
a1a4c96
feat(pipeline): add season config for cron worker
jackwatters45 Jan 22, 2026
edf355b
feat(api): add cron scheduled handler for pipeline extraction
jackwatters45 Jan 22, 2026
bf26b27
feat(infra): add API worker with cron trigger to Alchemy
jackwatters45 Jan 22, 2026
daa2098
chore(api): add pipeline dependency and remove unused import
jackwatters45 Jan 22, 2026
68a49a0
docs(plans): add pipeline MVP feature plan
jackwatters45 Jan 22, 2026
59040a6
fix(pipeline): address lint errors in RPC repos and config
jackwatters45 Jan 22, 2026
8315f94
fix(pipeline): resolve oxlint errors and warnings
jackwatters45 Jan 22, 2026
983567f
fix(pipeline): resolve unbound-method lint errors in cache service tests
jackwatters45 Jan 22, 2026
dd7cce5
fix(pipeline,web): resolve lint errors and add stats RPC client
jackwatters45 Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions alchemy.run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
KVNamespace,
R2Bucket,
TanStackStart,
Worker,
} from "alchemy/cloudflare";

export const app = await alchemy("laxdb", {
Expand Down Expand Up @@ -133,20 +134,29 @@ if (app.local) {
// KV
export const kv = await KVNamespace("kv", {});

// Pipeline KV for cron worker caching
export const pipelineKV = await KVNamespace("pipeline-kv", {
title: `laxdb-pipeline-${stage}`,
});

// Storage
export const storage = await R2Bucket("storage", {});

// export const worker = await Worker("api", {
// entrypoint: "packages/api/src/index.ts",
// url: true,
// bindings: {
// DB: db,
// KV: kv,
// STORAGE: storage,
// DATABASE_URL: dbRole.connectionUrl,
// ...secrets,
// },
// });
// API Worker with cron triggers for pipeline extraction
export const api = await Worker("api", {
entrypoint: "packages/api/src/index.ts",
url: true,
bindings: {
DB: db,
KV: kv,
PIPELINE_KV: pipelineKV,
STORAGE: storage,
DATABASE_URL: dbRole.connectionUrl,
...secrets,
},
// Hourly cron trigger for pipeline data extraction
crons: ["0 * * * *"],
});

export const web = await TanStackStart("web", {
cwd: "./packages/web",
Expand Down Expand Up @@ -174,12 +184,13 @@ export const docs = await TanStackStart("docs", {

console.log({
domain,
// web: web.url,
web: web.url,
marketing: marketing.url,
docs: docs.url,
// api: api.url,
api: api.url,
db: database.id,
kv: kv.namespaceId,
pipelineKV: pipelineKV.namespaceId,
r2: storage.name,
stage,
});
Expand All @@ -194,9 +205,10 @@ if (process.env.PULL_REQUEST) {

Your changes have been deployed to a preview environment:

**🌐 Website:** ${web.url}
**🌐 API:** ${api.url}
**🌐 Docs:** ${docs.url}
**🌐 Marketing:** ${marketing.url}
**🌐 Website:** ${web.url}

Built from commit ${process.env.GITHUB_SHA?.slice(0, 7)}

Expand Down
31 changes: 20 additions & 11 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"destroy": "alchemy destroy",
"dev": "alchemy dev",
"test": "turbo test",
"test:unit": "turbo test:unit",
"typecheck": "turbo typecheck",
"lint": "turbo lint",
"format": "turbo format",
Expand Down
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@effect-atom/atom-react": "^0.4.4",
"@effect/platform": "0.94.1",
"@effect/rpc": "^0.73.0",
"@laxdb/core": "workspace:*"
"@laxdb/core": "workspace:*",
"@laxdb/pipeline": "workspace:*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241218.0"
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Layer } from "effect";
import { RpcAuthClient } from "./auth/auth.client";
import { RpcGameClient } from "./game/game.client";
import { RpcOrganizationClient } from "./organization/organization.client";
import { RpcStatsClient } from "./pipeline/stats.client";
import { RpcContactInfoClient } from "./player/contact-info/contact-info.client";
import { RpcPlayerClient } from "./player/player.client";
import { RpcSeasonClient } from "./season/season.client";
Expand All @@ -16,4 +17,5 @@ export const RpcClientLive = Layer.mergeAll(
RpcTeamClient.Default,
RpcOrganizationClient.Default,
RpcAuthClient.Default,
RpcStatsClient.Default,
);
132 changes: 132 additions & 0 deletions packages/api/src/cron/scheduled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Cron scheduled handler for pipeline data extraction
*
* Runs hourly to extract and load data from active leagues.
* Each league is processed independently - one failure doesn't stop others.
*/

import {
getActiveLeagues,
type LeagueAbbreviation,
} from "@laxdb/pipeline/config/seasons";

/**
* Cloudflare Worker environment bindings
*/
interface Env {
PIPELINE_KV: KVNamespace;
DATABASE_URL: string;
}

/**
* Extract data from a league's source API
*
* @param league - The league abbreviation
* @param env - Cloudflare Worker environment
*/
async function extractLeague(
league: LeagueAbbreviation,
env: Env,
): Promise<void> {
console.log(`[${league}] Starting extraction...`);

// TODO: Implement actual extraction using @laxdb/pipeline extractors
// For MVP, this is a placeholder that logs progress
// Will call extractNLL, extractPLL, etc. based on league

console.log(`[${league}] Extraction complete`);
}

/**
* Load extracted data into the database
*
* @param league - The league abbreviation
* @param env - Cloudflare Worker environment
*/
async function loadLeague(league: LeagueAbbreviation, env: Env): Promise<void> {
console.log(`[${league}] Loading data...`);

// TODO: Implement actual data loading
// Will insert/update records in pipeline_* tables

console.log(`[${league}] Load complete`);
}

/**
* Invalidate cached data for a league
*
* @param league - The league abbreviation
* @param env - Cloudflare Worker environment
*/
async function invalidateCache(
league: LeagueAbbreviation,
env: Env,
): Promise<void> {
console.log(`[${league}] Invalidating cache...`);

// Delete cached leaderboard data for this league
const cacheKeys = [
`cache:leaderboard:${league}`,
`cache:players:${league}`,
`cache:teams:${league}`,
];

await Promise.all(cacheKeys.map((key) => env.PIPELINE_KV.delete(key)));

console.log(`[${league}] Cache invalidated`);
}

/**
* Scheduled cron handler
*
* Called by Cloudflare Workers on the configured schedule (hourly).
* Processes all currently active leagues in parallel with error isolation.
*/
export async function scheduled(
_controller: ScheduledController,
env: Env,
_ctx: ExecutionContext,
): Promise<void> {
const startTime = Date.now();
console.log(
`[Cron] Starting scheduled extraction at ${new Date().toISOString()}`,
);

// Get leagues that are currently in-season
const activeLeagues = getActiveLeagues(new Date());

if (activeLeagues.length === 0) {
console.log("[Cron] No active leagues, skipping extraction");
return;
}

console.log(`[Cron] Active leagues: ${activeLeagues.join(", ")}`);

// Run all extractions in parallel, don't let one failure stop others
const results = await Promise.allSettled(
activeLeagues.map(async (league) => {
await extractLeague(league, env);
await loadLeague(league, env);
await invalidateCache(league, env);
}),
);

// Log results
let successCount = 0;
let failureCount = 0;

for (const [i, result] of results.entries()) {
if (result.status === "fulfilled") {
successCount++;
console.log(`[Cron] ${activeLeagues[i]} completed successfully`);
} else {
failureCount++;
console.error(`[Cron] ${activeLeagues[i]} failed:`, result.reason);
}
}

const duration = Date.now() - startTime;
console.log(
`[Cron] Completed in ${duration}ms - Success: ${successCount}, Failed: ${failureCount}`,
);
}
4 changes: 3 additions & 1 deletion packages/api/src/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HttpApi } from "@effect/platform";
import { AuthGroup } from "./auth/auth.api";
import { GamesGroup } from "./game/game.api";
import { OrganizationsGroup } from "./organization/organization.api";
import { StatsApiGroup } from "./pipeline/stats.api";
import { ContactInfoGroup } from "./player/contact-info/contact-info.api";
import { PlayersGroup } from "./player/player.api";
import { SeasonsGroup } from "./season/season.api";
Expand All @@ -16,4 +17,5 @@ export class LaxdbApi extends HttpApi.make("LaxdbApi")
.add(ContactInfoGroup)
.add(TeamsGroup)
.add(OrganizationsGroup)
.add(AuthGroup) {}
.add(AuthGroup)
.add(StatsApiGroup) {}
2 changes: 2 additions & 0 deletions packages/api/src/groups/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Layer } from "effect";
import { AuthHandlersLive } from "../auth/auth.handlers";
import { GamesHandlersLive } from "../game/game.handlers";
import { OrganizationsHandlersLive } from "../organization/organization.handlers";
import { StatsHandlersLive } from "../pipeline/stats.handlers";
import { ContactInfoHandlersLive } from "../player/contact-info/contact-info.handlers";
import { PlayersHandlersLive } from "../player/player.handlers";
import { SeasonsHandlersLive } from "../season/season.handlers";
Expand All @@ -17,4 +18,5 @@ export const HttpGroupsLive = Layer.mergeAll(
TeamsHandlersLive,
OrganizationsHandlersLive,
AuthHandlersLive,
StatsHandlersLive,
);
3 changes: 3 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ const { handler } = HttpLayerRouter.toWebHandler(AllRoutes, {
middleware: HttpMiddleware.logger,
});

// Re-export scheduled handler for cron triggers
export { scheduled } from "./cron/scheduled";

export default { fetch: handler };
28 changes: 28 additions & 0 deletions packages/api/src/pipeline/players.rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Rpc, RpcGroup } from "@effect/rpc";
import { PlayersContract } from "@laxdb/core/pipeline/players.contract";
import { PlayersService } from "@laxdb/pipeline/rpc/players.service";
import { Effect, Layer } from "effect";

export class PlayersRpcs extends RpcGroup.make(
Rpc.make("PlayersGetPlayer", {
success: PlayersContract.getPlayer.success,
error: PlayersContract.getPlayer.error,
payload: PlayersContract.getPlayer.payload,
}),
Rpc.make("PlayersSearchPlayers", {
success: PlayersContract.searchPlayers.success,
error: PlayersContract.searchPlayers.error,
payload: PlayersContract.searchPlayers.payload,
}),
) {}

export const PlayersHandlers = PlayersRpcs.toLayer(
Effect.gen(function* () {
const service = yield* PlayersService;

return {
PlayersGetPlayer: (payload) => service.getPlayer(payload),
PlayersSearchPlayers: (payload) => service.searchPlayers(payload),
};
}),
).pipe(Layer.provide(PlayersService.Default));
Loading
Loading