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