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