@zmnatz/usa-club-rugby-data 0.1.0 → 0.2.0

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 CHANGED
@@ -82,4 +82,4 @@ bun run test # in this package
82
82
  bun run build # dist/, ESM with declarations
83
83
  ```
84
84
 
85
- Releases are published from GitHub releases by `.github/workflows/publish.yml`, using npm trusted publishing (no stored token).
85
+ Releases are published from GitHub releases by `.github/workflows/publish.yml`, using npm trusted publishing (no stored token). The workflow stages the version; approve it with 2FA on npmjs.com to release it.
package/dist/data.d.ts CHANGED
@@ -17,6 +17,16 @@ export interface PublishedClubFile extends ParticipationSnapshot {
17
17
  /** When the data last changed; always present in a published file. */
18
18
  generatedAt: string;
19
19
  }
20
+ /** Which clubs each player is in the record of, by CMS id, for finding a player from
21
+ * their id. Not published: worked out from the club files by `buildPlayerIndex`, which
22
+ * leaves out players whose profile isn't public. */
23
+ export interface PublishedPlayersFile {
24
+ season: string;
25
+ players: Record<string, {
26
+ name: string;
27
+ clubs: string[];
28
+ }>;
29
+ }
20
30
  /** `archive/fixtures/<season>.json`: every senior club fixture that season by id. */
21
31
  export interface ArchiveFixturesFile {
22
32
  season: string;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  export * from './api.ts';
2
2
  export * from './archive.ts';
3
3
  export * from './csv.ts';
4
- export type { ArchiveFixturesFile, ArchiveMatchesFile, PublishedClubFile, PublishedFixturesFile, PublishedIndexFile, } from './data.ts';
4
+ export type { ArchiveFixturesFile, ArchiveMatchesFile, PublishedClubFile, PublishedFixturesFile, PublishedIndexFile, PublishedPlayersFile, } from './data.ts';
5
5
  export * from './eligibility.ts';
6
6
  export * from './feeds.ts';
7
7
  export * from './live.ts';
8
8
  export type * from './match.ts';
9
9
  export * from './participation.ts';
10
+ export * from './players.ts';
10
11
  export * from './playerHistory.ts';
11
12
  export * from './published.ts';
12
13
  export * from './rugbyXplorer.ts';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from './eligibility.js';
7
7
  export * from './feeds.js';
8
8
  export * from './live.js';
9
9
  export * from './participation.js';
10
+ export * from './players.js';
10
11
  export * from './playerHistory.js';
11
12
  export * from './published.js';
12
13
  export * from './rugbyXplorer.js';
@@ -0,0 +1,74 @@
1
+ import type { PublishedPlayersFile } from './data.ts';
2
+ import type { FeedFixture } from './feeds.ts';
3
+ import { type ParticipationSnapshot, type PlayerRound, type PossibleFixture } from './participation.ts';
4
+ import type { PublishedClub } from './published.ts';
5
+ import type { Club, EligibilityBreakdown, EligibilityRules, PlayerEligibility } from './types.ts';
6
+ export interface PlayerOptions {
7
+ rules?: EligibilityRules;
8
+ /** The time possible remaining matches are counted from. Defaults to now. */
9
+ now?: Date;
10
+ }
11
+ /** One club's published file, with what's worked out from it. */
12
+ export interface ClubContext {
13
+ club: PublishedClub;
14
+ snapshot: ParticipationSnapshot;
15
+ /** The fixtures in the club's divisions. */
16
+ fixtures: FeedFixture[];
17
+ breakdown: EligibilityBreakdown;
18
+ }
19
+ /** What a player's fixtures are worked out from: a club, its published file and its fixtures. */
20
+ export interface PlayerRecordInput {
21
+ club: Pick<Club, 'sides'>;
22
+ snapshot: ParticipationSnapshot;
23
+ fixtures: FeedFixture[];
24
+ }
25
+ /** A club's verdicts and the fixtures they're counted from. */
26
+ export declare function clubContext(club: PublishedClub, snapshot: ParticipationSnapshot, allFixtures: FeedFixture[], { rules, now }?: PlayerOptions): ClubContext;
27
+ /** One side's fixtures for one player. */
28
+ export interface SideRounds {
29
+ teamName: string;
30
+ rounds: PlayerRound[];
31
+ }
32
+ /** The fixtures behind a verdict. */
33
+ export interface PlayerFixtures {
34
+ /** A round table per side of the club, the player's own side first. */
35
+ sides: SideRounds[];
36
+ /** Why an on-track player is on track; empty for anyone else. */
37
+ possible: PossibleFixture[];
38
+ }
39
+ /** Each side's rounds for the player, own side first; every side feeds the verdict. */
40
+ export declare function sideRounds(player: PlayerEligibility, club: Pick<Club, 'sides'>, snapshot: ParticipationSnapshot, fixtures: FeedFixture[], rules?: EligibilityRules): SideRounds[];
41
+ /** The fixtures a verdict counted as possible, on the player's side and the sides
42
+ * below it, or every side for the top one. */
43
+ export declare function possibleFor(player: PlayerEligibility, club: Pick<Club, 'sides'>, fixtures: FeedFixture[], { rules, now }?: PlayerOptions): PossibleFixture[];
44
+ /** The fixtures behind one player's verdict in a club. */
45
+ export declare function playerFixtures(player: PlayerEligibility, { club, snapshot, fixtures }: PlayerRecordInput, options?: PlayerOptions): PlayerFixtures;
46
+ /** A player's verdict on one side of one club, with the fixtures behind it. */
47
+ export interface PlayerClubEntry {
48
+ clubKey: string;
49
+ clubName: string;
50
+ /** The side this verdict is for, e.g. "D1". */
51
+ sideLabel: string;
52
+ /** Another player in this side shares the name, so show the id to tell them apart. */
53
+ showCmsId: boolean;
54
+ /** Without its raw rows, which only the fixtures were worked out from. */
55
+ player: PlayerEligibility;
56
+ fixtures: PlayerFixtures;
57
+ }
58
+ /** A player and their verdict on every club side they're listed on. */
59
+ export interface PlayerProfile {
60
+ /** The player's CMS id. */
61
+ id: string;
62
+ name: string;
63
+ clubs: PlayerClubEntry[];
64
+ }
65
+ /** A player's verdicts in one club: one per side they're listed on. */
66
+ export declare function playerEntries(playerId: string, context: ClubContext, options?: PlayerOptions): PlayerClubEntry[];
67
+ /** Clubs and sides in the order a profile lists them. */
68
+ export declare function sortEntries(entries: PlayerClubEntry[]): PlayerClubEntry[];
69
+ /** Which clubs each player is in the record of, for finding a player from their id.
70
+ * A player whose profile isn't public ("Name Withheld") is left out. */
71
+ export declare function buildPlayerIndex(season: string, clubs: {
72
+ club: Pick<PublishedClub, 'key'>;
73
+ breakdown: EligibilityBreakdown;
74
+ }[]): PublishedPlayersFile;
@@ -0,0 +1,92 @@
1
+ import { computeBreakdown } from './eligibility.js';
2
+ import { clubFixtures, participationRows, playerRounds, possibleFixtures, remainingMatches, } from './participation.js';
3
+ import { isNameWithheld } from './playerHistory.js';
4
+ import { DEFAULT_RULES } from './rules.js';
5
+ import { hasSharedName, sharedNames } from './eligibility.js';
6
+ /** A club's verdicts and the fixtures they're counted from. */
7
+ export function clubContext(club, snapshot, allFixtures, { rules = DEFAULT_RULES, now = new Date() } = {}) {
8
+ const fixtures = clubFixtures(allFixtures, club);
9
+ const remaining = remainingMatches(fixtures, club, rules, now);
10
+ return {
11
+ club,
12
+ snapshot,
13
+ fixtures,
14
+ breakdown: computeBreakdown(participationRows(snapshot, club, rules), club.sides, rules, remaining),
15
+ };
16
+ }
17
+ /** Each side's rounds for the player, own side first; every side feeds the verdict. */
18
+ export function sideRounds(player, club, snapshot, fixtures, rules = DEFAULT_RULES) {
19
+ const sides = club.sides.map((side) => side.teamName);
20
+ return [player.teamName, ...sides.filter((side) => side !== player.teamName)].map((teamName) => ({
21
+ teamName,
22
+ rounds: playerRounds({
23
+ fixtures,
24
+ teamName,
25
+ season: snapshot.season,
26
+ rows: player.rows,
27
+ gaps: snapshot.gaps,
28
+ resultIds: snapshot.resultIds ?? [],
29
+ rules,
30
+ }),
31
+ }));
32
+ }
33
+ /** The fixtures a verdict counted as possible, on the player's side and the sides
34
+ * below it, or every side for the top one. */
35
+ export function possibleFor(player, club, fixtures, { rules = DEFAULT_RULES, now = new Date() } = {}) {
36
+ const lists = possibleFixtures(fixtures, club, rules, now);
37
+ const index = club.sides.findIndex((side) => side.teamName === player.teamName);
38
+ return club.sides
39
+ .slice(Math.max(index, 0))
40
+ .flatMap((side) => lists[side.teamName] ?? [])
41
+ .sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime());
42
+ }
43
+ /** The fixtures behind one player's verdict in a club. */
44
+ export function playerFixtures(player, { club, snapshot, fixtures }, options = {}) {
45
+ const { rules = DEFAULT_RULES } = options;
46
+ return {
47
+ sides: sideRounds(player, club, snapshot, fixtures, rules),
48
+ possible: player.pending.length > 0 ? possibleFor(player, club, fixtures, options) : [],
49
+ };
50
+ }
51
+ /** A player's verdicts in one club: one per side they're listed on. */
52
+ export function playerEntries(playerId, context, options = {}) {
53
+ const { club, breakdown } = context;
54
+ return breakdown.sides.flatMap((side) => {
55
+ const player = side.players.find((candidate) => candidate.playerId === playerId);
56
+ if (!player)
57
+ return [];
58
+ return [
59
+ {
60
+ clubKey: club.key,
61
+ clubName: club.name,
62
+ sideLabel: side.label,
63
+ showCmsId: hasSharedName(player, sharedNames(side.players)),
64
+ player: { ...player, rows: [] },
65
+ fixtures: playerFixtures(player, context, options),
66
+ },
67
+ ];
68
+ });
69
+ }
70
+ /** Clubs and sides in the order a profile lists them. */
71
+ export function sortEntries(entries) {
72
+ return [...entries].sort((a, b) => a.clubName.localeCompare(b.clubName) || a.sideLabel.localeCompare(b.sideLabel));
73
+ }
74
+ /** Which clubs each player is in the record of, for finding a player from their id.
75
+ * A player whose profile isn't public ("Name Withheld") is left out. */
76
+ export function buildPlayerIndex(season, clubs) {
77
+ const found = new Map();
78
+ for (const { club, breakdown } of clubs) {
79
+ for (const side of breakdown.sides) {
80
+ for (const player of side.players) {
81
+ if (isNameWithheld(player.name))
82
+ continue;
83
+ const entry = found.get(player.playerId) ?? { name: player.name, clubs: [] };
84
+ if (!entry.clubs.includes(club.key))
85
+ entry.clubs.push(club.key);
86
+ found.set(player.playerId, entry);
87
+ }
88
+ }
89
+ }
90
+ const players = Object.fromEntries([...found].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
91
+ return { season, players };
92
+ }
@@ -1,6 +1,8 @@
1
+ import type { PublishedPlayersFile } from './data.ts';
1
2
  import type { Club, EligibilityBreakdown, EligibilityRules, PlayerEligibility } from './types.ts';
2
3
  import type { FeedFixture } from './feeds.ts';
3
4
  import { type ParticipationSnapshot } from './participation.ts';
5
+ import { type PlayerProfile } from './players.ts';
4
6
  import { type TeamSide } from './teamSide.ts';
5
7
  /** Where the reference deployment publishes its nightly data. */
6
8
  export declare const DEFAULT_PUBLISHED_URL = "https://zmnatz.github.io/usa-club-rugby-eligibility/data";
@@ -59,6 +61,14 @@ export declare function createPublishedClient({ baseUrl, fetch: fetcher, }?: Pub
59
61
  teams(): Promise<TeamRecord[]>;
60
62
  /** A club's verdicts side by side. */
61
63
  eligibility(key: string, options?: EligibilityOptions): Promise<EligibilityBreakdown>;
64
+ /** Which clubs each player is in the record of, by CMS id. Not a published file: worked
65
+ * out from every club's file, so it reads them all. */
66
+ playerIndex(options?: EligibilityOptions): Promise<PublishedPlayersFile>;
67
+ /** A player from their CMS id: their verdict on every club side they're listed on, with
68
+ * the fixtures behind each. Null when the id isn't in the published record (players
69
+ * whose profile isn't public aren't). Reads every club to find them, so for many lookups
70
+ * build the index once with `playerIndex`. */
71
+ player(id: string, options?: EligibilityOptions): Promise<PlayerProfile | null>;
62
72
  /** Every player with a verdict on each of a club's sides, one record per side. */
63
73
  players(key: string, options?: EligibilityOptions): Promise<PlayerRecord[]>;
64
74
  };
package/dist/published.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { computeBreakdown } from './eligibility.js';
2
2
  import { clubFixtures, participationRows, remainingMatches } from './participation.js';
3
+ import { buildPlayerIndex, clubContext, playerEntries, sortEntries } from './players.js';
3
4
  import { DEFAULT_RULES } from './rules.js';
4
5
  import { parseTeamSide } from './teamSide.js';
5
6
  // Readers for the data the nightly job publishes: public/data/index.json,
@@ -58,6 +59,32 @@ export function createPublishedClient({ baseUrl = DEFAULT_PUBLISHED_URL, fetch:
58
59
  const [{ club: found, snapshot }, all] = await Promise.all([club(key), fixtures()]);
59
60
  return clubEligibility(snapshot, all, found, options);
60
61
  },
62
+ /** Which clubs each player is in the record of, by CMS id. Not a published file: worked
63
+ * out from every club's file, so it reads them all. */
64
+ async playerIndex(options = {}) {
65
+ const [{ clubs, season }, all] = await Promise.all([index(), fixtures()]);
66
+ const contexts = await Promise.all(clubs.map(async (published) => {
67
+ const { snapshot } = await club(published.key);
68
+ return { club: published, breakdown: clubContext(published, snapshot, all, options).breakdown };
69
+ }));
70
+ return buildPlayerIndex(season, contexts);
71
+ },
72
+ /** A player from their CMS id: their verdict on every club side they're listed on, with
73
+ * the fixtures behind each. Null when the id isn't in the published record (players
74
+ * whose profile isn't public aren't). Reads every club to find them, so for many lookups
75
+ * build the index once with `playerIndex`. */
76
+ async player(id, options = {}) {
77
+ const found = (await this.playerIndex(options)).players[id];
78
+ if (!found)
79
+ return null;
80
+ const all = await fixtures();
81
+ const entries = [];
82
+ for (const key of found.clubs) {
83
+ const { club: published, snapshot } = await club(key);
84
+ entries.push(...playerEntries(id, clubContext(published, snapshot, all, options), options));
85
+ }
86
+ return { id, name: found.name, clubs: sortEntries(entries) };
87
+ },
61
88
  /** Every player with a verdict on each of a club's sides, one record per side. */
62
89
  async players(key, options = {}) {
63
90
  const [{ club: found, snapshot }, all] = await Promise.all([club(key), fixtures()]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmnatz/usa-club-rugby-data",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "USA Club Rugby senior club teams, players and NCS eligibility from the Rugby Xplorer match record: typed readers for the nightly published data, a rate-limited CMS client, and the reproducible eligibility rules.",
5
5
  "keywords": [
6
6
  "rugby",
@@ -53,7 +53,7 @@
53
53
  "prepublishOnly": "bun run build && bun run test"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/node": "^22.0.0",
56
+ "@types/node": "^26.0.0",
57
57
  "typescript": "^7.0.2",
58
58
  "vitest": "^5.1.0"
59
59
  }