xpt-shared-types 1.13.0 → 1.15.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
@@ -110,11 +110,19 @@ edits are picked up after `yarn build` with no publish step.
110
110
 
111
111
  `prepublishOnly` runs `yarn build`, so `dist/` cannot go stale.
112
112
 
113
+ Publishing runs in GitHub Actions ([`.github/workflows/publish.yml`](.github/workflows/publish.yml))
114
+ using npm trusted publishing, so no npm token or 2FA prompt is involved.
115
+
113
116
  1. `yarn sync` and commit, if schemas changed
114
- 2. `npm login`
115
- 3. `yarn version --patch`
116
- 4. `npm publish`
117
- 5. `git push --follow-tags`
117
+ 2. `yarn version --patch` (or `--minor`), which commits and tags `vX.Y.Z`
118
+ 3. `git push --follow-tags`
119
+
120
+ The workflow fires on the `v*` tag, refuses to run if the tag and
121
+ `package.json` version disagree, then builds and publishes.
122
+
123
+ The trusted publisher on npmjs.com is linked to user `otemu`, repository
124
+ `xpt-shared-types`, workflow `publish.yml`, no environment. Renaming the
125
+ workflow file breaks publishing until that link is recreated.
118
126
 
119
127
  ## License
120
128
 
@@ -12,7 +12,7 @@
12
12
  * on the server and a glyph on the client — the `Record`s below fail to
13
13
  * compile until the group is chosen.
14
14
  */
15
- export declare const NOTIFICATION_TYPES: readonly ["match_ready", "match_result", "match_dispute", "tournament_registered", "tournament_status", "tournament_eliminated", "tournament_placement", "tournament_withdrawn", "tournament_roster", "tournament_join_request", "tournament_join_approved", "tournament_join_declined", "tournament_join_expired", "team_invite", "team_kicked", "team_captaincy", "team_joined", "friend_request", "friend_request_accepted", "xp_earned", "wallet_credit", "wallet_debit"];
15
+ export declare const NOTIFICATION_TYPES: readonly ["match_ready", "match_result", "match_dispute", "tournament_registered", "tournament_status", "tournament_eliminated", "tournament_placement", "tournament_withdrawn", "tournament_roster", "tournament_join_request", "tournament_join_approved", "tournament_join_declined", "tournament_join_expired", "tournament_announcement", "team_invite", "team_kicked", "team_captaincy", "team_joined", "friend_request", "friend_request_accepted", "xp_earned", "wallet_credit", "wallet_debit"];
16
16
  export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
17
17
  /** The in-app toggles on the notification settings page, one per group. */
18
18
  export declare const NOTIFICATION_GROUPS: readonly ["matchAlerts", "tournamentUpdates", "teamSocial", "accountRewards"];
