@bagelink/vue 1.4.42 → 1.4.46
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/bin/generateFormSchema.ts +1038 -0
- package/bin/utils.ts +230 -0
- package/dist/components/Carousel2.vue.d.ts +89 -0
- package/dist/components/Carousel2.vue.d.ts.map +1 -0
- package/dist/components/calendar/views/CalendarPopover.vue.d.ts +2 -2
- package/dist/components/calendar/views/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/dashboard/Lineart.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/style.css +1 -1
- package/dist/utils/BagelFormUtils.d.ts +3 -1
- package/dist/utils/BagelFormUtils.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/components/Rating.vue +57 -0
- package/src/components/form/inputs/NumberInput.vue +2 -1
- package/src/styles/layout.css +16 -0
- package/src/styles/mobilLayout.css +20 -0
- package/src/utils/BagelFormUtils.ts +3 -3
- package/dist/chunk-CsX-DzYB.cjs +0 -1
- package/dist/components/Icon.vue.d.ts +0 -16
- package/dist/components/Icon.vue.d.ts.map +0 -1
- package/dist/components/form/BglFieldSet.vue.d.ts +0 -25
- package/dist/components/form/BglFieldSet.vue.d.ts.map +0 -1
- package/dist/editor-BpbYtUrx.js +0 -2
- package/dist/editor-CogxCqoB.cjs +0 -3
- package/dist/editor-CtWHU9QA.js +0 -1
- package/dist/editor-DDp0A6Ty.cjs +0 -1
- package/dist/editor-DQOZvXuf.cjs +0 -1
- package/dist/editor-Dk9B96i8.cjs +0 -2
- package/dist/editor-Dpod7RSB.js +0 -1
- package/dist/editor-UnqmwfaW.js +0 -2
- package/dist/editor-fhxKzja8.cjs +0 -2
- package/dist/editor-mLVzxfYa.js +0 -2
- package/dist/editor-smCr3pmQ.cjs +0 -1
- package/dist/iconify-0J3vK-m1.cjs +0 -1693
- package/dist/iconify-Bc1B42Ak.cjs +0 -1771
- package/dist/iconify-BiLGk5km.js +0 -1693
- package/dist/iconify-DVnNdzog.js +0 -1771
- package/dist/types/timeago.d.ts +0 -23
- package/dist/types/timeago.d.ts.map +0 -1
- package/dist/vue3-charts.esm-CkXK48ud.js +0 -54
- package/dist/vue3-charts.esm-uVj96p6_.cjs +0 -54
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
OpenAPIObject,
|
|
5
|
+
SchemaObject,
|
|
6
|
+
ReferenceObject,
|
|
7
|
+
} from '@bagelink/sdk'
|
|
8
|
+
import type {
|
|
9
|
+
ArrayBagelField,
|
|
10
|
+
ArrayFieldVal,
|
|
11
|
+
BaseBagelField,
|
|
12
|
+
BglFormSchemaT,
|
|
13
|
+
Path,
|
|
14
|
+
TextInputOptions,
|
|
15
|
+
} from '@bagelink/vue'
|
|
16
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { argv, cwd, exit } from 'node:process'
|
|
19
|
+
import { isSchemaObject, isReferenceObject } from '@bagelink/sdk'
|
|
20
|
+
import { fmtDate, useForm } from '@bagelink/vue'
|
|
21
|
+
import axios from 'axios'
|
|
22
|
+
import { toValue } from 'vue'
|
|
23
|
+
import {
|
|
24
|
+
formatAndWriteCode,
|
|
25
|
+
runEsLintOnDir,
|
|
26
|
+
parseConfig,
|
|
27
|
+
validateConfig,
|
|
28
|
+
printHelp,
|
|
29
|
+
} from './utils'
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// FORM UTILITIES (TOP-LEVEL SCOPE)
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
txtField,
|
|
37
|
+
emailField,
|
|
38
|
+
dateField,
|
|
39
|
+
selectField,
|
|
40
|
+
telField,
|
|
41
|
+
numField,
|
|
42
|
+
arrField,
|
|
43
|
+
checkField,
|
|
44
|
+
} = useForm()
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// TYPES & INTERFACES
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
interface FieldOptions {
|
|
51
|
+
readonly helptext?: string
|
|
52
|
+
readonly required: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface GeneratorConfig {
|
|
56
|
+
readonly genSchemaId: string
|
|
57
|
+
readonly output: string
|
|
58
|
+
readonly url: string
|
|
59
|
+
readonly useUtilityFunctions?: boolean
|
|
60
|
+
readonly useTableSchema?: boolean
|
|
61
|
+
readonly verbose?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SchemaContext {
|
|
65
|
+
readonly openApiSchema: OpenAPIObject
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type FormSchema<T> = BglFormSchemaT<T>
|
|
69
|
+
let USING_FMT_DATE = false
|
|
70
|
+
let USING_VERBOSE_FLAG = false
|
|
71
|
+
|
|
72
|
+
function logVerbose(...args: any[]): void {
|
|
73
|
+
if (USING_VERBOSE_FLAG) {
|
|
74
|
+
console.log('🔍', ...args)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// CONFIGURATION
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
function parseConfiguration(): GeneratorConfig {
|
|
83
|
+
const config = parseConfig({
|
|
84
|
+
schemaId: undefined,
|
|
85
|
+
defaultOutputFile: 'src/generated/exampleFormSchema.ts',
|
|
86
|
+
allowedFlags: ['utility-functions', 'table-schema', 'help', 'verbose'],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
90
|
+
printHelp('generateFormSchema')
|
|
91
|
+
exit(0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
validateConfig(config, ['baseUrl', 'schemaId'])
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
console.error('❌', error?.message)
|
|
98
|
+
exit(1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
USING_VERBOSE_FLAG = config.verbose || false
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
genSchemaId: config.schemaId || '',
|
|
105
|
+
output: config.outputFile || 'src/generated/exampleFormSchema.ts',
|
|
106
|
+
url: `${config.baseUrl}/openapi.json`,
|
|
107
|
+
useUtilityFunctions: config.useUtilityFunctions,
|
|
108
|
+
useTableSchema: config.useTableSchema,
|
|
109
|
+
} as const
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// SCHEMA UTILITIES
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
function resolveSchemaReference(
|
|
117
|
+
schemaOrRef: SchemaObject | ReferenceObject,
|
|
118
|
+
openApiSchema: OpenAPIObject,
|
|
119
|
+
visitedRefs = new Set<string>()
|
|
120
|
+
): SchemaObject | undefined {
|
|
121
|
+
if (!isReferenceObject(schemaOrRef)) {
|
|
122
|
+
return isSchemaObject(schemaOrRef) ? schemaOrRef : undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const refPath = schemaOrRef.$ref
|
|
126
|
+
if (visitedRefs.has(refPath)) {
|
|
127
|
+
logVerbose(`Circular reference detected: ${refPath}`)
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const refName = refPath.split('/').pop()
|
|
132
|
+
if (!refName || !openApiSchema.components?.schemas?.[refName]) {
|
|
133
|
+
logVerbose(`Could not resolve reference: ${refPath}`)
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const resolvedSchema = openApiSchema.components.schemas[refName]
|
|
138
|
+
if (isReferenceObject(resolvedSchema)) {
|
|
139
|
+
visitedRefs.add(refPath)
|
|
140
|
+
return resolveSchemaReference(resolvedSchema, openApiSchema, visitedRefs)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return isSchemaObject(resolvedSchema) ? resolvedSchema : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getSchemaById(
|
|
147
|
+
schemaId: string,
|
|
148
|
+
openApiSchema: OpenAPIObject
|
|
149
|
+
): SchemaObject | undefined {
|
|
150
|
+
const schemaOrRef = openApiSchema.components?.schemas?.[schemaId]
|
|
151
|
+
return schemaOrRef
|
|
152
|
+
? resolveSchemaReference(schemaOrRef, openApiSchema)
|
|
153
|
+
: undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolves a schema reference and returns the actual schema object
|
|
158
|
+
*/
|
|
159
|
+
function resolveFieldSchema(
|
|
160
|
+
fieldSchema: SchemaObject | ReferenceObject,
|
|
161
|
+
openApiSchema: OpenAPIObject
|
|
162
|
+
): SchemaObject | undefined {
|
|
163
|
+
if (isReferenceObject(fieldSchema)) {
|
|
164
|
+
return resolveSchemaReference(fieldSchema, openApiSchema)
|
|
165
|
+
}
|
|
166
|
+
return isSchemaObject(fieldSchema) ? fieldSchema : undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Gets the property schema for a nested field path
|
|
171
|
+
*/
|
|
172
|
+
function getNestedPropertySchema(
|
|
173
|
+
fieldPath: string[],
|
|
174
|
+
targetSchema: SchemaObject,
|
|
175
|
+
openApiSchema: OpenAPIObject
|
|
176
|
+
): SchemaObject | undefined {
|
|
177
|
+
if (fieldPath.length === 0) return targetSchema
|
|
178
|
+
|
|
179
|
+
const [currentProp, ...remainingPath] = fieldPath
|
|
180
|
+
const currentProperty = targetSchema.properties?.[currentProp]
|
|
181
|
+
|
|
182
|
+
if (!currentProperty) {
|
|
183
|
+
logVerbose(`Property '${currentProp}' not found in schema`)
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const resolvedProperty = resolveFieldSchema(
|
|
188
|
+
currentProperty,
|
|
189
|
+
openApiSchema
|
|
190
|
+
)
|
|
191
|
+
if (!resolvedProperty) {
|
|
192
|
+
logVerbose(`Could not resolve property '${currentProp}'`)
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If there are more path segments, continue resolving
|
|
197
|
+
if (remainingPath.length > 0) {
|
|
198
|
+
if (resolvedProperty.type === 'object' && resolvedProperty.properties) {
|
|
199
|
+
return getNestedPropertySchema(
|
|
200
|
+
remainingPath,
|
|
201
|
+
resolvedProperty,
|
|
202
|
+
openApiSchema
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
logVerbose(
|
|
206
|
+
`Cannot resolve nested path beyond '${currentProp}' - not an object type`
|
|
207
|
+
)
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return resolvedProperty
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// STRING FORMATTING
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
function formatPropertyName(name: string): string {
|
|
219
|
+
return name
|
|
220
|
+
.replace(/([A-Z])/g, ' $1')
|
|
221
|
+
.replace(/_/g, ' ')
|
|
222
|
+
.replace(/^\w/, c => c.toUpperCase())
|
|
223
|
+
.trim()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatTypeName(name: string): string {
|
|
227
|
+
return name
|
|
228
|
+
.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
|
229
|
+
.replace(/^\w/, c => c.toUpperCase())
|
|
230
|
+
.replace(/\s+(\w)/g, (_, letter: string) => letter.toUpperCase())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getHtmlInputType(format?: string): string | undefined {
|
|
234
|
+
const formatMap: Record<string, string> = {
|
|
235
|
+
'date': 'date',
|
|
236
|
+
'date-time': 'datetime-local',
|
|
237
|
+
'email': 'email',
|
|
238
|
+
'phone': 'tel',
|
|
239
|
+
'password': 'password',
|
|
240
|
+
'binary': 'file',
|
|
241
|
+
'uri': 'url',
|
|
242
|
+
'double': 'number',
|
|
243
|
+
'float': 'number',
|
|
244
|
+
'int32': 'number',
|
|
245
|
+
'int64': 'number',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return format ? formatMap[format] : undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// =============================================================================
|
|
252
|
+
// FIELD CREATION
|
|
253
|
+
// =============================================================================
|
|
254
|
+
|
|
255
|
+
function createStringField<T>(
|
|
256
|
+
propName: Path<T>,
|
|
257
|
+
schema: SchemaObject,
|
|
258
|
+
options: FieldOptions
|
|
259
|
+
): BaseBagelField<T, Path<T>> {
|
|
260
|
+
const label = formatPropertyName(propName as string)
|
|
261
|
+
|
|
262
|
+
// Email fields
|
|
263
|
+
if (schema.format === 'email') {
|
|
264
|
+
return emailField(propName, label, {
|
|
265
|
+
required: options.required,
|
|
266
|
+
placeholder: options.helptext,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Date fields
|
|
271
|
+
if (schema.format === 'date' || schema.format === 'date-time') {
|
|
272
|
+
return dateField<T, Path<T>>(propName, label, {
|
|
273
|
+
required: options.required,
|
|
274
|
+
placeholder: options.helptext,
|
|
275
|
+
enableTime: schema.format === 'date-time',
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Phone fields
|
|
280
|
+
if (schema.format === 'phone') {
|
|
281
|
+
return telField<T, Path<T>>(propName, label, {
|
|
282
|
+
required: options.required,
|
|
283
|
+
placeholder: options.helptext,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Enum fields as select
|
|
288
|
+
if (schema.enum?.length) {
|
|
289
|
+
const selectOptions = schema.enum.map(value => ({
|
|
290
|
+
label: String(value)
|
|
291
|
+
.replace(/_/g, ' ')
|
|
292
|
+
.replace(/\b\w/g, l => l.toUpperCase()),
|
|
293
|
+
value,
|
|
294
|
+
}))
|
|
295
|
+
return selectField<T, Path<T>>(propName, label, selectOptions, {
|
|
296
|
+
required: options.required,
|
|
297
|
+
placeholder: options.helptext,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Default text field
|
|
302
|
+
const textOptions: TextInputOptions<T, Path<T>> = {
|
|
303
|
+
required: options.required,
|
|
304
|
+
helptext: options.helptext,
|
|
305
|
+
multiline: (schema.maxLength ?? 0) > 255,
|
|
306
|
+
pattern: schema.pattern,
|
|
307
|
+
type: getHtmlInputType(schema.format),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return txtField<T, Path<T>>(propName, label, textOptions)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function createNumberField<T>(
|
|
314
|
+
propName: Path<T>,
|
|
315
|
+
schema: SchemaObject,
|
|
316
|
+
options: FieldOptions
|
|
317
|
+
): BaseBagelField<T, Path<T>> {
|
|
318
|
+
return numField<T, Path<T>>(
|
|
319
|
+
propName,
|
|
320
|
+
formatPropertyName(propName as string),
|
|
321
|
+
{
|
|
322
|
+
required: options.required,
|
|
323
|
+
placeholder: options.helptext,
|
|
324
|
+
min: schema.minimum,
|
|
325
|
+
max: schema.maximum,
|
|
326
|
+
step: schema.type === 'integer' ? 1 : undefined,
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function createBooleanField<T>(
|
|
332
|
+
propName: Path<T>,
|
|
333
|
+
options: FieldOptions
|
|
334
|
+
): BaseBagelField<T, Path<T>> {
|
|
335
|
+
return checkField(propName, formatPropertyName(propName as string), {
|
|
336
|
+
required: options.required,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function createArrayField<T>(
|
|
341
|
+
propName: Path<T>,
|
|
342
|
+
schema: SchemaObject,
|
|
343
|
+
options: FieldOptions,
|
|
344
|
+
schemaType: string,
|
|
345
|
+
context: SchemaContext
|
|
346
|
+
): BaseBagelField<T, Path<T>> {
|
|
347
|
+
if (!schema.items) {
|
|
348
|
+
logVerbose(`Array field '${propName as string}' has no items schema`)
|
|
349
|
+
return createFallbackField(propName, options)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const itemsSchema = resolveSchemaReference(
|
|
353
|
+
schema.items,
|
|
354
|
+
context.openApiSchema
|
|
355
|
+
)
|
|
356
|
+
if (!itemsSchema) {
|
|
357
|
+
logVerbose(
|
|
358
|
+
`Array field '${propName as string}' has unresolvable items schema`
|
|
359
|
+
)
|
|
360
|
+
return createFallbackField(propName, options)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Object array
|
|
364
|
+
if (itemsSchema.type === 'object' && itemsSchema.properties) {
|
|
365
|
+
const nestedSchema = convertSchemaToForm(
|
|
366
|
+
itemsSchema,
|
|
367
|
+
`${schemaType}Item`,
|
|
368
|
+
context
|
|
369
|
+
) as FormSchema<ArrayFieldVal<T, Path<T>>>
|
|
370
|
+
|
|
371
|
+
return arrField<T, Path<T>>(
|
|
372
|
+
propName,
|
|
373
|
+
formatPropertyName(propName as string),
|
|
374
|
+
nestedSchema
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Primitive array
|
|
379
|
+
const arrayType
|
|
380
|
+
= itemsSchema.type === 'number' || itemsSchema.type === 'integer'
|
|
381
|
+
? 'number'
|
|
382
|
+
: 'text'
|
|
383
|
+
return arrField<T, Path<T>>(
|
|
384
|
+
propName,
|
|
385
|
+
formatPropertyName(propName as string),
|
|
386
|
+
arrayType
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function createFallbackField<T>(
|
|
391
|
+
propName: Path<T>,
|
|
392
|
+
options: FieldOptions
|
|
393
|
+
): BaseBagelField<T, Path<T>> {
|
|
394
|
+
return txtField<T, Path<T>>(
|
|
395
|
+
propName,
|
|
396
|
+
formatPropertyName(propName as string),
|
|
397
|
+
{
|
|
398
|
+
required: options.required,
|
|
399
|
+
placeholder: options.helptext,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createFieldByType<T>(
|
|
405
|
+
propName: Path<T>,
|
|
406
|
+
schema: SchemaObject,
|
|
407
|
+
options: FieldOptions,
|
|
408
|
+
schemaType: string,
|
|
409
|
+
context: SchemaContext
|
|
410
|
+
): BaseBagelField<T, Path<T>> | null {
|
|
411
|
+
// Handle nullable union types
|
|
412
|
+
const actualSchema = getNonNullSchema(schema) || schema
|
|
413
|
+
|
|
414
|
+
switch (actualSchema.type) {
|
|
415
|
+
case 'string':
|
|
416
|
+
return createStringField(propName, actualSchema, options)
|
|
417
|
+
case 'number':
|
|
418
|
+
case 'integer':
|
|
419
|
+
return createNumberField(propName, actualSchema, options)
|
|
420
|
+
case 'boolean':
|
|
421
|
+
return createBooleanField(propName, options)
|
|
422
|
+
case 'array':
|
|
423
|
+
return createArrayField(
|
|
424
|
+
propName,
|
|
425
|
+
actualSchema,
|
|
426
|
+
options,
|
|
427
|
+
schemaType,
|
|
428
|
+
context
|
|
429
|
+
)
|
|
430
|
+
case 'object':
|
|
431
|
+
return null // Will be handled separately for flattening
|
|
432
|
+
default:
|
|
433
|
+
logVerbose(
|
|
434
|
+
`Unknown type '${actualSchema.type}' for field '${propName as string}', using fallback`
|
|
435
|
+
)
|
|
436
|
+
return createFallbackField(propName, options)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getNonNullSchema(schema: SchemaObject): SchemaObject | undefined {
|
|
441
|
+
// Handle anyOf union types
|
|
442
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
443
|
+
const nonNullSchema = schema.anyOf.find((subSchema) => {
|
|
444
|
+
if (isReferenceObject(subSchema)) {
|
|
445
|
+
// We'd need to resolve this, but for simplicity, assume it's not null
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
return subSchema.type !== 'null'
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
return nonNullSchema && !isReferenceObject(nonNullSchema)
|
|
452
|
+
? nonNullSchema
|
|
453
|
+
: undefined
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Handle oneOf union types
|
|
457
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
458
|
+
const nonNullSchema = schema.oneOf.find((subSchema) => {
|
|
459
|
+
if (isReferenceObject(subSchema)) {
|
|
460
|
+
return true
|
|
461
|
+
}
|
|
462
|
+
return subSchema.type !== 'null'
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
return nonNullSchema && !isReferenceObject(nonNullSchema)
|
|
466
|
+
? nonNullSchema
|
|
467
|
+
: undefined
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function processObjectField<T>(
|
|
474
|
+
propName: Path<T>,
|
|
475
|
+
schema: SchemaObject,
|
|
476
|
+
schemaType: string,
|
|
477
|
+
formSchema: FormSchema<T>,
|
|
478
|
+
context: SchemaContext
|
|
479
|
+
): void {
|
|
480
|
+
if (!schema.properties || Object.keys(schema.properties).length === 0) return
|
|
481
|
+
|
|
482
|
+
logVerbose(`Creating nested fields for object: ${propName as string}`)
|
|
483
|
+
const nestedSchema = convertSchemaToForm(
|
|
484
|
+
schema,
|
|
485
|
+
`${schemaType}${formatTypeName(propName as string)}`,
|
|
486
|
+
context
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
// Flatten nested fields with proper namespacing
|
|
490
|
+
nestedSchema.forEach((subField) => {
|
|
491
|
+
if (!subField.id) return
|
|
492
|
+
|
|
493
|
+
const nestedField = {
|
|
494
|
+
...subField,
|
|
495
|
+
id: `${propName as string}.${subField.id}` as Path<T>,
|
|
496
|
+
}
|
|
497
|
+
formSchema.push(nestedField as FormSchema<T>[number])
|
|
498
|
+
logVerbose(`Added nested field: ${propName as string}.${subField.id}`)
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// =============================================================================
|
|
503
|
+
// SCHEMA CONVERSION
|
|
504
|
+
// =============================================================================
|
|
505
|
+
|
|
506
|
+
function convertSchemaToForm<T = Record<string, unknown>>(
|
|
507
|
+
schema: SchemaObject,
|
|
508
|
+
schemaType: string,
|
|
509
|
+
context: SchemaContext
|
|
510
|
+
): FormSchema<T> {
|
|
511
|
+
const propertyKeys = Object.keys(schema.properties ?? {})
|
|
512
|
+
logVerbose(`Processing schema for ${schemaType}:`, propertyKeys)
|
|
513
|
+
|
|
514
|
+
if (!schema.properties) {
|
|
515
|
+
logVerbose('Schema has no properties')
|
|
516
|
+
return []
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const formSchema: FormSchema<T> = []
|
|
520
|
+
const requiredFields = Array.isArray(schema.required) ? schema.required : []
|
|
521
|
+
|
|
522
|
+
Object.entries(schema.properties).forEach(([propName, propSchemaOrRef]) => {
|
|
523
|
+
const resolvedPropSchema = resolveSchemaReference(
|
|
524
|
+
propSchemaOrRef,
|
|
525
|
+
context.openApiSchema
|
|
526
|
+
)
|
|
527
|
+
if (!resolvedPropSchema) {
|
|
528
|
+
logVerbose(`Skipping unresolvable property: ${propName}`)
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const fieldOptions: FieldOptions = {
|
|
533
|
+
required: requiredFields.includes(propName),
|
|
534
|
+
helptext: resolvedPropSchema.description,
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
logVerbose(
|
|
538
|
+
`Processing field: ${propName}, type: ${resolvedPropSchema.type ?? 'undefined'}, format: ${resolvedPropSchema.format ?? 'undefined'}`
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
// Handle object fields by flattening
|
|
542
|
+
if (resolvedPropSchema.type === 'object') {
|
|
543
|
+
processObjectField(
|
|
544
|
+
propName as Path<T>,
|
|
545
|
+
resolvedPropSchema,
|
|
546
|
+
schemaType,
|
|
547
|
+
formSchema,
|
|
548
|
+
context
|
|
549
|
+
)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const field = createFieldByType(
|
|
554
|
+
propName as Path<T>,
|
|
555
|
+
resolvedPropSchema,
|
|
556
|
+
fieldOptions,
|
|
557
|
+
schemaType,
|
|
558
|
+
context
|
|
559
|
+
)
|
|
560
|
+
if (!field) return
|
|
561
|
+
|
|
562
|
+
// Apply default value if specified
|
|
563
|
+
if (resolvedPropSchema.default !== undefined) {
|
|
564
|
+
field.defaultValue = resolvedPropSchema.default
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
formSchema.push(field as FormSchema<T>[number])
|
|
568
|
+
logVerbose(`Added field: ${propName} with $el: ${field.$el}`)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
logVerbose(`Generated ${formSchema.length} fields for ${schemaType}`)
|
|
572
|
+
return formSchema
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// =============================================================================
|
|
576
|
+
// API OPERATIONS
|
|
577
|
+
// =============================================================================
|
|
578
|
+
|
|
579
|
+
async function fetchOpenApiSchema(url: string): Promise<OpenAPIObject> {
|
|
580
|
+
try {
|
|
581
|
+
const basicAuthHeader = {
|
|
582
|
+
Authorization: 'Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs',
|
|
583
|
+
}
|
|
584
|
+
const { data } = await axios.get<OpenAPIObject>(url, {
|
|
585
|
+
headers: basicAuthHeader,
|
|
586
|
+
timeout: 10000,
|
|
587
|
+
})
|
|
588
|
+
return data
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('🚨 Failed to fetch OpenAPI schema:', error)
|
|
591
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
592
|
+
throw new Error(`Failed to fetch OpenAPI schema: ${message}`)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// =============================================================================
|
|
597
|
+
// TABLE SCHEMA CONVERSION
|
|
598
|
+
// =============================================================================
|
|
599
|
+
|
|
600
|
+
function addTransformForType(
|
|
601
|
+
field: BaseBagelField<any, any>,
|
|
602
|
+
schemaType: string,
|
|
603
|
+
format?: string
|
|
604
|
+
): void {
|
|
605
|
+
switch (schemaType) {
|
|
606
|
+
case 'boolean':
|
|
607
|
+
field.transform = (value: any) => {
|
|
608
|
+
if (value === true) return 'Yes'
|
|
609
|
+
if (value === false) return 'No'
|
|
610
|
+
return value
|
|
611
|
+
}
|
|
612
|
+
field.class = (value: any) => `pill ${value === true ? 'green' : 'light'}`
|
|
613
|
+
break
|
|
614
|
+
|
|
615
|
+
case 'string':
|
|
616
|
+
if (format === 'date' || format === 'date-time') {
|
|
617
|
+
USING_FMT_DATE = true
|
|
618
|
+
field.transform = (value: any) => fmtDate(value, { fmt: 'YYYY-MM-DD' })
|
|
619
|
+
} else if (format === 'email') {
|
|
620
|
+
field.attrs = { ...field.attrs, dir: 'ltr' }
|
|
621
|
+
} else if (format === 'phone') {
|
|
622
|
+
field.attrs = { ...field.attrs, dir: 'ltr' }
|
|
623
|
+
} else if (format === 'uri' || format === 'url') {
|
|
624
|
+
field.attrs = { ...field.attrs, dir: 'ltr' }
|
|
625
|
+
}
|
|
626
|
+
break
|
|
627
|
+
|
|
628
|
+
case 'number':
|
|
629
|
+
case 'integer':
|
|
630
|
+
field.transform = (value: any) => Number.isNaN(value) ? '' : value?.toLocaleString()
|
|
631
|
+
field.attrs = { ...field.attrs, dir: 'ltr' }
|
|
632
|
+
break
|
|
633
|
+
|
|
634
|
+
case 'array':
|
|
635
|
+
field.transform = (value: any) => {
|
|
636
|
+
if (!Array.isArray(value) || value.length === 0) return ''
|
|
637
|
+
if (typeof value[0] === 'object') return `${value.length} items`
|
|
638
|
+
return value.join(', ')
|
|
639
|
+
}
|
|
640
|
+
field.attrs = { ...field.attrs, dir: 'ltr' }
|
|
641
|
+
break
|
|
642
|
+
|
|
643
|
+
case 'object':
|
|
644
|
+
field.transform = (value: any) => {
|
|
645
|
+
if (!value || typeof value !== 'object') return ''
|
|
646
|
+
return 'Object'
|
|
647
|
+
}
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
default:
|
|
651
|
+
// Default string handling
|
|
652
|
+
break
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Adds fallback transform based on field element type
|
|
658
|
+
*/
|
|
659
|
+
function addFallbackTransform<T>(
|
|
660
|
+
tableField: BaseBagelField<T, Path<T>>,
|
|
661
|
+
fieldElementType: string | undefined
|
|
662
|
+
): void {
|
|
663
|
+
switch (fieldElementType) {
|
|
664
|
+
case 'date':
|
|
665
|
+
USING_FMT_DATE = true
|
|
666
|
+
tableField.transform = (value: any) => fmtDate(value, { fmt: 'YYYY-MM-DD' })
|
|
667
|
+
break
|
|
668
|
+
|
|
669
|
+
case 'check':
|
|
670
|
+
tableField.transform = (value: unknown) => value === true ? 'Yes' : value === false ? 'No' : value
|
|
671
|
+
tableField.class = (value: unknown) => `pill ${value === true ? 'green' : 'light'}`
|
|
672
|
+
break
|
|
673
|
+
|
|
674
|
+
case 'email':
|
|
675
|
+
tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
|
|
676
|
+
break
|
|
677
|
+
|
|
678
|
+
case 'tel':
|
|
679
|
+
tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
|
|
680
|
+
break
|
|
681
|
+
|
|
682
|
+
case 'number':
|
|
683
|
+
tableField.transform = (value: any) => {
|
|
684
|
+
if (value === null || value === undefined) return ''
|
|
685
|
+
return Number(value).toLocaleString()
|
|
686
|
+
}
|
|
687
|
+
tableField.attrs = { ...tableField.attrs, dir: 'ltr' }
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function processFieldForTable<T>(
|
|
692
|
+
field: BaseBagelField<T, Path<T>>,
|
|
693
|
+
targetSchema: SchemaObject,
|
|
694
|
+
openApiSchema: OpenAPIObject
|
|
695
|
+
): BaseBagelField<T, Path<T>> | undefined {
|
|
696
|
+
if (!field.id) return undefined
|
|
697
|
+
|
|
698
|
+
const fieldPath = field.id.toString().split('.')
|
|
699
|
+
const resolvedFieldSchema = getNestedPropertySchema(
|
|
700
|
+
fieldPath,
|
|
701
|
+
targetSchema,
|
|
702
|
+
openApiSchema
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if (!resolvedFieldSchema) {
|
|
706
|
+
logVerbose(`No schema found for field: ${field.id}`)
|
|
707
|
+
return undefined
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const tableField: BaseBagelField<T, Path<T>> = {
|
|
711
|
+
id: field.id,
|
|
712
|
+
label: field.label,
|
|
713
|
+
attrs: {},
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Handle union types (anyOf) - extract the non-null type
|
|
717
|
+
const actualSchema
|
|
718
|
+
= getNonNullSchema(resolvedFieldSchema) || resolvedFieldSchema
|
|
719
|
+
|
|
720
|
+
// Safely extract the schema type as a string
|
|
721
|
+
const schemaType = getSchemaTypeAsString(actualSchema.type)
|
|
722
|
+
addTransformForType(tableField, schemaType, actualSchema.format)
|
|
723
|
+
|
|
724
|
+
// Handle fallback based on original field element type
|
|
725
|
+
if (!tableField.transform) {
|
|
726
|
+
addFallbackTransform(tableField, field.$el)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return tableField
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Safely converts schema type to string, handling arrays and undefined values
|
|
734
|
+
*/
|
|
735
|
+
function getSchemaTypeAsString(type: SchemaObject['type']): string {
|
|
736
|
+
if (Array.isArray(type)) {
|
|
737
|
+
// For arrays of types, prefer non-null types or return the first one
|
|
738
|
+
const nonNullType = type.find(t => t !== 'null' && t !== 'undefined')
|
|
739
|
+
return nonNullType || type[0] || 'string'
|
|
740
|
+
}
|
|
741
|
+
return type || 'string'
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function convertToTableSchema<T = Record<string, unknown>>(
|
|
745
|
+
formSchema: FormSchema<T>,
|
|
746
|
+
targetSchema: SchemaObject,
|
|
747
|
+
openApiSchema: OpenAPIObject
|
|
748
|
+
): FormSchema<T> {
|
|
749
|
+
const tableSchema: FormSchema<T> = []
|
|
750
|
+
|
|
751
|
+
formSchema.forEach((field) => {
|
|
752
|
+
const processedField = processFieldForTable(
|
|
753
|
+
field,
|
|
754
|
+
targetSchema,
|
|
755
|
+
openApiSchema
|
|
756
|
+
)
|
|
757
|
+
if (processedField) {
|
|
758
|
+
tableSchema.push(processedField)
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
logVerbose(
|
|
763
|
+
`Converted ${formSchema.length} form fields to ${tableSchema.length} table fields`
|
|
764
|
+
)
|
|
765
|
+
return tableSchema
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// =============================================================================
|
|
769
|
+
// CODE GENERATION
|
|
770
|
+
// =============================================================================
|
|
771
|
+
|
|
772
|
+
function cleanOptions(options: Record<string, any>): string {
|
|
773
|
+
if (!options || typeof options !== 'object') return '{}'
|
|
774
|
+
|
|
775
|
+
const cleaned: Record<string, any> = {}
|
|
776
|
+
const skipDefaults = ['step', 'enableTime', 'multiline', 'required']
|
|
777
|
+
|
|
778
|
+
Object.entries(options).forEach(([key, value]) => {
|
|
779
|
+
const shouldSkip
|
|
780
|
+
= value === undefined
|
|
781
|
+
|| value === null
|
|
782
|
+
|| value === false
|
|
783
|
+
|| value === ''
|
|
784
|
+
|| value === 0
|
|
785
|
+
|| (skipDefaults.includes(key) && (value === 1 || value === false))
|
|
786
|
+
|| (key === 'attrs'
|
|
787
|
+
&& typeof value === 'object'
|
|
788
|
+
&& Object.keys(value).length === 0)
|
|
789
|
+
|
|
790
|
+
if (!shouldSkip) {
|
|
791
|
+
cleaned[key] = value
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
return Object.keys(cleaned).length > 0 ? JSON.stringify(cleaned) : '{}'
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function extractFieldOptions<T>(field: BaseBagelField<T, Path<T>>) {
|
|
799
|
+
const fieldProps = [
|
|
800
|
+
'required',
|
|
801
|
+
'placeholder',
|
|
802
|
+
'helptext',
|
|
803
|
+
'defaultValue',
|
|
804
|
+
'class',
|
|
805
|
+
'disabled',
|
|
806
|
+
'vIf',
|
|
807
|
+
'transform',
|
|
808
|
+
'onUpdate',
|
|
809
|
+
'validate',
|
|
810
|
+
] as const
|
|
811
|
+
|
|
812
|
+
type OptFieldProps = typeof fieldProps[number] | (string & {})
|
|
813
|
+
const options: Partial<Record<OptFieldProps, any>> = {}
|
|
814
|
+
|
|
815
|
+
fieldProps.forEach((prop) => {
|
|
816
|
+
if (field[prop] !== undefined) {
|
|
817
|
+
options[prop] = field[prop]
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
if (field.attrs && typeof field.attrs === 'object') {
|
|
822
|
+
Object.entries(field.attrs).forEach(([key, value]) => {
|
|
823
|
+
if (value !== undefined && value !== null) {
|
|
824
|
+
options[key] = value
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return options
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function generateFieldCall<T>(field: BaseBagelField<T, Path<T>>, useTableSchema: boolean): string {
|
|
833
|
+
const fieldId = field.id as string
|
|
834
|
+
const fieldLabel = field.label || formatPropertyName(fieldId)
|
|
835
|
+
const fieldOptions = extractFieldOptions(field)
|
|
836
|
+
const cleanedOptions = cleanOptions(fieldOptions)
|
|
837
|
+
|
|
838
|
+
if (useTableSchema) {
|
|
839
|
+
const options: string[] = []
|
|
840
|
+
const specialProps = ['transform', 'class', 'attrs'] as const
|
|
841
|
+
|
|
842
|
+
specialProps.forEach((prop) => {
|
|
843
|
+
if (field[prop]) {
|
|
844
|
+
const value = prop === 'attrs'
|
|
845
|
+
? JSON.stringify(field[prop])
|
|
846
|
+
: field[prop].toString()
|
|
847
|
+
options.push(`${prop}: ${value}`)
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
const optionsStr = options.length > 0 ? `{ ${options.join(', ')} }` : ''
|
|
852
|
+
return `getBaseField('${fieldId}', '${fieldLabel}'${optionsStr ? `, ${optionsStr}` : ''})`
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const fieldTypeMap: Record<string, string> = {
|
|
856
|
+
text: 'txtField',
|
|
857
|
+
email: 'emailField',
|
|
858
|
+
date: 'dateField',
|
|
859
|
+
tel: 'telField',
|
|
860
|
+
number: 'numField',
|
|
861
|
+
check: 'checkField',
|
|
862
|
+
checkbox: 'checkField',
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const fieldType = fieldTypeMap[field.$el] || 'txtField'
|
|
866
|
+
|
|
867
|
+
if (field.$el === 'select') {
|
|
868
|
+
const selectOptions
|
|
869
|
+
= field.options || (field.attrs && field.attrs.options) || []
|
|
870
|
+
const optionsStr = JSON.stringify(selectOptions)
|
|
871
|
+
return `selectField('${fieldId}', '${fieldLabel}', ${optionsStr}, ${cleanedOptions})`
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (field.$el === 'array') {
|
|
875
|
+
const arrayField = field as ArrayBagelField<any, any>
|
|
876
|
+
if (arrayField.attrs?.schema) {
|
|
877
|
+
const nestedCalls = toValue(arrayField.attrs.schema).map(
|
|
878
|
+
(nestedField: any) => generateFieldCall(nestedField, useTableSchema)
|
|
879
|
+
)
|
|
880
|
+
const nestedSchemaStr = `[${nestedCalls.join(',')}]`
|
|
881
|
+
return `arrField('${fieldId}', '${fieldLabel}', ${nestedSchemaStr})`
|
|
882
|
+
}
|
|
883
|
+
const arrayType = arrayField.attrs?.type || 'text'
|
|
884
|
+
return `arrField('${fieldId}', '${fieldLabel}', '${arrayType}')`
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return `${fieldType}('${fieldId}', '${fieldLabel}', ${cleanedOptions})`
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function generateCodeWithUtilities<T>(
|
|
891
|
+
schemaId: string,
|
|
892
|
+
formSchema: FormSchema<T>,
|
|
893
|
+
config: GeneratorConfig
|
|
894
|
+
): string {
|
|
895
|
+
const imports = new Set<string>()
|
|
896
|
+
const fieldCalls: string[] = []
|
|
897
|
+
|
|
898
|
+
formSchema.forEach((field) => {
|
|
899
|
+
const call = generateFieldCall(field, config.useTableSchema || false)
|
|
900
|
+
fieldCalls.push(call)
|
|
901
|
+
|
|
902
|
+
// Extract import from function call
|
|
903
|
+
const functionName = call.split('(')[0]
|
|
904
|
+
imports.add(functionName)
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
if (config.useTableSchema) {
|
|
908
|
+
imports.add('getBaseField')
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const importList = Array.from(imports).sort().join(', ')
|
|
912
|
+
const importStatement
|
|
913
|
+
= imports.size > 0 ? `const { ${importList} } = useForm()` : ''
|
|
914
|
+
|
|
915
|
+
return `import type { BglFormSchemaT } from '@bagelink/vue'
|
|
916
|
+
import type { ${schemaId} } from '@/client/types'
|
|
917
|
+
import { useForm ${USING_FMT_DATE ? ', fmtDate' : ''} } from '@bagelink/vue'
|
|
918
|
+
|
|
919
|
+
${importStatement}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Generated form schema for ${schemaId}
|
|
923
|
+
*/
|
|
924
|
+
export function generate${schemaId}FormSchema(): BglFormSchemaT<${schemaId}> {
|
|
925
|
+
return [${fieldCalls.join(',\n\t\t')}]
|
|
926
|
+
}`
|
|
927
|
+
.replaceAll(`value === !0`, 'value === true')
|
|
928
|
+
.replaceAll(`value === !1`, 'value === false')
|
|
929
|
+
.replaceAll(`attrs: {}`, '')
|
|
930
|
+
.replaceAll('{}', '')
|
|
931
|
+
.replaceAll('{ }', '')
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function generateCodeWithJson(
|
|
935
|
+
schemaId: string,
|
|
936
|
+
formSchema: FormSchema<any>
|
|
937
|
+
): string {
|
|
938
|
+
const stringifiedSchema = JSON.stringify(formSchema)
|
|
939
|
+
.replaceAll(`"step":1`, '')
|
|
940
|
+
.replaceAll(`"enableTime":false`, '')
|
|
941
|
+
.replaceAll(`"multiline":false`, '')
|
|
942
|
+
.replaceAll(`"required":false`, '')
|
|
943
|
+
.replaceAll(`"attrs":{}`, '')
|
|
944
|
+
.replaceAll(`,,`, ',')
|
|
945
|
+
|
|
946
|
+
return `import type { BglFormSchemaT } from '@bagelink/vue'
|
|
947
|
+
import type { ${schemaId} } from '@/client/types'
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Generated form schema for ${schemaId}
|
|
951
|
+
*/
|
|
952
|
+
export function generate${schemaId}FormSchema(): BglFormSchemaT<${schemaId}> {
|
|
953
|
+
return ${stringifiedSchema}
|
|
954
|
+
}
|
|
955
|
+
`
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function generateCode(
|
|
959
|
+
schemaId: string,
|
|
960
|
+
formSchema: FormSchema<any>,
|
|
961
|
+
config: GeneratorConfig
|
|
962
|
+
): string {
|
|
963
|
+
return config.useUtilityFunctions
|
|
964
|
+
? generateCodeWithUtilities(schemaId, formSchema, config)
|
|
965
|
+
: generateCodeWithJson(schemaId, formSchema)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// =============================================================================
|
|
969
|
+
// FILE OPERATIONS
|
|
970
|
+
// =============================================================================
|
|
971
|
+
|
|
972
|
+
async function writeGeneratedCode(
|
|
973
|
+
outputFile: string,
|
|
974
|
+
content: string
|
|
975
|
+
): Promise<void> {
|
|
976
|
+
const outputDir = path.dirname(outputFile)
|
|
977
|
+
if (!existsSync(outputDir)) {
|
|
978
|
+
mkdirSync(outputDir, { recursive: true })
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const fullOutputPath = path.resolve(cwd(), outputFile)
|
|
982
|
+
await formatAndWriteCode(fullOutputPath, content)
|
|
983
|
+
await runEsLintOnDir(outputDir)
|
|
984
|
+
|
|
985
|
+
console.log(`📁 Output file location: ${fullOutputPath}`)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// =============================================================================
|
|
989
|
+
// MAIN EXECUTION
|
|
990
|
+
// =============================================================================
|
|
991
|
+
|
|
992
|
+
async function main(): Promise<void> {
|
|
993
|
+
try {
|
|
994
|
+
console.log('⚙️ Parsing configuration...')
|
|
995
|
+
const config = parseConfiguration()
|
|
996
|
+
|
|
997
|
+
console.log(`🌐 Fetching OpenAPI schema from ${config.url}...`)
|
|
998
|
+
const openApiSchema = await fetchOpenApiSchema(config.url)
|
|
999
|
+
|
|
1000
|
+
if (!openApiSchema.components?.schemas) {
|
|
1001
|
+
throw new Error('OpenAPI schema has no components.schemas section')
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
console.log(`🔍 Finding schema: ${config.genSchemaId}`)
|
|
1005
|
+
const targetSchema = getSchemaById(config.genSchemaId, openApiSchema)
|
|
1006
|
+
if (!targetSchema) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`Schema with ID "${config.genSchemaId}" not found or is not resolvable`
|
|
1009
|
+
)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const context: SchemaContext = { openApiSchema }
|
|
1013
|
+
|
|
1014
|
+
console.log(`🔄 Converting schema to form fields...`)
|
|
1015
|
+
let formSchema = convertSchemaToForm(targetSchema, config.genSchemaId, context)
|
|
1016
|
+
|
|
1017
|
+
if (config.useTableSchema) {
|
|
1018
|
+
console.log('📊 Converting form schema to table schema...')
|
|
1019
|
+
formSchema = convertToTableSchema(formSchema, targetSchema, openApiSchema)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
console.log('🧑💻 Generating TypeScript code...')
|
|
1023
|
+
const codeContent = generateCode(config.genSchemaId, formSchema, config)
|
|
1024
|
+
|
|
1025
|
+
logVerbose(`Generated code for schema "${config.genSchemaId}":\n${codeContent}`)
|
|
1026
|
+
|
|
1027
|
+
console.log(`💾 Writing output to ${config.output}...`)
|
|
1028
|
+
await writeGeneratedCode(config.output, codeContent)
|
|
1029
|
+
|
|
1030
|
+
console.log('✅ Form schema generation completed successfully!')
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.error('❌ Error generating form schema:', error)
|
|
1033
|
+
exit(1)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Execute the main pipeline
|
|
1038
|
+
main().catch(console.error)
|