@malevich-studio/strapi-sdk-typescript 1.0.3 → 1.0.4

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.
Files changed (48) hide show
  1. package/dist/cli.cjs +618 -0
  2. package/dist/cli.cjs.map +1 -0
  3. package/dist/cli.d.ts +0 -1
  4. package/dist/cli.mjs +596 -0
  5. package/dist/cli.mjs.map +1 -0
  6. package/dist/generate-strapi-types.d.ts +1 -1
  7. package/dist/generator/attributes/base-relation.d.ts +2 -1
  8. package/dist/generator/attributes/blocks.d.ts +2 -1
  9. package/dist/generator/attributes/boolean.d.ts +2 -1
  10. package/dist/generator/attributes/component.d.ts +2 -2
  11. package/dist/generator/attributes/date-time.d.ts +2 -1
  12. package/dist/generator/attributes/enumeration.d.ts +2 -1
  13. package/dist/generator/attributes/index.d.ts +2 -1
  14. package/dist/generator/attributes/json.d.ts +3 -2
  15. package/dist/generator/attributes/media.d.ts +3 -2
  16. package/dist/generator/attributes/number.d.ts +2 -1
  17. package/dist/generator/attributes/relation.d.ts +2 -2
  18. package/dist/generator/attributes/string.d.ts +2 -1
  19. package/dist/generator/index.d.ts +5 -0
  20. package/dist/index.cjs +99 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/{main.d.ts → index.d.ts} +55 -7
  23. package/dist/index.mjs +97 -0
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/test.cjs +114 -0
  26. package/dist/test.cjs.map +1 -0
  27. package/dist/test.d.ts +1 -0
  28. package/dist/test.mjs +112 -0
  29. package/dist/test.mjs.map +1 -0
  30. package/package.json +20 -8
  31. package/dist/cli.js +0 -10
  32. package/dist/generate-strapi-types.js +0 -205
  33. package/dist/generator/attributes/base-relation.js +0 -56
  34. package/dist/generator/attributes/base.js +0 -53
  35. package/dist/generator/attributes/blocks.js +0 -29
  36. package/dist/generator/attributes/boolean.js +0 -17
  37. package/dist/generator/attributes/component.js +0 -36
  38. package/dist/generator/attributes/date-time.js +0 -17
  39. package/dist/generator/attributes/enumeration.js +0 -17
  40. package/dist/generator/attributes/index.js +0 -40
  41. package/dist/generator/attributes/json.js +0 -20
  42. package/dist/generator/attributes/media.js +0 -29
  43. package/dist/generator/attributes/number.js +0 -17
  44. package/dist/generator/attributes/relation.js +0 -66
  45. package/dist/generator/attributes/string.js +0 -17
  46. package/dist/generator/utils/get-component-name.js +0 -13
  47. package/dist/generator/utils/get-content-type-name.js +0 -18
  48. package/dist/main.js +0 -67