@@ -31,6 +31,8 @@ exports.NOTIFICATION_TYPES = [
31
31
  'tournament_join_approved',
32
32
  'tournament_join_declined',
33
33
  'tournament_join_expired',
34
+ // a free-text message from the XPT team to every entrant of a tournament
35
+ 'tournament_announcement',
34
36
  // teamSocial
35
37
  'team_invite',
36
38
  'team_kicked',
@@ -69,6 +71,7 @@ exports.NOTIFICATION_TYPE_GROUP = {
69
71
  tournament_join_approved: 'tournamentUpdates',
70
72
  tournament_join_declined: 'tournamentUpdates',
71
73
  tournament_join_expired: 'tournamentUpdates',
74
+ tournament_announcement: 'tournamentUpdates',
72
75
  team_invite: 'teamSocial',
73
76
  team_kicked: 'teamSocial',
74
77
  team_captaincy: 'teamSocial',
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
- {
2
- "name": "xpt-shared-types",
3
- "version": "1.13.0",
4
- "description": "Shared types and data for XPT projects",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist",
9
- "src",
10
- "scripts"
11
- ],
12
- "scripts": {
13
- "sync": "node scripts/sync-from-strapi.js",
14
- "build": "tsc",
15
- "prepublishOnly": "yarn build"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/otemu/xpt-shared-types"
20
- },
21
- "author": "",
22
- "license": "MIT",
23
- "devDependencies": {
24
- "typescript": "^5.0.0"
25
- }
26
- }
1
+ {
2
+ "name": "xpt-shared-types",
3
+ "version": "1.15.0",
4
+ "description": "Shared types and data for XPT projects",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "src",
10
+ "scripts"
11
+ ],
12
+ "scripts": {
13
+ "sync": "node scripts/sync-from-strapi.js",
14
+ "build": "tsc",
15
+ "prepublishOnly": "yarn build"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/otemu/xpt-shared-types"
20
+ },
21
+ "author": "",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "typescript": "^5.0.0"
25
+ }
26
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copies this package (package.json + dist) over the consumers' installed copy,
3
+ * so both apps see an unpublished build. This is what has been done by hand
4
+ * for every local iteration; a plain `yarn install` in either app reverts it.
5
+ *
6
+ * yarn build && node scripts/overlay-local.mjs
7
+ */
8
+ import { cpSync, existsSync, rmSync } from 'node:fs';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const root = resolve(here, '..');
14
+ const consumers = ['xpt-strapi', 'xpt-client'].map((name) =>
15
+ resolve(root, '..', name, 'node_modules', 'xpt-shared-types')
16
+ );
17
+
18
+ if (!existsSync(join(root, 'dist', 'index.js'))) {
19
+ console.error('dist/ is missing - run `yarn build` first');
20
+ process.exit(1);
21
+ }
22
+
23
+ for (const target of consumers) {
24
+ if (!existsSync(dirname(target))) {
25
+ console.warn(`skip ${target} (no node_modules)`);
26
+ continue;
27
+ }
28
+ rmSync(join(target, 'dist'), { recursive: true, force: true });
29
+ cpSync(join(root, 'dist'), join(target, 'dist'), { recursive: true });
30
+ cpSync(join(root, 'package.json'), join(target, 'package.json'));
31
+ console.log(`overlaid ${target}`);
32
+ }
package/src/index.ts CHANGED
@@ -1,23 +1,23 @@
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
- // Stream links — one pasted URL parsed the same way by the backend that stores
18
- // the derived columns and the client that embeds the player
19
- export * from './stream';
20
-
21
- // Data
22
- export * from './data/countriesList';
23
- export * from './data/gameGenres';
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
+ // Stream links — one pasted URL parsed the same way by the backend that stores
18
+ // the derived columns and the client that embeds the player
19
+ export * from './stream';
20
+
21
+ // Data
22
+ export * from './data/countriesList';
23
+ export * from './data/gameGenres';
@@ -1,6 +1,6 @@
1
- export interface Country {
2
- countryName: string;
3
- alpha2: string;
4
- alpha3: string;
5
- numeric: string;
6
- }
1
+ export interface Country {
2
+ countryName: string;
3
+ alpha2: string;
4
+ alpha3: string;
5
+ numeric: string;
6
+ }
@@ -29,6 +29,8 @@ export const NOTIFICATION_TYPES = [
29
29
  'tournament_join_approved',
30
30
  'tournament_join_declined',
31
31
  'tournament_join_expired',
32
+ // a free-text message from the XPT team to every entrant of a tournament
33
+ 'tournament_announcement',
32
34
  // teamSocial
33
35
  'team_invite',
34
36
  'team_kicked',
@@ -77,6 +79,7 @@ export const NOTIFICATION_TYPE_GROUP: Record<
77
79
  tournament_join_approved: 'tournamentUpdates',
78
80
  tournament_join_declined: 'tournamentUpdates',
79
81
  tournament_join_expired: 'tournamentUpdates',
82
+ tournament_announcement: 'tournamentUpdates',
80
83
  team_invite: 'teamSocial',
81
84
  team_kicked: 'teamSocial',
82
85
  team_captaincy: 'teamSocial',
@@ -1,281 +0,0 @@
1
- /* eslint-disable no-console */
2
- /**
3
- * Generates the shared API contract from the Strapi content-type schemas.
4
- *
5
- * `schema.json` is the source of truth — not `types/generated/contentTypes.d.ts`,
6
- * which is gitignored in xpt-strapi and only exists after `strapi develop` has run.
7
- *
8
- * node scripts/sync-from-strapi.js [pathToStrapiRepo]
9
- *
10
- * Output is committed to this repo; run this whenever a content type changes.
11
- */
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- const STRAPI_ROOT =
16
- process.argv[2] ||
17
- process.env.XPT_STRAPI_PATH ||
18
- path.resolve(__dirname, '../../xpt-strapi');
19
- const OUT = path.resolve(__dirname, '../src/generated');
20
-
21
- const BANNER = [
22
- '// AUTO-GENERATED by scripts/sync-from-strapi.js — DO NOT EDIT MANUALLY.',
23
- '// Source: xpt-strapi/src/**/content-types/**/schema.json',
24
- '// Regenerate with: yarn sync',
25
- '',
26
- ].join('\n');
27
-
28
- // ── helpers ────────────────────────────────────────────────────────────────
29
- /** Quote a property name only when it is not a bare identifier. */
30
- const prop = (name) =>
31
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
32
-
33
- const pascal = (s) =>
34
- String(s)
35
- .replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase())
36
- .replace(/^([a-z])/, (_, c) => c.toUpperCase())
37
- .replace(/[^A-Za-z0-9]/g, '');
38
-
39
- /** api::tournament-participant.tournament-participant -> TournamentParticipant */
40
- function modelNameFromUid(uid) {
41
- if (uid === 'plugin::users-permissions.user') return 'User';
42
- if (uid === 'plugin::users-permissions.role') return 'UserRole';
43
- if (uid === 'plugin::upload.file') return 'StrapiMedia';
44
- const m = /^api::[^.]+\.(.+)$/.exec(uid);
45
- return m ? pascal(m[1]) : null;
46
- }
47
-
48
- const SCALARS = {
49
- string: 'string',
50
- text: 'string',
51
- richtext: 'string',
52
- uid: 'string',
53
- email: 'string',
54
- password: 'string',
55
- datetime: 'string',
56
- date: 'string',
57
- time: 'string',
58
- biginteger: 'string', // Strapi serialises bigint as string
59
- integer: 'number',
60
- float: 'number',
61
- decimal: 'number',
62
- boolean: 'boolean',
63
- json: 'unknown',
64
- blocks: 'BlocksContent',
65
- };
66
-
67
- // ── collect schemas ────────────────────────────────────────────────────────
68
- function findSchemas(root) {
69
- const out = [];
70
- const roots = [
71
- path.join(root, 'src', 'api'),
72
- path.join(root, 'src', 'extensions'),
73
- ];
74
- for (const base of roots) {
75
- if (!fs.existsSync(base)) continue;
76
- for (const dir of fs.readdirSync(base)) {
77
- const ctDir = path.join(base, dir, 'content-types');
78
- if (!fs.existsSync(ctDir)) continue;
79
- for (const ct of fs.readdirSync(ctDir)) {
80
- const file = path.join(ctDir, ct, 'schema.json');
81
- if (fs.existsSync(file)) out.push(file);
82
- }
83
- }
84
- }
85
- return out.sort();
86
- }
87
-
88
- const files = findSchemas(STRAPI_ROOT);
89
- if (!files.length) {
90
- console.error(
91
- 'No schema.json found under ' +
92
- STRAPI_ROOT +
93
- '. Pass the xpt-strapi path as an argument.'
94
- );
95
- process.exit(1);
96
- }
97
-
98
- const collected = [];
99
- for (const file of files) {
100
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
101
- const info = raw.info || {};
102
- const singular = info.singularName || path.basename(path.dirname(file));
103
- // The users-permissions extension augments the plugin's own user model.
104
- const isUserExt = file.split(path.sep).includes('users-permissions');
105
- collected.push({
106
- name: isUserExt ? 'User' : pascal(singular),
107
- singular: isUserExt ? 'user' : singular,
108
- attributes: raw.attributes || {},
109
- });
110
- }
111
-
112
- // Merge any model declared more than once (e.g. the user extension).
113
- const byName = new Map();
114
- for (const m of collected) {
115
- if (byName.has(m.name)) {
116
- Object.assign(byName.get(m.name).attributes, m.attributes);
117
- } else {
118
- byName.set(m.name, m);
119
- }
120
- }
121
- const allModels = [...byName.values()].sort((a, b) =>
122
- a.name.localeCompare(b.name)
123
- );
124
-
125
- // ── enums ──────────────────────────────────────────────────────────────────
126
- const enums = [];
127
- for (const m of allModels) {
128
- for (const [field, def] of Object.entries(m.attributes)) {
129
- if (def.type !== 'enumeration' || !Array.isArray(def.enum)) continue;
130
- enums.push({
131
- name: m.name + pascal(field),
132
- values: def.enum,
133
- model: m.name,
134
- field,
135
- });
136
- }
137
- }
138
- enums.sort((a, b) => a.name.localeCompare(b.name));
139
-
140
- let enumsOut = BANNER + '\n';
141
- for (const e of enums) {
142
- enumsOut +=
143
- '/** `' +
144
- e.model +
145
- '.' +
146
- e.field +
147
- '` */\nexport type ' +
148
- e.name +
149
- ' =\n' +
150
- e.values.map((v) => ' | ' + JSON.stringify(v)).join('\n') +
151
- ';\n\n';
152
- }
153
- enumsOut +=
154
- '/** Readability alias for `tournament.currentStatus`. */\n' +
155
- 'export type TournamentStatus = TournamentCurrentStatus;\n';
156
-
157
- // ── models (read shapes) ───────────────────────────────────────────────────
158
- const enumLookup = new Map(enums.map((e) => [e.model + '.' + e.field, e.name]));
159
- const usedEnums = new Set();
160
-
161
- function readTypeFor(model, field, def) {
162
- if (def.type === 'enumeration') {
163
- return enumLookup.get(model.name + '.' + field) || 'string';
164
- }
165
- if (def.type === 'media') {
166
- return def.multiple ? 'StrapiMedia[]' : 'StrapiMedia';
167
- }
168
- if (def.type === 'relation') {
169
- const target = modelNameFromUid(def.target);
170
- if (!target) return 'unknown';
171
- const many =
172
- def.relation === 'oneToMany' || def.relation === 'manyToMany';
173
- return many ? target + '[]' : target;
174
- }
175
- return SCALARS[def.type] || 'unknown';
176
- }
177
-
178
- let modelsBody = '';
179
- for (const m of allModels) {
180
- const lines = [];
181
- for (const [field, def] of Object.entries(m.attributes)) {
182
- // `private` attributes are never returned by the content API.
183
- if (def.private) continue;
184
- const t = readTypeFor(m, field, def);
185
- if (def.type === 'enumeration') usedEnums.add(t);
186
- // Everything except id/documentId is optional, because nothing else is
187
- // guaranteed on the wire:
188
- // - relations and media appear only when the query populates them;
189
- // - any scalar disappears when the query uses `fields`, which this
190
- // codebase does pervasively — so even a schema-`required` field like
191
- // user.email is absent from most responses.
192
- // The base model is therefore the minimum guarantee; use `Populated<T, K>`
193
- // at a call site to record what that particular query actually returned.
194
- // Strapi also sends `null` rather than omitting an unset nullable value,
195
- // so single-valued optionals are `?: T | null`. Lists come back as `[]`.
196
- const isList = t.endsWith('[]');
197
- const rendered = isList ? t : t + ' | null';
198
- lines.push(' ' + prop(field) + '?: ' + rendered + ';');
199
- }
200
- modelsBody +=
201
- '/** `' +
202
- m.singular +
203
- '` */\nexport interface ' +
204
- m.name +
205
- ' extends StrapiDocument {\n' +
206
- lines.join('\n') +
207
- '\n}\n\n';
208
- }
209
-
210
- const enumImportList = [...usedEnums]
211
- .filter((e) => !e.endsWith('[]'))
212
- .sort()
213
- .map((e) => ' ' + e + ',')
214
- .join('\n');
215
-
216
- const modelsOut =
217
- BANNER +
218
- "\nimport type {\n BlocksContent,\n StrapiDocument,\n StrapiMedia,\n UserRole,\n} from '../contracts';\n" +
219
- (enumImportList ? 'import type {\n' + enumImportList + "\n} from './enums';\n" : '') +
220
- '\n' +
221
- modelsBody;
222
-
223
- // ── inputs (write shapes) ──────────────────────────────────────────────────
224
- // Write payloads differ structurally from read shapes: relations are sent as
225
- // documentIds or connect/set/disconnect, media as numeric file ids.
226
- let inputsBody = '';
227
- for (const m of allModels) {
228
- const lines = [];
229
- for (const [field, def] of Object.entries(m.attributes)) {
230
- if (def.private) continue;
231
- let t;
232
- if (def.type === 'relation') {
233
- const many =
234
- def.relation === 'oneToMany' || def.relation === 'manyToMany';
235
- t = many ? 'RelationInput | RelationInput[]' : 'RelationInput';
236
- } else if (def.type === 'media') {
237
- t = def.multiple ? 'MediaInput[]' : 'MediaInput';
238
- } else if (def.type === 'enumeration') {
239
- t = enumLookup.get(m.name + '.' + field) || 'string';
240
- } else {
241
- t = SCALARS[def.type] || 'unknown';
242
- }
243
- lines.push(' ' + prop(field) + '?: ' + t + ';');
244
- }
245
- inputsBody +=
246
- '/** Write payload for `' +
247
- m.singular +
248
- '`. */\nexport interface ' +
249
- m.name +
250
- 'Input {\n' +
251
- lines.join('\n') +
252
- '\n}\n\n';
253
- }
254
-
255
- const inputsOut =
256
- BANNER +
257
- "\nimport type {\n BlocksContent,\n MediaInput,\n RelationInput,\n} from '../contracts';\n" +
258
- (enumImportList ? 'import type {\n' + enumImportList + "\n} from './enums';\n" : '') +
259
- '\n' +
260
- inputsBody;
261
-
262
- // ── write ──────────────────────────────────────────────────────────────────
263
- fs.mkdirSync(OUT, { recursive: true });
264
- fs.writeFileSync(path.join(OUT, 'enums.ts'), enumsOut);
265
- fs.writeFileSync(path.join(OUT, 'models.ts'), modelsOut);
266
- fs.writeFileSync(path.join(OUT, 'inputs.ts'), inputsOut);
267
- fs.writeFileSync(
268
- path.join(OUT, 'index.ts'),
269
- BANNER +
270
- "\nexport * from './enums';\nexport * from './models';\nexport * from './inputs';\n"
271
- );
272
-
273
- console.log(
274
- 'Generated ' +
275
- allModels.length +
276
- ' models and ' +
277
- enums.length +
278
- ' enums from ' +
279
- files.length +
280
- ' schemas.'
281
- );