@strapi/typescript-utils 4.3.0-beta.2 → 4.3.2-alpha.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.
@@ -6,21 +6,97 @@ const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation();
6
6
 
7
7
  const ts = require('typescript');
8
8
 
9
- const { getAttributeType } = require('../../../generators/schemas/attributes');
9
+ const attributeToPropertySignature = require('../../../generators/schemas/attributes');
10
+ const {
11
+ getAttributeType,
12
+ getAttributeModifiers,
13
+ } = require('../../../generators/schemas/attributes');
10
14
  const { addImport } = require('../../../generators/schemas/imports');
11
15
 
16
+ // TODO: emit definition (to a string) & also check snapshots based on that. It would allow checking both the structure & the output.
12
17
  describe('Attributes', () => {
13
18
  afterEach(() => {
14
19
  jest.resetAllMocks();
15
20
  });
16
21
 
17
- // TODO
18
- // describe('Attribute to Property Signature', () => {});
22
+ describe('Attribute to Property Signature', () => {
23
+ const schema = { uid: 'api::foo.foo' };
24
+ const attributeName = 'foo';
19
25
 
20
- // TODO
21
- // describe('Mappers', () => {});
26
+ const toPropertySignature = attribute => {
27
+ return attributeToPropertySignature(schema, attributeName, attribute);
28
+ };
22
29
 
23
- describe('Get Attribute Type', () => {
30
+ const defaultAssertion = node => {
31
+ expect(node.kind).toBe(ts.SyntaxKind.PropertySignature);
32
+ expect(node.name.escapedText).toBe(attributeName);
33
+ expect(node.type.kind).toBe(ts.SyntaxKind.IntersectionType);
34
+ };
35
+
36
+ test('Invalid attribute type', () => {
37
+ const attribute = { type: 'invalid' };
38
+ const prop = toPropertySignature(attribute);
39
+
40
+ expect(prop).toBeNull();
41
+ });
42
+
43
+ test('Attribute without type argument', () => {
44
+ const attribute = { type: 'string' };
45
+ const prop = toPropertySignature(attribute);
46
+
47
+ defaultAssertion(prop);
48
+
49
+ expect(prop.type.types).toHaveLength(1);
50
+ expect(prop.type.types[0].kind).toBe(ts.SyntaxKind.TypeReference);
51
+ expect(prop.type.types[0].typeName.escapedText).toBe('StringAttribute');
52
+ expect(prop.type.types[0].typeArguments).toBeUndefined();
53
+ });
54
+
55
+ test('Attribute with type argument', () => {
56
+ const attribute = { type: 'component', component: 'default.comp' };
57
+ const prop = toPropertySignature(attribute);
58
+
59
+ defaultAssertion(prop);
60
+
61
+ expect(prop.type.types).toHaveLength(1);
62
+ expect(prop.type.types[0].kind).toBe(ts.SyntaxKind.TypeReference);
63
+ expect(prop.type.types[0].typeName.escapedText).toBe('ComponentAttribute');
64
+ expect(prop.type.types[0].typeArguments).toHaveLength(1);
65
+ expect(prop.type.types[0].typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
66
+ expect(prop.type.types[0].typeArguments[0].text).toBe('default.comp');
67
+ });
68
+
69
+ test('Attribute with type argument and options', () => {
70
+ const attribute = {
71
+ type: 'enumeration',
72
+ enum: ['a', 'b'],
73
+ default: 'b',
74
+ configurable: false,
75
+ };
76
+ const prop = toPropertySignature(attribute);
77
+
78
+ defaultAssertion(prop);
79
+
80
+ expect(prop.type.types).toHaveLength(2);
81
+
82
+ const [attributeType, requiredOptionType] = prop.type.types;
83
+
84
+ expect(attributeType.kind).toBe(ts.SyntaxKind.TypeReference);
85
+ expect(attributeType.typeName.escapedText).toBe('EnumerationAttribute');
86
+ expect(attributeType.typeArguments).toHaveLength(1);
87
+ expect(attributeType.typeArguments[0].kind).toBe(ts.SyntaxKind.TupleType);
88
+ expect(attributeType.typeArguments[0].elements[0].text).toBe('a');
89
+ expect(attributeType.typeArguments[0].elements[1].text).toBe('b');
90
+
91
+ expect(requiredOptionType.kind).toBe(ts.SyntaxKind.TypeReference);
92
+ expect(requiredOptionType.typeName.escapedText).toBe('DefaultTo');
93
+ expect(requiredOptionType.typeArguments).toHaveLength(1);
94
+ expect(requiredOptionType.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
95
+ expect(requiredOptionType.typeArguments[0].text).toBe('b');
96
+ });
97
+ });
98
+
99
+ describe('Get Attribute Type / Mappers', () => {
24
100
  test('If the attribute type is not valid then log an error and exit early without importing the type', () => {
25
101
  const typeNode = getAttributeType('foo', { type: 'invalid', uid: 'api::foo.foo' });
26
102
 
@@ -31,17 +107,34 @@ describe('Attributes', () => {
31
107
  expect(addImport).not.toHaveBeenCalled();
32
108
  });
33
109
 
34
- test('Return a basic type node without generic type parameter', () => {
35
- const typeNode = getAttributeType('foo', { type: 'string' });
110
+ test.each([
111
+ ['string', 'StringAttribute'],
112
+ ['text', 'TextAttribute'],
113
+ ['richtext', 'RichTextAttribute'],
114
+ ['password', 'PasswordAttribute'],
115
+ ['email', 'EmailAttribute'],
116
+ ['date', 'DateAttribute'],
117
+ ['time', 'TimeAttribute'],
118
+ ['datetime', 'DateTimeAttribute'],
119
+ ['timestamp', 'TimestampAttribute'],
120
+ ['integer', 'IntegerAttribute'],
121
+ ['biginteger', 'BigIntegerAttribute'],
122
+ ['float', 'FloatAttribute'],
123
+ ['decimal', 'DecimalAttribute'],
124
+ ['boolean', 'BooleanAttribute'],
125
+ ['json', 'JSONAttribute'],
126
+ ['media', 'MediaAttribute'],
127
+ ])('Basic %p attribute should map to a %p type', (type, expectedType) => {
128
+ const typeNode = getAttributeType('foo', { type });
36
129
 
37
130
  expect(ts.isTypeNode(typeNode)).toBeTruthy();
38
131
 
39
132
  expect(typeNode.kind).toBe(ts.SyntaxKind.TypeReference);
40
- expect(typeNode.typeName.escapedText).toBe('StringAttribute');
133
+ expect(typeNode.typeName.escapedText).toBe(expectedType);
41
134
  expect(typeNode.typeArguments).toBeUndefined();
42
135
 
43
136
  expect(consoleWarnMock).not.toHaveBeenCalled();
44
- expect(addImport).toHaveBeenCalledWith('StringAttribute');
137
+ expect(addImport).toHaveBeenCalledWith(expectedType);
45
138
  });
46
139
 
47
140
  describe('Complex types (with generic type parameters)', () => {
@@ -55,27 +148,556 @@ describe('Attributes', () => {
55
148
  expect(addImport).toHaveBeenCalledWith(typeName);
56
149
  };
57
150
 
58
- test('Enumeration', () => {
59
- const attribute = { type: 'enumeration', enum: ['a', 'b', 'c'] };
60
- const typeNode = getAttributeType('foo', attribute);
151
+ describe('Enumeration', () => {
152
+ test('Enumeration with an enum property', () => {
153
+ const attribute = { type: 'enumeration', enum: ['a', 'b', 'c'] };
154
+ const typeNode = getAttributeType('foo', attribute);
155
+
156
+ defaultAssertions(typeNode, 'EnumerationAttribute');
157
+
158
+ expect(typeNode.typeArguments).toHaveLength(1);
159
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.TupleType);
160
+
161
+ const tupleElements = typeNode.typeArguments[0].elements;
162
+
163
+ attribute.enum.forEach((value, index) => {
164
+ const element = tupleElements[index];
165
+
166
+ expect(element.kind).toBe(ts.SyntaxKind.StringLiteral);
167
+ expect(element.text).toBe(value);
168
+ });
169
+ });
170
+ });
171
+
172
+ describe('UID', () => {
173
+ test('UID with no options and no target field', () => {
174
+ const attribute = { type: 'uid' };
175
+ const typeNode = getAttributeType('foo', attribute);
176
+
177
+ defaultAssertions(typeNode, 'UIDAttribute');
178
+
179
+ expect(typeNode.typeArguments).toBeUndefined();
180
+ });
181
+
182
+ test('UID with a target field and no options', () => {
183
+ const attribute = { type: 'uid', targetField: 'bar' };
184
+ const typeNode = getAttributeType('foo', attribute, 'api::bar.bar');
185
+
186
+ defaultAssertions(typeNode, 'UIDAttribute');
187
+
188
+ expect(typeNode.typeArguments).not.toBeUndefined();
189
+ expect(typeNode.typeArguments).toHaveLength(2);
190
+
191
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
192
+ expect(typeNode.typeArguments[0].text).toBe('api::bar.bar');
193
+
194
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.StringLiteral);
195
+ expect(typeNode.typeArguments[1].text).toBe('bar');
196
+ });
197
+
198
+ test('UID with partial options and no target field', () => {
199
+ const attribute = { type: 'uid', options: { separator: '_' } };
200
+ const typeNode = getAttributeType('foo', attribute);
201
+
202
+ defaultAssertions(typeNode, 'UIDAttribute');
203
+
204
+ expect(typeNode.typeArguments).toHaveLength(3);
205
+
206
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.UndefinedKeyword);
207
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.UndefinedKeyword);
208
+
209
+ const optionsLiteralNode = typeNode.typeArguments[2];
210
+
211
+ expect(optionsLiteralNode.kind).toBe(ts.SyntaxKind.TypeLiteral);
212
+ expect(optionsLiteralNode.members).toHaveLength(1);
213
+
214
+ expect(optionsLiteralNode.members[0].kind).toBe(ts.SyntaxKind.PropertyDeclaration);
215
+
216
+ expect(optionsLiteralNode.members[0].name.kind).toBe(ts.SyntaxKind.Identifier);
217
+ expect(optionsLiteralNode.members[0].name.escapedText).toBe('separator');
218
+
219
+ expect(optionsLiteralNode.members[0].type.kind).toBe(ts.SyntaxKind.StringLiteral);
220
+ expect(optionsLiteralNode.members[0].type.text).toBe('_');
221
+ });
222
+
223
+ test('UID with options and a target field', () => {
224
+ const attribute = { type: 'uid', options: { separator: '_' }, targetField: 'bar' };
225
+ const typeNode = getAttributeType('foo', attribute, 'api::bar.bar');
61
226
 
62
- defaultAssertions(typeNode, 'EnumerationAttribute');
227
+ defaultAssertions(typeNode, 'UIDAttribute');
63
228
 
64
- expect(typeNode.typeArguments).toHaveLength(1);
65
- expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.TupleType);
229
+ expect(typeNode.typeArguments).toHaveLength(3);
66
230
 
67
- const tupleElements = typeNode.typeArguments[0].elements;
231
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
232
+ expect(typeNode.typeArguments[0].text).toBe('api::bar.bar');
68
233
 
69
- attribute.enum.forEach((value, index) => {
70
- const element = tupleElements[index];
234
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.StringLiteral);
235
+ expect(typeNode.typeArguments[1].text).toBe('bar');
71
236
 
72
- expect(element.kind).toBe(ts.SyntaxKind.StringLiteral);
73
- expect(element.text).toBe(value);
237
+ const optionsLiteralNode = typeNode.typeArguments[2];
238
+
239
+ expect(optionsLiteralNode.kind).toBe(ts.SyntaxKind.TypeLiteral);
240
+ expect(optionsLiteralNode.members).toHaveLength(1);
241
+
242
+ expect(optionsLiteralNode.members[0].kind).toBe(ts.SyntaxKind.PropertyDeclaration);
243
+
244
+ expect(optionsLiteralNode.members[0].name.kind).toBe(ts.SyntaxKind.Identifier);
245
+ expect(optionsLiteralNode.members[0].name.escapedText).toBe('separator');
246
+
247
+ expect(optionsLiteralNode.members[0].type.kind).toBe(ts.SyntaxKind.StringLiteral);
248
+ expect(optionsLiteralNode.members[0].type.text).toBe('_');
249
+ });
250
+ });
251
+
252
+ describe('Relation', () => {
253
+ test('Basic relation', () => {
254
+ const attribute = { type: 'relation', relation: 'oneToOne', target: 'api::bar.bar' };
255
+ const typeNode = getAttributeType('foo', attribute, 'api::foo.foo');
256
+
257
+ defaultAssertions(typeNode, 'RelationAttribute');
258
+
259
+ expect(typeNode.typeArguments).toHaveLength(3);
260
+
261
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
262
+ expect(typeNode.typeArguments[0].text).toBe('api::foo.foo');
263
+
264
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.StringLiteral);
265
+ expect(typeNode.typeArguments[1].text).toBe('oneToOne');
266
+
267
+ expect(typeNode.typeArguments[2].kind).toBe(ts.SyntaxKind.StringLiteral);
268
+ expect(typeNode.typeArguments[2].text).toBe('api::bar.bar');
269
+ });
270
+
271
+ test('Polymorphic relation', () => {
272
+ const attribute = { type: 'relation', relation: 'morphMany' };
273
+ const typeNode = getAttributeType('foo', attribute, 'api::foo.foo');
274
+
275
+ defaultAssertions(typeNode, 'RelationAttribute');
276
+
277
+ expect(typeNode.typeArguments).toHaveLength(2);
278
+
279
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
280
+ expect(typeNode.typeArguments[0].text).toBe('api::foo.foo');
281
+
282
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.StringLiteral);
283
+ expect(typeNode.typeArguments[1].text).toBe('morphMany');
284
+ });
285
+ });
286
+
287
+ describe('Component', () => {
288
+ test('Repeatable component', () => {
289
+ const attribute = { type: 'component', component: 'default.comp', repeatable: true };
290
+ const typeNode = getAttributeType('foo', attribute);
291
+
292
+ defaultAssertions(typeNode, 'ComponentAttribute');
293
+
294
+ expect(typeNode.typeArguments).toHaveLength(2);
295
+
296
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
297
+ expect(typeNode.typeArguments[0].text).toBe('default.comp');
298
+
299
+ expect(typeNode.typeArguments[1].kind).toBe(ts.SyntaxKind.TrueKeyword);
300
+ });
301
+
302
+ test('Non repeatable component', () => {
303
+ const attribute = { type: 'component', component: 'default.comp' };
304
+ const typeNode = getAttributeType('foo', attribute);
305
+
306
+ defaultAssertions(typeNode, 'ComponentAttribute');
307
+
308
+ expect(typeNode.typeArguments).toHaveLength(1);
309
+
310
+ expect(typeNode.typeArguments[0].kind).toBe(ts.SyntaxKind.StringLiteral);
311
+ expect(typeNode.typeArguments[0].text).toBe('default.comp');
312
+ });
313
+ });
314
+
315
+ describe('Dynamic Zone', () => {
316
+ test('Dynamic Zone with an array of components (targets)', () => {
317
+ const attribute = { type: 'dynamiczone', components: ['default.comp1', 'default.comp2'] };
318
+ const typeNode = getAttributeType('foo', attribute);
319
+
320
+ defaultAssertions(typeNode, 'DynamicZoneAttribute');
321
+
322
+ expect(typeNode.typeArguments).toHaveLength(1);
323
+
324
+ const [typeArgument] = typeNode.typeArguments;
325
+
326
+ expect(typeArgument.kind).toBe(ts.SyntaxKind.TupleType);
327
+ expect(typeArgument.elements).toHaveLength(2);
328
+
329
+ expect(typeArgument.elements[0].kind).toBe(ts.SyntaxKind.StringLiteral);
330
+ expect(typeArgument.elements[0].text).toBe('default.comp1');
331
+
332
+ expect(typeArgument.elements[1].kind).toBe(ts.SyntaxKind.StringLiteral);
333
+ expect(typeArgument.elements[1].text).toBe('default.comp2');
74
334
  });
75
335
  });
76
336
  });
77
337
  });
78
338
 
79
- // TODO
80
- // describe('Get Attribute Modifiers', () => {});
339
+ describe('Get Attribute Modifiers', () => {
340
+ describe('Units', () => {
341
+ describe('Required', () => {
342
+ test('No required', () => {
343
+ const attribute = {};
344
+ const modifiers = getAttributeModifiers(attribute);
345
+
346
+ expect(modifiers).toHaveLength(0);
347
+ });
348
+
349
+ test('Required: false', () => {
350
+ const attribute = { required: false };
351
+ const modifiers = getAttributeModifiers(attribute);
352
+
353
+ expect(modifiers).toHaveLength(0);
354
+ });
355
+
356
+ test('Required: true', () => {
357
+ const attribute = { required: true };
358
+ const modifiers = getAttributeModifiers(attribute);
359
+
360
+ expect(modifiers).toHaveLength(1);
361
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
362
+ expect(modifiers[0].typeName.escapedText).toBe('RequiredAttribute');
363
+ });
364
+ });
365
+
366
+ describe('Private', () => {
367
+ test('No private', () => {
368
+ const attribute = {};
369
+ const modifiers = getAttributeModifiers(attribute);
370
+
371
+ expect(modifiers).toHaveLength(0);
372
+ });
373
+
374
+ test('Private: false', () => {
375
+ const attribute = { private: false };
376
+ const modifiers = getAttributeModifiers(attribute);
377
+
378
+ expect(modifiers).toHaveLength(0);
379
+ });
380
+
381
+ test('Private: true', () => {
382
+ const attribute = { private: true };
383
+ const modifiers = getAttributeModifiers(attribute);
384
+
385
+ expect(modifiers).toHaveLength(1);
386
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
387
+ expect(modifiers[0].typeName.escapedText).toBe('PrivateAttribute');
388
+ });
389
+ });
390
+
391
+ describe('Unique', () => {
392
+ test('No unique', () => {
393
+ const attribute = {};
394
+ const modifiers = getAttributeModifiers(attribute);
395
+
396
+ expect(modifiers).toHaveLength(0);
397
+ });
398
+
399
+ test('Unique: false', () => {
400
+ const attribute = { unique: false };
401
+ const modifiers = getAttributeModifiers(attribute);
402
+
403
+ expect(modifiers).toHaveLength(0);
404
+ });
405
+
406
+ test('Unique: true', () => {
407
+ const attribute = { unique: true };
408
+ const modifiers = getAttributeModifiers(attribute);
409
+
410
+ expect(modifiers).toHaveLength(1);
411
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
412
+ expect(modifiers[0].typeName.escapedText).toBe('UniqueAttribute');
413
+ });
414
+ });
415
+
416
+ describe('Configurable', () => {
417
+ test('No configurable', () => {
418
+ const attribute = {};
419
+ const modifiers = getAttributeModifiers(attribute);
420
+
421
+ expect(modifiers).toHaveLength(0);
422
+ });
423
+
424
+ test('Configurable: false', () => {
425
+ const attribute = { configurable: false };
426
+ const modifiers = getAttributeModifiers(attribute);
427
+
428
+ expect(modifiers).toHaveLength(0);
429
+ });
430
+
431
+ test('Configurable: true', () => {
432
+ const attribute = { configurable: true };
433
+ const modifiers = getAttributeModifiers(attribute);
434
+
435
+ expect(modifiers).toHaveLength(1);
436
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
437
+ expect(modifiers[0].typeName.escapedText).toBe('ConfigurableAttribute');
438
+ });
439
+ });
440
+
441
+ describe('Plugin Options', () => {
442
+ test('No plugin options', () => {
443
+ const attribute = {};
444
+ const modifiers = getAttributeModifiers(attribute);
445
+
446
+ expect(modifiers).toHaveLength(0);
447
+ });
448
+
449
+ test('Plugin Options: { foo: { enabled: true } }', () => {
450
+ const attribute = { pluginOptions: { foo: { enabled: true } } };
451
+ const modifiers = getAttributeModifiers(attribute);
452
+
453
+ expect(modifiers).toHaveLength(1);
454
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
455
+ expect(modifiers[0].typeName.escapedText).toBe('SetPluginOptions');
456
+ expect(modifiers[0].typeArguments).toHaveLength(1);
457
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
458
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
459
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
460
+ ts.SyntaxKind.PropertyDeclaration
461
+ );
462
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('foo');
463
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
464
+ ts.SyntaxKind.TypeLiteral
465
+ );
466
+ expect(modifiers[0].typeArguments[0].members[0].type.members).toHaveLength(1);
467
+ expect(modifiers[0].typeArguments[0].members[0].type.members[0].kind).toBe(
468
+ ts.SyntaxKind.PropertyDeclaration
469
+ );
470
+ expect(modifiers[0].typeArguments[0].members[0].type.members[0].name.escapedText).toBe(
471
+ 'enabled'
472
+ );
473
+ expect(modifiers[0].typeArguments[0].members[0].type.members[0].type.kind).toBe(
474
+ ts.SyntaxKind.TrueKeyword
475
+ );
476
+ });
477
+ });
478
+
479
+ describe('Min / Max', () => {
480
+ test('No min or max', () => {
481
+ const attribute = {};
482
+ const modifiers = getAttributeModifiers(attribute);
483
+
484
+ expect(modifiers).toHaveLength(0);
485
+ });
486
+
487
+ test('Min: 2, no Max', () => {
488
+ const attribute = { min: 2 };
489
+ const modifiers = getAttributeModifiers(attribute);
490
+
491
+ expect(modifiers).toHaveLength(1);
492
+
493
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
494
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMax');
495
+
496
+ expect(modifiers[0].typeArguments).toHaveLength(1);
497
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
498
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
499
+
500
+ // Min
501
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
502
+ ts.SyntaxKind.PropertyDeclaration
503
+ );
504
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('min');
505
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
506
+ ts.SyntaxKind.NumericLiteral
507
+ );
508
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('2');
509
+ });
510
+
511
+ test('No Min, Max: 3', () => {
512
+ const attribute = { max: 3 };
513
+ const modifiers = getAttributeModifiers(attribute);
514
+
515
+ expect(modifiers).toHaveLength(1);
516
+
517
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
518
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMax');
519
+
520
+ expect(modifiers[0].typeArguments).toHaveLength(1);
521
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
522
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
523
+
524
+ // Min
525
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
526
+ ts.SyntaxKind.PropertyDeclaration
527
+ );
528
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('max');
529
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
530
+ ts.SyntaxKind.NumericLiteral
531
+ );
532
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('3');
533
+ });
534
+
535
+ test('Min: 4, Max: 12', () => {
536
+ const attribute = { min: 4, max: 12 };
537
+ const modifiers = getAttributeModifiers(attribute);
538
+
539
+ expect(modifiers).toHaveLength(1);
540
+
541
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
542
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMax');
543
+
544
+ expect(modifiers[0].typeArguments).toHaveLength(1);
545
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
546
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(2);
547
+
548
+ // Min
549
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
550
+ ts.SyntaxKind.PropertyDeclaration
551
+ );
552
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('min');
553
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
554
+ ts.SyntaxKind.NumericLiteral
555
+ );
556
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('4');
557
+
558
+ expect(modifiers[0].typeArguments[0].members[1].kind).toBe(
559
+ ts.SyntaxKind.PropertyDeclaration
560
+ );
561
+ expect(modifiers[0].typeArguments[0].members[1].name.escapedText).toBe('max');
562
+ expect(modifiers[0].typeArguments[0].members[1].type.kind).toBe(
563
+ ts.SyntaxKind.NumericLiteral
564
+ );
565
+ expect(modifiers[0].typeArguments[0].members[1].type.text).toBe('12');
566
+ });
567
+ });
568
+
569
+ describe('MinLength / MaxLength', () => {
570
+ test('No minLength or maxLength', () => {
571
+ const attribute = {};
572
+ const modifiers = getAttributeModifiers(attribute);
573
+
574
+ expect(modifiers).toHaveLength(0);
575
+ });
576
+
577
+ test('MinLength: 2, no MaxLength', () => {
578
+ const attribute = { minLength: 2 };
579
+ const modifiers = getAttributeModifiers(attribute);
580
+
581
+ expect(modifiers).toHaveLength(1);
582
+
583
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
584
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMaxLength');
585
+
586
+ expect(modifiers[0].typeArguments).toHaveLength(1);
587
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
588
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
589
+
590
+ // Min
591
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
592
+ ts.SyntaxKind.PropertyDeclaration
593
+ );
594
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('minLength');
595
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
596
+ ts.SyntaxKind.NumericLiteral
597
+ );
598
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('2');
599
+ });
600
+
601
+ test('No MinLength, MaxLength: 3', () => {
602
+ const attribute = { maxLength: 3 };
603
+ const modifiers = getAttributeModifiers(attribute);
604
+
605
+ expect(modifiers).toHaveLength(1);
606
+
607
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
608
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMaxLength');
609
+
610
+ expect(modifiers[0].typeArguments).toHaveLength(1);
611
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
612
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
613
+
614
+ // Min
615
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
616
+ ts.SyntaxKind.PropertyDeclaration
617
+ );
618
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('maxLength');
619
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
620
+ ts.SyntaxKind.NumericLiteral
621
+ );
622
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('3');
623
+ });
624
+
625
+ test('MinLength: 4, MaxLength: 12', () => {
626
+ const attribute = { minLength: 4, maxLength: 12 };
627
+ const modifiers = getAttributeModifiers(attribute);
628
+
629
+ expect(modifiers).toHaveLength(1);
630
+
631
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
632
+ expect(modifiers[0].typeName.escapedText).toBe('SetMinMaxLength');
633
+
634
+ expect(modifiers[0].typeArguments).toHaveLength(1);
635
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
636
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(2);
637
+
638
+ // Min
639
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
640
+ ts.SyntaxKind.PropertyDeclaration
641
+ );
642
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('minLength');
643
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
644
+ ts.SyntaxKind.NumericLiteral
645
+ );
646
+ expect(modifiers[0].typeArguments[0].members[0].type.text).toBe('4');
647
+
648
+ expect(modifiers[0].typeArguments[0].members[1].kind).toBe(
649
+ ts.SyntaxKind.PropertyDeclaration
650
+ );
651
+ expect(modifiers[0].typeArguments[0].members[1].name.escapedText).toBe('maxLength');
652
+ expect(modifiers[0].typeArguments[0].members[1].type.kind).toBe(
653
+ ts.SyntaxKind.NumericLiteral
654
+ );
655
+ expect(modifiers[0].typeArguments[0].members[1].type.text).toBe('12');
656
+ });
657
+ });
658
+
659
+ describe('Default', () => {
660
+ test('No default', () => {
661
+ const attribute = {};
662
+ const modifiers = getAttributeModifiers(attribute);
663
+
664
+ expect(modifiers).toHaveLength(0);
665
+ });
666
+
667
+ test('Default: true', () => {
668
+ const attribute = { default: true };
669
+ const modifiers = getAttributeModifiers(attribute);
670
+
671
+ expect(modifiers).toHaveLength(1);
672
+
673
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
674
+ expect(modifiers[0].typeName.escapedText).toBe('DefaultTo');
675
+
676
+ expect(modifiers[0].typeArguments).toHaveLength(1);
677
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TrueKeyword);
678
+ });
679
+
680
+ test('Default: { enabled: true }', () => {
681
+ const attribute = { default: { enabled: true } };
682
+ const modifiers = getAttributeModifiers(attribute);
683
+
684
+ expect(modifiers).toHaveLength(1);
685
+
686
+ expect(modifiers[0].kind).toBe(ts.SyntaxKind.TypeReference);
687
+ expect(modifiers[0].typeName.escapedText).toBe('DefaultTo');
688
+
689
+ expect(modifiers[0].typeArguments).toHaveLength(1);
690
+ expect(modifiers[0].typeArguments[0].kind).toBe(ts.SyntaxKind.TypeLiteral);
691
+ expect(modifiers[0].typeArguments[0].members).toHaveLength(1);
692
+ expect(modifiers[0].typeArguments[0].members[0].kind).toBe(
693
+ ts.SyntaxKind.PropertyDeclaration
694
+ );
695
+ expect(modifiers[0].typeArguments[0].members[0].name.escapedText).toBe('enabled');
696
+ expect(modifiers[0].typeArguments[0].members[0].type.kind).toBe(
697
+ ts.SyntaxKind.TrueKeyword
698
+ );
699
+ });
700
+ });
701
+ });
702
+ });
81
703
  });
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ jest.mock('../../../generators/schemas/utils', () => ({
4
+ getSchemaInterfaceName: jest.fn(),
5
+ }));
6
+
7
+ const ts = require('typescript');
8
+ const { get } = require('lodash/fp');
9
+
10
+ const { generateGlobalDefinition } = require('../../../generators/schemas/global');
11
+ const { getSchemaInterfaceName } = require('../../../generators/schemas/utils');
12
+
13
+ const getSchemasInterfaceNode = get('body.statements[0].body.statements[0]');
14
+
15
+ describe('Global', () => {
16
+ afterAll(() => {
17
+ jest.resetAllMocks();
18
+ });
19
+
20
+ const assertGlobalNodeStructure = node => {
21
+ // "declare global"
22
+ expect(node.kind).toBe(ts.SyntaxKind.ModuleDeclaration);
23
+ expect(node.modifiers).toHaveLength(1);
24
+ expect(node.modifiers[0].kind).toBe(ts.SyntaxKind.DeclareKeyword);
25
+ expect(node.name.originalKeywordKind).toBe(ts.SyntaxKind.GlobalKeyword);
26
+ expect(node.name.escapedText).toBe('global');
27
+
28
+ // "namespace Strapi"
29
+ const [strapiNamespace] = node.body.statements;
30
+
31
+ expect(strapiNamespace.kind).toBe(ts.SyntaxKind.ModuleDeclaration);
32
+ expect(strapiNamespace.name.kind).toBe(ts.SyntaxKind.Identifier);
33
+ expect(strapiNamespace.name.escapedText).toBe('Strapi');
34
+
35
+ // "interface Schemas"
36
+ const [schemasInterface] = strapiNamespace.body.statements;
37
+
38
+ expect(schemasInterface.kind).toBe(ts.SyntaxKind.InterfaceDeclaration);
39
+ expect(schemasInterface.name.escapedText).toBe('Schemas');
40
+ };
41
+
42
+ describe('Generate Global Definition', () => {
43
+ beforeEach(() => {
44
+ jest.resetAllMocks();
45
+ });
46
+
47
+ test('With empty definition', () => {
48
+ const definitions = [];
49
+
50
+ const globalNode = generateGlobalDefinition(definitions);
51
+
52
+ assertGlobalNodeStructure(globalNode);
53
+
54
+ expect(getSchemaInterfaceName).not.toHaveBeenCalled();
55
+
56
+ const schemasNode = getSchemasInterfaceNode(globalNode);
57
+
58
+ expect(schemasNode.members).toHaveLength(0);
59
+ });
60
+
61
+ test('With no definition', () => {
62
+ const globalNode = generateGlobalDefinition();
63
+
64
+ assertGlobalNodeStructure(globalNode);
65
+
66
+ expect(getSchemaInterfaceName).not.toHaveBeenCalled();
67
+
68
+ const schemasNode = getSchemasInterfaceNode(globalNode);
69
+
70
+ expect(schemasNode.members).toHaveLength(0);
71
+ });
72
+
73
+ test('With multiple definitions', () => {
74
+ const definitions = [
75
+ { schema: { uid: 'api::foo.foo' } },
76
+ { schema: { uid: 'api::bar.bar' } },
77
+ { schema: { uid: 'api::foobar.foobar' } },
78
+ { schema: { uid: 'default.barfoo' } },
79
+ ];
80
+
81
+ getSchemaInterfaceName.mockReturnValue('Placeholder');
82
+
83
+ const globalNode = generateGlobalDefinition(definitions);
84
+
85
+ assertGlobalNodeStructure(globalNode);
86
+
87
+ const schemasNode = getSchemasInterfaceNode(globalNode);
88
+
89
+ expect(schemasNode.members).toHaveLength(definitions.length);
90
+
91
+ definitions.forEach(({ schema }, index) => {
92
+ const { uid } = schema;
93
+ const node = schemasNode.members[index];
94
+
95
+ expect(node.kind).toBe(ts.SyntaxKind.PropertySignature);
96
+
97
+ expect(getSchemaInterfaceName).toHaveBeenCalledWith(uid);
98
+
99
+ expect(node.name.kind).toBe(ts.SyntaxKind.StringLiteral);
100
+ expect(node.name.text).toBe(uid);
101
+ expect(node.name.singleQuote).toBeTruthy();
102
+
103
+ expect(node.type.kind).toBe(ts.SyntaxKind.TypeReference);
104
+ expect(node.type.typeName.escapedText).toBe('Placeholder');
105
+ });
106
+ });
107
+ });
108
+ });
@@ -22,7 +22,7 @@ const attributeToPropertySignature = (schema, attributeName, attribute) => {
22
22
  return null;
23
23
  }
