xpt-shared-types 1.3.0 → 1.4.1

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
@@ -54,7 +54,7 @@ frontend cannot consume.
54
54
  **Everything except `id` and `documentId` is optional.** Not just relations.
55
55
  This codebase uses Strapi's `fields` selection pervasively, so even a
56
56
  schema-`required` attribute like `user.email` is absent from most responses.
57
- The base model is the *minimum guarantee*, not the full row.
57
+ The base model is the _minimum guarantee_, not the full row.
58
58
 
59
59
  **Single values are `?: T | null`.** Strapi sends `null` for an unset value
60
60
  rather than omitting the key. Lists come back as `[]`, so they are not nullable.
@@ -83,7 +83,7 @@ asked for:
83
83
  ```ts
84
84
  const tournament = await getTournament(slug); // Populated<Tournament, 'game' | 'prizes'>
85
85
 
86
- tournament.game.title; // ok — the annotation says it was populated
86
+ tournament.game.title; // ok — the annotation says it was populated
87
87
  tournament.region?.name; // still optional — this query did not populate it
88
88
  ```
89
89
 
@@ -95,10 +95,10 @@ silently disables exactly the checking this package exists to provide.
95
95
 
96
96
  The two apps are on different Yarn majors, so they link differently:
97
97
 
98
- | Repo | Yarn | Dependency |
99
- | --- | --- | --- |
100
- | `xpt-strapi` | 1.22.5 (classic) | `"xpt-shared-types": "link:../xpt-shared-types"` |
101
- | `xpt-client` | 4.1.1 (Berry) | `"xpt-shared-types": "portal:../xpt-shared-types"` |
98
+ | Repo | Yarn | Dependency |
99
+ | ------------ | ---------------- | -------------------------------------------------- |
100
+ | `xpt-strapi` | 1.22.5 (classic) | `"xpt-shared-types": "link:../xpt-shared-types"` |
101
+ | `xpt-client` | 4.1.1 (Berry) | `"xpt-shared-types": "portal:../xpt-shared-types"` |
102
102
 
103
103
  `portal:` is Berry-only and fails on Yarn 1. Both symlink to this directory, so
104
104
  edits are picked up after `yarn build` with no publish step.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Which platforms a game runs on, and which account networks a player needs
