strapi-typed-client 1.0.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/cli/commands/check.d.ts +27 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +91 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +28 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +129 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +21 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +113 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +40 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/api-client.d.ts +41 -0
- package/dist/cli/utils/api-client.d.ts.map +1 -0
- package/dist/cli/utils/api-client.js +81 -0
- package/dist/cli/utils/api-client.js.map +1 -0
- package/dist/cli/utils/file-writer.d.ts +44 -0
- package/dist/cli/utils/file-writer.d.ts.map +1 -0
- package/dist/cli/utils/file-writer.js +89 -0
- package/dist/cli/utils/file-writer.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +622 -0
- package/dist/client.js +688 -0
- package/dist/core/endpoint-converter.d.ts +26 -0
- package/dist/core/endpoint-converter.d.ts.map +1 -0
- package/dist/core/endpoint-converter.js +161 -0
- package/dist/core/endpoint-converter.js.map +1 -0
- package/dist/core/generator/filters-generator.d.ts +22 -0
- package/dist/core/generator/filters-generator.d.ts.map +1 -0
- package/dist/core/generator/filters-generator.js +236 -0
- package/dist/core/generator/filters-generator.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +11 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/schema-transformer.d.ts +19 -0
- package/dist/core/schema-transformer.d.ts.map +1 -0
- package/dist/core/schema-transformer.js +306 -0
- package/dist/core/schema-transformer.js.map +1 -0
- package/dist/generator/auth-api-generator.d.ts +14 -0
- package/dist/generator/auth-api-generator.d.ts.map +1 -0
- package/dist/generator/auth-api-generator.js +518 -0
- package/dist/generator/auth-api-generator.js.map +1 -0
- package/dist/generator/blocks-types-template.d.ts +134 -0
- package/dist/generator/blocks-types-template.d.ts.map +1 -0
- package/dist/generator/blocks-types-template.js +4 -0
- package/dist/generator/blocks-types-template.js.map +1 -0
- package/dist/generator/client-generator.d.ts +19 -0
- package/dist/generator/client-generator.d.ts.map +1 -0
- package/dist/generator/client-generator.js +707 -0
- package/dist/generator/client-generator.js.map +1 -0
- package/dist/generator/custom-api-generator.d.ts +26 -0
- package/dist/generator/custom-api-generator.d.ts.map +1 -0
- package/dist/generator/custom-api-generator.js +145 -0
- package/dist/generator/custom-api-generator.js.map +1 -0
- package/dist/generator/index-generator.d.ts +4 -0
- package/dist/generator/index-generator.d.ts.map +1 -0
- package/dist/generator/index-generator.js +11 -0
- package/dist/generator/index-generator.js.map +1 -0
- package/dist/generator/index.d.ts +14 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +94 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/types-generator.d.ts +35 -0
- package/dist/generator/types-generator.d.ts.map +1 -0
- package/dist/generator/types-generator.js +641 -0
- package/dist/generator/types-generator.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/next/index.d.ts +16 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +224 -0
- package/dist/next/index.js.map +1 -0
- package/dist/parser/custom-types-parser.d.ts +39 -0
- package/dist/parser/custom-types-parser.d.ts.map +1 -0
- package/dist/parser/custom-types-parser.js +206 -0
- package/dist/parser/custom-types-parser.js.map +1 -0
- package/dist/parser/index.d.ts +23 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +457 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/routes-parser.d.ts +21 -0
- package/dist/parser/routes-parser.d.ts.map +1 -0
- package/dist/parser/routes-parser.js +184 -0
- package/dist/parser/routes-parser.js.map +1 -0
- package/dist/plugin/admin/src/index.d.ts +16 -0
- package/dist/plugin/admin/src/index.d.ts.map +1 -0
- package/dist/plugin/admin/src/index.js +30 -0
- package/dist/plugin/admin/src/index.js.map +1 -0
- package/dist/plugin/admin/src/pages/HomePage.d.ts +2 -0
- package/dist/plugin/admin/src/pages/HomePage.d.ts.map +1 -0
- package/dist/plugin/admin/src/pages/HomePage.js +273 -0
- package/dist/plugin/admin/src/pages/HomePage.js.map +1 -0
- package/dist/plugin/admin/src/pluginId.d.ts +2 -0
- package/dist/plugin/admin/src/pluginId.d.ts.map +1 -0
- package/dist/plugin/admin/src/pluginId.js +2 -0
- package/dist/plugin/admin/src/pluginId.js.map +1 -0
- package/dist/plugin/index.d.ts +22 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +22 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/server/src/config/index.d.ts +31 -0
- package/dist/plugin/server/src/config/index.d.ts.map +1 -0
- package/dist/plugin/server/src/config/index.js +35 -0
- package/dist/plugin/server/src/config/index.js.map +1 -0
- package/dist/plugin/server/src/controllers/schema.d.ts +41 -0
- package/dist/plugin/server/src/controllers/schema.d.ts.map +1 -0
- package/dist/plugin/server/src/controllers/schema.js +51 -0
- package/dist/plugin/server/src/controllers/schema.js.map +1 -0
- package/dist/plugin/server/src/index.d.ts +114 -0
- package/dist/plugin/server/src/index.d.ts.map +1 -0
- package/dist/plugin/server/src/index.js +76 -0
- package/dist/plugin/server/src/index.js.map +1 -0
- package/dist/plugin/server/src/routes/index.d.ts +14 -0
- package/dist/plugin/server/src/routes/index.d.ts.map +1 -0
- package/dist/plugin/server/src/routes/index.js +23 -0
- package/dist/plugin/server/src/routes/index.js.map +1 -0
- package/dist/plugin/server/src/services/endpoints.d.ts +36 -0
- package/dist/plugin/server/src/services/endpoints.d.ts.map +1 -0
- package/dist/plugin/server/src/services/endpoints.js +523 -0
- package/dist/plugin/server/src/services/endpoints.js.map +1 -0
- package/dist/plugin/server/src/services/schema.d.ts +25 -0
- package/dist/plugin/server/src/services/schema.d.ts.map +1 -0
- package/dist/plugin/server/src/services/schema.js +164 -0
- package/dist/plugin/server/src/services/schema.js.map +1 -0
- package/dist/schema-meta.ts +7 -0
- package/dist/schema-types.d.ts +104 -0
- package/dist/schema-types.d.ts.map +1 -0
- package/dist/schema-types.js +3 -0
- package/dist/schema-types.js.map +1 -0
- package/dist/shared/constants.d.ts +99 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +89 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/endpoint-types.d.ts +42 -0
- package/dist/shared/endpoint-types.d.ts.map +1 -0
- package/dist/shared/endpoint-types.js +6 -0
- package/dist/shared/endpoint-types.js.map +1 -0
- package/dist/shared/index.d.ts +12 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +13 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/naming-utils.d.ts +55 -0
- package/dist/shared/naming-utils.d.ts.map +1 -0
- package/dist/shared/naming-utils.js +106 -0
- package/dist/shared/naming-utils.js.map +1 -0
- package/dist/shared/schema-hash.d.ts +39 -0
- package/dist/shared/schema-hash.d.ts.map +1 -0
- package/dist/shared/schema-hash.js +67 -0
- package/dist/shared/schema-hash.js.map +1 -0
- package/dist/shared/strapi-schema-types.d.ts +77 -0
- package/dist/shared/strapi-schema-types.d.ts.map +1 -0
- package/dist/shared/strapi-schema-types.js +10 -0
- package/dist/shared/strapi-schema-types.js.map +1 -0
- package/dist/shared/string-utils.d.ts +68 -0
- package/dist/shared/string-utils.d.ts.map +1 -0
- package/dist/shared/string-utils.js +124 -0
- package/dist/shared/string-utils.js.map +1 -0
- package/dist/transformer/index.d.ts +17 -0
- package/dist/transformer/index.d.ts.map +1 -0
- package/dist/transformer/index.js +83 -0
- package/dist/transformer/index.js.map +1 -0
- package/dist/types.d.ts +3430 -0
- package/dist/types.js +3 -0
- package/package.json +152 -0
- package/strapi-admin.js +2 -0
- package/strapi-server.js +2 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { TypeTransformer } from '../transformer/index.js';
|
|
2
|
+
import { generateFilterUtilityTypes, generateEntityFilters, generateTypedQueryParams, } from '../core/generator/filters-generator.js';
|
|
3
|
+
export class TypesGenerator {
|
|
4
|
+
transformer;
|
|
5
|
+
schema;
|
|
6
|
+
constructor() {
|
|
7
|
+
this.transformer = new TypeTransformer();
|
|
8
|
+
}
|
|
9
|
+
generate(schema) {
|
|
10
|
+
this.schema = schema;
|
|
11
|
+
const lines = [];
|
|
12
|
+
// Add header comment
|
|
13
|
+
lines.push('// Auto-generated TypeScript types from Strapi schema');
|
|
14
|
+
lines.push('// Do not edit manually');
|
|
15
|
+
lines.push('');
|
|
16
|
+
// Generate base types
|
|
17
|
+
lines.push(this.generateBaseTypes());
|
|
18
|
+
lines.push('');
|
|
19
|
+
// Generate helper types for populate params
|
|
20
|
+
lines.push('// Helper types for field and sort options in populate');
|
|
21
|
+
lines.push("type _EntityField<T> = Exclude<keyof T & string, '__typename'>");
|
|
22
|
+
lines.push('type _SortValue<T> = _EntityField<T> | `${_EntityField<T>}:${"asc" | "desc"}`');
|
|
23
|
+
lines.push('');
|
|
24
|
+
lines.push('// Apply fields narrowing from populate entry (e.g. populate: { item: { fields: ["title"] } })');
|
|
25
|
+
lines.push("type _ApplyFields<TFull, TBase, TEntry> = TEntry extends true ? TFull : TEntry extends { fields: readonly (infer F)[] } ? F extends string ? Pick<TBase, Extract<F | 'id' | 'documentId', keyof TBase>> & Omit<TFull, keyof TBase> : TFull : TFull");
|
|
26
|
+
lines.push('');
|
|
27
|
+
// Generate component types
|
|
28
|
+
for (const component of schema.components) {
|
|
29
|
+
lines.push(this.generateComponent(component));
|
|
30
|
+
lines.push('');
|
|
31
|
+
}
|
|
32
|
+
// Generate component Input types
|
|
33
|
+
for (const component of schema.components) {
|
|
34
|
+
lines.push(this.generateComponentInput(component));
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
// Generate content type interfaces
|
|
38
|
+
for (const contentType of schema.contentTypes) {
|
|
39
|
+
lines.push(this.generateContentType(contentType));
|
|
40
|
+
lines.push('');
|
|
41
|
+
}
|
|
42
|
+
// Generate Input types for create/update operations
|
|
43
|
+
for (const contentType of schema.contentTypes) {
|
|
44
|
+
lines.push(this.generateInputType(contentType));
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
// Generate PopulateParam types for type-safe populate
|
|
48
|
+
lines.push(this.generatePopulateParams(schema));
|
|
49
|
+
lines.push('');
|
|
50
|
+
// Generate Payload utility types
|
|
51
|
+
lines.push(this.generatePayloadUtilityTypes(schema));
|
|
52
|
+
lines.push('');
|
|
53
|
+
// Generate filter utility types
|
|
54
|
+
lines.push(generateFilterUtilityTypes());
|
|
55
|
+
lines.push('');
|
|
56
|
+
// Generate typed query params
|
|
57
|
+
lines.push(generateTypedQueryParams());
|
|
58
|
+
lines.push('');
|
|
59
|
+
// Generate entity-specific filters
|
|
60
|
+
for (const contentType of schema.contentTypes) {
|
|
61
|
+
lines.push(generateEntityFilters(contentType));
|
|
62
|
+
lines.push('');
|
|
63
|
+
}
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
generateBaseTypes() {
|
|
67
|
+
return `// Base types
|
|
68
|
+
|
|
69
|
+
export interface MediaFile {
|
|
70
|
+
id: number
|
|
71
|
+
name: string
|
|
72
|
+
alternativeText: string | null
|
|
73
|
+
caption: string | null
|
|
74
|
+
width: number | null
|
|
75
|
+
height: number | null
|
|
76
|
+
formats: unknown
|
|
77
|
+
hash: string
|
|
78
|
+
ext: string
|
|
79
|
+
mime: string
|
|
80
|
+
size: number
|
|
81
|
+
url: string
|
|
82
|
+
previewUrl: string | null
|
|
83
|
+
provider: string
|
|
84
|
+
createdAt: string
|
|
85
|
+
updatedAt: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Strapi Blocks Editor API Types
|
|
89
|
+
// Based on: https://docs.strapi.io/dev-docs/api/document/blocks
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Main type for Strapi Blocks content
|
|
93
|
+
*/
|
|
94
|
+
export type BlocksContent = Block[]
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* All possible block types
|
|
98
|
+
*/
|
|
99
|
+
export type Block =
|
|
100
|
+
| ParagraphBlock
|
|
101
|
+
| HeadingBlock
|
|
102
|
+
| QuoteBlock
|
|
103
|
+
| CodeBlock
|
|
104
|
+
| ListBlock
|
|
105
|
+
| ImageBlock
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Paragraph block - default text block
|
|
109
|
+
*/
|
|
110
|
+
export interface ParagraphBlock {
|
|
111
|
+
type: 'paragraph'
|
|
112
|
+
children: InlineNode[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Heading block - h1 to h6
|
|
117
|
+
*/
|
|
118
|
+
export interface HeadingBlock {
|
|
119
|
+
type: 'heading'
|
|
120
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
121
|
+
children: InlineNode[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Quote block - blockquote
|
|
126
|
+
*/
|
|
127
|
+
export interface QuoteBlock {
|
|
128
|
+
type: 'quote'
|
|
129
|
+
children: InlineNode[]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Code block - preformatted code with optional language
|
|
134
|
+
*/
|
|
135
|
+
export interface CodeBlock {
|
|
136
|
+
type: 'code'
|
|
137
|
+
language?: string
|
|
138
|
+
children: InlineNode[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* List block - ordered or unordered
|
|
143
|
+
*/
|
|
144
|
+
export interface ListBlock {
|
|
145
|
+
type: 'list'
|
|
146
|
+
format: 'ordered' | 'unordered'
|
|
147
|
+
children: ListItemBlock[]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List item - individual item in a list
|
|
152
|
+
*/
|
|
153
|
+
export interface ListItemBlock {
|
|
154
|
+
type: 'list-item'
|
|
155
|
+
children: InlineNode[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Image block - embedded image with optional caption
|
|
160
|
+
*/
|
|
161
|
+
export interface ImageBlock {
|
|
162
|
+
type: 'image'
|
|
163
|
+
image: {
|
|
164
|
+
name: string
|
|
165
|
+
alternativeText?: string | null
|
|
166
|
+
url: string
|
|
167
|
+
caption?: string | null
|
|
168
|
+
width?: number
|
|
169
|
+
height?: number
|
|
170
|
+
formats?: unknown
|
|
171
|
+
hash: string
|
|
172
|
+
ext: string
|
|
173
|
+
mime: string
|
|
174
|
+
size: number
|
|
175
|
+
previewUrl?: string | null
|
|
176
|
+
provider: string
|
|
177
|
+
createdAt: string
|
|
178
|
+
updatedAt: string
|
|
179
|
+
}
|
|
180
|
+
children: InlineNode[]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Inline nodes - text formatting and inline elements
|
|
185
|
+
*/
|
|
186
|
+
export type InlineNode = TextNode | LinkInline
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Plain text node with optional formatting
|
|
190
|
+
*/
|
|
191
|
+
export interface TextNode {
|
|
192
|
+
type: 'text'
|
|
193
|
+
text: string
|
|
194
|
+
bold?: boolean
|
|
195
|
+
italic?: boolean
|
|
196
|
+
underline?: boolean
|
|
197
|
+
strikethrough?: boolean
|
|
198
|
+
code?: boolean
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Inline link
|
|
203
|
+
*/
|
|
204
|
+
export interface LinkInline {
|
|
205
|
+
type: 'link'
|
|
206
|
+
url: string
|
|
207
|
+
children: TextNode[]
|
|
208
|
+
}`;
|
|
209
|
+
}
|
|
210
|
+
generateComponent(component) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
lines.push(`export interface ${component.cleanName} {`);
|
|
213
|
+
// Add base id field (components have id in Strapi)
|
|
214
|
+
lines.push(' id: number');
|
|
215
|
+
// Add scalar attributes
|
|
216
|
+
for (const attr of component.attributes) {
|
|
217
|
+
const tsType = this.transformer.toTypeScript(attr.type, attr.required);
|
|
218
|
+
lines.push(` ${attr.name}: ${tsType}`);
|
|
219
|
+
}
|
|
220
|
+
// Add media fields
|
|
221
|
+
for (const mediaField of component.media) {
|
|
222
|
+
const mediaType = mediaField.multiple ? 'MediaFile[]' : 'MediaFile';
|
|
223
|
+
const suffix = mediaField.required ? '' : ' | null';
|
|
224
|
+
lines.push(` ${mediaField.name}: ${mediaType}${suffix}`);
|
|
225
|
+
}
|
|
226
|
+
// Add relations (reference by ID)
|
|
227
|
+
for (const rel of component.relations) {
|
|
228
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
229
|
+
rel.relationType === 'manyToMany';
|
|
230
|
+
const relType = isArray
|
|
231
|
+
? '{ id: number; documentId: string }[]'
|
|
232
|
+
: '{ id: number; documentId: string } | null';
|
|
233
|
+
lines.push(` ${rel.name}: ${relType}`);
|
|
234
|
+
}
|
|
235
|
+
// Add components
|
|
236
|
+
for (const compField of component.components) {
|
|
237
|
+
const compType = compField.repeatable
|
|
238
|
+
? `${compField.componentType}[]`
|
|
239
|
+
: compField.componentType;
|
|
240
|
+
const suffix = compField.required ? '' : ' | null';
|
|
241
|
+
lines.push(` ${compField.name}: ${compType}${suffix}`);
|
|
242
|
+
}
|
|
243
|
+
// Add dynamic zones
|
|
244
|
+
for (const dzField of component.dynamicZones) {
|
|
245
|
+
const dzType = `(${dzField.componentTypes.join(' | ')})[]`;
|
|
246
|
+
const suffix = dzField.required ? '' : ' | null';
|
|
247
|
+
lines.push(` ${dzField.name}: ${dzType}${suffix}`);
|
|
248
|
+
}
|
|
249
|
+
lines.push('}');
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
generateComponentInput(component) {
|
|
253
|
+
const lines = [];
|
|
254
|
+
lines.push(`// Input type for creating/updating ${component.cleanName}`);
|
|
255
|
+
lines.push(`export interface ${component.cleanName}Input {`);
|
|
256
|
+
// id is optional for input (omit when creating, include when updating existing)
|
|
257
|
+
lines.push(' id?: number');
|
|
258
|
+
// Add scalar attributes (all optional)
|
|
259
|
+
for (const attr of component.attributes) {
|
|
260
|
+
const tsType = this.transformer.toTypeScript(attr.type, false); // Always optional
|
|
261
|
+
lines.push(` ${attr.name}?: ${tsType}`);
|
|
262
|
+
}
|
|
263
|
+
// Add media fields as ID or ID array
|
|
264
|
+
for (const mediaField of component.media) {
|
|
265
|
+
const mediaType = mediaField.multiple ? 'number[]' : 'number';
|
|
266
|
+
lines.push(` ${mediaField.name}?: ${mediaType} | null`);
|
|
267
|
+
}
|
|
268
|
+
// Add relations as ID or ID array
|
|
269
|
+
for (const rel of component.relations) {
|
|
270
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
271
|
+
rel.relationType === 'manyToMany';
|
|
272
|
+
const relType = isArray ? 'number[]' : 'number';
|
|
273
|
+
lines.push(` ${rel.name}?: ${relType} | null`);
|
|
274
|
+
}
|
|
275
|
+
// Add nested components
|
|
276
|
+
for (const compField of component.components) {
|
|
277
|
+
const compType = compField.repeatable
|
|
278
|
+
? `${compField.componentType}Input[]`
|
|
279
|
+
: `${compField.componentType}Input`;
|
|
280
|
+
lines.push(` ${compField.name}?: ${compType} | null`);
|
|
281
|
+
}
|
|
282
|
+
// Add dynamic zones
|
|
283
|
+
for (const dzField of component.dynamicZones) {
|
|
284
|
+
const dzType = `(${dzField.componentTypes.map(ct => `${ct}Input`).join(' | ')})[]`;
|
|
285
|
+
lines.push(` ${dzField.name}?: ${dzType} | null`);
|
|
286
|
+
}
|
|
287
|
+
lines.push('}');
|
|
288
|
+
return lines.join('\n');
|
|
289
|
+
}
|
|
290
|
+
generateContentType(contentType) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
lines.push(`export interface ${contentType.cleanName} {`);
|
|
293
|
+
// Add nominal typing field to make types structurally unique
|
|
294
|
+
lines.push(` readonly __typename?: '${contentType.cleanName}'`);
|
|
295
|
+
// Add base fields
|
|
296
|
+
lines.push(' id: number');
|
|
297
|
+
lines.push(' documentId: string');
|
|
298
|
+
lines.push(' createdAt: string');
|
|
299
|
+
lines.push(' updatedAt: string');
|
|
300
|
+
// Add custom attributes
|
|
301
|
+
for (const attr of contentType.attributes) {
|
|
302
|
+
const tsType = this.transformer.toTypeScript(attr.type, attr.required);
|
|
303
|
+
lines.push(` ${attr.name}: ${tsType}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push('}');
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
308
|
+
generateInputType(contentType) {
|
|
309
|
+
const lines = [];
|
|
310
|
+
lines.push(`// Input type for creating/updating ${contentType.cleanName}`);
|
|
311
|
+
lines.push(`export interface ${contentType.cleanName}Input {`);
|
|
312
|
+
// Add scalar attributes (all optional for partial updates)
|
|
313
|
+
for (const attr of contentType.attributes) {
|
|
314
|
+
const tsType = this.transformer.toTypeScript(attr.type, false); // Always optional
|
|
315
|
+
lines.push(` ${attr.name}?: ${tsType}`);
|
|
316
|
+
}
|
|
317
|
+
// Add media fields as ID or ID array
|
|
318
|
+
for (const mediaField of contentType.media) {
|
|
319
|
+
const mediaType = mediaField.multiple ? 'number[]' : 'number';
|
|
320
|
+
lines.push(` ${mediaField.name}?: ${mediaType} | null`);
|
|
321
|
+
}
|
|
322
|
+
// Add relations as ID or ID array
|
|
323
|
+
for (const rel of contentType.relations) {
|
|
324
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
325
|
+
rel.relationType === 'manyToMany';
|
|
326
|
+
const relType = isArray ? 'number[]' : 'number';
|
|
327
|
+
lines.push(` ${rel.name}?: ${relType} | null`);
|
|
328
|
+
}
|
|
329
|
+
// Add components as objects (using component type, but with optional fields)
|
|
330
|
+
for (const compField of contentType.components) {
|
|
331
|
+
const compType = compField.repeatable
|
|
332
|
+
? `${compField.componentType}Input[]`
|
|
333
|
+
: `${compField.componentType}Input`;
|
|
334
|
+
lines.push(` ${compField.name}?: ${compType} | null`);
|
|
335
|
+
}
|
|
336
|
+
// Add dynamic zones
|
|
337
|
+
for (const dzField of contentType.dynamicZones) {
|
|
338
|
+
const dzType = `(${dzField.componentTypes.map(ct => `${ct}Input`).join(' | ')})[]`;
|
|
339
|
+
lines.push(` ${dzField.name}?: ${dzType} | null`);
|
|
340
|
+
}
|
|
341
|
+
lines.push('}');
|
|
342
|
+
return lines.join('\n');
|
|
343
|
+
}
|
|
344
|
+
generatePopulateParams(schema) {
|
|
345
|
+
const lines = [];
|
|
346
|
+
lines.push('// ============================================');
|
|
347
|
+
lines.push('// PopulateParam types for type-safe populate');
|
|
348
|
+
lines.push('// ============================================');
|
|
349
|
+
lines.push('');
|
|
350
|
+
// Generate for components with populatable fields
|
|
351
|
+
for (const component of schema.components) {
|
|
352
|
+
const result = this.generatePopulateParamForType(component);
|
|
353
|
+
if (result) {
|
|
354
|
+
lines.push(result);
|
|
355
|
+
lines.push('');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Generate for content types with populatable fields
|
|
359
|
+
for (const contentType of schema.contentTypes) {
|
|
360
|
+
const result = this.generatePopulateParamForType(contentType);
|
|
361
|
+
if (result) {
|
|
362
|
+
lines.push(result);
|
|
363
|
+
lines.push('');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return lines.join('\n');
|
|
367
|
+
}
|
|
368
|
+
generatePopulateParamForType(type) {
|
|
369
|
+
const populatableFields = [];
|
|
370
|
+
// Collect all populatable field entries
|
|
371
|
+
for (const rel of type.relations) {
|
|
372
|
+
const t = rel.targetType;
|
|
373
|
+
const targetHasPopulate = this.hasPopulatableFields(t);
|
|
374
|
+
const options = [`fields?: _EntityField<${t}>[]`];
|
|
375
|
+
if (targetHasPopulate) {
|
|
376
|
+
options.push(`populate?: ${t}PopulateParam | (keyof ${t}PopulateParam & string)[] | '*'`);
|
|
377
|
+
}
|
|
378
|
+
options.push(`filters?: ${t}Filters`);
|
|
379
|
+
options.push(`sort?: _SortValue<${t}> | _SortValue<${t}>[]`);
|
|
380
|
+
options.push(`limit?: number`);
|
|
381
|
+
options.push(`start?: number`);
|
|
382
|
+
populatableFields.push(` ${rel.name}?: true | { ${options.join('; ')} }`);
|
|
383
|
+
}
|
|
384
|
+
for (const media of type.media) {
|
|
385
|
+
populatableFields.push(` ${media.name}?: true | { fields?: (keyof MediaFile & string)[] }`);
|
|
386
|
+
}
|
|
387
|
+
for (const comp of type.components) {
|
|
388
|
+
const t = comp.componentType;
|
|
389
|
+
const targetHasPopulate = this.hasPopulatableFields(t);
|
|
390
|
+
const options = [`fields?: (keyof ${t} & string)[]`];
|
|
391
|
+
if (targetHasPopulate) {
|
|
392
|
+
options.push(`populate?: ${t}PopulateParam | (keyof ${t}PopulateParam & string)[] | '*'`);
|
|
393
|
+
}
|
|
394
|
+
populatableFields.push(` ${comp.name}?: true | { ${options.join('; ')} }`);
|
|
395
|
+
}
|
|
396
|
+
for (const dz of type.dynamicZones) {
|
|
397
|
+
// Dynamic zone: support Strapi v5 "on" fragment syntax
|
|
398
|
+
const onEntries = [];
|
|
399
|
+
for (let i = 0; i < dz.components.length; i++) {
|
|
400
|
+
const uid = dz.components[i];
|
|
401
|
+
const cleanType = dz.componentTypes[i];
|
|
402
|
+
const hasPopulate = this.hasPopulatableFields(cleanType);
|
|
403
|
+
const options = [`fields?: (keyof ${cleanType} & string)[]`];
|
|
404
|
+
if (hasPopulate) {
|
|
405
|
+
options.push(`populate?: ${cleanType}PopulateParam | (keyof ${cleanType}PopulateParam & string)[] | '*'`);
|
|
406
|
+
}
|
|
407
|
+
onEntries.push(`'${uid}'?: true | { ${options.join('; ')} }`);
|
|
408
|
+
}
|
|
409
|
+
populatableFields.push(` ${dz.name}?: true | { on?: { ${onEntries.join('; ')} } }`);
|
|
410
|
+
}
|
|
411
|
+
if (populatableFields.length === 0)
|
|
412
|
+
return null;
|
|
413
|
+
const lines = [];
|
|
414
|
+
lines.push(`export type ${type.cleanName}PopulateParam = {`);
|
|
415
|
+
lines.push(...populatableFields);
|
|
416
|
+
lines.push('}');
|
|
417
|
+
return lines.join('\n');
|
|
418
|
+
}
|
|
419
|
+
generatePayloadUtilityTypes(schema) {
|
|
420
|
+
const lines = [];
|
|
421
|
+
lines.push('// Prisma-like Payload types for populate support');
|
|
422
|
+
lines.push('// These types allow type-safe population of relations');
|
|
423
|
+
lines.push('//');
|
|
424
|
+
lines.push('// Usage example:');
|
|
425
|
+
lines.push('// type ItemWithCategory = ItemGetPayload<{ populate: { category: true } }>');
|
|
426
|
+
lines.push('// const items = await strapi.items.find({ populate: { category: true } }) as ItemWithCategory[]');
|
|
427
|
+
lines.push('//');
|
|
428
|
+
lines.push('// This ensures that relations are only included in the type when populate is used');
|
|
429
|
+
lines.push('');
|
|
430
|
+
// Generate GetPayload type for each component with populatable fields
|
|
431
|
+
for (const component of schema.components) {
|
|
432
|
+
const hasPopulatableFields = component.relations.length > 0 ||
|
|
433
|
+
component.media.length > 0 ||
|
|
434
|
+
component.components.length > 0 ||
|
|
435
|
+
component.dynamicZones.length > 0;
|
|
436
|
+
if (hasPopulatableFields) {
|
|
437
|
+
lines.push(this.generateComponentGetPayloadType(component));
|
|
438
|
+
lines.push('');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Generate GetPayload type for each content type with populatable fields
|
|
442
|
+
for (const contentType of schema.contentTypes) {
|
|
443
|
+
const hasPopulatableFields = contentType.relations.length > 0 ||
|
|
444
|
+
contentType.media.length > 0 ||
|
|
445
|
+
contentType.components.length > 0 ||
|
|
446
|
+
contentType.dynamicZones.length > 0;
|
|
447
|
+
if (hasPopulatableFields) {
|
|
448
|
+
lines.push(this.generateGetPayloadType(contentType));
|
|
449
|
+
lines.push('');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return lines.join('\n');
|
|
453
|
+
}
|
|
454
|
+
generateGetPayloadType(contentType) {
|
|
455
|
+
const lines = [];
|
|
456
|
+
lines.push(`// Payload type for ${contentType.cleanName} with populate support`);
|
|
457
|
+
lines.push(`export type ${contentType.cleanName}GetPayload<P extends { populate?: any } = {}> =`);
|
|
458
|
+
lines.push(` ${contentType.cleanName} &`);
|
|
459
|
+
const hasPopulatableFields = contentType.relations.length > 0 ||
|
|
460
|
+
contentType.media.length > 0 ||
|
|
461
|
+
contentType.components.length > 0 ||
|
|
462
|
+
contentType.dynamicZones.length > 0;
|
|
463
|
+
if (!hasPopulatableFields) {
|
|
464
|
+
lines.push(` {}`);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
lines.push(` (P extends { populate: infer Pop }`);
|
|
468
|
+
// Branch 1: Pop extends '*' | true → populate ALL fields (1 level deep)
|
|
469
|
+
lines.push(` ? Pop extends '*' | true`);
|
|
470
|
+
lines.push(` ? {`);
|
|
471
|
+
this.generateAllPopulatedFields(lines, contentType);
|
|
472
|
+
lines.push(` }`);
|
|
473
|
+
// Branch 2: Pop is a string array → check field name in array values
|
|
474
|
+
lines.push(` : Pop extends readonly (infer _)[]`);
|
|
475
|
+
lines.push(` ? {`);
|
|
476
|
+
this.generateArrayPopulatedFields(lines, contentType);
|
|
477
|
+
lines.push(` }`);
|
|
478
|
+
// Branch 3: Pop is an object → per-field conditional populate
|
|
479
|
+
lines.push(` : {`);
|
|
480
|
+
this.generatePerFieldPopulate(lines, contentType);
|
|
481
|
+
lines.push(` }`);
|
|
482
|
+
lines.push(` : {})`);
|
|
483
|
+
}
|
|
484
|
+
return lines.join('\n');
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Generate fields for populate: '*' | true — all populatable fields with base types
|
|
488
|
+
*/
|
|
489
|
+
generateAllPopulatedFields(lines, type) {
|
|
490
|
+
for (const rel of type.relations) {
|
|
491
|
+
const isNullable = rel.relationType === 'oneToOne' ||
|
|
492
|
+
rel.relationType === 'manyToOne';
|
|
493
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
494
|
+
rel.relationType === 'manyToMany';
|
|
495
|
+
const nullSuffix = isNullable ? ' | null' : '';
|
|
496
|
+
const arraySuffix = isArray ? '[]' : '';
|
|
497
|
+
lines.push(` ${rel.name}?: ${rel.targetType}${arraySuffix}${nullSuffix}`);
|
|
498
|
+
}
|
|
499
|
+
for (const mediaField of type.media) {
|
|
500
|
+
const mediaType = mediaField.multiple ? 'MediaFile[]' : 'MediaFile';
|
|
501
|
+
lines.push(` ${mediaField.name}?: ${mediaType}`);
|
|
502
|
+
}
|
|
503
|
+
for (const componentField of type.components) {
|
|
504
|
+
const arraySuffix = componentField.repeatable ? '[]' : '';
|
|
505
|
+
lines.push(` ${componentField.name}?: ${componentField.componentType}${arraySuffix}`);
|
|
506
|
+
}
|
|
507
|
+
for (const dzField of type.dynamicZones) {
|
|
508
|
+
const unionType = dzField.componentTypes.join(' | ');
|
|
509
|
+
lines.push(` ${dzField.name}?: (${unionType})[]`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Generate fields for array-style populate (e.g. populate: ['category', 'image'])
|
|
514
|
+
* Each field checks if its name is in Pop[number]
|
|
515
|
+
*/
|
|
516
|
+
generateArrayPopulatedFields(lines, type) {
|
|
517
|
+
for (const rel of type.relations) {
|
|
518
|
+
const isNullable = rel.relationType === 'oneToOne' ||
|
|
519
|
+
rel.relationType === 'manyToOne';
|
|
520
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
521
|
+
rel.relationType === 'manyToMany';
|
|
522
|
+
const nullSuffix = isNullable ? ' | null' : '';
|
|
523
|
+
const arraySuffix = isArray ? '[]' : '';
|
|
524
|
+
lines.push(` ${rel.name}?: '${rel.name}' extends Pop[number] ? ${rel.targetType}${arraySuffix}${nullSuffix} : never`);
|
|
525
|
+
}
|
|
526
|
+
for (const mediaField of type.media) {
|
|
527
|
+
const mediaType = mediaField.multiple ? 'MediaFile[]' : 'MediaFile';
|
|
528
|
+
lines.push(` ${mediaField.name}?: '${mediaField.name}' extends Pop[number] ? ${mediaType} : never`);
|
|
529
|
+
}
|
|
530
|
+
for (const componentField of type.components) {
|
|
531
|
+
const arraySuffix = componentField.repeatable ? '[]' : '';
|
|
532
|
+
lines.push(` ${componentField.name}?: '${componentField.name}' extends Pop[number] ? ${componentField.componentType}${arraySuffix} : never`);
|
|
533
|
+
}
|
|
534
|
+
for (const dzField of type.dynamicZones) {
|
|
535
|
+
const unionType = dzField.componentTypes.join(' | ');
|
|
536
|
+
lines.push(` ${dzField.name}?: '${dzField.name}' extends Pop[number] ? (${unionType})[] : never`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Generate per-field conditional populate for object-style populate params
|
|
541
|
+
*/
|
|
542
|
+
generatePerFieldPopulate(lines, type) {
|
|
543
|
+
for (const rel of type.relations) {
|
|
544
|
+
const isNullable = rel.relationType === 'oneToOne' ||
|
|
545
|
+
rel.relationType === 'manyToOne';
|
|
546
|
+
const isArray = rel.relationType === 'oneToMany' ||
|
|
547
|
+
rel.relationType === 'manyToMany';
|
|
548
|
+
const baseType = rel.targetType;
|
|
549
|
+
const nullSuffix = isNullable ? ' | null' : '';
|
|
550
|
+
const arraySuffix = isArray ? '[]' : '';
|
|
551
|
+
const hasPopulate = this.hasPopulatableFields(baseType);
|
|
552
|
+
lines.push(` ${rel.name}?: '${rel.name}' extends keyof Pop`);
|
|
553
|
+
if (hasPopulate) {
|
|
554
|
+
lines.push(` ? _ApplyFields<Pop['${rel.name}'] extends { populate: infer NestedPop } ? ${baseType}GetPayload<{ populate: NestedPop }> : ${baseType}, ${baseType}, Pop['${rel.name}']>${arraySuffix}${nullSuffix}`);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
lines.push(` ? _ApplyFields<${baseType}, ${baseType}, Pop['${rel.name}']>${arraySuffix}${nullSuffix}`);
|
|
558
|
+
}
|
|
559
|
+
lines.push(` : never`);
|
|
560
|
+
}
|
|
561
|
+
for (const mediaField of type.media) {
|
|
562
|
+
const arraySuffix = mediaField.multiple ? '[]' : '';
|
|
563
|
+
lines.push(` ${mediaField.name}?: '${mediaField.name}' extends keyof Pop ? _ApplyFields<MediaFile, MediaFile, Pop['${mediaField.name}']>${arraySuffix} : never`);
|
|
564
|
+
}
|
|
565
|
+
for (const componentField of type.components) {
|
|
566
|
+
const baseType = componentField.componentType;
|
|
567
|
+
const arraySuffix = componentField.repeatable ? '[]' : '';
|
|
568
|
+
const hasPopulate = this.hasPopulatableFields(baseType);
|
|
569
|
+
lines.push(` ${componentField.name}?: '${componentField.name}' extends keyof Pop`);
|
|
570
|
+
if (hasPopulate) {
|
|
571
|
+
lines.push(` ? _ApplyFields<Pop['${componentField.name}'] extends { populate: infer NestedPop } ? ${baseType}GetPayload<{ populate: NestedPop }> : ${baseType}, ${baseType}, Pop['${componentField.name}']>${arraySuffix}`);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
lines.push(` ? _ApplyFields<${baseType}, ${baseType}, Pop['${componentField.name}']>${arraySuffix}`);
|
|
575
|
+
}
|
|
576
|
+
lines.push(` : never`);
|
|
577
|
+
}
|
|
578
|
+
for (const dzField of type.dynamicZones) {
|
|
579
|
+
const unionType = dzField.componentTypes.join(' | ');
|
|
580
|
+
const dzType = `(${unionType})[]`;
|
|
581
|
+
lines.push(` ${dzField.name}?: '${dzField.name}' extends keyof Pop ? ${dzType} : never`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
generateComponentGetPayloadType(component) {
|
|
585
|
+
const lines = [];
|
|
586
|
+
lines.push(`// Payload type for ${component.cleanName} with populate support`);
|
|
587
|
+
lines.push(`export type ${component.cleanName}GetPayload<P extends { populate?: any } = {}> =`);
|
|
588
|
+
lines.push(` ${component.cleanName} &`);
|
|
589
|
+
const hasPopulatableFields = component.relations.length > 0 ||
|
|
590
|
+
component.media.length > 0 ||
|
|
591
|
+
component.components.length > 0 ||
|
|
592
|
+
component.dynamicZones.length > 0;
|
|
593
|
+
if (!hasPopulatableFields) {
|
|
594
|
+
lines.push(` {}`);
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
lines.push(` (P extends { populate: infer Pop }`);
|
|
598
|
+
// Branch 1: Pop extends '*' | true → populate ALL fields (1 level deep)
|
|
599
|
+
lines.push(` ? Pop extends '*' | true`);
|
|
600
|
+
lines.push(` ? {`);
|
|
601
|
+
this.generateAllPopulatedFields(lines, component);
|
|
602
|
+
lines.push(` }`);
|
|
603
|
+
// Branch 2: Pop is a string array → check field name in array values
|
|
604
|
+
lines.push(` : Pop extends readonly (infer _)[]`);
|
|
605
|
+
lines.push(` ? {`);
|
|
606
|
+
this.generateArrayPopulatedFields(lines, component);
|
|
607
|
+
lines.push(` }`);
|
|
608
|
+
// Branch 3: Pop is an object → per-field conditional populate
|
|
609
|
+
lines.push(` : {`);
|
|
610
|
+
this.generatePerFieldPopulate(lines, component);
|
|
611
|
+
lines.push(` }`);
|
|
612
|
+
lines.push(` : {})`);
|
|
613
|
+
}
|
|
614
|
+
return lines.join('\n');
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Check if a type (by name) has populatable fields (relations, media, components, or dynamic zones)
|
|
618
|
+
*/
|
|
619
|
+
hasPopulatableFields(typeName) {
|
|
620
|
+
if (!this.schema)
|
|
621
|
+
return false;
|
|
622
|
+
// Check in content types
|
|
623
|
+
const contentType = this.schema.contentTypes.find(ct => ct.cleanName === typeName);
|
|
624
|
+
if (contentType) {
|
|
625
|
+
return (contentType.relations.length > 0 ||
|
|
626
|
+
contentType.media.length > 0 ||
|
|
627
|
+
contentType.components.length > 0 ||
|
|
628
|
+
contentType.dynamicZones.length > 0);
|
|
629
|
+
}
|
|
630
|
+
// Check in components
|
|
631
|
+
const component = this.schema.components.find(comp => comp.cleanName === typeName);
|
|
632
|
+
if (component) {
|
|
633
|
+
return (component.relations.length > 0 ||
|
|
634
|
+
component.media.length > 0 ||
|
|
635
|
+
component.components.length > 0 ||
|
|
636
|
+
component.dynamicZones.length > 0);
|
|
637
|
+
}
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
//# sourceMappingURL=types-generator.js.map
|