xpt-shared-types 1.0.4 → 1.1.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 +121 -38
- package/dist/contracts/index.d.ts +139 -0
- package/dist/contracts/index.js +8 -0
- package/dist/data/countriesList.d.ts +1 -1
- package/dist/generated/enums.d.ts +48 -0
- package/dist/generated/enums.js +5 -0
- package/dist/generated/index.d.ts +3 -0
- package/dist/generated/index.js +22 -0
- package/dist/generated/inputs.d.ts +420 -0
- package/dist/generated/inputs.js +5 -0
- package/dist/generated/models.d.ts +420 -0
- package/dist/generated/models.js +5 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -5
- package/dist/manual/index.d.ts +4 -0
- package/dist/manual/index.js +20 -0
- package/package.json +26 -23
- package/scripts/sync-from-strapi.js +281 -0
- package/src/contracts/index.ts +158 -0
- package/src/data/countriesList.ts +1156 -1156
- package/src/generated/enums.ts +171 -0
- package/src/generated/index.ts +7 -0
- package/src/generated/inputs.ts +496 -0
- package/src/generated/models.ts +497 -0
- package/src/index.ts +11 -7
- package/src/manual/index.ts +4 -0
- package/dist/types/match.d.ts +0 -1
- package/dist/types/match.js +0 -2
- package/dist/types/tournament.d.ts +0 -1
- package/dist/types/tournament.js +0 -2
- package/src/types/match.ts +0 -8
- package/src/types/tournament.ts +0 -4
- /package/dist/{types → manual}/country.d.ts +0 -0
- /package/dist/{types → manual}/country.js +0 -0
- /package/src/{types → manual}/country.ts +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written contract primitives.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is maintained by hand — the generated models in
|
|
5
|
+
* `src/generated` import from this module, so keep the names stable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Strapi 5 returns flat documents: no `attributes` envelope. */
|
|
9
|
+
export interface StrapiDocument {
|
|
10
|
+
id: number;
|
|
11
|
+
documentId: string;
|
|
12
|
+
createdAt?: string | null;
|
|
13
|
+
updatedAt?: string | null;
|
|
14
|
+
publishedAt?: string | null;
|
|
15
|
+
locale?: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A single size variant produced by the upload plugin. */
|
|
19
|
+
export interface StrapiMediaFormat {
|
|
20
|
+
name: string;
|
|
21
|
+
hash: string;
|
|
22
|
+
ext: string;
|
|
23
|
+
mime: string;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
size: number;
|
|
27
|
+
url: string;
|
|
28
|
+
path?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Size variants the upload plugin generates. The four named breakpoints are
|
|
33
|
+
* Strapi's defaults; the index signature covers custom ones.
|
|
34
|
+
*/
|
|
35
|
+
export interface StrapiMediaFormats {
|
|
36
|
+
thumbnail?: StrapiMediaFormat;
|
|
37
|
+
small?: StrapiMediaFormat;
|
|
38
|
+
medium?: StrapiMediaFormat;
|
|
39
|
+
large?: StrapiMediaFormat;
|
|
40
|
+
[breakpoint: string]: StrapiMediaFormat | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `plugin::upload.file` */
|
|
44
|
+
export interface StrapiMedia {
|
|
45
|
+
id: number;
|
|
46
|
+
documentId?: string;
|
|
47
|
+
name: string;
|
|
48
|
+
alternativeText?: string | null;
|
|
49
|
+
caption?: string | null;
|
|
50
|
+
width?: number | null;
|
|
51
|
+
height?: number | null;
|
|
52
|
+
formats?: StrapiMediaFormats | null;
|
|
53
|
+
hash: string;
|
|
54
|
+
ext?: string | null;
|
|
55
|
+
mime: string;
|
|
56
|
+
size: number;
|
|
57
|
+
url: string;
|
|
58
|
+
previewUrl?: string | null;
|
|
59
|
+
provider?: string;
|
|
60
|
+
provider_metadata?: unknown;
|
|
61
|
+
folderPath?: string;
|
|
62
|
+
createdAt?: string;
|
|
63
|
+
updatedAt?: string;
|
|
64
|
+
publishedAt?: string;
|
|
65
|
+
locale?: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Rich-text `blocks` payload; shape is defined by the blocks renderer. */
|
|
69
|
+
export type BlocksContent = unknown[];
|
|
70
|
+
|
|
71
|
+
// ── write payloads ─────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* How a relation is sent on create/update. Strapi accepts a bare documentId,
|
|
75
|
+
* a numeric id, or the connect/set/disconnect long form.
|
|
76
|
+
*/
|
|
77
|
+
export type RelationInput =
|
|
78
|
+
| string
|
|
79
|
+
| number
|
|
80
|
+
| { id: number }
|
|
81
|
+
| { documentId: string }
|
|
82
|
+
| { connect?: Array<string | number>; disconnect?: Array<string | number> }
|
|
83
|
+
| { set: Array<string | number> };
|
|
84
|
+
|
|
85
|
+
/** Media is written as the uploaded file's numeric id. */
|
|
86
|
+
export type MediaInput = number | number[] | null;
|
|
87
|
+
|
|
88
|
+
// ── responses ──────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export interface Pagination {
|
|
91
|
+
page: number;
|
|
92
|
+
pageSize: number;
|
|
93
|
+
pageCount: number;
|
|
94
|
+
total: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Meta {
|
|
98
|
+
pagination?: Pagination;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Single-entity response. Strapi still sends `meta`, usually `{}`. */
|
|
103
|
+
export interface StrapiResponse<T> {
|
|
104
|
+
data: T;
|
|
105
|
+
meta?: Meta;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Collection response. */
|
|
109
|
+
export interface StrapiCollectionResponse<T> {
|
|
110
|
+
data: T[];
|
|
111
|
+
meta?: Meta;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface StrapiError {
|
|
115
|
+
status: number;
|
|
116
|
+
name: string;
|
|
117
|
+
message: string;
|
|
118
|
+
details?: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface StrapiErrorResponse {
|
|
122
|
+
data: null;
|
|
123
|
+
error: StrapiError;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── populate awareness ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Marks relations as present.
|
|
130
|
+
*
|
|
131
|
+
* Generated models leave every relation optional, because whether one comes
|
|
132
|
+
* back depends entirely on the query's `populate`. Use this at a call site to
|
|
133
|
+
* record what that particular query actually asked for:
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* type WithGame = Populated<Tournament, 'game' | 'prizes'>;
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* This is the type-level counterpart to remembering to populate — if the query
|
|
140
|
+
* and the annotation disagree, that is a bug the reader can see.
|
|
141
|
+
*/
|
|
142
|
+
export type Populated<T, K extends keyof T> = T & {
|
|
143
|
+
[P in K]-?: NonNullable<T[P]>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Recursively marks every relation on `T` as optional (the default shape). */
|
|
147
|
+
export type Unpopulated<T> = {
|
|
148
|
+
[P in keyof T]?: T[P];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** `plugin::users-permissions.role` */
|
|
152
|
+
export interface UserRole {
|
|
153
|
+
id: number;
|
|
154
|
+
documentId?: string;
|
|
155
|
+
name: string;
|
|
156
|
+
description?: string | null;
|
|
157
|
+
type?: string | null;
|
|
158
|
+
}
|