3
+ * on each of them.
4
+ *
5
+ * A tournament carries three catalogue relations — `game`, `platform` and
6
+ * `gameAccount` — and until these functions existed they were three
7
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
8
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
9
+ *
10
+ * The chain is game -> platform -> account, each step narrowing the next:
11
+ *
12
+ * - a game is tagged with the platforms it runs on, and with the account
13
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
14
+ * - a platform is tagged with the networks that exist on that box
15
+ * (PlayStation -> PSN, EA, Epic, Riot);
16
+ * - the accounts valid for a tournament are the *intersection* of the two.
17
+ *
18
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
19
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
20
+ * platform alone over-generates. And a game's platforms are not the union of
21
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
22
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
23
+ * Both edges are real; the intersection is the answer.
24
+ *
25
+ * Lives here rather than in xpt-strapi because the client filters its radio
26
+ * groups with the identical rule. Two implementations of an intersection is
27
+ * two chances to disagree about what a host is allowed to pick.
28
+ *
29
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
30
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
31
+ */
32
+ /** The identifying fields of a populated Strapi relation, all optional. */
33
+ export interface CatalogueRef {
34
+ id?: number | string | null;
35
+ documentId?: string | null;
36
+ name?: string | null;
37
+ }
38
+ /** A platform, with the account networks that exist on it. */
39
+ export interface CataloguePlatformLike extends CatalogueRef {
40
+ game_accounts?: readonly CatalogueRef[] | null;
41
+ }
42
+ /** A game, with the platforms it runs on and the networks it uses. */
43
+ export interface CatalogueGameLike extends CatalogueRef {
44
+ title?: string | null;
45
+ platforms?: readonly CatalogueRef[] | null;
46
+ game_accounts?: readonly CatalogueRef[] | null;
47
+ }
48
+ /**
49
+ * Whether two relation references point at the same row.
50
+ *
51
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
52
+ * between a draft and its published entry while the documentId does not. Two
53
+ * references that share *neither* field are never equal — the tempting
54
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
55
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
56
+ * account set at all.
57
+ */
58
+ export declare function sameRef(a?: CatalogueRef | null, b?: CatalogueRef | null): boolean;
59
+ /**
60
+ * The platforms a game can be played on.
61
+ *
62
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
63
+ * the order of `allPlatforms` so the radio group a host sees is stable and
64
+ * matches the unfiltered list they saw before any tagging happened.
65
+ */
66
+ export declare function platformsForGame<P extends CataloguePlatformLike>(game: CatalogueGameLike | null | undefined, allPlatforms: readonly P[]): P[];
67
+ /**
68
+ * The account networks valid for a game on a platform.
69
+ *
70
+ * With no platform chosen yet the game's own list is returned unfiltered —
71
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
72
+ * its account at creation time, before a platform exists.
73
+ *
74
+ * An empty result when both sides *are* tagged is returned as-is rather than
75
+ * falling back to the full catalogue. It means the two tag lists disagree,
76
+ * which is a data bug: surfacing it as "no account type available" gets it
77
+ * fixed, whereas quietly showing all ten hides it forever.
78
+ */
79
+ export declare function gameAccountsFor<A extends CatalogueRef>(game: CatalogueGameLike | null | undefined, platform: CataloguePlatformLike | null | undefined, allGameAccounts: readonly A[]): A[];
80
+ /**
81
+ * The one candidate, when there is exactly one.
82
+ *
83
+ * Drives auto-connect: a single valid option is not a choice, so the server
84
+ * connects it and the host is never shown a dialog with one radio in it.
85
+ */
86
+ export declare function soleCandidate<T>(candidates: readonly T[]): T | null;
87
+ /**
88
+ * The candidate platforms that carry this account network — the chain read
89
+ * backwards.
90
+ *
91
+ * Most networks answer the platform question by themselves: a PSN ID only
92
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
93
+ * `soleCandidate` and choosing the account fills the platform in, which is the
94
+ * other half of what makes these two cards feel like one decision.
95
+ *
96
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
97
+ * and so infer nothing — correctly, because an EA ID really does not say which
98
+ * box you are on.
99
+ *
100
+ * An **untagged** platform is excluded rather than treated as permissive. That
101
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
102
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
103
+ * this.
104
+ */
105
+ export declare function platformsCarrying<P extends CataloguePlatformLike>(gameAccount: CatalogueRef | null | undefined, candidates: readonly P[]): P[];
106
+ /** Whether this platform is one the game actually runs on. */
107
+ export declare function isPlatformValidForGame(game: CatalogueGameLike | null | undefined, platform: CatalogueRef | null | undefined, allPlatforms: readonly CataloguePlatformLike[]): boolean;
108
+ /** Whether this account network is one the game uses on this platform. */
109
+ export declare function isGameAccountValidFor(game: CatalogueGameLike | null | undefined, platform: CataloguePlatformLike | null | undefined, gameAccount: CatalogueRef | null | undefined, allGameAccounts: readonly CatalogueRef[]): boolean;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ /**
3
+ * Which platforms a game runs on, and which account networks a player needs
4
+ * on each of them.
5
+ *
6
+ * A tournament carries three catalogue relations — `game`, `platform` and
7
+ * `gameAccount` — and until these functions existed they were three
8
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
9
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
10
+ *
11
+ * The chain is game -> platform -> account, each step narrowing the next:
12
+ *
13
+ * - a game is tagged with the platforms it runs on, and with the account
14
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
15
+ * - a platform is tagged with the networks that exist on that box
16
+ * (PlayStation -> PSN, EA, Epic, Riot);
17
+ * - the accounts valid for a tournament are the *intersection* of the two.
18
+ *
19
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
20
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
21
+ * platform alone over-generates. And a game's platforms are not the union of
22
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
23
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
24
+ * Both edges are real; the intersection is the answer.
25
+ *
26
+ * Lives here rather than in xpt-strapi because the client filters its radio
27
+ * groups with the identical rule. Two implementations of an intersection is
28
+ * two chances to disagree about what a host is allowed to pick.
29
+ *
30
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
31
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.sameRef = sameRef;
35
+ exports.platformsForGame = platformsForGame;
36
+ exports.gameAccountsFor = gameAccountsFor;
37
+ exports.soleCandidate = soleCandidate;
38
+ exports.platformsCarrying = platformsCarrying;
39
+ exports.isPlatformValidForGame = isPlatformValidForGame;
40
+ exports.isGameAccountValidFor = isGameAccountValidFor;
41
+ /**
42
+ * Whether two relation references point at the same row.
43
+ *
44
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
45
+ * between a draft and its published entry while the documentId does not. Two
46
+ * references that share *neither* field are never equal — the tempting
47
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
48
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
49
+ * account set at all.
50
+ */
51
+ function sameRef(a, b) {
52
+ if (!a || !b)
53
+ return false;
54
+ if (a.documentId && b.documentId)
55
+ return a.documentId === b.documentId;
56
+ const aId = a.id;
57
+ const bId = b.id;
58
+ if (aId === null || aId === undefined)
59
+ return false;
60
+ if (bId === null || bId === undefined)
61
+ return false;
62
+ return String(aId) === String(bId);
63
+ }
64
+ /**
65
+ * Whether a catalogue tag list says anything.
66
+ *
67
+ * An absent list and an empty one mean the same thing: nobody has tagged this
68
+ * row yet. Strapi serialises an unpopulated relation and a populated-but-empty
69
+ * one identically once it reaches the client, so there is no way to tell them
70
+ * apart and no reason to try — either way we have no opinion, and an untagged
71
+ * row must never lock a host out of a game we simply forgot to configure.
72
+ */
73
+ function isTagged(list) {
74
+ return Array.isArray(list) && list.length > 0;
75
+ }
76
+ /** The master list, minus any row that repeats one already seen. */
77
+ function dedupe(rows) {
78
+ const seen = [];
79
+ for (const row of rows) {
80
+ if (!seen.some((kept) => sameRef(kept, row)))
81
+ seen.push(row);
82
+ }
83
+ return seen;
84
+ }
85
+ /**
86
+ * The platforms a game can be played on.
87
+ *
88
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
89
+ * the order of `allPlatforms` so the radio group a host sees is stable and
90
+ * matches the unfiltered list they saw before any tagging happened.
91
+ */
92
+ function platformsForGame(game, allPlatforms) {
93
+ const all = dedupe(allPlatforms);
94
+ if (!game || !isTagged(game.platforms))
95
+ return all;
96
+ return all.filter((platform) => game.platforms.some((tag) => sameRef(tag, platform)));
97
+ }
98
+ /**
99
+ * The account networks valid for a game on a platform.
100
+ *
101
+ * With no platform chosen yet the game's own list is returned unfiltered —
102
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
103
+ * its account at creation time, before a platform exists.
104
+ *
105
+ * An empty result when both sides *are* tagged is returned as-is rather than
106
+ * falling back to the full catalogue. It means the two tag lists disagree,
107
+ * which is a data bug: surfacing it as "no account type available" gets it
108
+ * fixed, whereas quietly showing all ten hides it forever.
109
+ */
110
+ function gameAccountsFor(game, platform, allGameAccounts) {
111
+ const all = dedupe(allGameAccounts);
112
+ const byGame = !game || !isTagged(game.game_accounts)
113
+ ? all
114
+ : all.filter((account) => game.game_accounts.some((tag) => sameRef(tag, account)));
115
+ // No platform yet, or a platform nobody has tagged: the game is the only
116
+ // authority we have.
117
+ if (!platform || !isTagged(platform.game_accounts))
118
+ return byGame;
119
+ return byGame.filter((account) => platform.game_accounts.some((tag) => sameRef(tag, account)));
120
+ }
121
+ /**
122
+ * The one candidate, when there is exactly one.
123
+ *
124
+ * Drives auto-connect: a single valid option is not a choice, so the server
125
+ * connects it and the host is never shown a dialog with one radio in it.
126
+ */
127
+ function soleCandidate(candidates) {
128
+ return candidates.length === 1 ? candidates[0] : null;
129
+ }
130
+ /**
131
+ * The candidate platforms that carry this account network — the chain read
132
+ * backwards.
133
+ *
134
+ * Most networks answer the platform question by themselves: a PSN ID only
135
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
136
+ * `soleCandidate` and choosing the account fills the platform in, which is the
137
+ * other half of what makes these two cards feel like one decision.
138
+ *
139
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
140
+ * and so infer nothing — correctly, because an EA ID really does not say which
141
+ * box you are on.
142
+ *
143
+ * An **untagged** platform is excluded rather than treated as permissive. That
144
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
145
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
146
+ * this.
147
+ */
148
+ function platformsCarrying(gameAccount, candidates) {
149
+ if (!gameAccount)
150
+ return [];
151
+ return dedupe(candidates).filter((platform) => { var _a; return ((_a = platform.game_accounts) !== null && _a !== void 0 ? _a : []).some((tag) => sameRef(tag, gameAccount)); });
152
+ }
153
+ /** Whether this platform is one the game actually runs on. */
154
+ function isPlatformValidForGame(game, platform, allPlatforms) {
155
+ if (!platform)
156
+ return false;
157
+ return platformsForGame(game, allPlatforms).some((valid) => sameRef(valid, platform));
158
+ }
159
+ /** Whether this account network is one the game uses on this platform. */
160
+ function isGameAccountValidFor(game, platform, gameAccount, allGameAccounts) {
161
+ if (!gameAccount)
162
+ return false;
163
+ return gameAccountsFor(game, platform, allGameAccounts).some((valid) => sameRef(valid, gameAccount));
164
+ }
@@ -14,8 +14,8 @@ export type MatchRound = "Round 128" | "Round 64" | "Round 32" | "Round 16" | "Q
14
14
  export type MatchStreamPlatform = "twitch" | "youtube" | "kick" | "other";