24
24
 
25
- const modifiers = getAttributeModifiers(attributeName, attribute);
25
+ const modifiers = getAttributeModifiers(attribute);
26
26
 
27
27
  const nodes = [baseType, ...modifiers];
28
28
 
@@ -61,11 +61,10 @@ const getAttributeType = (attributeName, attribute, uid) => {
61
61
  /**
62
62
  * Collect every modifier node from an attribute
63
63
  *
64
- * @param {string} _attributeName
65
64
  * @param {object} attribute
66
65
  * @returns {object[]}
67
66
  */
68
- const getAttributeModifiers = (_attributeName, attribute) => {
67
+ const getAttributeModifiers = attribute => {
69
68
  const modifiers = [];
70
69
 
71
70
  // Required
@@ -112,6 +111,7 @@ const getAttributeModifiers = (_attributeName, attribute) => {
112
111
  }
113
112
 
114
113
  // Min / Max
114
+ // TODO: Always provide a second type argument for min/max (ie: resolve the attribute scalar type with a `GetAttributeType<${mappers[attribute][0]}>` (useful for biginter (string values)))
115
115
  if (!_.isNil(attribute.min) || !_.isNil(attribute.max)) {
116
116
  addImport('SetMinMax');
117
117
 
@@ -223,11 +223,7 @@ const mappers = {
223
223
  enumeration({ attribute }) {
224
224
  const { enum: enumValues } = attribute;
225
225
 
226
- if (Array.isArray(enumValues)) {
227
- return ['EnumerationAttribute', [toTypeLiteral(enumValues)]];
228
- }
229
-
230
- return ['EnumerationAttribute'];
226
+ return ['EnumerationAttribute', [toTypeLiteral(enumValues)]];
231
227
  },
232
228
  boolean() {
233
229
  return ['BooleanAttribute'];
@@ -34,7 +34,13 @@ const DEFAULT_OUT_FILENAME = 'schemas.d.ts';
34
34
  * @param {boolean} [options.verbose]
35
35
  */
36
36
  const generateSchemasDefinitions = async (options = {}) => {
37
- const { strapi, outDir = process.cwd(), file = DEFAULT_OUT_FILENAME, verbose = false } = options;
37
+ const {
38
+ strapi,
39
+ outDir = process.cwd(),
40
+ file = DEFAULT_OUT_FILENAME,
41
+ verbose = false,
42
+ silent = false,
43
+ } = options;
38
44
 
39
45
  const schemas = getAllStrapiSchemas(strapi);
40
46
 
@@ -74,9 +80,7 @@ const generateSchemasDefinitions = async (options = {}) => {
74
80
 
75
81
  const definitionFilepath = await saveDefinitionToFileSystem(outDir, file, formattedOutput);
76
82
 
77
- if (verbose) {
78
- logDebugInformation(schemasDefinitions, { filepath: definitionFilepath });
79
- }
83
+ logDebugInformation(schemasDefinitions, { filepath: definitionFilepath, verbose, silent });
80
84
  };
81
85
 
82
86
  const emitDefinitions = definitions => {
@@ -108,7 +112,7 @@ const saveDefinitionToFileSystem = async (dir, file, content) => {
108
112
  * Uses the existing config if one is defined in the project.
109
113
  *
110
114
  * @param {string} content
111
- * @returns {string}
115
+ * @returns {Promise<string>}
112
116
  */
113
117
  const format = async content => {
114
118
  const configFile = await prettier.resolveConfigFile();
@@ -127,51 +131,55 @@ const format = async content => {
127
131
  };
128
132
 
129
133
  const logDebugInformation = (definitions, options = {}) => {
130
- const { filepath } = options;
131
-
132
- const table = new CLITable({
133
- head: [
134
- chalk.bold(chalk.green('Model Type')),
135
- chalk.bold(chalk.blue('UID')),
136
- chalk.bold(chalk.blue('Type')),
137
- chalk.bold(chalk.gray('Attributes Count')),
138
- ],
139
- colAligns: ['center', 'left', 'left', 'center'],
140
- });
141
-
142
- const sortedDefinitions = definitions.map(def => ({
143
- ...def,
144
- attributesCount: getDefinitionAttributesCount(def.definition),
145
- }));
134
+ const { filepath, verbose, silent } = options;
146
135
 
147
- for (const { schema, attributesCount } of sortedDefinitions) {
148
- const modelType = fp.upperFirst(getSchemaModelType(schema));
149
- const interfaceType = getSchemaInterfaceName(schema.uid);
150
-
151
- table.push([
152
- chalk.greenBright(modelType),
153
- chalk.blue(schema.uid),
154
- chalk.blue(interfaceType),
155
- chalk.grey(fp.isNil(attributesCount) ? 'N/A' : attributesCount),
156
- ]);
136
+ if (verbose) {
137
+ const table = new CLITable({
138
+ head: [
139
+ chalk.bold(chalk.green('Model Type')),
140
+ chalk.bold(chalk.blue('UID')),
141
+ chalk.bold(chalk.blue('Type')),
142
+ chalk.bold(chalk.gray('Attributes Count')),
143
+ ],
144
+ colAligns: ['center', 'left', 'left', 'center'],
145
+ });
146
+
147
+ const sortedDefinitions = definitions.map(def => ({
148
+ ...def,
149
+ attributesCount: getDefinitionAttributesCount(def.definition),
150
+ }));
151
+
152
+ for (const { schema, attributesCount } of sortedDefinitions) {
153
+ const modelType = fp.upperFirst(getSchemaModelType(schema));
154
+ const interfaceType = getSchemaInterfaceName(schema.uid);
155
+
156
+ table.push([
157
+ chalk.greenBright(modelType),
158
+ chalk.blue(schema.uid),
159
+ chalk.blue(interfaceType),
160
+ chalk.grey(fp.isNil(attributesCount) ? 'N/A' : attributesCount),
161
+ ]);
162
+ }
163
+
164
+ // Table
165
+ console.log(table.toString());
157
166
  }
158
167
 
159
- // Table
160
- console.log(table.toString());
161
-
162
- // Metrics
163
- console.log(
164
- chalk.greenBright(
165
- `Generated ${definitions.length} type definition for your Strapi application's schemas.`
166
- )
167
- );
168
+ if (!silent) {
169
+ // Metrics
170
+ console.log(
171
+ chalk.greenBright(
172
+ `Generated ${definitions.length} type definition for your Strapi application's schemas.`
173
+ )
174
+ );
168
175
 
169
- // Filepath
170
- const relativePath = path.relative(process.cwd(), filepath);
176
+ // Filepath
177
+ const relativePath = path.relative(process.cwd(), filepath);
171
178
 
172
- console.log(
173
- chalk.grey(`The definitions file has been generated here: ${chalk.bold(relativePath)}`)
174
- );
179
+ console.log(
180
+ chalk.grey(`The definitions file has been generated here: ${chalk.bold(relativePath)}`)
181
+ );
182
+ }
175
183
  };
176
184
 
177
185
  module.exports = generateSchemasDefinitions;
@@ -11,6 +11,7 @@ const {
11
11
  isNull,
12
12
  isString,
13
13
  isNumber,
14
+ isDate,
14
15
  isArray,
15
16
  isBoolean,
16
17
  propEq,
@@ -101,6 +102,10 @@ const toTypeLiteral = data => {
101
102
  return factory.createTupleTypeNode(data.map(item => toTypeLiteral(item)));
102
103
  }
103
104
 
105
+ if (isDate(data)) {
106
+ return factory.createStringLiteral(data.toISOString());
107
+ }
108
+
104
109
  if (typeof data !== 'object') {
105
110
  throw new Error(`Cannot convert to object literal. Unknown type "${typeof data}"`);
106
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/typescript-utils",
3
- "version": "4.3.0-beta.2",
3
+ "version": "4.3.2-alpha.0",
4
4
  "description": "Typescript support for Strapi",
5
5
  "keywords": [
6
6
  "strapi",
@@ -35,5 +35,5 @@
35
35
  "node": ">=12.22.0 <=16.x.x",
36
36
  "npm": ">=6.0.0"
37
37
  },
38
- "gitHead": "42aba356ad1b0751584d3b375e83baa4a2c18f65"
38
+ "gitHead": "8ab2db1497c3a5a805173b2d92248648b40a6f65"
39
39
  }