package/dist/cli.mjs ADDED
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import _ from 'lodash';
5
+ import qs from 'qs';
6
+ import mime from 'mime';
7
+ import { readFile } from 'fs/promises';
8
+ import { basename } from 'node:path';
9
+ import 'dotenv/config';
10
+
11
+ class Strapi {
12
+ constructor(url, token) {
13
+ this.url = url;
14
+ this.token = token;
15
+ }
16
+ async request(endpoint, data = {}, params = {}) {
17
+ const queryString = params.method === 'GET' ? qs.stringify(data) : '';
18
+ return await this.baseRequest(queryString ? `${endpoint}?${queryString}` : endpoint, _.merge({
19
+ headers: {
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ ...(params.method && !['GET', 'DELETE'].includes(params.method) ? {
23
+ body: JSON.stringify({
24
+ data,
25
+ })
26
+ } : {}),
27
+ }, params));
28
+ }
29
+ async getDocuments(endpoint, data, params = {}) {
30
+ return await this.request(endpoint, data, {
31
+ method: 'GET',
32
+ ...params,
33
+ });
34
+ }
35
+ async getDocument(endpoint, data, params = {}) {
36
+ return await this.request(endpoint, data, {
37
+ method: 'GET',
38
+ ...params,
39
+ });
40
+ }
41
+ async create(endpoint, data, params = {}) {
42
+ return await this.request(endpoint, data, {
43
+ method: 'POST',
44
+ ...params,
45
+ });
46
+ }
47
+ async update(endpoint, id, data, params = {}) {
48
+ return await this.request(`${endpoint}/${id}`, data, {
49
+ method: 'PUT',
50
+ ...params,
51
+ });
52
+ }
53
+ async delete(endpoint, id, params = {}) {
54
+ return await this.request(`${endpoint}/${id}`, {}, {
55
+ method: 'DELETE',
56
+ ...params,
57
+ });
58
+ }
59
+ /**
60
+ * For Node.js
61
+ *
62
+ * @param files list of files names which will be uploaded, example: ['/app/data/cover.js']
63
+ */
64
+ async upload(files) {
65
+ const form = new FormData();
66
+ await Promise.all(files.map(async (item) => {
67
+ const fileBuffer = await readFile(item.path);
68
+ const file = new File([fileBuffer], item.filename || basename(item.path), { type: mime.getType(item.path) || 'image/jpeg' });
69
+ form.append('files', file);
70
+ }));
71
+ return await this.uploadForm(form);
72
+ }
73
+ async uploadForm(form) {
74
+ return await this.baseRequest('upload', {
75
+ method: 'POST',
76
+ body: form,
77
+ });
78
+ }
79
+ async baseRequest(endpoint, params = {}) {
80
+ const response = await fetch(`${this.url}/api/${endpoint}`, _.merge({
81
+ headers: {
82
+ Authorization: `Bearer ${this.token}`,
83
+ },
84
+ }, params));
85
+ if (!response.ok) {
86
+ console.log(`${this.url}/api/${endpoint}`);
87
+ console.log(_.merge({
88
+ headers: {
89
+ Authorization: `Bearer ${this.token}`,
90
+ },
91
+ }, params));
92
+ console.log(response);
93
+ console.log(await response.json());
94
+ throw new Error(`Помилка запиту до Strapi: ${response.status} ${response.statusText}`);
95
+ }
96
+ return (await response.json());
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Returns the TypeScript interface name from a component UID
102
+ * e.g. "default.test-component" => "DefaultTestComponent"
103
+ */
104
+ function getComponentName(uid) {
105
+ return uid
106
+ .split(/[\.\-]/)
107
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
108
+ .join('');
109
+ }
110
+
111
+ var AttributeMode;
112
+ (function (AttributeMode) {
113
+ AttributeMode["Field"] = "field";
114
+ AttributeMode["Relation"] = "relation";
115
+ })(AttributeMode || (AttributeMode = {}));
116
+ class Base {
117
+ constructor(name, attribute) {
118
+ this.name = name;
119
+ this.attribute = attribute;
120
+ }
121
+ getType() {
122
+ return 'any';
123
+ }
124
+ getInputType() {
125
+ return this.getType();
126
+ }
127
+ getImports() {
128
+ return [];
129
+ }
130
+ getPackages() {
131
+ return [];
132
+ }
133
+ getFields() {
134
+ return [
135
+ this.name,
136
+ ];
137
+ }
138
+ getSortFields() {
139
+ return [
140
+ this.name,
141
+ `${this.name}:asc`,
142
+ `${this.name}:desc`,
143
+ ];
144
+ }
145
+ getPopulates() {
146
+ return [];
147
+ }
148
+ getFilters() {
149
+ return [
150
+ {
151
+ name: this.name,
152
+ type: `FilterValue<${this.getType()}>`,
153
+ },
154
+ ];
155
+ }
156
+ getMode() {
157
+ return AttributeMode.Field;
158
+ }
159
+ }
160
+
161
+ class BaseRelation extends Base {
162
+ constructor(name, attribute) {
163
+ super(name, attribute);
164
+ this.name = name;
165
+ this.attribute = attribute;
166
+ }
167
+ getType() {
168
+ return 'any';
169
+ }
170
+ getFields() {
171
+ return [];
172
+ }
173
+ getSortFields() {
174
+ return [];
175
+ }
176
+ getMode() {
177
+ return AttributeMode.Relation;
178
+ }
179
+ }
180
+
181
+ class Media extends BaseRelation {
182
+ constructor(name, attribute) {
183
+ super(name, attribute);
184
+ this.name = name;
185
+ this.attribute = attribute;
186
+ }
187
+ getType() {
188
+ return this.attribute.multiple ? 'File[]' : 'File';
189
+ }
190
+ getInputType() {
191
+ return 'RelationInput';
192
+ }
193
+ getPopulates() {
194
+ return [{
195
+ name: this.name,
196
+ type: 'FileQuery',
197
+ }];
198
+ }
199
+ getFilters() {
200
+ return [{
201
+ name: this.name,
202
+ type: 'FileFilters',
203
+ }];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Returns the TypeScript interface name from a content type UID
209
+ * e.g. "api::article.article" => "Article"
210
+ */
211
+ function getContentTypeName(uid) {
212
+ // Usually, UIDs look like "api::<api-name>.<model-name>"
213
+ // We'll split at "::" and then take the part after the dot.
214
+ const namePart = uid.split('::')[1] || uid;
215
+ const modelName = namePart.split('.')[1] || namePart;
216
+ // Convert to PascalCase
217
+ return modelName
218
+ .split('-')
219
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
220
+ .join('');
221
+ }
222
+
223
+ var AttributeRelation;
224
+ (function (AttributeRelation) {
225
+ AttributeRelation["MorphToMany"] = "morphToMany";
226
+ AttributeRelation["ManyToOne"] = "manyToOne";
227
+ AttributeRelation["ManyToMany"] = "manyToMany";
228
+ AttributeRelation["OneToMany"] = "oneToMany";
229
+ AttributeRelation["OneToOne"] = "oneToOne";
230
+ })(AttributeRelation || (AttributeRelation = {}));
231
+ class Relation extends BaseRelation {
232
+ constructor(name, attribute) {
233
+ super(name, attribute);
234
+ this.name = name;
235
+ this.attribute = attribute;
236
+ }
237
+ getType() {
238
+ if (this.attribute.relation === AttributeRelation.MorphToMany) {
239
+ return 'any';
240
+ }
241
+ const ContentTypeName = getContentTypeName(this.attribute.target);
242
+ switch (this.attribute.relation) {
243
+ case AttributeRelation.ManyToMany:
244
+ case AttributeRelation.OneToMany:
245
+ return `${ContentTypeName}[]`;
246
+ case AttributeRelation.ManyToOne:
247
+ case AttributeRelation.OneToOne:
248
+ default:
249
+ return ContentTypeName;
250
+ }
251
+ }
252
+ getInputType() {
253
+ return 'RelationInput';
254
+ }
255
+ getPopulates() {
256
+ if (this.attribute.relation === AttributeRelation.MorphToMany) {
257
+ return [];
258
+ }
259
+ return [{
260
+ name: this.name,
261
+ type: `${getContentTypeName(this.attribute.target)}Query`,
262
+ }];
263
+ }
264
+ getFilters() {
265
+ if (this.attribute.relation === AttributeRelation.MorphToMany) {
266
+ return [];
267
+ }
268
+ return [{
269
+ name: this.name,
270
+ type: `${getContentTypeName(this.attribute.target)}Filters`,
271
+ }];
272
+ }
273
+ getImports() {
274
+ return [
275
+ ...super.getImports(),
276
+ 'import {RelationInput} from "@strapi/blocks-react-renderer";',
277
+ ];
278
+ }
279
+ }
280
+
281
+ class Enumeration extends Base {
282
+ constructor(name, attribute) {
283
+ super(name, attribute);
284
+ this.name = name;
285
+ this.attribute = attribute;
286
+ }
287
+ getType() {
288
+ return `'${this.attribute.enum.join('\' | \'')}'`;
289
+ }
290
+ }
291
+
292
+ class DateTime extends Base {
293
+ constructor(name, attribute) {
294
+ super(name, attribute);
295
+ this.name = name;
296
+ this.attribute = attribute;
297
+ }
298
+ getType() {
299
+ return 'string';
300
+ }
301
+ }
302
+
303
+ class Component extends BaseRelation {
304
+ constructor(name, attribute) {
305
+ super(name, attribute);
306
+ this.name = name;
307
+ this.attribute = attribute;
308
+ }
309
+ getType() {
310
+ const componentName = getComponentName(this.attribute.component);
311
+ return this.attribute.repeatable ? `${componentName}[]` : componentName;
312
+ }
313
+ getInputType() {
314
+ return `${getComponentName(this.attribute.component)}Input`;
315
+ }
316
+ getPopulates() {
317
+ return [{
318
+ name: this.name,
319
+ type: `${getComponentName(this.attribute.component)}Query`,
320
+ }];
321
+ }
322
+ getFilters() {
323
+ return [
324
+ // {
325
+ // name: this.name,
326
+ // type: `${getComponentName(this.attribute.component)}Filters`,
327
+ // }
328
+ ];
329
+ }
330
+ }
331
+
332
+ class Blocks extends Base {
333
+ constructor(name, attribute) {
334
+ super(name, attribute);
335
+ this.name = name;
336
+ this.attribute = attribute;
337
+ }
338
+ getType() {
339
+ return 'BlocksContent';
340
+ }
341
+ getImports() {
342
+ return [
343
+ ...super.getImports(),
344
+ 'import {BlocksContent} from "@strapi/blocks-react-renderer";',
345
+ ];
346
+ }
347
+ getPackages() {
348
+ return [
349
+ ...super.getPackages(),
350
+ '@strapi/blocks-react-renderer',
351
+ ];
352
+ }
353
+ }
354
+
355
+ class Json extends Base {
356
+ constructor(name, attribute) {
357
+ super(name, attribute);
358
+ this.name = name;
359
+ this.attribute = attribute;
360
+ }
361
+ getType() {
362
+ return 'object';
363
+ }
364
+ getFilters() {
365
+ return [];
366
+ }
367
+ }
368
+
369
+ class String extends Base {
370
+ constructor(name, attribute) {
371
+ super(name, attribute);
372
+ this.name = name;
373
+ this.attribute = attribute;
374
+ }
375
+ getType() {
376
+ return 'string';
377
+ }
378
+ }
379
+
380
+ class Number extends Base {
381
+ constructor(name, attribute) {
382
+ super(name, attribute);
383
+ this.name = name;
384
+ this.attribute = attribute;
385
+ }
386
+ getType() {
387
+ return 'number';
388
+ }
389
+ }
390
+
391
+ class Boolean extends Base {
392
+ constructor(name, attribute) {
393
+ super(name, attribute);
394
+ this.name = name;
395
+ this.attribute = attribute;
396
+ }
397
+ getType() {
398
+ return 'boolean';
399
+ }
400
+ }
401
+
402
+ const types = {
403
+ 'string': String,
404
+ 'text': String,
405
+ 'password': String,
406
+ 'email': String,
407
+ 'integer': Number,
408
+ 'biginteger': Number,
409
+ 'decimal': Number,
410
+ 'float': Number,
411
+ 'boolean': Boolean,
412
+ 'media': Media,
413
+ 'relation': Relation,
414
+ 'enumeration': Enumeration,
415
+ 'datetime': DateTime,
416
+ 'component': Component,
417
+ 'blocks': Blocks,
418
+ 'json': Json,
419
+ };
420
+ function getAttributeGenerator(name, attribute) {
421
+ if (!types[attribute.type]) {
422
+ throw new Error(`Attribute type "${attribute.type}" is not defined`);
423
+ }
424
+ return new types[attribute.type](name, attribute);
425
+ }
426
+
427
+ var ContentTypeKind;
428
+ (function (ContentTypeKind) {
429
+ ContentTypeKind["CollectionType"] = "collectionType";
430
+ ContentTypeKind["SingleType"] = "singleType";
431
+ })(ContentTypeKind || (ContentTypeKind = {}));
432
+ function getContentTypeMethodName(uid) {
433
+ const typeName = getContentTypeName(uid);
434
+ return typeName.charAt(0).toLowerCase() + typeName.slice(1);
435
+ }
436
+ /**
437
+ * Generates a TS interface from a content type or component definition
438
+ */
439
+ function generateResponseTypeCode(name, attributes) {
440
+ const lines = [];
441
+ lines.push(`export type ${name} = {`);
442
+ for (const attributeName in attributes) {
443
+ const attribute = attributes[attributeName];
444
+ const isRequired = attribute.required ? '' : '?';
445
+ lines.push(` ${attributeName}${isRequired}: ${getAttributeGenerator(attributeName, attribute).getType()};`);
446
+ }
447
+ lines.push(`}`);
448
+ return lines.join('\n');
449
+ }
450
+ function generateQueryTypeCode(name, attributes) {
451
+ const fields = [];
452
+ const sortFields = [];
453
+ const filters = [];
454
+ const populates = [];
455
+ for (const attributeName in attributes) {
456
+ const attribute = attributes[attributeName];
457
+ const attributeGenerator = getAttributeGenerator(attributeName, attribute);
458
+ fields.push(...attributeGenerator.getFields());
459
+ sortFields.push(...attributeGenerator.getSortFields());
460
+ filters.push(...attributeGenerator.getFilters());
461
+ populates.push(...attributeGenerator.getPopulates());
462
+ }
463
+ const lines = [];
464
+ lines.push(`export type ${name}Filters = Filters<{`);
465
+ lines.push(...filters.map(({ name, type }) => ` ${name}?: ${type};`));
466
+ lines.push(`}>`);
467
+ lines.push('');
468
+ lines.push(`export type ${name}Populate = {`);
469
+ lines.push(...populates.map(({ name, type }) => ` ${name}?: ${type};`));
470
+ lines.push(`}`);
471
+ lines.push('');
472
+ lines.push(`export type ${name}Query = Query<`);
473
+ lines.push(` ${fields.map(field => `'${field}'`).join(' | ')},`);
474
+ lines.push(` ${sortFields.map(field => `'${field}'`).join(' | ')},`);
475
+ lines.push(` ${name}Filters,`);
476
+ lines.push(` ${name}Populate`);
477
+ lines.push(`>`);
478
+ return lines.join('\n');
479
+ }
480
+ function generateInputTypeCode(name, attributes) {
481
+ const fields = [];
482
+ for (const attributeName in attributes) {
483
+ const attribute = attributes[attributeName];
484
+ const attributeGenerator = getAttributeGenerator(attributeName, attribute);
485
+ fields.push({
486
+ name: attributeName,
487
+ type: attributeGenerator.getInputType(),
488
+ });
489
+ }
490
+ const lines = [];
491
+ lines.push(`export type ${name}Input = {`);
492
+ lines.push(...fields.map(({ name, type }) => ` ${name}?: ${type};`));
493
+ lines.push(`}`);
494
+ return lines.join('\n');
495
+ }
496
+ function generateMethodsCode(contentType) {
497
+ const methods = [];
498
+ const modelName = getContentTypeName(contentType.uid);
499
+ if (contentType.schema.kind === ContentTypeKind.CollectionType) {
500
+ methods.push([
501
+ ` public async ${getContentTypeMethodName(contentType.schema.pluralName)}(query?: ${modelName}Query, params?: RequestInit) {`,
502
+ ` return await this.getDocuments<${modelName}, ${modelName}Query>('${contentType.schema.pluralName}', query, params);`,
503
+ ' }',
504
+ ].join('\n'));
505
+ }
506
+ methods.push([
507
+ ` public async ${getContentTypeMethodName(contentType.schema.singularName)}(query?: ${modelName}Query, params?: RequestInit) {`,
508
+ ` return await this.getDocument<${modelName}, ${modelName}Query>('${contentType.schema.singularName}', query, params);`,
509
+ ' }',
510
+ ].join('\n'));
511
+ methods.push([
512
+ ` public async create${getContentTypeName(contentType.schema.singularName)}(data: ${modelName}Input, params?: RequestInit) {`,
513
+ ` return await this.create<${modelName}, ${modelName}Input>('${contentType.schema.pluralName}', data, params);`,
514
+ ' }',
515
+ ].join('\n'));
516
+ methods.push([
517
+ ` public async update${getContentTypeName(contentType.schema.singularName)}(id: string, data: ${modelName}Input, params?: RequestInit) {`,
518
+ ` return await this.update<${modelName}, ${modelName}Input>('${contentType.schema.pluralName}', id, data, params);`,
519
+ ' }',
520
+ ].join('\n'));
521
+ methods.push([
522
+ ` public async delete${getContentTypeName(contentType.schema.singularName)}(id: string, params?: RequestInit) {`,
523
+ ` return await this.delete<${modelName}>('${contentType.schema.pluralName}', id, params);`,
524
+ ' }',
525
+ ].join('\n'));
526
+ return methods;
527
+ }
528
+ /**
529
+ * Main function to fetch Strapi (v5) data and generate the d.ts file
530
+ */
531
+ async function generateStrapiTypes(strapi) {
532
+ const contentTypes = (await strapi.request('content-type-builder/content-types')).data;
533
+ const components = (await strapi.request('content-type-builder/components')).data;
534
+ const allInterfaces = [];
535
+ const methods = [];
536
+ for (const component of components) {
537
+ const componentName = getComponentName(component.uid);
538
+ const attributes = {
539
+ id: {
540
+ type: 'integer',
541
+ },
542
+ ...component.schema.attributes,
543
+ };
544
+ allInterfaces.push(generateResponseTypeCode(componentName, attributes));
545
+ allInterfaces.push(generateQueryTypeCode(componentName, attributes));
546
+ allInterfaces.push(generateInputTypeCode(componentName, attributes));
547
+ }
548
+ for (const contentType of contentTypes) {
549
+ if (!['api::', 'plugin::upload', 'plugin::users-permissions'].filter(prefix => contentType.uid.startsWith(prefix)).length) {
550
+ continue;
551
+ }
552
+ methods.push(...generateMethodsCode(contentType));
553
+ const modelName = getContentTypeName(contentType.uid);
554
+ const attributes = {
555
+ id: {
556
+ type: 'integer',
557
+ },
558
+ documentId: {
559
+ type: 'string',
560
+ },
561
+ createdAt: {
562
+ type: 'datetime',
563
+ },
564
+ updatedAt: {
565
+ type: 'datetime',
566
+ },
567
+ ...contentType.schema.attributes,
568
+ };
569
+ allInterfaces.push(generateResponseTypeCode(modelName, attributes));
570
+ allInterfaces.push(generateQueryTypeCode(modelName, attributes));
571
+ allInterfaces.push(generateInputTypeCode(modelName, attributes));
572
+ }
573
+ const output = [
574
+ 'import {Strapi as StrapiBase, Query, Filters, FilterValue, RelationInput} from "@malevich-studio/strapi-sdk-typescript";',
575
+ 'import {BlocksContent} from "@strapi/blocks-react-renderer";',
576
+ '',
577
+ 'export default class Strapi extends StrapiBase {',
578
+ methods.join('\n\n'),
579
+ '}',
580
+ '',
581
+ allInterfaces.join('\n\n'),
582
+ '',
583
+ ].join('\n');
584
+ const outPath = path.join(process.cwd(), 'strapi.ts');
585
+ fs.writeFileSync(outPath, output, 'utf-8');
586
+ console.log(`✅ "strapi.ts" has been successfully generated!`);
587
+ }
588
+
589
+ if (!process.env.STRAPI_URL || !process.env.STRAPI_TOKEN) {
590
+ throw new Error('STRAPI_URL and STRAPI_TOKEN must be provided.');
591
+ }
592
+ const strapi = new Strapi(process.env.STRAPI_URL, process.env.STRAPI_TOKEN);
593
+ (async () => {
594
+ await generateStrapiTypes(strapi);
595
+ })();
596
+ //# sourceMappingURL=cli.mjs.map