fansunited-data-layer 0.18.0 → 0.18.3

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
@@ -43,6 +43,11 @@ const board = await getFansUnitedSportsCompetitionHydrated("fb:c:1", {
43
43
  });
44
44
 
45
45
  for (const stage of board.season.stages) {
46
+ // Stage-name labels are hydrated alongside competitors —
47
+ // entities.get(stageNameId) returns a localized FUSportsStageName.
48
+ const label = board.entities.get(stage.stageNameId);
49
+ console.log(`\n=== ${label?.name ?? stage.stageNameId} ===`);
50
+
46
51
  for (const group of stage.groups ?? []) {
47
52
  for (const entry of group.standings) {
48
53
  const team = board.entities.get(entry.competitorId);
@@ -2,8 +2,13 @@
2
2
  * Hydrated wrapper for the Fans United Sports API competition endpoint.
3
3
  *
4
4
  * Composes the (cached) raw competition response with a (cached) batch
5
- * resolution of every entity ID in `meta.entityIds` via the Search API,
6
- * and returns one object the consumer can render directly.
5
+ * resolution of every referenced entity via the Search API, and returns one
6
+ * object the consumer can render directly. Hydration covers:
7
+ * - everything listed in `meta.entityIds` (competitors, venues, …)
8
+ * - every `stage.stageNameId` referenced by `season.stages[]`
9
+ * ({@link FUSportsStageName}) — these are pulled from the response
10
+ * structure because the upstream `meta.entity_ids` field has been
11
+ * observed not to include them.
7
12
  *
8
13
  * Cache topology:
9
14
  * - sports/competition response → cached under "sportsCompetition"
@@ -12,12 +17,23 @@
12
17
  * by id only; the raw multi-locale payload serves every locale via
13
18
  * transformEntity(raw, lang).
14
19
  *
15
- * @example
20
+ * @example Standings + localized stage labels
21
+ * ```ts
16
22
  * const data = await getFansUnitedSportsCompetitionHydrated("fb:c:1", { locale: "BG" });
17
- * for (const entry of data.season.stages[0].groups[0].standings) {
18
- * const team = data.entities.get(entry.competitorId);
19
- * console.log(`${entry.rank} ${team?.name}`);
23
+ *
24
+ * for (const stage of data.season.stages) {
25
+ * // Stage name lookup — same shape as any other entity in the map.
26
+ * const label = data.entities.get(stage.stageNameId);
27
+ * console.log(`\n=== ${label?.name ?? stage.stageNameId} ===`);
28
+ *
29
+ * for (const group of stage.groups ?? []) {
30
+ * for (const entry of group.standings) {
31
+ * const team = data.entities.get(entry.competitorId);
32
+ * console.log(`${entry.rank} ${team?.name ?? entry.competitorId}`);
33
+ * }
34
+ * }
20
35
  * }
36
+ * ```
21
37
  */
22
38
  import type { FUSportsCompetitionDetails } from "../../../../types/canonical/sports-competition.types";
23
39
  import type { DataLayerConfig } from "../../../../config";
@@ -34,8 +50,13 @@ export interface GetSportsCompetitionHydratedOptions {
34
50
  /**
35
51
  * Sports API competition response plus a lookup map for every referenced entity.
36
52
  *
37
- * The map is keyed by entity ID. Missing IDs (404s from the Search API) are
38
- * silently omitted render code should fall back gracefully on a missing entry:
53
+ * The map is keyed by entity ID and holds the localized {@link SearchEntityResult}
54
+ * unioncompetitors (teams/athletes), venues, countries, coaches, and
55
+ * stage-name labels (`FUSportsStageName & { entityType: "stageName" }`) for
56
+ * every `stage.stageNameId` referenced by `season.stages[]`.
57
+ *
58
+ * Missing IDs (404s from the Search API) are silently omitted — render code
59
+ * should fall back gracefully on a missing entry:
39
60
  * `entities.get(id) ?? { name: id }`.
40
61
  */
41
62
  export interface FUSportsCompetitionHydrated extends FUSportsCompetitionDetails {
@@ -1 +1 @@
1
- {"version":3,"file":"hydrated.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAI7D,MAAM,WAAW,mCAAmC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,2BAA4B,SAAQ,0BAA0B;IAC3E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C;AAED,wBAAsB,sCAAsC,CACxD,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,mCAAmC,EAC7C,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,2BAA2B,CAAC,CAsBtC"}
1
+ {"version":3,"file":"hydrated.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAkB7D,MAAM,WAAW,mCAAmC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,2BAA4B,SAAQ,0BAA0B;IAC3E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C;AAED,wBAAsB,sCAAsC,CACxD,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,mCAAmC,EAC7C,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,2BAA2B,CAAC,CA0BtC"}
@@ -1,12 +1,21 @@
1
1
  import { getFansUnitedSportsCompetition } from "./index.js";
2
2
  import { getFansUnitedEntitiesByIds } from "../../search/index.js";
3
+ function collectStageNameIds(details) {
4
+ const ids = [];
5
+ for (const stage of details.season.stages) {
6
+ if (stage.stageNameId) ids.push(stage.stageNameId);
7
+ }
8
+ return ids;
9
+ }
3
10
  async function getFansUnitedSportsCompetitionHydrated(competitionId, options, config) {
4
11
  const raw = await getFansUnitedSportsCompetition(
5
12
  competitionId,
6
13
  { seasonId: options?.seasonId, next: options?.next },
7
14
  config
8
15
  );
9
- const entityIds = raw.meta.entityIds;
16
+ const entityIds = Array.from(
17
+ /* @__PURE__ */ new Set([...raw.meta.entityIds, ...collectStageNameIds(raw)])
18
+ );
10
19
  const entities = /* @__PURE__ */ new Map();
11
20
  if (entityIds.length > 0) {
12
21
  const resolved = await getFansUnitedEntitiesByIds(entityIds, {
@@ -1 +1 @@
1
- {"version":3,"file":"hydrated.js","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"sourcesContent":["/**\n * Hydrated wrapper for the Fans United Sports API competition endpoint.\n *\n * Composes the (cached) raw competition response with a (cached) batch\n * resolution of every entity ID in `meta.entityIds` via the Search API,\n * and returns one object the consumer can render directly.\n *\n * Cache topology:\n * - sports/competition response → cached under \"sportsCompetition\"\n * (5min stale / 1h max) by id+season.\n * - search/entity responses → cached under \"search\" (24h stale / 7d max)\n * by id only; the raw multi-locale payload serves every locale via\n * transformEntity(raw, lang).\n *\n * @example\n * const data = await getFansUnitedSportsCompetitionHydrated(\"fb:c:1\", { locale: \"BG\" });\n * for (const entry of data.season.stages[0].groups[0].standings) {\n * const team = data.entities.get(entry.competitorId);\n * console.log(`${entry.rank} ${team?.name}`);\n * }\n */\n\nimport type { FUSportsCompetitionDetails } from \"../../../../types/canonical/sports-competition.types\";\nimport type { DataLayerConfig } from \"../../../../config\";\nimport type { NextCacheOptions } from \"../../http\";\nimport type { SearchEntityResult } from \"../../search/types\";\nimport { getFansUnitedSportsCompetition } from \"./index\";\nimport { getFansUnitedEntitiesByIds } from \"../../search\";\n\nexport interface GetSportsCompetitionHydratedOptions {\n /** Season identifier (e.g. \"fb:c:1:2025/26\"). Defaults to the active season. */\n seasonId?: string;\n /** Locale code (e.g. \"EN\", \"BG\") used to resolve entity translations. Falls back to EN. */\n locale?: string;\n /** Next.js cache options for ISR/on-demand revalidation. */\n next?: NextCacheOptions;\n}\n\n/**\n * Sports API competition response plus a lookup map for every referenced entity.\n *\n * The map is keyed by entity ID. Missing IDs (404s from the Search API) are\n * silently omitted — render code should fall back gracefully on a missing entry:\n * `entities.get(id) ?? { name: id }`.\n */\nexport interface FUSportsCompetitionHydrated extends FUSportsCompetitionDetails {\n entities: Map<string, SearchEntityResult>;\n}\n\nexport async function getFansUnitedSportsCompetitionHydrated(\n competitionId: string,\n options?: GetSportsCompetitionHydratedOptions,\n config?: DataLayerConfig\n): Promise<FUSportsCompetitionHydrated> {\n const raw = await getFansUnitedSportsCompetition(\n competitionId,\n { seasonId: options?.seasonId, next: options?.next },\n config\n );\n\n const entityIds = raw.meta.entityIds;\n const entities = new Map<string, SearchEntityResult>();\n\n if (entityIds.length > 0) {\n const resolved = await getFansUnitedEntitiesByIds(entityIds, {\n lang: options?.locale,\n next: options?.next,\n config,\n });\n for (const e of resolved) {\n entities.set(e.id, e);\n }\n }\n\n return { ...raw, entities };\n}\n"],"names":[],"mappings":";;AAiDA,eAAsB,uCAClB,eACA,SACA,QACoC;AACpC,QAAM,MAAM,MAAM;AAAA,IACd;AAAA,IACA,EAAE,UAAU,SAAS,UAAU,MAAM,SAAS,KAAA;AAAA,IAC9C;AAAA,EAAA;AAGJ,QAAM,YAAY,IAAI,KAAK;AAC3B,QAAM,+BAAe,IAAA;AAErB,MAAI,UAAU,SAAS,GAAG;AACtB,UAAM,WAAW,MAAM,2BAA2B,WAAW;AAAA,MACzD,MAAM,SAAS;AAAA,MACf,MAAM,SAAS;AAAA,MACf;AAAA,IAAA,CACH;AACD,eAAW,KAAK,UAAU;AACtB,eAAS,IAAI,EAAE,IAAI,CAAC;AAAA,IACxB;AAAA,EACJ;AAEA,SAAO,EAAE,GAAG,KAAK,SAAA;AACrB;"}
1
+ {"version":3,"file":"hydrated.js","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"sourcesContent":["/**\n * Hydrated wrapper for the Fans United Sports API competition endpoint.\n *\n * Composes the (cached) raw competition response with a (cached) batch\n * resolution of every referenced entity via the Search API, and returns one\n * object the consumer can render directly. Hydration covers:\n * - everything listed in `meta.entityIds` (competitors, venues, …)\n * - every `stage.stageNameId` referenced by `season.stages[]`\n * ({@link FUSportsStageName}) these are pulled from the response\n * structure because the upstream `meta.entity_ids` field has been\n * observed not to include them.\n *\n * Cache topology:\n * - sports/competition response → cached under \"sportsCompetition\"\n * (5min stale / 1h max) by id+season.\n * - search/entity responses → cached under \"search\" (24h stale / 7d max)\n * by id only; the raw multi-locale payload serves every locale via\n * transformEntity(raw, lang).\n *\n * @example Standings + localized stage labels\n * ```ts\n * const data = await getFansUnitedSportsCompetitionHydrated(\"fb:c:1\", { locale: \"BG\" });\n *\n * for (const stage of data.season.stages) {\n * // Stage name lookup — same shape as any other entity in the map.\n * const label = data.entities.get(stage.stageNameId);\n * console.log(`\\n=== ${label?.name ?? stage.stageNameId} ===`);\n *\n * for (const group of stage.groups ?? []) {\n * for (const entry of group.standings) {\n * const team = data.entities.get(entry.competitorId);\n * console.log(`${entry.rank} ${team?.name ?? entry.competitorId}`);\n * }\n * }\n * }\n * ```\n */\n\nimport type { FUSportsCompetitionDetails } from \"../../../../types/canonical/sports-competition.types\";\nimport type { DataLayerConfig } from \"../../../../config\";\nimport type { NextCacheOptions } from \"../../http\";\nimport type { SearchEntityResult } from \"../../search/types\";\nimport { getFansUnitedSportsCompetition } from \"./index\";\nimport { getFansUnitedEntitiesByIds } from \"../../search\";\n\n/**\n * Stage-name IDs are referenced via {@link FUSportsCompetitionStage.stageNameId}\n * but the upstream `meta.entity_ids` field has been observed not to include\n * them. Collect explicitly so they're guaranteed to hydrate into the entities\n * map alongside teams, players, etc.\n */\nfunction collectStageNameIds(details: FUSportsCompetitionDetails): string[] {\n const ids: string[] = [];\n for (const stage of details.season.stages) {\n if (stage.stageNameId) ids.push(stage.stageNameId);\n }\n return ids;\n}\n\nexport interface GetSportsCompetitionHydratedOptions {\n /** Season identifier (e.g. \"fb:c:1:2025/26\"). Defaults to the active season. */\n seasonId?: string;\n /** Locale code (e.g. \"EN\", \"BG\") used to resolve entity translations. Falls back to EN. */\n locale?: string;\n /** Next.js cache options for ISR/on-demand revalidation. */\n next?: NextCacheOptions;\n}\n\n/**\n * Sports API competition response plus a lookup map for every referenced entity.\n *\n * The map is keyed by entity ID and holds the localized {@link SearchEntityResult}\n * union — competitors (teams/athletes), venues, countries, coaches, and\n * stage-name labels (`FUSportsStageName & { entityType: \"stageName\" }`) for\n * every `stage.stageNameId` referenced by `season.stages[]`.\n *\n * Missing IDs (404s from the Search API) are silently omitted — render code\n * should fall back gracefully on a missing entry:\n * `entities.get(id) ?? { name: id }`.\n */\nexport interface FUSportsCompetitionHydrated extends FUSportsCompetitionDetails {\n entities: Map<string, SearchEntityResult>;\n}\n\nexport async function getFansUnitedSportsCompetitionHydrated(\n competitionId: string,\n options?: GetSportsCompetitionHydratedOptions,\n config?: DataLayerConfig\n): Promise<FUSportsCompetitionHydrated> {\n const raw = await getFansUnitedSportsCompetition(\n competitionId,\n { seasonId: options?.seasonId, next: options?.next },\n config\n );\n\n // Union meta.entity_ids with stage_name_ids walked from season.stages —\n // dedupe so duplicates only cost one Search API row.\n const entityIds = Array.from(\n new Set<string>([...raw.meta.entityIds, ...collectStageNameIds(raw)])\n );\n const entities = new Map<string, SearchEntityResult>();\n\n if (entityIds.length > 0) {\n const resolved = await getFansUnitedEntitiesByIds(entityIds, {\n lang: options?.locale,\n next: options?.next,\n config,\n });\n for (const e of resolved) {\n entities.set(e.id, e);\n }\n }\n\n return { ...raw, entities };\n}\n"],"names":[],"mappings":";;AAmDA,SAAS,oBAAoB,SAA+C;AACxE,QAAM,MAAgB,CAAA;AACtB,aAAW,SAAS,QAAQ,OAAO,QAAQ;AACvC,QAAI,MAAM,YAAa,KAAI,KAAK,MAAM,WAAW;AAAA,EACrD;AACA,SAAO;AACX;AA2BA,eAAsB,uCAClB,eACA,SACA,QACoC;AACpC,QAAM,MAAM,MAAM;AAAA,IACd;AAAA,IACA,EAAE,UAAU,SAAS,UAAU,MAAM,SAAS,KAAA;AAAA,IAC9C;AAAA,EAAA;AAKJ,QAAM,YAAY,MAAM;AAAA,IACpB,oBAAI,IAAY,CAAC,GAAG,IAAI,KAAK,WAAW,GAAG,oBAAoB,GAAG,CAAC,CAAC;AAAA,EAAA;AAExE,QAAM,+BAAe,IAAA;AAErB,MAAI,UAAU,SAAS,GAAG;AACtB,UAAM,WAAW,MAAM,2BAA2B,WAAW;AAAA,MACzD,MAAM,SAAS;AAAA,MACf,MAAM,SAAS;AAAA,MACf;AAAA,IAAA,CACH;AACD,eAAW,KAAK,UAAU;AACtB,eAAS,IAAI,EAAE,IAAI,CAAC;AAAA,IACxB;AAAA,EACJ;AAEA,SAAO,EAAE,GAAG,KAAK,SAAA;AACrB;"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "description": "A TypeScript library for fetching and transforming sports data from multiple API providers. Returns clean, canonical types that are provider-agnostic.",
5
5
  "homepage": "https://fansunited.com/",
6
6
  "private": false,
7
- "version": "0.18.0",
7
+ "version": "0.18.3",
8
8
  "type": "module",
9
9
  "sideEffects": false,
10
10
  "module": "./fansunited-data-layer.js",