15
15
  /** `Match.winnerSlot` */
16
16
  export type MatchWinnerSlot = "home" | "away";
17
- /** `Prize.rank` */
18
- export type PrizeRank = "one" | "two" | "three";
17
+ /** `Platform.code` */
18
+ export type PlatformCode = "pc" | "playstation" | "xbox" | "nintendo" | "mobile";
19
19
  /** `Referral.status` */
20
20
  export type ReferralStatus = "pending" | "completed";
21
21
  /** `Team.current_status` */
@@ -34,8 +34,6 @@ export type TournamentParticipantEntryType = "solo" | "team";
34
34
  export type TournamentParticipantStatus = "registered" | "active" | "eliminated" | "completed";
35
35
  /** `TournamentRole.role` */
36
36
  export type TournamentRoleRole = "moderator" | "admin";
37
- /** `TournamentStage.stageName` */
38
- export type TournamentStageStageName = "Round 128" | "Round 64" | "Round 32" | "Round 16" | "Quarter Final" | "Semi-Final" | "Third Round" | "Finals" | "League";
39
37
  /** `Tournament.teamSize` */
40
38
  export type TournamentTeamSize = "one" | "two" | "three" | "four" | "five";
41
39
  /** `Tournament.type` */
@@ -1,5 +1,5 @@
1
1
  import type { BlocksContent, MediaInput, RelationInput } from '../contracts';
2
- import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PrizeRank, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentStageStageName, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
2
+ import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
3
3
  /** Write payload for `about`. */
