@bagelink/vue 1.4.42 → 1.4.44

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,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)