@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/backend/index.cjs +1023 -0
- package/dist/backend/index.d.cts +10 -0
- package/dist/backend/index.d.ts +10 -0
- package/dist/backend/index.js +1 -0
- package/dist/chunk-WY4YMBWZ.js +1044 -0
- package/dist/frontend/bundle.iife.js +2163 -0
- package/dist/frontend/bundle.iife.js.map +1 -0
- package/dist/index.cjs +1135 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +76 -0
- package/package.json +81 -0
- package/src/frontend/index.ts +110 -0
- package/src/frontend/pages/Playground/Editor/AddFieldDialog.tsx +187 -0
- package/src/frontend/pages/Playground/Editor/CodePreview.tsx +59 -0
- package/src/frontend/pages/Playground/Editor/FieldCard.tsx +161 -0
- package/src/frontend/pages/Playground/Editor/FieldList.tsx +121 -0
- package/src/frontend/pages/Playground/Editor/FieldSettingsPanel.tsx +652 -0
- package/src/frontend/pages/Playground/Editor/RelationConfigModal.tsx +292 -0
- package/src/frontend/pages/Playground/Editor/SchemaList.tsx +76 -0
- package/src/frontend/pages/Playground/Editor/SchemaOptionsDialog.tsx +109 -0
- package/src/frontend/pages/Playground/Editor/index.tsx +322 -0
- package/src/frontend/pages/Playground/constants/field-types.ts +384 -0
- package/src/frontend/pages/Playground/hooks/useSchemaBuilder.ts +280 -0
- package/src/frontend/pages/Playground/index.tsx +19 -0
- package/src/frontend/pages/Playground/types/builder.types.ts +191 -0
- package/src/frontend/pages/Playground/utils/code-generator.ts +319 -0
|
@@ -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
|
+
}
|