4
4
  export interface AboutInput {
5
5
  heroTitle?: string;
@@ -48,6 +48,8 @@ export interface GameInput {
48
48
  user_game_stats?: RelationInput | RelationInput[];
49
49
  team_game_stats?: RelationInput | RelationInput[];
50
50
  imgMainPosition?: string;
51
+ platforms?: RelationInput | RelationInput[];
52
+ game_accounts?: RelationInput | RelationInput[];
51
53
  }
52
54
  /** Write payload for `game-account`. */
53
55
  export interface GameAccountInput {
@@ -55,6 +57,8 @@ export interface GameAccountInput {
55
57
  tournaments?: RelationInput | RelationInput[];
56
58
  user_game_accounts?: RelationInput | RelationInput[];
57
59
  imgThumb?: MediaInput;
60
+ games?: RelationInput | RelationInput[];
61
+ platforms?: RelationInput | RelationInput[];
58
62
  }
59
63
  /** Write payload for `game-request`. */
60
64
  export interface GameRequestInput {
@@ -148,7 +152,10 @@ export interface NotificationInput {
148
152
  /** Write payload for `platform`. */
149
153
  export interface PlatformInput {
150
154
  name?: string;
155
+ code?: PlatformCode;
151
156
  tournaments?: RelationInput | RelationInput[];
157
+ games?: RelationInput | RelationInput[];
158
+ game_accounts?: RelationInput | RelationInput[];
152
159
  }
153
160
  /** Write payload for `preset-avatar`. */
154
161
  export interface PresetAvatarInput {
@@ -171,13 +178,6 @@ export interface PrivacyInput {
171
178
  content?: BlocksContent;
172
179
  title?: string;
173
180
  }
174
- /** Write payload for `prize`. */
175
- export interface PrizeInput {
176
- description?: string;
177
- value?: number;
178
- tournament?: RelationInput;
179
- rank?: PrizeRank;
180
- }
181
181
  /** Write payload for `referral`. */
182
182
  export interface ReferralInput {
183
183
  referrer?: RelationInput;
@@ -277,18 +277,19 @@ export interface TournamentInput {
277
277
  game?: RelationInput;
278
278
  currentStatus?: TournamentCurrentStatus;
279
279
  group?: RelationInput;
280
- prizes?: RelationInput | RelationInput[];
280
+ prizes?: unknown;
281
281
  tournament_participants?: RelationInput | RelationInput[];
282
282
  tournament_roles?: RelationInput | RelationInput[];
283
283
  league_tables?: RelationInput | RelationInput[];
284
284
  rules?: string;
285
285
  hasThirdPlace?: boolean;
286
+ hasMatchLobby?: boolean;
286
287
  isPrivate?: boolean;
287
288
  checkInTime?: number;
288
289
  region?: RelationInput;
289
290
  platform?: RelationInput;
290
291
  gameAccount?: RelationInput;
291
- tournament_stages?: RelationInput | RelationInput[];
292
+ tournament_stages?: unknown;
292
293
  prizesDistributed?: boolean;
293
294
  discordUrl?: string;
294
295
  twitterUrl?: string;
@@ -320,13 +321,6 @@ export interface TournamentRoleInput {
320
321
  users_permissions_user?: RelationInput;
321
322
  tournament?: RelationInput;
322
323
  }
323
- /** Write payload for `tournament-stage`. */
324
- export interface TournamentStageInput {
325
- stageName?: TournamentStageStageName;
326
- bestOf?: number;
327
- tournament?: RelationInput;
328
- isThirdPlace?: boolean;
329
- }
330
324
  /** Write payload for `user`. */
331
325
  export interface UserInput {
332
326
  username?: string;
@@ -354,6 +348,7 @@ export interface UserInput {
354
348
  notificationPreferences?: unknown;
355
349
  referralCode?: string;
356
350
  referralRewardClaimed?: boolean;
351
+ lastSeenAt?: string;
357
352
  }
358
353
  /** Write payload for `user-game-account`. */
359
354
  export interface UserGameAccountInput {
@@ -1,5 +1,5 @@
1
1
  import type { BlocksContent, StrapiDocument, StrapiMedia, UserRole } from '../contracts';
2
- import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PrizeRank, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentStageStageName, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
2
+ import type { FriendshipStatus, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, MatchLobbyStatus, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentCurrentStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, UserTransactionStripeStatus, UserTransactionType } from './enums';
3
3
  /** `about` */
4
4
  export interface About extends StrapiDocument {
5
5
  heroTitle?: string | null;
@@ -48,6 +48,8 @@ export interface Game extends StrapiDocument {
48
48
  user_game_stats?: UserGameStat[];
49
49
  team_game_stats?: TeamGameStat[];
50
50
  imgMainPosition?: string | null;
51
+ platforms?: Platform[];
52
+ game_accounts?: GameAccount[];
51
53
  }
52
54
  /** `game-account` */
53
55
  export interface GameAccount extends StrapiDocument {
@@ -55,6 +57,8 @@ export interface GameAccount extends StrapiDocument {
55
57
  tournaments?: Tournament[];
56
58
  user_game_accounts?: UserGameAccount[];
57
59
  imgThumb?: StrapiMedia | null;
60
+ games?: Game[];
61
+ platforms?: Platform[];
58
62
  }
59
63
  /** `game-request` */
60
64
  export interface GameRequest extends StrapiDocument {
@@ -148,7 +152,10 @@ export interface Notification extends StrapiDocument {
148
152
  /** `platform` */
149
153
  export interface Platform extends StrapiDocument {
150
154
  name?: string | null;
155
+ code?: PlatformCode | null;
151
156
  tournaments?: Tournament[];
157
+ games?: Game[];
158
+ game_accounts?: GameAccount[];
152
159
  }
153
160
  /** `preset-avatar` */
154
161
  export interface PresetAvatar extends StrapiDocument {
@@ -171,13 +178,6 @@ export interface Privacy extends StrapiDocument {
171
178
  content?: BlocksContent | null;
172
179
  title?: string | null;
173
180
  }
174
- /** `prize` */
175
- export interface Prize extends StrapiDocument {
176
- description?: string | null;
177
- value?: number | null;
178
- tournament?: Tournament | null;
179
- rank?: PrizeRank | null;
180
- }
181
181
  /** `referral` */
182
182
  export interface Referral extends StrapiDocument {
183
183
  referrer?: User | null;
@@ -277,18 +277,19 @@ export interface Tournament extends StrapiDocument {
277
277
  game?: Game | null;
278
278
  currentStatus?: TournamentCurrentStatus | null;
279
279
  group?: Group | null;
280
- prizes?: Prize[];
280
+ prizes?: unknown | null;
281
281
  tournament_participants?: TournamentParticipant[];
282
282
  tournament_roles?: TournamentRole[];
283
283
  league_tables?: LeagueTable[];
284
284
  rules?: string | null;
285
285
  hasThirdPlace?: boolean | null;
286
+ hasMatchLobby?: boolean | null;
286
287
  isPrivate?: boolean | null;
287
288
  checkInTime?: number | null;
288
289
  region?: Region | null;
289
290
  platform?: Platform | null;
290
291
  gameAccount?: GameAccount | null;
291
- tournament_stages?: TournamentStage[];
292
+ tournament_stages?: unknown | null;
292
293
  prizesDistributed?: boolean | null;
293
294
  discordUrl?: string | null;
294
295
  twitterUrl?: string | null;
@@ -320,13 +321,6 @@ export interface TournamentRole extends StrapiDocument {
320
321
  users_permissions_user?: User | null;
321
322
  tournament?: Tournament | null;
322
323
  }
323
- /** `tournament-stage` */
324
- export interface TournamentStage extends StrapiDocument {
325
- stageName?: TournamentStageStageName | null;
326
- bestOf?: number | null;
327
- tournament?: Tournament | null;
328
- isThirdPlace?: boolean | null;
329
- }
330
324
  /** `user` */
331
325
  export interface User extends StrapiDocument {
332
326
  username?: string | null;
@@ -354,6 +348,7 @@ export interface User extends StrapiDocument {
354
348
  notificationPreferences?: unknown | null;
355
349
  referralCode?: string | null;
356
350
  referralRewardClaimed?: boolean | null;
351
+ lastSeenAt?: string | null;
357
352
  }
358
353
  /** `user-game-account` */
359
354
  export interface UserGameAccount extends StrapiDocument {
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from './generated';
2
2
  export * from './contracts';
3
3
  export * from './manual';
4
4
  export * from './bracket';
5
+ export * from './gameCatalogue';
5
6
  export * from './data/countriesList';
package/dist/index.js CHANGED
@@ -22,5 +22,8 @@ __exportStar(require("./contracts"), exports);
22
22
  __exportStar(require("./manual"), exports);
23
23
  // Bracket shape — shared by the backend generator and Storybook fixtures
24
24
  __exportStar(require("./bracket"), exports);
25
+ // Game/platform/account catalogue — the game -> platform -> account chain,
26
+ // applied identically by the backend resolver and the client's pickers
27
+ __exportStar(require("./gameCatalogue"), exports);
25
28
  // Data
26
29
  __exportStar(require("./data/countriesList"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpt-shared-types",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Shared types and data for XPT projects",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Which platforms a game runs on, and which account networks a player needs
3
+ * on each of them.
4
+ *
5
+ * A tournament carries three catalogue relations — `game`, `platform` and
6
+ * `gameAccount` — and until these functions existed they were three
7
+ * unconnected choices. A host could put Super Smash Bros on PlayStation, or
8
+ * ask for a Steam ID in an Xbox tournament, and nothing anywhere objected.
9
+ *
10
+ * The chain is game -> platform -> account, each step narrowing the next:
11
+ *
12
+ * - a game is tagged with the platforms it runs on, and with the account
13
+ * networks it uses (`Valorant` -> Riot, `Counter-Strike 2` -> Steam);
14
+ * - a platform is tagged with the networks that exist on that box
15
+ * (PlayStation -> PSN, EA, Epic, Riot);
16
+ * - the accounts valid for a tournament are the *intersection* of the two.
17
+ *
18
+ * Neither list is derivable from the other. PC hosts {Steam, Epic, Battle.net,
19
+ * Riot, EA}, but Overwatch 2 on PC is only Battle.net or Steam — so the
20
+ * platform alone over-generates. And a game's platforms are not the union of
21
+ * its networks' platforms: 2XKO is PC-only, yet Riot must be tagged with
22
+ * PlayStation and Xbox for Valorant, which would wrongly put 2XKO on consoles.
23
+ * Both edges are real; the intersection is the answer.
24
+ *
25
+ * Lives here rather than in xpt-strapi because the client filters its radio
26
+ * groups with the identical rule. Two implementations of an intersection is
27
+ * two chances to disagree about what a host is allowed to pick.
28
+ *
29
+ * Pure on purpose: no Strapi, no I/O, so the rules are unit-tested directly
30
+ * (see xpt-strapi `tests/unit/utils/gameCatalogue.test.ts`).
31
+ */
32
+
33
+ /** The identifying fields of a populated Strapi relation, all optional. */
34
+ export interface CatalogueRef {
35
+ id?: number | string | null;
36
+ documentId?: string | null;
37
+ name?: string | null;
38
+ }
39
+
40
+ /** A platform, with the account networks that exist on it. */
41
+ export interface CataloguePlatformLike extends CatalogueRef {
42
+ game_accounts?: readonly CatalogueRef[] | null;
43
+ }
44
+
45
+ /** A game, with the platforms it runs on and the networks it uses. */
46
+ export interface CatalogueGameLike extends CatalogueRef {
47
+ title?: string | null;
48
+ platforms?: readonly CatalogueRef[] | null;
49
+ game_accounts?: readonly CatalogueRef[] | null;
50
+ }
51
+
52
+ /**
53
+ * Whether two relation references point at the same row.
54
+ *
55
+ * `documentId` wins when both sides carry one, because a numeric `id` differs
56
+ * between a draft and its published entry while the documentId does not. Two
57
+ * references that share *neither* field are never equal — the tempting
58
+ * shorthand `a.id === b.id` reads as true when both are `undefined`, which is
59
+ * how `hasGameAccountFor` used to admit any player to a tournament that had no
60
+ * account set at all.
61
+ */
62
+ export function sameRef(
63
+ a?: CatalogueRef | null,
64
+ b?: CatalogueRef | null
65
+ ): boolean {
66
+ if (!a || !b) return false;
67
+
68
+ if (a.documentId && b.documentId) return a.documentId === b.documentId;
69
+
70
+ const aId = a.id;
71
+ const bId = b.id;
72
+ if (aId === null || aId === undefined) return false;
73
+ if (bId === null || bId === undefined) return false;
74
+
75
+ return String(aId) === String(bId);
76
+ }
77
+
78
+ /**
79
+ * Whether a catalogue tag list says anything.
80
+ *
81
+ * An absent list and an empty one mean the same thing: nobody has tagged this
82
+ * row yet. Strapi serialises an unpopulated relation and a populated-but-empty
83
+ * one identically once it reaches the client, so there is no way to tell them
84
+ * apart and no reason to try — either way we have no opinion, and an untagged
85
+ * row must never lock a host out of a game we simply forgot to configure.
86
+ */
87
+ function isTagged(list?: readonly CatalogueRef[] | null): boolean {
88
+ return Array.isArray(list) && list.length > 0;
89
+ }
90
+
91
+ /** The master list, minus any row that repeats one already seen. */
92
+ function dedupe<T extends CatalogueRef>(rows: readonly T[]): T[] {
93
+ const seen: T[] = [];
94
+ for (const row of rows) {
95
+ if (!seen.some((kept) => sameRef(kept, row))) seen.push(row);
96
+ }
97
+ return seen;
98
+ }
99
+
100
+ /**
101
+ * The platforms a game can be played on.
102
+ *
103
+ * An untagged game returns the whole catalogue — see `isTagged`. Results keep
104
+ * the order of `allPlatforms` so the radio group a host sees is stable and
105
+ * matches the unfiltered list they saw before any tagging happened.
106
+ */
107
+ export function platformsForGame<P extends CataloguePlatformLike>(
108
+ game: CatalogueGameLike | null | undefined,
109
+ allPlatforms: readonly P[]
110
+ ): P[] {
111
+ const all = dedupe(allPlatforms);
112
+
113
+ if (!game || !isTagged(game.platforms)) return all;
114
+
115
+ return all.filter((platform) =>
116
+ game.platforms!.some((tag) => sameRef(tag, platform))
117
+ );
118
+ }
119
+
120
+ /**
121
+ * The account networks valid for a game on a platform.
122
+ *
123
+ * With no platform chosen yet the game's own list is returned unfiltered —
124
+ * that is what lets a single-network game (Fortnite, Valorant, Dota 2) resolve
125
+ * its account at creation time, before a platform exists.
126
+ *
127
+ * An empty result when both sides *are* tagged is returned as-is rather than
128
+ * falling back to the full catalogue. It means the two tag lists disagree,
129
+ * which is a data bug: surfacing it as "no account type available" gets it
130
+ * fixed, whereas quietly showing all ten hides it forever.
131
+ */
132
+ export function gameAccountsFor<A extends CatalogueRef>(
133
+ game: CatalogueGameLike | null | undefined,
134
+ platform: CataloguePlatformLike | null | undefined,
135
+ allGameAccounts: readonly A[]
136
+ ): A[] {
137
+ const all = dedupe(allGameAccounts);
138
+
139
+ const byGame =
140
+ !game || !isTagged(game.game_accounts)
141
+ ? all
142
+ : all.filter((account) =>
143
+ game.game_accounts!.some((tag) => sameRef(tag, account))
144
+ );
145
+
146
+ // No platform yet, or a platform nobody has tagged: the game is the only
147
+ // authority we have.
148
+ if (!platform || !isTagged(platform.game_accounts)) return byGame;
149
+
150
+ return byGame.filter((account) =>
151
+ platform.game_accounts!.some((tag) => sameRef(tag, account))
152
+ );
153
+ }
154
+
155
+ /**
156
+ * The one candidate, when there is exactly one.
157
+ *
158
+ * Drives auto-connect: a single valid option is not a choice, so the server
159
+ * connects it and the host is never shown a dialog with one radio in it.
160
+ */
161
+ export function soleCandidate<T>(candidates: readonly T[]): T | null {
162
+ return candidates.length === 1 ? candidates[0] : null;
163
+ }
164
+
165
+ /**
166
+ * The candidate platforms that carry this account network — the chain read
167
+ * backwards.
168
+ *
169
+ * Most networks answer the platform question by themselves: a PSN ID only
170
+ * exists on a PlayStation, a Steam ID only on PC. Pair that with
171
+ * `soleCandidate` and choosing the account fills the platform in, which is the
172
+ * other half of what makes these two cards feel like one decision.
173
+ *
174
+ * The four cross-platform networks (EA, Epic, Battle.net, Riot) match several
175
+ * and so infer nothing — correctly, because an EA ID really does not say which
176
+ * box you are on.
177
+ *
178
+ * An **untagged** platform is excluded rather than treated as permissive. That
179
+ * is the opposite of `gameAccountsFor`, and deliberate: not knowing what a
180
+ * platform carries is a reason to stay quiet, not a licence to claim it carries
181
+ * this.
182
+ */
183
+ export function platformsCarrying<P extends CataloguePlatformLike>(
184
+ gameAccount: CatalogueRef | null | undefined,
185
+ candidates: readonly P[]
186
+ ): P[] {
187
+ if (!gameAccount) return [];
188
+
189
+ return dedupe(candidates).filter((platform) =>
190
+ (platform.game_accounts ?? []).some((tag) => sameRef(tag, gameAccount))
191
+ );
192
+ }
193
+
194
+ /** Whether this platform is one the game actually runs on. */
195
+ export function isPlatformValidForGame(
196
+ game: CatalogueGameLike | null | undefined,
197
+ platform: CatalogueRef | null | undefined,
198
+ allPlatforms: readonly CataloguePlatformLike[]
199
+ ): boolean {
200
+ if (!platform) return false;
201
+ return platformsForGame(game, allPlatforms).some((valid) =>
202
+ sameRef(valid, platform)
203
+ );
204
+ }
205
+
206
+ /** Whether this account network is one the game uses on this platform. */
207
+ export function isGameAccountValidFor(
208
+ game: CatalogueGameLike | null | undefined,
209
+ platform: CataloguePlatformLike | null | undefined,
210
+ gameAccount: CatalogueRef | null | undefined,
211
+ allGameAccounts: readonly CatalogueRef[]
212
+ ): boolean {
213
+ if (!gameAccount) return false;
214
+ return gameAccountsFor(game, platform, allGameAccounts).some((valid) =>
215
+ sameRef(valid, gameAccount)
216
+ );
217
+ }
@@ -64,11 +64,13 @@ export type MatchWinnerSlot =
64
64
  | "home"
65
65
  | "away";
66
66
 
67
- /** `Prize.rank` */
68
- export type PrizeRank =
69
- | "one"
70
- | "two"
71
- | "three";
67
+ /** `Platform.code` */
68
+ export type PlatformCode =
69
+ | "pc"
70
+ | "playstation"
71
+ | "xbox"
72
+ | "nintendo"
73
+ | "mobile";
72
74
 
73
75
  /** `Referral.status` */
74
76
  export type ReferralStatus =
@@ -129,18 +131,6 @@ export type TournamentRoleRole =
129
131
  | "moderator"
130
132
  | "admin";
131
133
 
132
- /** `TournamentStage.stageName` */
133
- export type TournamentStageStageName =
134
- | "Round 128"
135
- | "Round 64"
136
- | "Round 32"
137
- | "Round 16"
138
- | "Quarter Final"
139
- | "Semi-Final"
140
- | "Third Round"
141
- | "Finals"
142
- | "League";
143
-
144
134
  /** `Tournament.teamSize` */
145
135
  export type TournamentTeamSize =
146
136
  | "one"
@@ -16,7 +16,7 @@ import type {
16
16
  MatchRound,
17
17
  MatchStreamPlatform,
18
18
  MatchWinnerSlot,
19
- PrizeRank,
19
+ PlatformCode,
20
20
  ReferralStatus,
21
21
  TeamCurrentStatus,
22
22
  TeamInviteStatus,
@@ -26,7 +26,6 @@ import type {
26
26
  TournamentParticipantEntryType,
27
27
  TournamentParticipantStatus,
28
28
  TournamentRoleRole,
29
- TournamentStageStageName,
30
29
  TournamentTeamSize,
31
30
  TournamentType,
32
31
  UserTransactionStripeStatus,
@@ -87,6 +86,8 @@ export interface GameInput {
87
86
  user_game_stats?: RelationInput | RelationInput[];
88
87
  team_game_stats?: RelationInput | RelationInput[];
89
88
  imgMainPosition?: string;
89
+ platforms?: RelationInput | RelationInput[];
90
+ game_accounts?: RelationInput | RelationInput[];
90
91
  }
91
92
 
92
93
  /** Write payload for `game-account`. */
@@ -95,6 +96,8 @@ export interface GameAccountInput {
95
96
  tournaments?: RelationInput | RelationInput[];
96
97
  user_game_accounts?: RelationInput | RelationInput[];
97
98
  imgThumb?: MediaInput;
99
+ games?: RelationInput | RelationInput[];
100
+ platforms?: RelationInput | RelationInput[];
98
101
  }
99
102
 
100
103
  /** Write payload for `game-request`. */
@@ -196,7 +199,10 @@ export interface NotificationInput {
196
199
  /** Write payload for `platform`. */
197
200
  export interface PlatformInput {
198
201
  name?: string;
202
+ code?: PlatformCode;
199
203
  tournaments?: RelationInput | RelationInput[];
204
+ games?: RelationInput | RelationInput[];
205
+ game_accounts?: RelationInput | RelationInput[];
200
206
  }
201
207
 
202
208
  /** Write payload for `preset-avatar`. */
@@ -225,14 +231,6 @@ export interface PrivacyInput {
225
231
  title?: string;
226
232
  }
227
233
 
228
- /** Write payload for `prize`. */
229
- export interface PrizeInput {
230
- description?: string;
231
- value?: number;
232
- tournament?: RelationInput;
233
- rank?: PrizeRank;
234
- }
235
-
236
234
  /** Write payload for `referral`. */
237
235
  export interface ReferralInput {
238
236
  referrer?: RelationInput;
@@ -341,18 +339,19 @@ export interface TournamentInput {
341
339
  game?: RelationInput;
342
340
  currentStatus?: TournamentCurrentStatus;
343
341
  group?: RelationInput;
344
- prizes?: RelationInput | RelationInput[];
342
+ prizes?: unknown;
345
343
  tournament_participants?: RelationInput | RelationInput[];
346
344
  tournament_roles?: RelationInput | RelationInput[];
347
345
  league_tables?: RelationInput | RelationInput[];
348
346
  rules?: string;
349
347
  hasThirdPlace?: boolean;
348
+ hasMatchLobby?: boolean;
350
349
  isPrivate?: boolean;
351
350
  checkInTime?: number;
352
351
  region?: RelationInput;
353
352
  platform?: RelationInput;
354
353
  gameAccount?: RelationInput;
355
- tournament_stages?: RelationInput | RelationInput[];
354
+ tournament_stages?: unknown;
356
355
  prizesDistributed?: boolean;
357
356
  discordUrl?: string;
358
357
  twitterUrl?: string;
@@ -388,14 +387,6 @@ export interface TournamentRoleInput {
388
387
  tournament?: RelationInput;
389
388
  }
390
389
 
391
- /** Write payload for `tournament-stage`. */
392
- export interface TournamentStageInput {
393
- stageName?: TournamentStageStageName;
394
- bestOf?: number;
395
- tournament?: RelationInput;
396
- isThirdPlace?: boolean;
397
- }
398
-
399
390
  /** Write payload for `user`. */
400
391
  export interface UserInput {
401
392
  username?: string;
@@ -423,6 +414,7 @@ export interface UserInput {
423
414
  notificationPreferences?: unknown;
424
415
  referralCode?: string;
425
416
  referralRewardClaimed?: boolean;
417
+ lastSeenAt?: string;
426
418
  }
427
419
 
428
420
  /** Write payload for `user-game-account`. */
@@ -17,7 +17,7 @@ import type {
17
17
  MatchRound,
18
18
  MatchStreamPlatform,
19
19
  MatchWinnerSlot,
20
- PrizeRank,
20
+ PlatformCode,
21
21
  ReferralStatus,
22
22
  TeamCurrentStatus,
23
23
  TeamInviteStatus,
@@ -27,7 +27,6 @@ import type {
27
27
  TournamentParticipantEntryType,
28
28
  TournamentParticipantStatus,
29
29
  TournamentRoleRole,
30
- TournamentStageStageName,
31
30
  TournamentTeamSize,
32
31
  TournamentType,
33
32
  UserTransactionStripeStatus,
@@ -88,6 +87,8 @@ export interface Game extends StrapiDocument {
88
87
  user_game_stats?: UserGameStat[];
89
88
  team_game_stats?: TeamGameStat[];
90
89
  imgMainPosition?: string | null;
90
+ platforms?: Platform[];
91
+ game_accounts?: GameAccount[];
91
92
  }
92
93
 
93
94
  /** `game-account` */
@@ -96,6 +97,8 @@ export interface GameAccount extends StrapiDocument {
96
97
  tournaments?: Tournament[];
97
98
  user_game_accounts?: UserGameAccount[];
98
99
  imgThumb?: StrapiMedia | null;
100
+ games?: Game[];
101
+ platforms?: Platform[];
99
102
  }
100
103
 
101
104
  /** `game-request` */
@@ -197,7 +200,10 @@ export interface Notification extends StrapiDocument {
197
200
  /** `platform` */
198
201
  export interface Platform extends StrapiDocument {
199
202
  name?: string | null;
203
+ code?: PlatformCode | null;
200
204
  tournaments?: Tournament[];
205
+ games?: Game[];
206
+ game_accounts?: GameAccount[];
201
207
  }
202
208
 
203
209
  /** `preset-avatar` */
@@ -226,14 +232,6 @@ export interface Privacy extends StrapiDocument {
226
232
  title?: string | null;
227
233
  }
228
234
 
229
- /** `prize` */
230
- export interface Prize extends StrapiDocument {
231
- description?: string | null;
232
- value?: number | null;
233
- tournament?: Tournament | null;
234
- rank?: PrizeRank | null;
235
- }
236
-
237
235
  /** `referral` */
238
236
  export interface Referral extends StrapiDocument {
239
237
  referrer?: User | null;
@@ -342,18 +340,19 @@ export interface Tournament extends StrapiDocument {
342
340
  game?: Game | null;
343
341
  currentStatus?: TournamentCurrentStatus | null;
344
342
  group?: Group | null;
345
- prizes?: Prize[];
343
+ prizes?: unknown | null;
346
344
  tournament_participants?: TournamentParticipant[];
347
345
  tournament_roles?: TournamentRole[];
348
346
  league_tables?: LeagueTable[];
349
347
  rules?: string | null;
350
348
  hasThirdPlace?: boolean | null;
349
+ hasMatchLobby?: boolean | null;
351
350
  isPrivate?: boolean | null;
352
351
  checkInTime?: number | null;
353
352
  region?: Region | null;
354
353
  platform?: Platform | null;
355
354
  gameAccount?: GameAccount | null;
356
- tournament_stages?: TournamentStage[];
355
+ tournament_stages?: unknown | null;
357
356
  prizesDistributed?: boolean | null;
358
357
  discordUrl?: string | null;
359
358
  twitterUrl?: string | null;
@@ -389,14 +388,6 @@ export interface TournamentRole extends StrapiDocument {
389
388
  tournament?: Tournament | null;
390
389
  }
391
390
 
392
- /** `tournament-stage` */
393
- export interface TournamentStage extends StrapiDocument {
394
- stageName?: TournamentStageStageName | null;
395
- bestOf?: number | null;
396
- tournament?: Tournament | null;
397
- isThirdPlace?: boolean | null;
398
- }
399
-
400
391
  /** `user` */
401
392
  export interface User extends StrapiDocument {
402
393
  username?: string | null;
@@ -424,6 +415,7 @@ export interface User extends StrapiDocument {
424
415
  notificationPreferences?: unknown | null;
425
416
  referralCode?: string | null;
426
417
  referralRewardClaimed?: boolean | null;
418
+ lastSeenAt?: string | null;
427
419
  }
428
420
 
429
421
  /** `user-game-account` */
package/src/index.ts CHANGED
@@ -1,14 +1,18 @@
1
- // Generated from the Strapi schemas — see scripts/sync-from-strapi.js
2
- export * from './generated';
3
-
4
- // Hand-written contract primitives (response envelopes, Populated, media)
5
- export * from './contracts';
6
-
7
- // Hand-written types with no Strapi content type behind them
8
- export * from './manual';
9
-
10
- // Bracket shape — shared by the backend generator and Storybook fixtures
11
- export * from './bracket';
12
-
13
- // Data
14
- export * from './data/countriesList';
1
+ // Generated from the Strapi schemas — see scripts/sync-from-strapi.js
2
+ export * from './generated';
3
+
4
+ // Hand-written contract primitives (response envelopes, Populated, media)
5
+ export * from './contracts';
6
+
7
+ // Hand-written types with no Strapi content type behind them
8
+ export * from './manual';
9
+
10
+ // Bracket shape — shared by the backend generator and Storybook fixtures
11
+ export * from './bracket';
12
+
13
+ // Game/platform/account catalogue — the game -> platform -> account chain,
14
+ // applied identically by the backend resolver and the client's pickers
15
+ export * from './gameCatalogue';
16
+
17
+ // Data
18
+ export * from './data/countriesList';