@magnet-cms/plugin-playground 2.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.
@@ -0,0 +1,1023 @@
1
+ 'use strict';
2
+
3
+ var common = require('@nestjs/common');
4
+ var core = require('@magnet-cms/core');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var common$1 = require('@magnet-cms/common');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return Object.freeze(n);
25
+ }
26
+
27
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
29
+
30
+ var __defProp = Object.defineProperty;
31
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
32
+ function _ts_decorate(decorators, target, key, desc) {
33
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
34
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
35
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
36
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
37
+ }
38
+ __name(_ts_decorate, "_ts_decorate");
39
+ function _ts_metadata(k, v) {
40
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
41
+ }
42
+ __name(_ts_metadata, "_ts_metadata");
43
+ function _ts_param(paramIndex, decorator) {
44
+ return function(target, key) {
45
+ decorator(target, key, paramIndex);
46
+ };
47
+ }
48
+ __name(_ts_param, "_ts_param");
49
+ var PlaygroundService = class _PlaygroundService {
50
+ static {
51
+ __name(this, "PlaygroundService");
52
+ }
53
+ magnetOptions;
54
+ pluginOptions;
55
+ logger;
56
+ constructor(magnetOptions, pluginOptions) {
57
+ this.magnetOptions = magnetOptions;
58
+ this.pluginOptions = pluginOptions;
59
+ this.logger = new common.Logger(_PlaygroundService.name);
60
+ }
61
+ /**
62
+ * Get the modules directory path from options or use default.
63
+ * When MAGNET_PLAYGROUND_MODULES_PATH is set (e.g. for e2e), use it so
64
+ * generated modules are not written into the app's src/modules.
65
+ */
66
+ getModulesDir() {
67
+ const envPath = process.env.MAGNET_PLAYGROUND_MODULES_PATH;
68
+ if (envPath?.trim()) {
69
+ return path__namespace.resolve(envPath.trim());
70
+ }
71
+ if (this.pluginOptions?.modulesPath) {
72
+ return this.pluginOptions.modulesPath;
73
+ }
74
+ const opts = this.magnetOptions;
75
+ return opts?.playground?.modulesPath || opts?.playground?.schemasPath || path__namespace.join(process.cwd(), "src", "modules");
76
+ }
77
+ /**
78
+ * List all schemas by scanning module directories
79
+ */
80
+ async listSchemas() {
81
+ const modulesDir = this.getModulesDir();
82
+ if (!fs__namespace.existsSync(modulesDir)) {
83
+ return [];
84
+ }
85
+ const schemas = [];
86
+ const entries = fs__namespace.readdirSync(modulesDir, {
87
+ withFileTypes: true
88
+ });
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory()) continue;
91
+ const schemaFile = path__namespace.join(modulesDir, entry.name, `${entry.name}.schema.ts`);
92
+ const schemaFileAlt = path__namespace.join(modulesDir, entry.name, "schemas", `${entry.name}.schema.ts`);
93
+ const filePath = fs__namespace.existsSync(schemaFile) ? schemaFile : fs__namespace.existsSync(schemaFileAlt) ? schemaFileAlt : null;
94
+ if (!filePath) continue;
95
+ const content = fs__namespace.readFileSync(filePath, "utf-8");
96
+ const parsed = this.parseSchemaFile(content);
97
+ if (parsed) {
98
+ const stats = fs__namespace.statSync(filePath);
99
+ schemas.push({
100
+ name: parsed.name,
101
+ apiId: parsed.name.toLowerCase(),
102
+ fieldCount: parsed.fields.length,
103
+ hasVersioning: parsed.options?.versioning ?? true,
104
+ hasI18n: parsed.options?.i18n ?? true,
105
+ createdAt: stats.birthtime,
106
+ updatedAt: stats.mtime
107
+ });
108
+ }
109
+ }
110
+ return schemas;
111
+ }
112
+ /**
113
+ * Get a schema by name
114
+ */
115
+ async getSchema(name) {
116
+ const modulesDir = this.getModulesDir();
117
+ const lowerName = name.toLowerCase();
118
+ const schemaFile = path__namespace.join(modulesDir, lowerName, `${lowerName}.schema.ts`);
119
+ const schemaFileAlt = path__namespace.join(modulesDir, lowerName, "schemas", `${lowerName}.schema.ts`);
120
+ const filePath = fs__namespace.existsSync(schemaFile) ? schemaFile : fs__namespace.existsSync(schemaFileAlt) ? schemaFileAlt : null;
121
+ if (!filePath) {
122
+ return null;
123
+ }
124
+ const content = fs__namespace.readFileSync(filePath, "utf-8");
125
+ const parsed = this.parseSchemaFile(content);
126
+ if (!parsed) {
127
+ return null;
128
+ }
129
+ return {
130
+ name: parsed.name,
131
+ apiId: parsed.name.toLowerCase(),
132
+ options: parsed.options || {
133
+ versioning: true,
134
+ i18n: true
135
+ },
136
+ fields: parsed.fields,
137
+ generatedCode: content
138
+ };
139
+ }
140
+ /**
141
+ * Check if a schema/module already exists
142
+ */
143
+ schemaExists(name) {
144
+ const modulesDir = this.getModulesDir();
145
+ const lowerName = name.toLowerCase();
146
+ const moduleDir = path__namespace.join(modulesDir, lowerName);
147
+ return fs__namespace.existsSync(moduleDir);
148
+ }
149
+ /**
150
+ * Create a new module with all files (schema, controller, service, module, dto)
151
+ */
152
+ async createModule(dto) {
153
+ const modulesDir = this.getModulesDir();
154
+ const lowerName = dto.name.toLowerCase();
155
+ const moduleDir = path__namespace.join(modulesDir, lowerName);
156
+ const dtoDir = path__namespace.join(moduleDir, "dto");
157
+ fs__namespace.mkdirSync(dtoDir, {
158
+ recursive: true
159
+ });
160
+ const createdFiles = [];
161
+ const schemaCode = this.generateSchemaCode(dto);
162
+ const files = [
163
+ {
164
+ name: `${lowerName}.schema.ts`,
165
+ content: schemaCode
166
+ },
167
+ {
168
+ name: `${lowerName}.module.ts`,
169
+ content: this.generateModuleCode(dto.name)
170
+ },
171
+ {
172
+ name: `${lowerName}.controller.ts`,
173
+ content: this.generateControllerCode(dto.name)
174
+ },
175
+ {
176
+ name: `${lowerName}.service.ts`,
177
+ content: this.generateServiceCode(dto.name)
178
+ },
179
+ {
180
+ name: `dto/create-${lowerName}.dto.ts`,
181
+ content: this.generateDtoCode(dto.name, dto.fields)
182
+ }
183
+ ];
184
+ for (const file of files) {
185
+ const filePath = path__namespace.join(moduleDir, file.name);
186
+ fs__namespace.writeFileSync(filePath, file.content, "utf-8");
187
+ createdFiles.push(filePath);
188
+ this.logger.log(`Created: ${filePath}`);
189
+ }
190
+ return {
191
+ name: dto.name,
192
+ apiId: lowerName,
193
+ options: dto.options || {
194
+ versioning: true,
195
+ i18n: true
196
+ },
197
+ fields: dto.fields,
198
+ generatedCode: schemaCode,
199
+ createdFiles,
200
+ message: `Module "${dto.name}" created successfully. Import ${dto.name}Module in your app.module.ts to use it.`
201
+ };
202
+ }
203
+ /**
204
+ * Update only the schema file for an existing module
205
+ */
206
+ async updateSchema(name, dto) {
207
+ const modulesDir = this.getModulesDir();
208
+ const lowerName = name.toLowerCase();
209
+ const schemaFile = path__namespace.join(modulesDir, lowerName, `${lowerName}.schema.ts`);
210
+ const schemaFileAlt = path__namespace.join(modulesDir, lowerName, "schemas", `${lowerName}.schema.ts`);
211
+ const filePath = fs__namespace.existsSync(schemaFile) ? schemaFile : fs__namespace.existsSync(schemaFileAlt) ? schemaFileAlt : null;
212
+ if (!filePath) {
213
+ throw new Error(`Schema "${name}" not found`);
214
+ }
215
+ const existingContent = fs__namespace.readFileSync(filePath, "utf-8");
216
+ const existingParsed = this.parseSchemaFile(existingContent);
217
+ const conflicts = existingParsed ? this.detectConflicts(existingParsed.fields, dto.fields) : [];
218
+ const newCode = this.generateSchemaCode(dto);
219
+ fs__namespace.writeFileSync(filePath, newCode, "utf-8");
220
+ this.logger.log(`Schema updated: ${filePath}`);
221
+ return {
222
+ detail: {
223
+ name: dto.name,
224
+ apiId: dto.name.toLowerCase(),
225
+ options: dto.options || {
226
+ versioning: true,
227
+ i18n: true
228
+ },
229
+ fields: dto.fields,
230
+ generatedCode: newCode
231
+ },
232
+ conflicts
233
+ };
234
+ }
235
+ /**
236
+ * Detect conflicts between existing and updated schema fields
237
+ */
238
+ detectConflicts(existing, updated) {
239
+ const conflicts = [];
240
+ for (const updatedField of updated) {
241
+ const existingField = existing.find((f) => f.name === updatedField.name);
242
+ if (existingField) {
243
+ if (existingField.tsType !== updatedField.tsType) {
244
+ conflicts.push({
245
+ fieldName: updatedField.name,
246
+ type: "type_change",
247
+ message: `Type changed from "${existingField.tsType}" to "${updatedField.tsType}". Update your DTO accordingly.`,
248
+ oldValue: existingField.tsType,
249
+ newValue: updatedField.tsType
250
+ });
251
+ }
252
+ if (existingField.prop.required && !updatedField.prop.required) {
253
+ conflicts.push({
254
+ fieldName: updatedField.name,
255
+ type: "required_change",
256
+ message: "Field changed from required to optional. Consider updating your DTO.",
257
+ oldValue: "required",
258
+ newValue: "optional"
259
+ });
260
+ }
261
+ }
262
+ }
263
+ for (const existingField of existing) {
264
+ const stillExists = updated.find((f) => f.name === existingField.name);
265
+ if (!stillExists) {
266
+ conflicts.push({
267
+ fieldName: existingField.name,
268
+ type: "field_removed",
269
+ message: `Field "${existingField.name}" was removed. Update your DTO and service accordingly.`,
270
+ oldValue: existingField.name,
271
+ newValue: void 0
272
+ });
273
+ }
274
+ }
275
+ return conflicts;
276
+ }
277
+ /**
278
+ * Delete a schema and its entire module directory (controller, service, dto, etc.)
279
+ */
280
+ async deleteSchema(name) {
281
+ const modulesDir = this.getModulesDir();
282
+ const lowerName = name.toLowerCase();
283
+ const moduleDir = path__namespace.join(modulesDir, lowerName);
284
+ const schemaFile = path__namespace.join(moduleDir, `${lowerName}.schema.ts`);
285
+ const schemaFileAlt = path__namespace.join(moduleDir, "schemas", `${lowerName}.schema.ts`);
286
+ const filePath = fs__namespace.existsSync(schemaFile) ? schemaFile : fs__namespace.existsSync(schemaFileAlt) ? schemaFileAlt : null;
287
+ if (!filePath) {
288
+ return false;
289
+ }
290
+ if (fs__namespace.existsSync(moduleDir)) {
291
+ fs__namespace.rmSync(moduleDir, {
292
+ recursive: true
293
+ });
294
+ this.logger.log(`Module deleted: ${moduleDir}`);
295
+ }
296
+ return true;
297
+ }
298
+ /**
299
+ * Generate code preview without saving
300
+ */
301
+ previewCode(dto) {
302
+ const code = this.generateSchemaCode(dto);
303
+ const json = this.generateSchemaJSON(dto);
304
+ return {
305
+ code,
306
+ json
307
+ };
308
+ }
309
+ // ============================================================================
310
+ // Code Generation Methods
311
+ // ============================================================================
312
+ /**
313
+ * Generate module.ts code
314
+ */
315
+ generateModuleCode(name) {
316
+ const lowerName = name.toLowerCase();
317
+ return `import { MagnetModule } from '@magnet-cms/core'
318
+ import { Module } from '@nestjs/common'
319
+ import { ${name}Controller } from './${lowerName}.controller'
320
+ import { ${name}Service } from './${lowerName}.service'
321
+ import { ${name} } from './${lowerName}.schema'
322
+
323
+ @Module({
324
+ imports: [MagnetModule.forFeature(${name})],
325
+ controllers: [${name}Controller],
326
+ providers: [${name}Service],
327
+ })
328
+ export class ${name}Module {}
329
+ `;
330
+ }
331
+ /**
332
+ * Generate controller.ts code
333
+ */
334
+ generateControllerCode(name) {
335
+ const lowerName = name.toLowerCase();
336
+ return `import { Resolve } from '@magnet-cms/common'
337
+ import {
338
+ Body,
339
+ Controller,
340
+ Delete,
341
+ Get,
342
+ Param,
343
+ Post,
344
+ Put,
345
+ } from '@nestjs/common'
346
+ import { Create${name}Dto } from './dto/create-${lowerName}.dto'
347
+ import { ${name}Service } from './${lowerName}.service'
348
+ import { ${name} } from './${lowerName}.schema'
349
+
350
+ @Controller('${lowerName}')
351
+ export class ${name}Controller {
352
+ constructor(private readonly ${lowerName}Service: ${name}Service) {}
353
+
354
+ @Post()
355
+ @Resolve(() => ${name})
356
+ create(@Body() dto: Create${name}Dto) {
357
+ return this.${lowerName}Service.create(dto)
358
+ }
359
+
360
+ @Get()
361
+ @Resolve(() => [${name}])
362
+ findAll() {
363
+ return this.${lowerName}Service.findAll()
364
+ }
365
+
366
+ @Get(':id')
367
+ @Resolve(() => ${name})
368
+ findOne(@Param('id') id: string) {
369
+ return this.${lowerName}Service.findOne(id)
370
+ }
371
+
372
+ @Put(':id')
373
+ @Resolve(() => Boolean)
374
+ update(@Param('id') id: string, @Body() dto: Create${name}Dto) {
375
+ return this.${lowerName}Service.update(id, dto)
376
+ }
377
+
378
+ @Delete(':id')
379
+ @Resolve(() => Boolean)
380
+ remove(@Param('id') id: string) {
381
+ return this.${lowerName}Service.remove(id)
382
+ }
383
+ }
384
+ `;
385
+ }
386
+ /**
387
+ * Generate service.ts code
388
+ */
389
+ generateServiceCode(name) {
390
+ const lowerName = name.toLowerCase();
391
+ return `import { InjectModel, Model } from '@magnet-cms/common'
392
+ import { Injectable } from '@nestjs/common'
393
+ import { Create${name}Dto } from './dto/create-${lowerName}.dto'
394
+ import { ${name} } from './${lowerName}.schema'
395
+
396
+ @Injectable()
397
+ export class ${name}Service {
398
+ constructor(
399
+ @InjectModel(${name})
400
+ private model: Model<${name}>,
401
+ ) {}
402
+
403
+ create(dto: Create${name}Dto) {
404
+ return this.model.create(dto)
405
+ }
406
+
407
+ findAll() {
408
+ return this.model.find()
409
+ }
410
+
411
+ findOne(id: string) {
412
+ return this.model.findById(id)
413
+ }
414
+
415
+ update(id: string, dto: Create${name}Dto) {
416
+ return this.model.update(id, dto)
417
+ }
418
+
419
+ remove(id: string) {
420
+ return this.model.delete(id)
421
+ }
422
+ }
423
+ `;
424
+ }
425
+ /**
426
+ * Generate DTO code
427
+ */
428
+ generateDtoCode(name, fields) {
429
+ const validatorImports = /* @__PURE__ */ new Set();
430
+ for (const field of fields) {
431
+ switch (field.tsType) {
432
+ case "string":
433
+ validatorImports.add("IsString");
434
+ break;
435
+ case "number":
436
+ validatorImports.add("IsNumber");
437
+ break;
438
+ case "boolean":
439
+ validatorImports.add("IsBoolean");
440
+ break;
441
+ case "Date":
442
+ validatorImports.add("IsDate");
443
+ break;
444
+ }
445
+ if (field.prop.required) {
446
+ validatorImports.add("IsNotEmpty");
447
+ } else {
448
+ validatorImports.add("IsOptional");
449
+ }
450
+ for (const v of field.validations) {
451
+ validatorImports.add(v.type);
452
+ }
453
+ }
454
+ const imports = Array.from(validatorImports).sort().join(",\n ");
455
+ const properties = fields.map((f) => this.generateDtoProperty(f)).join("\n\n");
456
+ return `import {
457
+ ${imports},
458
+ } from 'class-validator'
459
+
460
+ export class Create${name}Dto {
461
+ ${properties}
462
+ }
463
+ `;
464
+ }
465
+ /**
466
+ * Generate a single DTO property with decorators
467
+ */
468
+ generateDtoProperty(field) {
469
+ const decorators = [];
470
+ const indent = " ";
471
+ switch (field.tsType) {
472
+ case "string":
473
+ decorators.push(`${indent}@IsString()`);
474
+ break;
475
+ case "number":
476
+ decorators.push(`${indent}@IsNumber()`);
477
+ break;
478
+ case "boolean":
479
+ decorators.push(`${indent}@IsBoolean()`);
480
+ break;
481
+ case "Date":
482
+ decorators.push(`${indent}@IsDate()`);
483
+ break;
484
+ }
485
+ if (field.prop.required) {
486
+ decorators.push(`${indent}@IsNotEmpty()`);
487
+ } else {
488
+ decorators.push(`${indent}@IsOptional()`);
489
+ }
490
+ for (const v of field.validations) {
491
+ if ([
492
+ "IsString",
493
+ "IsNumber",
494
+ "IsBoolean",
495
+ "IsDate",
496
+ "IsNotEmpty"
497
+ ].includes(v.type)) {
498
+ continue;
499
+ }
500
+ decorators.push(`${indent}@${this.formatValidator(v)}`);
501
+ }
502
+ const optional = field.prop.required ? "" : "?";
503
+ const declaration = `${indent}${field.name}${optional}: ${field.tsType}`;
504
+ return [
505
+ ...decorators,
506
+ declaration
507
+ ].join("\n");
508
+ }
509
+ /**
510
+ * Generate TypeScript schema code from DTO
511
+ */
512
+ generateSchemaCode(dto) {
513
+ if (!dto.name) {
514
+ return "// Enter a schema name to generate code";
515
+ }
516
+ const imports = this.generateImports(dto.fields);
517
+ const classDecorator = this.generateSchemaDecorator(dto.options);
518
+ const properties = dto.fields.map((field) => this.generateFieldCode(field)).join("\n\n");
519
+ return `${imports}
520
+
521
+ ${classDecorator}
522
+ export class ${dto.name} {
523
+ ${properties}
524
+ }
525
+ `;
526
+ }
527
+ /**
528
+ * Generate import statements based on used features
529
+ */
530
+ generateImports(fields) {
531
+ const magnetImports = /* @__PURE__ */ new Set([
532
+ "Schema",
533
+ "Prop",
534
+ "UI"
535
+ ]);
536
+ const validatorImports = /* @__PURE__ */ new Set();
537
+ let needsTypeTransformer = false;
538
+ for (const field of fields) {
539
+ if (field.validations.length > 0) {
540
+ magnetImports.add("Validators");
541
+ for (const v of field.validations) {
542
+ validatorImports.add(v.type);
543
+ }
544
+ }
545
+ if (field.type === "date") {
546
+ needsTypeTransformer = true;
547
+ }
548
+ }
549
+ const lines = [];
550
+ lines.push(`import { ${Array.from(magnetImports).sort().join(", ")} } from '@magnet-cms/common'`);
551
+ if (needsTypeTransformer) {
552
+ lines.push(`import { Type } from 'class-transformer'`);
553
+ }
554
+ if (validatorImports.size > 0) {
555
+ lines.push(`import {
556
+ ${Array.from(validatorImports).sort().join(",\n ")},
557
+ } from 'class-validator'`);
558
+ }
559
+ return lines.join("\n");
560
+ }
561
+ /**
562
+ * Generate @Schema() decorator
563
+ */
564
+ generateSchemaDecorator(options) {
565
+ const opts = [];
566
+ if (options?.versioning !== void 0) {
567
+ opts.push(`versioning: ${options.versioning}`);
568
+ }
569
+ if (options?.i18n !== void 0) {
570
+ opts.push(`i18n: ${options.i18n}`);
571
+ }
572
+ if (opts.length === 0) {
573
+ return "@Schema()";
574
+ }
575
+ return `@Schema({ ${opts.join(", ")} })`;
576
+ }
577
+ /**
578
+ * Generate code for a single field
579
+ */
580
+ generateFieldCode(field) {
581
+ const decorators = [];
582
+ const indent = " ";
583
+ if (field.type === "date") {
584
+ decorators.push(`${indent}@Type(() => Date)`);
585
+ }
586
+ decorators.push(`${indent}@Prop(${this.generatePropOptions(field)})`);
587
+ if (field.validations.length > 0) {
588
+ const validators = field.validations.map((v) => this.formatValidator(v)).join(", ");
589
+ decorators.push(`${indent}@Validators(${validators})`);
590
+ }
591
+ decorators.push(`${indent}@UI(${this.generateUIOptions(field)})`);
592
+ const declaration = `${indent}${field.name}: ${field.tsType}`;
593
+ return [
594
+ ...decorators,
595
+ declaration
596
+ ].join("\n");
597
+ }
598
+ /**
599
+ * Generate @Prop() options object
600
+ */
601
+ generatePropOptions(field) {
602
+ const options = [];
603
+ if (field.prop.required) {
604
+ options.push("required: true");
605
+ }
606
+ if (field.prop.unique) {
607
+ options.push("unique: true");
608
+ }
609
+ if (field.prop.intl) {
610
+ options.push("intl: true");
611
+ }
612
+ if (field.prop.hidden) {
613
+ options.push("hidden: true");
614
+ }
615
+ if (field.prop.readonly) {
616
+ options.push("readonly: true");
617
+ }
618
+ if (field.prop.default !== void 0) {
619
+ options.push(`default: ${JSON.stringify(field.prop.default)}`);
620
+ }
621
+ if (options.length === 0) {
622
+ return "";
623
+ }
624
+ return `{ ${options.join(", ")} }`;
625
+ }
626
+ /**
627
+ * Generate @UI() options object
628
+ */
629
+ generateUIOptions(field) {
630
+ const options = [];
631
+ if (field.ui.tab) {
632
+ options.push(`tab: '${field.ui.tab}'`);
633
+ }
634
+ if (field.ui.side) {
635
+ options.push("side: true");
636
+ }
637
+ if (field.ui.type) {
638
+ options.push(`type: '${field.ui.type}'`);
639
+ }
640
+ if (field.ui.label && field.ui.label !== field.displayName) {
641
+ options.push(`label: '${this.escapeString(field.ui.label)}'`);
642
+ }
643
+ if (field.ui.description) {
644
+ options.push(`description: '${this.escapeString(field.ui.description)}'`);
645
+ }
646
+ if (field.ui.placeholder) {
647
+ options.push(`placeholder: '${this.escapeString(field.ui.placeholder)}'`);
648
+ }
649
+ if (field.ui.row) {
650
+ options.push("row: true");
651
+ }
652
+ if (field.ui.options && field.ui.options.length > 0) {
653
+ const optionsStr = field.ui.options.map((o) => `{ key: '${this.escapeString(o.key)}', value: '${this.escapeString(o.value)}' }`).join(", ");
654
+ options.push(`options: [${optionsStr}]`);
655
+ }
656
+ if (options.length === 0) {
657
+ return "{}";
658
+ }
659
+ return `{ ${options.join(", ")} }`;
660
+ }
661
+ /**
662
+ * Format a validator call
663
+ */
664
+ formatValidator(rule) {
665
+ if (!rule.constraints || rule.constraints.length === 0) {
666
+ return `${rule.type}()`;
667
+ }
668
+ const args = rule.constraints.map((c) => {
669
+ if (typeof c === "string") {
670
+ if (rule.type === "Matches") {
671
+ return c.startsWith("/") ? c : `/${c}/`;
672
+ }
673
+ return `'${this.escapeString(String(c))}'`;
674
+ }
675
+ return String(c);
676
+ }).join(", ");
677
+ return `${rule.type}(${args})`;
678
+ }
679
+ /**
680
+ * Escape string for use in generated code
681
+ */
682
+ escapeString(str) {
683
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
684
+ }
685
+ /**
686
+ * Generate JSON representation of the schema
687
+ */
688
+ generateSchemaJSON(dto) {
689
+ return {
690
+ name: dto.name,
691
+ options: {
692
+ versioning: dto.options?.versioning ?? true,
693
+ i18n: dto.options?.i18n ?? true
694
+ },
695
+ properties: dto.fields.map((field) => ({
696
+ name: field.name,
697
+ displayName: field.displayName,
698
+ type: field.type,
699
+ tsType: field.tsType,
700
+ required: field.prop.required,
701
+ unique: field.prop.unique,
702
+ intl: field.prop.intl,
703
+ ui: field.ui,
704
+ validations: field.validations,
705
+ ...field.relationConfig && {
706
+ relationConfig: field.relationConfig
707
+ }
708
+ }))
709
+ };
710
+ }
711
+ /**
712
+ * Parse a schema file content to extract metadata
713
+ */
714
+ parseSchemaFile(content) {
715
+ try {
716
+ const classMatch = content.match(/export\s+class\s+(\w+)/);
717
+ if (!classMatch?.[1]) return null;
718
+ const name = classMatch[1];
719
+ const schemaMatch = content.match(/@Schema\(\{([^}]*)\}\)/);
720
+ const options = {};
721
+ if (schemaMatch?.[1]) {
722
+ const optionsStr = schemaMatch[1];
723
+ if (optionsStr.includes("versioning: true")) options.versioning = true;
724
+ if (optionsStr.includes("versioning: false")) options.versioning = false;
725
+ if (optionsStr.includes("i18n: true")) options.i18n = true;
726
+ if (optionsStr.includes("i18n: false")) options.i18n = false;
727
+ }
728
+ const fields = [];
729
+ const fieldRegex = /@Prop\(([^)]*)\)[^@]*@UI\(([^)]*)\)[^:]*(\w+):\s*(\w+)/g;
730
+ let fieldMatch = fieldRegex.exec(content);
731
+ while (fieldMatch !== null) {
732
+ const propStr = fieldMatch[1];
733
+ const uiStr = fieldMatch[2];
734
+ const fieldName = fieldMatch[3];
735
+ const tsType = fieldMatch[4];
736
+ if (propStr !== void 0 && uiStr && fieldName && tsType) {
737
+ fields.push({
738
+ name: fieldName,
739
+ displayName: fieldName,
740
+ type: this.inferFieldType(tsType, uiStr),
741
+ tsType,
742
+ prop: {
743
+ required: propStr.includes("required: true"),
744
+ unique: propStr.includes("unique: true"),
745
+ intl: propStr.includes("intl: true")
746
+ },
747
+ ui: this.parseUIOptions(uiStr),
748
+ validations: []
749
+ });
750
+ }
751
+ fieldMatch = fieldRegex.exec(content);
752
+ }
753
+ return {
754
+ name,
755
+ options,
756
+ fields
757
+ };
758
+ } catch (error) {
759
+ this.logger.error("Failed to parse schema file", error);
760
+ return null;
761
+ }
762
+ }
763
+ /**
764
+ * Infer field type from TypeScript type and UI options
765
+ */
766
+ inferFieldType(tsType, uiStr) {
767
+ if (uiStr.includes("type: 'switch'") || uiStr.includes("type: 'checkbox'")) return "boolean";
768
+ if (uiStr.includes("type: 'date'")) return "date";
769
+ if (uiStr.includes("type: 'number'")) return "number";
770
+ if (uiStr.includes("type: 'select'") || uiStr.includes("type: 'radio'")) return "select";
771
+ if (uiStr.includes("type: 'relationship'")) return "relation";
772
+ const lowerType = tsType.toLowerCase();
773
+ if (lowerType === "number") return "number";
774
+ if (lowerType === "boolean") return "boolean";
775
+ if (lowerType === "date") return "date";
776
+ return "text";
777
+ }
778
+ /**
779
+ * Parse UI options from string
780
+ */
781
+ parseUIOptions(uiStr) {
782
+ const ui = {};
783
+ const tabMatch = uiStr.match(/tab:\s*'([^']*)'/);
784
+ if (tabMatch) ui.tab = tabMatch[1];
785
+ const typeMatch = uiStr.match(/type:\s*'([^']*)'/);
786
+ if (typeMatch) ui.type = typeMatch[1];
787
+ const labelMatch = uiStr.match(/label:\s*'([^']*)'/);
788
+ if (labelMatch) ui.label = labelMatch[1];
789
+ const descMatch = uiStr.match(/description:\s*'([^']*)'/);
790
+ if (descMatch) ui.description = descMatch[1];
791
+ if (uiStr.includes("side: true")) ui.side = true;
792
+ if (uiStr.includes("row: true")) ui.row = true;
793
+ return ui;
794
+ }
795
+ };
796
+ PlaygroundService = _ts_decorate([
797
+ common.Injectable(),
798
+ _ts_param(0, common.Optional()),
799
+ _ts_param(0, common.Inject(common$1.MagnetModuleOptions)),
800
+ _ts_param(1, common.Optional()),
801
+ _ts_param(1, core.InjectPluginOptions("playground")),
802
+ _ts_metadata("design:type", Function),
803
+ _ts_metadata("design:paramtypes", [
804
+ typeof common$1.MagnetModuleOptions === "undefined" ? Object : common$1.MagnetModuleOptions,
805
+ typeof PlaygroundPluginOptions === "undefined" ? Object : PlaygroundPluginOptions
806
+ ])
807
+ ], PlaygroundService);
808
+
809
+ // src/backend/playground.controller.ts
810
+ function _ts_decorate2(decorators, target, key, desc) {
811
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
812
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
813
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
814
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
815
+ }
816
+ __name(_ts_decorate2, "_ts_decorate");
817
+ function _ts_metadata2(k, v) {
818
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
819
+ }
820
+ __name(_ts_metadata2, "_ts_metadata");
821
+ function _ts_param2(paramIndex, decorator) {
822
+ return function(target, key) {
823
+ decorator(target, key, paramIndex);
824
+ };
825
+ }
826
+ __name(_ts_param2, "_ts_param");
827
+ var PlaygroundController = class {
828
+ static {
829
+ __name(this, "PlaygroundController");
830
+ }
831
+ playgroundService;
832
+ constructor(playgroundService) {
833
+ this.playgroundService = playgroundService;
834
+ }
835
+ /**
836
+ * List all schemas
837
+ * GET /playground/schemas
838
+ */
839
+ async listSchemas() {
840
+ try {
841
+ return await this.playgroundService.listSchemas();
842
+ } catch (error) {
843
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to list schemas", common.HttpStatus.INTERNAL_SERVER_ERROR);
844
+ }
845
+ }
846
+ /**
847
+ * Get a schema by name
848
+ * GET /playground/schemas/:name
849
+ */
850
+ async getSchema(name) {
851
+ try {
852
+ const schema = await this.playgroundService.getSchema(name);
853
+ if (!schema) {
854
+ throw new common.HttpException("Schema not found", common.HttpStatus.NOT_FOUND);
855
+ }
856
+ return schema;
857
+ } catch (error) {
858
+ if (error instanceof common.HttpException) throw error;
859
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to get schema", common.HttpStatus.INTERNAL_SERVER_ERROR);
860
+ }
861
+ }
862
+ /**
863
+ * Create a new module with schema, controller, service, and DTO
864
+ * POST /playground/schemas
865
+ */
866
+ async createSchema(body) {
867
+ try {
868
+ if (!body.name || !/^[A-Z][A-Za-z0-9]*$/.test(body.name)) {
869
+ throw new common.HttpException("Invalid schema name. Must start with uppercase letter and contain only alphanumeric characters.", common.HttpStatus.BAD_REQUEST);
870
+ }
871
+ if (this.playgroundService.schemaExists(body.name)) {
872
+ throw new common.HttpException("Module with this name already exists. Use PUT to update the schema.", common.HttpStatus.CONFLICT);
873
+ }
874
+ return await this.playgroundService.createModule(body);
875
+ } catch (error) {
876
+ if (error instanceof common.HttpException) throw error;
877
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to create module", common.HttpStatus.INTERNAL_SERVER_ERROR);
878
+ }
879
+ }
880
+ /**
881
+ * Update an existing schema (only the schema file, not the whole module)
882
+ * Returns conflicts if field types have changed
883
+ * PUT /playground/schemas/:name
884
+ */
885
+ async updateSchema(name, body) {
886
+ try {
887
+ if (!body.name || !/^[A-Z][A-Za-z0-9]*$/.test(body.name)) {
888
+ throw new common.HttpException("Invalid schema name. Must start with uppercase letter and contain only alphanumeric characters.", common.HttpStatus.BAD_REQUEST);
889
+ }
890
+ if (!this.playgroundService.schemaExists(name)) {
891
+ throw new common.HttpException("Schema not found. Use POST to create a new module.", common.HttpStatus.NOT_FOUND);
892
+ }
893
+ if (body.name.toLowerCase() !== name.toLowerCase()) {
894
+ throw new common.HttpException("Renaming schemas is not supported. Create a new module instead.", common.HttpStatus.BAD_REQUEST);
895
+ }
896
+ const result = await this.playgroundService.updateSchema(name, body);
897
+ return {
898
+ ...result.detail,
899
+ conflicts: result.conflicts
900
+ };
901
+ } catch (error) {
902
+ if (error instanceof common.HttpException) throw error;
903
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to update schema", common.HttpStatus.INTERNAL_SERVER_ERROR);
904
+ }
905
+ }
906
+ /**
907
+ * Delete a schema
908
+ * DELETE /playground/schemas/:name
909
+ */
910
+ async deleteSchema(name) {
911
+ try {
912
+ const deleted = await this.playgroundService.deleteSchema(name);
913
+ if (!deleted) {
914
+ throw new common.HttpException("Schema not found", common.HttpStatus.NOT_FOUND);
915
+ }
916
+ return {
917
+ success: true
918
+ };
919
+ } catch (error) {
920
+ if (error instanceof common.HttpException) throw error;
921
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to delete schema", common.HttpStatus.INTERNAL_SERVER_ERROR);
922
+ }
923
+ }
924
+ /**
925
+ * Generate code preview without saving
926
+ * POST /playground/preview
927
+ */
928
+ previewCode(body) {
929
+ try {
930
+ return this.playgroundService.previewCode(body);
931
+ } catch (error) {
932
+ throw new common.HttpException(error instanceof Error ? error.message : "Failed to generate preview", common.HttpStatus.INTERNAL_SERVER_ERROR);
933
+ }
934
+ }
935
+ };
936
+ _ts_decorate2([
937
+ common.Get("schemas"),
938
+ _ts_metadata2("design:type", Function),
939
+ _ts_metadata2("design:paramtypes", []),
940
+ _ts_metadata2("design:returntype", Promise)
941
+ ], PlaygroundController.prototype, "listSchemas", null);
942
+ _ts_decorate2([
943
+ common.Get("schemas/:name"),
944
+ _ts_param2(0, common.Param("name")),
945
+ _ts_metadata2("design:type", Function),
946
+ _ts_metadata2("design:paramtypes", [
947
+ String
948
+ ]),
949
+ _ts_metadata2("design:returntype", Promise)
950
+ ], PlaygroundController.prototype, "getSchema", null);
951
+ _ts_decorate2([
952
+ common.Post("schemas"),
953
+ _ts_param2(0, common.Body()),
954
+ _ts_metadata2("design:type", Function),
955
+ _ts_metadata2("design:paramtypes", [
956
+ typeof CreateSchemaDto === "undefined" ? Object : CreateSchemaDto
957
+ ]),
958
+ _ts_metadata2("design:returntype", Promise)
959
+ ], PlaygroundController.prototype, "createSchema", null);
960
+ _ts_decorate2([
961
+ common.Put("schemas/:name"),
962
+ _ts_param2(0, common.Param("name")),
963
+ _ts_param2(1, common.Body()),
964
+ _ts_metadata2("design:type", Function),
965
+ _ts_metadata2("design:paramtypes", [
966
+ String,
967
+ typeof CreateSchemaDto === "undefined" ? Object : CreateSchemaDto
968
+ ]),
969
+ _ts_metadata2("design:returntype", Promise)
970
+ ], PlaygroundController.prototype, "updateSchema", null);
971
+ _ts_decorate2([
972
+ common.Delete("schemas/:name"),
973
+ _ts_param2(0, common.Param("name")),
974
+ _ts_metadata2("design:type", Function),
975
+ _ts_metadata2("design:paramtypes", [
976
+ String
977
+ ]),
978
+ _ts_metadata2("design:returntype", Promise)
979
+ ], PlaygroundController.prototype, "deleteSchema", null);
980
+ _ts_decorate2([
981
+ common.Post("preview"),
982
+ _ts_param2(0, common.Body()),
983
+ _ts_metadata2("design:type", Function),
984
+ _ts_metadata2("design:paramtypes", [
985
+ typeof CreateSchemaDto === "undefined" ? Object : CreateSchemaDto
986
+ ]),
987
+ _ts_metadata2("design:returntype", void 0)
988
+ ], PlaygroundController.prototype, "previewCode", null);
989
+ PlaygroundController = _ts_decorate2([
990
+ common.Controller("playground"),
991
+ core.RestrictedRoute(),
992
+ _ts_metadata2("design:type", Function),
993
+ _ts_metadata2("design:paramtypes", [
994
+ typeof PlaygroundService === "undefined" ? Object : PlaygroundService
995
+ ])
996
+ ], PlaygroundController);
997
+
998
+ // src/backend/playground.module.ts
999
+ function _ts_decorate3(decorators, target, key, desc) {
1000
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1001
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1002
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1003
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1004
+ }
1005
+ __name(_ts_decorate3, "_ts_decorate");
1006
+ exports.PlaygroundModule = class PlaygroundModule {
1007
+ static {
1008
+ __name(this, "PlaygroundModule");
1009
+ }
1010
+ };
1011
+ exports.PlaygroundModule = _ts_decorate3([
1012
+ common.Module({
1013
+ controllers: [
1014
+ PlaygroundController
1015
+ ],
1016
+ providers: [
1017
+ PlaygroundService
1018
+ ],
1019
+ exports: [
1020
+ PlaygroundService
1021
+ ]
1022
+ })
1023
+ ], exports.PlaygroundModule);