@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,319 @@
1
+ import type {
2
+ SchemaBuilderState,
3
+ SchemaField,
4
+ ValidationRule,
5
+ } from '../types/builder.types'
6
+
7
+ /**
8
+ * Generate TypeScript schema code from builder state
9
+ */
10
+ export function generateSchemaCode(state: SchemaBuilderState): string {
11
+ if (!state.schema.name) {
12
+ return '// Enter a schema name to generate code'
13
+ }
14
+
15
+ const imports = generateImports(state.fields)
16
+ const classDecorator = generateSchemaDecorator(state.schema)
17
+ const properties = state.fields
18
+ .map((field) => generateFieldCode(field))
19
+ .join('\n\n')
20
+
21
+ return `${imports}\n\n${classDecorator}\nexport class ${state.schema.name} {\n${properties}\n}\n`
22
+ }
23
+
24
+ /**
25
+ * Generate import statements based on used features
26
+ */
27
+ function generateImports(fields: SchemaField[]): string {
28
+ const magnetImports = new Set(['Schema', 'Prop', 'UI'])
29
+ const validatorImports = new Set<string>()
30
+ let needsTypeTransformer = false
31
+
32
+ for (const field of fields) {
33
+ // Check if we need Validators decorator
34
+ if (field.validations.length > 0) {
35
+ magnetImports.add('Validators')
36
+ for (const v of field.validations) {
37
+ validatorImports.add(v.type)
38
+ }
39
+ }
40
+
41
+ // Check if we need Type transformer (for Date fields)
42
+ if (field.type === 'date') {
43
+ needsTypeTransformer = true
44
+ }
45
+ }
46
+
47
+ const lines: string[] = []
48
+
49
+ // Magnet imports
50
+ lines.push(
51
+ `import { ${Array.from(magnetImports).sort().join(', ')} } from '@magnet-cms/common'`,
52
+ )
53
+
54
+ // Class-transformer imports
55
+ if (needsTypeTransformer) {
56
+ lines.push(`import { Type } from 'class-transformer'`)
57
+ }
58
+
59
+ // Class-validator imports
60
+ if (validatorImports.size > 0) {
61
+ lines.push(
62
+ `import {\n\t${Array.from(validatorImports).sort().join(',\n\t')},\n} from 'class-validator'`,
63
+ )
64
+ }
65
+
66
+ return lines.join('\n')
67
+ }
68
+
69
+ /**
70
+ * Generate @Schema() decorator
71
+ */
72
+ function generateSchemaDecorator(schema: SchemaBuilderState['schema']): string {
73
+ const options: string[] = []
74
+
75
+ // Only add options if they differ from defaults or if we want to be explicit
76
+ if (schema.versioning !== undefined) {
77
+ options.push(`versioning: ${schema.versioning}`)
78
+ }
79
+ if (schema.i18n !== undefined) {
80
+ options.push(`i18n: ${schema.i18n}`)
81
+ }
82
+
83
+ if (options.length === 0) {
84
+ return '@Schema()'
85
+ }
86
+
87
+ return `@Schema({ ${options.join(', ')} })`
88
+ }
89
+
90
+ /**
91
+ * Generate code for a single field
92
+ */
93
+ function generateFieldCode(field: SchemaField): string {
94
+ const decorators: string[] = []
95
+ const indent = '\t'
96
+
97
+ // @Type() decorator for Date fields (must come first)
98
+ if (field.type === 'date') {
99
+ decorators.push(`${indent}@Type(() => Date)`)
100
+ }
101
+
102
+ // @Prop() decorator
103
+ decorators.push(`${indent}@Prop(${generatePropOptions(field)})`)
104
+
105
+ // @Validators() decorator
106
+ if (field.validations.length > 0) {
107
+ const validators = field.validations
108
+ .map((v) => formatValidator(v))
109
+ .join(', ')
110
+ decorators.push(`${indent}@Validators(${validators})`)
111
+ }
112
+
113
+ // @UI() decorator
114
+ decorators.push(`${indent}@UI(${generateUIOptions(field)})`)
115
+
116
+ // Property declaration
117
+ const declaration = `${indent}${field.name}: ${field.tsType}`
118
+
119
+ return [...decorators, declaration].join('\n')
120
+ }
121
+
122
+ /**
123
+ * Generate @Prop() options object
124
+ */
125
+ function generatePropOptions(field: SchemaField): string {
126
+ const options: string[] = []
127
+
128
+ if (field.prop.required) {
129
+ options.push('required: true')
130
+ }
131
+ if (field.prop.unique) {
132
+ options.push('unique: true')
133
+ }
134
+ if (field.prop.intl) {
135
+ options.push('intl: true')
136
+ }
137
+ if (field.prop.hidden) {
138
+ options.push('hidden: true')
139
+ }
140
+ if (field.prop.readonly) {
141
+ options.push('readonly: true')
142
+ }
143
+ if (field.prop.default !== undefined) {
144
+ options.push(`default: ${JSON.stringify(field.prop.default)}`)
145
+ }
146
+
147
+ if (options.length === 0) {
148
+ return ''
149
+ }
150
+
151
+ return `{ ${options.join(', ')} }`
152
+ }
153
+
154
+ /**
155
+ * Generate @UI() options object
156
+ */
157
+ function generateUIOptions(field: SchemaField): string {
158
+ const options: string[] = []
159
+
160
+ if (field.ui.tab) {
161
+ options.push(`tab: '${field.ui.tab}'`)
162
+ }
163
+ if (field.ui.side) {
164
+ options.push('side: true')
165
+ }
166
+ if (field.ui.type) {
167
+ options.push(`type: '${field.ui.type}'`)
168
+ }
169
+ if (field.ui.label && field.ui.label !== field.displayName) {
170
+ options.push(`label: '${escapeString(field.ui.label)}'`)
171
+ }
172
+ if (field.ui.description) {
173
+ options.push(`description: '${escapeString(field.ui.description)}'`)
174
+ }
175
+ if (field.ui.placeholder) {
176
+ options.push(`placeholder: '${escapeString(field.ui.placeholder)}'`)
177
+ }
178
+ if (field.ui.row) {
179
+ options.push('row: true')
180
+ }
181
+ if (field.ui.options && field.ui.options.length > 0) {
182
+ const optionsStr = field.ui.options
183
+ .map(
184
+ (o) =>
185
+ `{ key: '${escapeString(o.key)}', value: '${escapeString(o.value)}' }`,
186
+ )
187
+ .join(', ')
188
+ options.push(`options: [${optionsStr}]`)
189
+ }
190
+
191
+ if (options.length === 0) {
192
+ return '{}'
193
+ }
194
+
195
+ return `{ ${options.join(', ')} }`
196
+ }
197
+
198
+ /**
199
+ * Format a validator call
200
+ */
201
+ function formatValidator(rule: ValidationRule): string {
202
+ if (!rule.constraints || rule.constraints.length === 0) {
203
+ return `${rule.type}()`
204
+ }
205
+
206
+ const args = rule.constraints
207
+ .map((c) => {
208
+ if (typeof c === 'string') {
209
+ // Check if it's a regex pattern
210
+ if (rule.type === 'Matches') {
211
+ return c.startsWith('/') ? c : `/${c}/`
212
+ }
213
+ return `'${escapeString(String(c))}'`
214
+ }
215
+ return String(c)
216
+ })
217
+ .join(', ')
218
+
219
+ return `${rule.type}(${args})`
220
+ }
221
+
222
+ /**
223
+ * Escape string for use in generated code
224
+ */
225
+ function escapeString(str: string): string {
226
+ return str
227
+ .replace(/\\/g, '\\\\')
228
+ .replace(/'/g, "\\'")
229
+ .replace(/\n/g, '\\n')
230
+ .replace(/\r/g, '\\r')
231
+ .replace(/\t/g, '\\t')
232
+ }
233
+
234
+ /**
235
+ * Generate JSON representation of the schema
236
+ */
237
+ export function generateSchemaJSON(state: SchemaBuilderState): object {
238
+ return {
239
+ name: state.schema.name,
240
+ options: {
241
+ versioning: state.schema.versioning,
242
+ i18n: state.schema.i18n,
243
+ },
244
+ properties: state.fields.map((field) => ({
245
+ name: field.name,
246
+ displayName: field.displayName,
247
+ type: field.type,
248
+ tsType: field.tsType,
249
+ required: field.prop.required,
250
+ unique: field.prop.unique,
251
+ intl: field.prop.intl,
252
+ ui: field.ui,
253
+ validations: field.validations,
254
+ ...(field.relationConfig && { relationConfig: field.relationConfig }),
255
+ })),
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Parse class name to ensure it's valid
261
+ */
262
+ export function validateSchemaName(name: string): {
263
+ valid: boolean
264
+ error?: string
265
+ formatted?: string
266
+ } {
267
+ if (!name) {
268
+ return { valid: false, error: 'Schema name is required' }
269
+ }
270
+
271
+ // Must start with uppercase letter
272
+ if (!/^[A-Z]/.test(name)) {
273
+ return {
274
+ valid: false,
275
+ error: 'Schema name must start with an uppercase letter',
276
+ }
277
+ }
278
+
279
+ // Only alphanumeric characters
280
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name)) {
281
+ return {
282
+ valid: false,
283
+ error: 'Schema name can only contain letters and numbers',
284
+ }
285
+ }
286
+
287
+ return { valid: true, formatted: name }
288
+ }
289
+
290
+ /**
291
+ * Parse field name to ensure it's valid
292
+ */
293
+ export function validateFieldName(name: string): {
294
+ valid: boolean
295
+ error?: string
296
+ formatted?: string
297
+ } {
298
+ if (!name) {
299
+ return { valid: false, error: 'Field name is required' }
300
+ }
301
+
302
+ // Must start with lowercase letter
303
+ if (!/^[a-z]/.test(name)) {
304
+ return {
305
+ valid: false,
306
+ error: 'Field name must start with a lowercase letter',
307
+ }
308
+ }
309
+
310
+ // Only alphanumeric characters
311
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
312
+ return {
313
+ valid: false,
314
+ error: 'Field name can only contain letters and numbers (camelCase)',
315
+ }
316
+ }
317
+
318
+ return { valid: true, formatted: name }
319
+ }