mockup-data-create.component.html (1-296)


<div class="mt-5">
  <mat-card appearance="raised">
    <mat-card-content>
      <mat-stepper [orientation]="stepperOrientation()" [linear]="true">
        <mat-step>
          <form [formGroup]="getConfigurationsForm()">
            <ng-template matStepLabel>Type of Configuration</ng-template>
            <h5 class="mt-2">Type of Configuration</h5>
            <p class="mt-2 fst-italic">Schema configuration</p>
            <p class="mt-4">
              Please choose between manual or automatic configuration of schema
            </p>
            <div class="row mt-4">
              <div class="col-12 col-md-4">
                <mat-form-field class="w-100">
                  <mat-label>Type of Configuration</mat-label>
                  <mat-select
                    formControlName="type"
                    placeholder="Select configuration type"
                  >
                    @for (type of typeOfConfigurations; track type) {
                      <mat-option [value]="type.value">{{
                        type.viewValue
                      }}</mat-option>
                    }
                  </mat-select>
                </mat-form-field>
              </div>
            </div>
            <app-stepper-actions
              [formValidity]="getConfigurationsForm().valid"
            />
          </form>
        </mat-step>
        <mat-step>
          <ng-template matStepLabel>Configuration Process</ng-template>
          @if (selectedTypeOfConfiguration() === "manual") {
            <div class="d-flex justify-content-between">
              <h5 class="mt-2">Manual Configuration</h5>
              <button
                (click)="openSchemaInfoModal()"
                type="button"
                mat-stroked-button
                color="primary"
              >
                More info
              </button>
            </div>

            <p class="mt-2 fst-italic">Type of schema configuration</p>
            <p class="mt-4">Please choose and configure a generator.</p>
            <app-schema-config
              (updateSchema)="setSchemaConfig($event)"
              (formValidity)="handleFormValidityChange($event)"
            />
            <app-stepper-actions [formValidity]="formValidity()" />
          } @else {
            <form [formGroup]="getAutomaticConfigurationForm()">
              <h5 class="mt-2">Automatic Configuration</h5>
              <p class="mt-2 fst-italic">
                Configuration of the schema through an uploaded file
              </p>
              @if (!fileSchema()) {
                <p class="mt-4">
                  Upload a sample file to automatically configure the schema.
                </p>
                <div class="row mt-4">
                  <div class="col-12 col-md-4">
                    <input
                      class="file-input"
                      #fileUpload
                      (change)="uploadFile($event)"
                      type="file"
                      formControlName="file"
                      accept=".json,.csv,.xml"
                    />
                    <div
                      class="file-upload d-flex align-items-center justify-content-center p-5 border"
                    >
                      {{ fileName || "No file uploaded yet." }}
                      <button
                        class="ms-2"
                        (click)="fileUpload.click()"
                        mat-mini-fab
                        color="primary"
                      >
                        <mat-icon>attach_file</mat-icon>
                      </button>
                    </div>
                  </div>
                </div>
              } @else {
                <p class="mt-4">File uploaded, the json is visible here</p>
                <app-schema-config [schema]="fileSchema()" />
                <app-automatic-schema [schema]="fileSchema()" />
              }

              <app-stepper-actions />
            </form>
          }
        </mat-step>
        <mat-step>
          <form [formGroup]="getDataFormatForm()">
            <ng-template matStepLabel>Data Format</ng-template>
            <h5 class="mt-2">Data format</h5>
            <p class="mt-2 fst-italic">
              Format in which the data will be generated
            </p>
            <p class="mt-4">Please choose a data format type.</p>
            <div class="row mt-4">
              <div class="col-12 col-md-4">
                <mat-form-field class="w-100">
                  <mat-label>Data format</mat-label>
                  <mat-select formControlName="format">
                    @for (dataFormat of dataFormats; track dataFormat) {
                      <mat-option [value]="dataFormat.value">{{
                        dataFormat.viewValue
                      }}</mat-option>
                    }
                  </mat-select>
                </mat-form-field>
              </div>
            </div>
            <app-stepper-actions [formValidity]="getDataFormatForm().valid" />
          </form>
        </mat-step>
        <mat-step>
          <form [formGroup]="getGenerationModeForm()">
            <ng-template matStepLabel>Generation Mode</ng-template>
            <h5 class="mt-2">Generation mode</h5>
            <p class="mt-2 fst-italic">
              The mode in which the data will be generated.
            </p>
            <p class="mt-4">Please choose a generation mode.</p>
            <div class="row">
              <div class="col-12 col-md-4">
                <mat-form-field class="w-100">
                  <mat-label>Number of records</mat-label>
                  <input
                    [min]="0"
                    [max]="10000"
                    matInput
                    formControlName="numberOfRecords"
                  />
                </mat-form-field>
              </div>
            </div>
            <div class="row mt-4">
              <div class="col-12 col-md-4">
                <mat-form-field class="w-100">
                  <mat-label>Generation config</mat-label>
                  <mat-select formControlName="mode">
                    @for (
                      generationMode of generationModes;
                      track generationMode
                    ) {
                      <mat-option [value]="generationMode.value">{{
                        generationMode.viewValue
                      }}</mat-option>
                    }
                  </mat-select>
                </mat-form-field>
              </div>
            </div>
            @if (getGenerationModeForm().getRawValue().mode === "streaming") {
              <div class="row mt-4">
                <div class="col-12 col-md-4">
                  <mat-form-field class="w-100">
                    <mat-label>Stream batch</mat-label>
                    <input
                      [min]="0"
                      [max]="10000"
                      matInput
                      formControlName="streamBatch"
                    />
                  </mat-form-field>
                </div>
              </div>
              <div class="row mt-4">
                <div class="col-12 col-md-4">
                  <mat-form-field class="w-100">
                    <mat-label>Stream delay</mat-label>
                    <input
                      [min]="0"
                      [max]="10000"
                      matInput
                      formControlName="streamDelay"
                    />
                  </mat-form-field>
                </div>
              </div>
            }
            <app-stepper-actions
              [formValidity]="getGenerationModeForm().valid"
            />
          </form>
        </mat-step>
        <mat-step>
          <form [formGroup]="getOutputLocationsForm()">
            <ng-template matStepLabel>Output</ng-template>
            <h5 class="mt-2">Output Location</h5>
            <p class="mt-2 fst-italic">The location of the generated data.</p>
            <p class="mt-4">Please choose an output location.</p>
            <div class="row mt-4">
              <div class="col-12 col-md-4">
                <mat-form-field class="w-100">
                  <mat-label>Output location</mat-label>
                  <mat-select formControlName="output">
                    @for (
                      outputLocation of outputLocations;
                      track outputLocation
                    ) {
                      <mat-option [value]="outputLocation.value">{{
                        outputLocation.viewValue
                      }}</mat-option>
                    }
                  </mat-select>
                </mat-form-field>
              </div>
            </div>
            @if (
              getOutputLocationsForm().getRawValue().output ===
              "big_query_table"
            ) {
              <div class="row mt-4">
                <div class="col-12 col-md-4">
                  <mat-form-field class="w-100">
                    <mat-label>Dataset</mat-label>
                    <input matInput formControlName="dataset" />
                  </mat-form-field>
                </div>
              </div>
              <div class="row mt-4">
                <div class="col-12 col-md-4">
                  <mat-form-field class="w-100">
                    <mat-label>Table</mat-label>
                    <input matInput formControlName="table" />
                  </mat-form-field>
                </div>
              </div>
            }
            <div class="mt-5 d-flex flex-column">
              <div class="d-flex">
                <button
                  [disabled]="!areAllFormsValid()"
                  (click)="submitProject()"
                  mat-button
                  matStepperNext
                  mat-flat-button
                  color="primary"
                >
                  Submit
                </button>
                <button class="ms-2 text-black" mat-button routerLink="">
                  Cancel
                </button>
              </div>

              @if (!areAllFormsValid()) {
                <mat-error>
                  Please ensure all forms are filled out correctly before
                  submitting.
                </mat-error>
              }
            </div>
          </form>
        </mat-step>
        <mat-step>
          <ng-template matStepLabel>Result</ng-template>
          <div class="mt-5">
            @if (bigQuerySavedSucessfully === true) {
              <div class="alert alert-success" role="alert">
                Row in big query was succesfully added
              </div>
            } @else if (bigQuerySavedSucessfully === false) {
              <div class="alert alert-warning" role="alert">
                Failed to add row in big query
              </div>
            } @else if (bigQuerySavedSucessfully === null) {
              @defer (when consoleData().length > 0) {
                @if (consoleData() && consoleData().length > 0) {
                  <app-output-console [consoleData]="consoleData()" />
                }
              } @placeholder {
                <div class="w-100 d-flex align-items-center">
                  <mat-spinner class="mx-auto" />
                </div>
              }
            }
          </div>
        </mat-step>
      </mat-stepper>
    </mat-card-content>
  </mat-card>
</div>


mockup-data-create.component.ts (1-426)


import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  Signal,
  inject,
  signal,
} from '@angular/core';
import { StepperActionsComponent } from '../stepper-actions/stepper-actions.component';
import {
  FormArray,
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { SchemaConfigComponent } from '../schema-config/schema-config.component';
import { SchemaInfoModalComponent } from '../schema-info-modal/schema-info-modal.component';
import { MatDialog } from '@angular/material/dialog';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { apiUrl, wsUrl } from '../core/env-variables';
import { webSocket } from 'rxjs/webSocket';
import { OutputConsoleComponent } from '../output-console/output-console.component';
import { AutomaticSchemaComponent } from '../automatic-schema/automatic-schema.component';
import { BreakpointObserver } from '@angular/cdk/layout';
import { catchError, map, of, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MockDataProjectForm } from '../shared/interfaces/mock-data-project-form.interface';
import { FieldTypes } from '../schema-config/schema-config.model';
import {
  Schema,
  SchemaField,
  SchemaFieldResponse,
} from '../shared/interfaces/schema.interface';
import { MatCardModule } from '@angular/material/card';
import { MatStepperModule } from '@angular/material/stepper';
import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-mockup-data-create',
  standalone: true,
  imports: [
    StepperActionsComponent,
    SchemaConfigComponent,
    ReactiveFormsModule,
    SchemaInfoModalComponent,
    OutputConsoleComponent,
    AutomaticSchemaComponent,
    MatCardModule,
    MatButtonModule,
    MatStepperModule,
    MatSelectModule,
    MatIconModule,
    MatInputModule,
    MatProgressSpinnerModule,
  ],
  templateUrl: './mockup-data-create.component.html',
  styleUrl: './mockup-data-create.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MockupDataCreateComponent implements OnInit {
  public mockDataProjectForm!: FormGroup<MockDataProjectForm>;

  public typeOfConfigurations: {
    value: 'manual' | 'automatic';
    viewValue: string;
  }[] = [
    { value: 'manual', viewValue: 'Manual' },
    { value: 'automatic', viewValue: 'Automatic' },
  ];

  public dataFormats: {
    value: 'json_lines' | 'xml_lines' | 'csv_lines';
    viewValue: string;
  }[] = [
    { value: 'json_lines', viewValue: 'Json' },
    { value: 'xml_lines', viewValue: 'Xml' },
    { value: 'csv_lines', viewValue: 'Csv' },
  ];

  public generationModes: {
    value: 'batch' | 'streaming';
    viewValue: string;
  }[] = [
    { value: 'batch', viewValue: 'Batch' },
    { value: 'streaming', viewValue: 'Streaming' },
  ];

  public outputLocations: {
    value: 'console' | 'PubSub' | 'big_query_table';
    viewValue: string;
  }[] = [
    { value: 'console', viewValue: 'Console' },
    { value: 'PubSub', viewValue: 'PubSub' },
    { value: 'big_query_table', viewValue: 'BigQuery' },
  ];

  public selectedTypeOfConfiguration = signal<'manual' | 'automatic'>('manual');
  public fileName = '';
  public isSchemaInfoModalVisible = false;
  public fileSchema = signal<{
    fields: { name: string; type: FieldTypes }[];
  } | null>(null);
  public bigQuerySavedSucessfully: boolean | null = null;
  public consoleData = signal<string[]>([]);
  public formValidity = signal<boolean>(false);
  public stepperOrientation: Signal<'horizontal' | 'vertical'> =
    signal('horizontal');

  private schemaConfig: SchemaField[] = [];

  private readonly destroyRef = inject(DestroyRef);
  private readonly matDialog = inject(MatDialog);
  private readonly httpClient = inject(HttpClient);
  private readonly breakpointObserver = inject(BreakpointObserver);
  private readonly matSnackBar = inject(MatSnackBar);

  public getConfigurationsForm(): FormGroup<
    MockDataProjectForm['configuration']['controls']
  > {
    return this.mockDataProjectForm.controls.configuration;
  }

  public getAutomaticConfigurationForm(): FormGroup<
    MockDataProjectForm['automaticConfiguration']['controls']
  > {
    return this.mockDataProjectForm.controls.automaticConfiguration;
  }

  public getManualConfigurationForm(): FormArray<
    MockDataProjectForm['manualConfiguration']['controls'][number]
  > {
    return this.mockDataProjectForm.controls.manualConfiguration;
  }

  public getDataFormatForm(): FormGroup<
    MockDataProjectForm['dataFormat']['controls']
  > {
    return this.mockDataProjectForm.controls.dataFormat;
  }

  public getGenerationModeForm(): FormGroup<
    MockDataProjectForm['generationMode']['controls']
  > {
    return this.mockDataProjectForm.controls.generationMode;
  }

  public getOutputLocationsForm(): FormGroup<
    MockDataProjectForm['outputLocations']['controls']
  > {
    return this.mockDataProjectForm.controls.outputLocations;
  }

  constructor() {
    this.stepperOrientation = toSignal(
      this.breakpointObserver
        .observe('(min-width: 1000px)')
        .pipe(map(({ matches }) => (matches ? 'horizontal' : 'vertical'))),
      { initialValue: 'horizontal' },
    );
  }

  public ngOnInit(): void {
    this.createForm();

    this.getConfigurationsForm()
      .valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((response) => {
        if (response.type) this.selectedTypeOfConfiguration.set(response.type);
      });
  }

  public areAllFormsValid(): boolean {
    return (
      this.getConfigurationsForm().valid &&
      (this.selectedTypeOfConfiguration() === 'manual'
        ? this.formValidity()
        : this.getAutomaticConfigurationForm().valid) &&
      this.getDataFormatForm().valid &&
      this.getGenerationModeForm().valid &&
      this.getOutputLocationsForm().valid
    );
  }

  public handleFormValidityChange(isValid: boolean): void {
    this.formValidity.set(isValid);
  }

  public openSchemaInfoModal(): void {
    this.matDialog.open(SchemaInfoModalComponent, {
      width: '750px',
    });
  }

  public uploadFile($event: any): void {
    const selectedFile = $event.target.files[0];
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(selectedFile);

    fileReader.onloadend = (evt: any) => {
      const fileByteArray = [];
      if (evt.target.readyState === FileReader.DONE) {
        const arrayBuffer = evt.target.result;
        const array = new Uint8Array(arrayBuffer);

        for (const a of array) {
          fileByteArray.push(a);
        }
      }

      const body = {
        filename: selectedFile.name,
        contents: fileByteArray,
      };

      this.httpClient
        .post(apiUrl + '/generate-schema', body)
        .pipe(
          takeUntilDestroyed(this.destroyRef),
          tap((response: any) => {
            this.fileSchema.set(response);
          }),
          catchError(() => {
            return of(this.matSnackBar.open('Data generation failed', 'Ok'));
          }),
        )
        .subscribe();
    };
  }

  public setSchemaConfig($event: SchemaField[]): void {
    this.schemaConfig = $event.map((field) => {
      if (field.type === FieldTypes.Array) {
        return {
          ...field,
          schema: {
            type: FieldTypes.Array,
            col: field.schema.nestedSchema,
          },
        };
      }
      return field;
    }) as any;
  }
  public submitProject(): void {
    const fields =
      this.getConfigurationsForm().value.type === 'automatic'
        ? (this.fileSchema()?.fields as any)
        : (this.schemaConfig.map((response: SchemaField) => {
            let field: SchemaFieldResponse = {
              name: response.name,
              col: {
                type: response.type,
                generator: { ...response.schema },
              },
            };

            if (response.type === FieldTypes.Decimal) {
              field.col.scale = response.schema['scale'];
              delete field.col.generator!['scale'];
            } else if (
              response.type === FieldTypes.Date ||
              response.type === FieldTypes.DateTime ||
              response.type === FieldTypes.Time
            ) {
              field.col.pattern = response.schema['pattern'];
              delete field.col.generator!['pattern'];
            } else if (response.type === FieldTypes.Array) {
              field = {
                name: response.name,
                col: {
                  type: FieldTypes.Array,
                  length: response.arrayLength,
                  col: {
                    type: response.schema['col'][0].type,
                    generator: { ...response.schema['col'][0].schema },
                  },
                },
              };

              if (response.schema['col'][0].type === FieldTypes.Decimal) {
                field.col.col!.scale = response.schema['col'][0].schema.scale;
                delete field.col.col!.generator!['scale'];
              } else if (
                response.schema['col'][0].type === FieldTypes.Date ||
                response.schema['col'][0].type === FieldTypes.DateTime ||
                response.schema['col'][0].type === FieldTypes.Time
              ) {
                field.col.col!.pattern =
                  response.schema['col'][0].schema.pattern;
                delete field.col.col!.generator!['pattern'];
              }
            }

            return field;
          }) as SchemaFieldResponse[]);

    const schema: Schema = {
      num_records: this.getGenerationModeForm().value.numberOfRecords!,
      schema: {
        fields: fields,
      },
      data_format: {
        type: this.getDataFormatForm().value.format!,
      },
      output: {
        type: this.getOutputLocationsForm().value.output!,
        dataset: this.getOutputLocationsForm().value.dataset!,
        table: this.getOutputLocationsForm().value.table!,
        saving_mode: {
          type: this.getGenerationModeForm().value.mode!,
          delay: this.getGenerationModeForm().value.streamDelay!,
          num_of_records: this.getGenerationModeForm().value.numberOfRecords!,
        },
      },
    };

    if (this.getOutputLocationsForm().value.output === 'big_query_table') {
      this.httpClient
        .post(`${apiUrl}/generate`, schema, { responseType: 'text' })
        .pipe(
          takeUntilDestroyed(this.destroyRef),
          tap(() => {
            this.bigQuerySavedSucessfully = true;
            this.createForm();
          }),
          catchError((err: HttpErrorResponse) => {
            this.bigQuerySavedSucessfully = false;
            return of('Data generation failed');
          }),
        )
        .subscribe();
    } else if (this.getOutputLocationsForm().value.output === 'console') {
      console.log(schema);
      this.getConsoleResponse(schema);
    }
  }

  private getConsoleResponse(schema: Schema) {
    const customWebSocketConfig = {
      url: wsUrl,
      deserializer: (e: MessageEvent) => {
        return e.data.split('\n') as string[];
      },
    };

    const ws = webSocket<any>(customWebSocketConfig);
    ws.next(schema);
    this.createForm();

    ws.asObservable()
      .pipe(
        takeUntilDestroyed(this.destroyRef),
        tap((response: string[]) => {
          this.consoleData.update((previous) => [...response, ...previous]);
        }),
        catchError((err: any) => {
          return of(this.matSnackBar.open('Data generation failed', 'Ok'));
        }),
      )
      .subscribe();
  }

  private createForm() {
    this.consoleData.set([]);
    this.mockDataProjectForm = new FormGroup<MockDataProjectForm>({
      configuration: new FormGroup({
        type: new FormControl<'manual' | 'automatic'>('manual', {
          nonNullable: true,
          validators: [Validators.required],
        }),
      }),
      automaticConfiguration: new FormGroup({
        file: new FormControl<File | null>(null),
      }),
      manualConfiguration: new FormArray<FormGroup>([]),
      dataFormat: new FormGroup({
        format: new FormControl<'json_lines' | 'xml_lines' | 'csv_lines'>(
          'json_lines',
          { nonNullable: true, validators: [Validators.required] },
        ),
      }),
      generationMode: new FormGroup({
        mode: new FormControl<'batch' | 'streaming'>('batch', {
          nonNullable: true,
          validators: [Validators.required],
        }),
        numberOfRecords: new FormControl(10, {
          nonNullable: true,
          validators: [Validators.required],
        }),
        streamBatch: new FormControl(1, {
          nonNullable: true,
          validators: [Validators.required],
        }),
        streamDelay: new FormControl(2000, {
          nonNullable: true,
          validators: [Validators.required],
        }),
      }),
      outputLocations: new FormGroup({
        output: new FormControl<'console' | 'PubSub' | 'big_query_table'>(
          'console',
          { nonNullable: true, validators: [Validators.required] },
        ),
        dataset: new FormControl('marton_sandor_dataset', {
          nonNullable: true,
          validators: [Validators.required],
        }),
        table: new FormControl('alpakka_test', {
          nonNullable: true,
          validators: [Validators.required],
        }),
      }),
    });
  }
}

export default MockupDataCreateComponent;


int-field.component.html (1-99)


<form class="row" [formGroup]="fieldForm">
  <div class="col-12">
    <mat-form-field class="w-100">
      <mat-label>Generator</mat-label>
      <mat-select [formControl]="type">
        @for (generator of generatorTypes(); track generator) {
          <mat-option [value]="generator.value">{{
            generator.viewValue
          }}</mat-option>
        }
      </mat-select>
      <mat-hint>Choose how to generate values for this field</mat-hint>
    </mat-form-field>
  </div>

  @if (fieldForm.get("min")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Minimum Value</mat-label>
        <input matInput formControlName="min" placeholder="e.g., 1" />
        <mat-hint>Enter the minimum value of the range</mat-hint>
        <mat-error>{{ fieldForm.get("min") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("max")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Maximum Value</mat-label>
        <input matInput formControlName="max" placeholder="e.g., 100" />
        <mat-hint>Enter the maximum value of the range</mat-hint>
        <mat-error>{{ fieldForm.get("max") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("values")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Values</mat-label>
        <input
          placeholder="e.g., 10, 20, 30"
          matInput
          formControlName="values"
        />
        <mat-hint>Enter numeric values separated by commas</mat-hint>
        <mat-error>{{ fieldForm.get("values") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("weights")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Weights</mat-label>
        <input
          [readonly]="autoRenew()"
          placeholder="e.g., 0.3, 0.3, 0.4"
          matInput
          formControlName="weights"
        />
        <mat-hint>
          Enter decimal weights between 0 and 1, separated by commas. The
          weights should sum to 1.
        </mat-hint>
        <mat-error>{{ fieldForm.get("weights") | appErrorMessage }}</mat-error>
        <button (click)="onToggleAutorenew()" matIconSuffix mat-icon-button>
          <mat-icon>{{ autoRenew() ? "autorenew" : "pause" }}</mat-icon>
        </button>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("start")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Start</mat-label>
        <input placeholder="e.g., 0" matInput formControlName="start" />
        <mat-hint>Enter the starting value of the sequence</mat-hint>
        <mat-error>{{ fieldForm.get("start") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }

  @if (fieldForm.get("step")) {
    <div class="col-12">
      <mat-form-field class="w-100">
        <mat-label>Step</mat-label>
        <input placeholder="e.g., 1" matInput formControlName="step" />
        <mat-hint
          >Enter the step size to increment or decrement the value</mat-hint
        >
        <mat-error>{{ fieldForm.get("step") | appErrorMessage }}</mat-error>
      </mat-form-field>
    </div>
  }
</form>


int-field.component.ts (1-314)


import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  inject,
  input,
  output,
  signal,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
  FormControl,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { startWith } from 'rxjs';
import { fieldGeneratorMap, GeneratorType } from '../schema-config.model';
import { compareToOtherFieldValidator } from '../../shared/validators/compare-to-other-field-validator.validator';
import { matchingArrayLengthsValidator } from '../../shared/validators/matching-array-lenghts-validator.validator';
import { numbersCommaSeparatedValidator } from '../../shared/validators/numbers-comma-separate-validator.validator';
import { SchemaFormFieldGeneratorService } from '../../shared/services/schema-form-field-generator.service';
import { TitleCasePipe } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { RepeatMaskDirective } from '../../shared/directives/repeat-mask.directive';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ErrorMessagePipe } from '../../shared/pipes/error-message.pipe';

type IntFieldGenerator =
  | { type: GeneratorType.Uniform; min: number; max: number }
  | { type: GeneratorType.Enum; values: number[]; weights: number[] }
  | { type: GeneratorType.NumericRange; start: number; step: number }
  | { type: GeneratorType.Values; values: number[] };

interface IntFieldForm {
  type: FormControl<GeneratorType>;
  min?: FormControl<number>;
  max?: FormControl<number>;
  values?: FormControl<string>;
  weights?: FormControl<string>;
  start?: FormControl<number>;
  step?: FormControl<number>;
}

@Component({
  selector: 'app-int-field',
  standalone: true,
  imports: [
    MatFormFieldModule,
    MatSelectModule,
    ReactiveFormsModule,
    MatInputModule,
    ReactiveFormsModule,
    TitleCasePipe,
    RepeatMaskDirective,
    MatButtonModule,
    MatIconModule,
    ErrorMessagePipe,
  ],
  templateUrl: './int-field.component.html',
  styleUrl: './int-field.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IntFieldComponent implements OnInit {
  private readonly schemaFormFieldGeneratorSvc = inject(
    SchemaFormFieldGeneratorService,
  );
  private readonly destroyRef = inject(DestroyRef);

  public readonly autoRenew = signal(true);

  public readonly generatorTypes =
    input.required<typeof fieldGeneratorMap.int_col>();

  public readonly changeField = output<{
    schema: IntFieldGenerator | undefined;
    valid: boolean;
  }>();

  public readonly type = this.schemaFormFieldGeneratorSvc.getTypeField();
  public readonly min = this.schemaFormFieldGeneratorSvc.getMinField([
    Validators.min(-2147483648),
    compareToOtherFieldValidator(
      'max',
      (min: number, max: number) => min < max,
      'minGreaterThanMax',
    ),
  ]);
  public readonly max = this.schemaFormFieldGeneratorSvc.getMaxField([
    Validators.max(2147483648),
    compareToOtherFieldValidator(
      'min',
      (max: number, min: number) => min < max,
      'MaxSmallerThanMin',
    ),
  ]);
  public readonly values = this.schemaFormFieldGeneratorSvc.getValuesField([
    numbersCommaSeparatedValidator,
  ]);
  public readonly weights = this.schemaFormFieldGeneratorSvc.getWeightsField();
  public readonly start = this.schemaFormFieldGeneratorSvc.getStartField();
  public readonly step = this.schemaFormFieldGeneratorSvc.getStepField();
  public readonly fieldForm = new FormGroup<IntFieldForm>({
    type: this.type,
  });

  ngOnInit(): void {
    this.type.valueChanges
      .pipe(startWith(this.type.value), takeUntilDestroyed(this.destroyRef))
      .subscribe((type) => {
        this.updateFormControls(type);
      });

    this.fieldForm.valueChanges
      .pipe(
        startWith(this.fieldForm.value),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe(() => {
        const schema = this.getTypedFormValue();
        this.changeField.emit({
          schema,
          valid: this.fieldForm.valid,
        });
      });

    this.min.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.max.updateValueAndValidity({ emitEvent: false });
      });

    this.max.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.min.updateValueAndValidity({ emitEvent: false });
      });

    this.values.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        if (this.autoRenew() && this.values.valid) this.updateWeights();
      });

    this.weights.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        this.values.updateValueAndValidity({ emitEvent: false });
      });
  }

  private updateFormControls(type: GeneratorType): void {
    switch (type) {
      case GeneratorType.Uniform:
        this.fieldForm.removeControl('values');
        this.fieldForm.removeControl('weights');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('min', this.min);
        this.fieldForm.addControl('max', this.max);

        break;

      case GeneratorType.Enum:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('values', this.values);
        this.fieldForm.addControl('weights', this.weights);

        this.updateValuesAndWeightsValidation();

        break;

      case GeneratorType.NumericRange:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('values');
        this.fieldForm.removeControl('weights');

        this.fieldForm.addControl('start', this.start);
        this.fieldForm.addControl('step', this.step);
        break;

      case GeneratorType.Values:
        this.fieldForm.removeControl('min');
        this.fieldForm.removeControl('max');
        this.fieldForm.removeControl('weights');
        this.fieldForm.removeControl('start');
        this.fieldForm.removeControl('step');

        this.fieldForm.addControl('values', this.values);

        this.values.removeValidators(
          matchingArrayLengthsValidator(['weights']),
        );
        break;
    }
  }
  private getTypedFormValue(): IntFieldGenerator | undefined {
    const { type, min, max, values, weights, start, step } =
      this.fieldForm.value;

    switch (type) {
      case GeneratorType.Uniform:
        if (min && max) return { type, min: +min, max: +max };

        break;

      case GeneratorType.Enum:
        if (values && weights)
          return {
            type,
            values: values.split(',').map((v) => +v.trim()),
            weights: weights.split(',').map((v) => parseFloat(v)),
          };

        break;

      case GeneratorType.NumericRange:
        if (start && step)
          return {
            type,
            start: +start,
            step: +step,
          };

        break;

      case GeneratorType.Values:
        if (values)
          return {
            type,
            values: values.split(',').map((v) => +v.trim()),
          };

        break;
    }

    return undefined;
  }

  private calculateWeights(values: string): string {
    const valueArray = values
      .split(',')
      .map((v) => v.trim())
      .filter((v) => v !== '');

    if (valueArray.length === 0) return '';

    const weight = (1 / valueArray.length).toFixed(2);

    return Array(valueArray.length).fill(weight).join(', ');
  }

  private updateValuesAndWeightsValidation(): void {
    if (this.autoRenew()) {
      this.values.removeValidators(matchingArrayLengthsValidator(['weights']));
      this.weights.removeValidators(matchingArrayLengthsValidator(['values']));
    } else {
      this.values.addValidators(matchingArrayLengthsValidator(['weights']));
      this.weights.addValidators(matchingArrayLengthsValidator(['values']));
    }

    this.values.updateValueAndValidity({ emitEvent: false });
    this.weights.updateValueAndValidity({ emitEvent: false });
  }

  private updateWeights(): void {
    const calculatedWeights = this.calculateWeights(this.values.value || '');
    this.weights.setValue(calculatedWeights, { emitEvent: true });
  }

  public onToggleAutorenew(): void {
    this.autoRenew.update((value) => !value);

    this.updateValuesAndWeightsValidation();

    if (this.autoRenew()) this.updateWeights();
  }

  public getErrorMessage(controlName: string): string {
    const control = this.fieldForm.get(controlName);

    if (control && control.invalid && (control.touched || control.dirty)) {
      const errors = control.errors;
      if (errors) {
        const errorMessages: { [key: string]: string } = {
          required: 'This field is required',
          min: `Minimum value is ${errors['min'].min}`,
          max: `Maximum value is ${errors['max'].max}`,
          minGreaterThanMax: 'Minimum value should be less than maximum value',
          MaxSmallerThanMin:
            'Maximum value should be greater than minimum value',
          numbersCommaSeparated: 'Please enter valid comma-separated numbers',
          matchingArrayLengths: 'The number of values and weights should match',
        };

        const firstErrorKey = Object.keys(errors)[0];
        return errorMessages[firstErrorKey] || 'Invalid input';
      }
    }

    return '';
  }
}


schema-config.component.html (1-180)


<div>
  <form [formGroup]="schemaConfigForm">
    <ng-container formArrayName="generatorsArray">
      @for (
        generator of generatorsArray.controls;
        track generator;
        let idx = $index
      ) {
        <div class="border my-2" [formGroup]="generator">
          <div class="mt-3 row mx-2">
            @if (!isArray()) {
              <div class="col-12 col-md-2">
                <mat-form-field class="w-100">
                  <mat-label>Field name</mat-label>
                  <input matInput formControlName="name" />
                </mat-form-field>
              </div>
            }
            <div
              class="col-12"
              [ngClass]="{ 'col-md-3': !isArray(), 'col-md-5': isArray() }"
            >
              <mat-form-field class="w-100">
                <mat-label>Select field type</mat-label>
                <mat-select formControlName="type">
                  @for (fieldType of fieldTypeOptions; track fieldType) {
                    <mat-option [value]="fieldType.value">{{
                      fieldType.viewValue
                    }}</mat-option>
                  }
                </mat-select>
              </mat-form-field>
            </div>

            @if (generator.controls.type.value === fieldTypeEnum.Array) {
              <mat-form-field class="col-6">
                <mat-label>Array length</mat-label>
                <input matInput formControlName="arrayLength" />
              </mat-form-field>
            }

            @if (generator.controls.type.value === fieldTypeEnum.Array) {
              <div class="col-1 d-flex justify-content-center">
                <button
                  [disabled]="!(generatorsArray.length > 1)"
                  (click)="removeField(idx)"
                  mat-mini-fab
                  color="warn"
                  type="button"
                >
                  <mat-icon>delete</mat-icon>
                </button>
              </div>
            }

            <div
              [ngClass]="{
                'col-11': generator.controls.type.value === fieldTypeEnum.Array,
                'col-12 col-md-6':
                  generator.controls.type.value !== fieldTypeEnum.Array
              }"
            >
              @if (generator.controls.type.value === fieldTypeEnum.Array) {
                <div class="ml-4">
                  <app-schema-config
                    [isNested]="true"
                    [isArray]="true"
                    (updateSchema)="updateNestedSchema($event, generator)"
                  />
                </div>
              } @else {
                @switch (generator.controls.type.value) {
                  @case (fieldTypeEnum.Boolean) {
                    <app-boolean-field
                      [generatorTypes]="fieldGeneratorMap.boolean_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Int) {
                    <app-int-field
                      [generatorTypes]="fieldGeneratorMap.int_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Float) {
                    <app-float-field
                      [generatorTypes]="fieldGeneratorMap.float_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Date) {
                    <app-date-field
                      [generatorTypes]="fieldGeneratorMap.date_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Time) {
                    <app-time-field
                      [generatorTypes]="fieldGeneratorMap.time_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.DateTime) {
                    <app-date-time-field
                      [generatorTypes]="fieldGeneratorMap.date_time_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Time) {
                    <app-time-field
                      [generatorTypes]="fieldGeneratorMap.time_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Timestamp) {
                    <app-timestamp-field
                      [generatorTypes]="fieldGeneratorMap.date_time_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Decimal) {
                    <app-decimal-field
                      [generatorTypes]="fieldGeneratorMap.decimal_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Double) {
                    <app-double-field
                      [generatorTypes]="fieldGeneratorMap.double_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.Long) {
                    <app-long-field
                      [generatorTypes]="fieldGeneratorMap.long_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                  @case (fieldTypeEnum.String) {
                    <app-string-field
                      [generatorTypes]="fieldGeneratorMap.string_col"
                      (changeField)="setField($event, generator, idx)"
                    />
                  }
                }
              }
            </div>

            @if (generator.controls.type.value !== fieldTypeEnum.Array) {
              <div class="col-1 d-flex justify-content-center">
                <button
                  [disabled]="!(generatorsArray.length > 1)"
                  (click)="removeField(idx)"
                  mat-mini-fab
                  color="warn"
                  type="button"
                >
                  <mat-icon>delete</mat-icon>
                </button>
              </div>
            }
          </div>
        </div>
      }
    </ng-container>

    @if (!isNested()) {
      <button
        class="mb-3"
        (click)="addField()"
        mat-mini-fab
        color="primary"
        type="button"
      >
        <mat-icon>add</mat-icon>
      </button>
    }
  </form>
</div>


schema-config.component.ts (1-161)


import {
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  OnInit,
  inject,
  input,
  output,
} from '@angular/core';
import { FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {
  FieldTypes,
  GeneratorType,
  fieldGeneratorMap,
  fieldTypeOptions,
} from './schema-config.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BooleanFieldComponent } from './boolean-field/boolean-field.component';
import { DateFieldComponent } from './date-field/date-field.component';
import { DateTimeFieldComponent } from './date-time-field/date-time-field.component';
import { DecimalFieldComponent } from './decimal-field/decimal-field.component';
import { DoubleFieldComponent } from './double-field/double-field.component';
import { FloatFieldComponent } from './float-field/float-field.component';
import { IntFieldComponent } from './int-field/int-field.component';
import { LongFieldComponent } from './long-field/long-field.component';
import { StringFieldComponent } from './string-field/string-field.component';
import { TimestampFieldComponent } from './timestamp-field/timestamp-field.component';
import {
  SchemaField,
  SchemaFieldFormGroup,
} from '../shared/interfaces/schema.interface';
import { TimeFieldComponent } from './time-field/time-field.component';
import { SchemaFormFieldGeneratorService } from '../shared/services/schema-form-field-generator.service';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { NgClass } from '@angular/common';

@Component({
  selector: 'app-schema-config',
  standalone: true,
  imports: [
    NgClass,
    MatButtonModule,
    MatSelectModule,
    MatFormFieldModule,
    MatIconModule,
    MatInputModule,
    ReactiveFormsModule,
    MatCheckboxModule,
    BooleanFieldComponent,
    DateFieldComponent,
    DateTimeFieldComponent,
    DecimalFieldComponent,
    DoubleFieldComponent,
    FloatFieldComponent,
    IntFieldComponent,
    LongFieldComponent,
    StringFieldComponent,
    TimestampFieldComponent,
    TimeFieldComponent,
  ],
  templateUrl: './schema-config.component.html',
  styleUrl: './schema-config.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SchemaConfigComponent implements OnInit {
  private readonly schemaFormFieldGeneratorSvc = inject(
    SchemaFormFieldGeneratorService,
  );
  private readonly destroyRef = inject(DestroyRef);

  private readonly fieldValidities: { [key: number]: boolean } = {};

  public readonly fieldTypeOptions = fieldTypeOptions;
  public readonly fieldGeneratorMap = fieldGeneratorMap;
  public readonly fieldTypeEnum = FieldTypes;

  public readonly updateSchema = output<SchemaField[]>();
  public readonly formValidity = output<boolean>();
  public readonly schema = input<any>(null);
  public readonly isNested = input<boolean>(false);
  public readonly isArray = input<boolean>(false);

  public schemaConfigForm =
    this.schemaFormFieldGeneratorSvc.getSchemaConfigForm();

  public get generatorsArray(): FormArray<FormGroup<SchemaFieldFormGroup>> {
    return this.schemaConfigForm.controls.generatorsArray;
  }

  public updateNestedSchema(
    nestedSchema: SchemaField[],
    generator: FormGroup<SchemaFieldFormGroup>,
  ) {
    generator.patchValue({
      schema: { type: FieldTypes.Array, nestedSchema },
    });
  }

  public ngOnInit(): void {
    this.schemaConfigForm.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => {
        const updatedSchema = this.schemaConfigForm.value.generatorsArray?.map(
          (field) => {
            const baseField: Partial<SchemaField> = {
              name: field.name,
              type: field.type as FieldTypes,
              schema: field.schema as {
                type: GeneratorType;
                [key: string]: any;
                nestedSchema: SchemaField;
              },
            };

            baseField.arrayLength = +field.arrayLength!;

            return baseField as SchemaField;
          },
        );

        console.log(updatedSchema);

        this.updateFormValidity();
        this.updateSchema.emit(updatedSchema as SchemaField[]);
      });
  }

  public addField(): void {
    this.generatorsArray.push(
      this.schemaFormFieldGeneratorSvc.getGeneratorArray(),
    );
  }

  public removeField(index: number): void {
    this.generatorsArray.removeAt(index);
  }

  public setField(
    event: { schema: any; valid: boolean },
    generatorForm: FormGroup<SchemaFieldFormGroup>,
    index: number,
  ): void {
    generatorForm.controls.schema.setValue(event.schema);
    this.fieldValidities[index] = event.valid;
    this.updateFormValidity();
  }

  private updateFormValidity(): void {
    const allFieldsValid = Object.values(this.fieldValidities).every(
      (isValid) => isValid,
    );
    const formValid = this.schemaConfigForm.valid && allFieldsValid;
    this.formValidity.emit(formValid);
  }
}


schema-form-field-generator.service.ts (1-270)


import { Injectable } from '@angular/core';
import {
  FormArray,
  FormControl,
  FormGroup,
  ValidatorFn,
  Validators,
} from '@angular/forms';
import {
  FieldTypes,
  GeneratorType,
} from '../../schema-config/schema-config.model';
import {
  SchemaConfigForm,
  SchemaFieldFormGroup,
} from '../interfaces/schema.interface';
import { weightsSumToOneValidator } from '../validators/weights-sum-to-one-validator.validator';
import { numbersCommaSeparatedValidator } from '../validators/numbers-comma-separate-validator.validator';

@Injectable({
  providedIn: 'root',
})
export class SchemaFormFieldGeneratorService {
  public getScaleField(
    scale: number = 1,
    validators?: ValidatorFn[],
  ): FormControl<number> {
    return new FormControl<number>(scale, {
      nonNullable: true,
      validators: [
        Validators.required,
        Validators.pattern(/^[1-9]\d*$/),
        ...(validators || []),
      ],
    });
  }

  public getTypeField(
    generatorType: GeneratorType = GeneratorType.Uniform,
    validators?: ValidatorFn[],
  ) {
    return new FormControl<GeneratorType>(generatorType, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getMinField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(1, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getMaxField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(100, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getMeanField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(0, {
      nonNullable: true,
      validators: [
        Validators.required,
        Validators.min(0),
        ...(validators || []),
      ],
    });
  }

  public getStdDevField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(1, {
      nonNullable: true,
      validators: [
        Validators.required,
        Validators.min(0),
        ...(validators || []),
      ],
    });
  }

  public getValuesField(validators?: ValidatorFn[]): FormControl<string> {
    return new FormControl<string>('', {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getWeightsField(validators?: ValidatorFn[]): FormControl<string> {
    return new FormControl<string>('', {
      nonNullable: true,
      validators: [
        Validators.required,
        numbersCommaSeparatedValidator,
        weightsSumToOneValidator,
        ...(validators || []),
      ],
    });
  }

  public getStartField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(1, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getEndField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(1, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getStartTimeField(validators?: ValidatorFn[]) {
    return new FormControl<string>('00:00:00', {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getEndTimeField(validators?: ValidatorFn[]) {
    return new FormControl<string>('23:59:59', {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getStartDateField(validators?: ValidatorFn[]) {
    return new FormControl<Date>(new Date(), {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getEndDateField(validators?: ValidatorFn[]) {
    const nextWeek = new Date();
    nextWeek.setDate(nextWeek.getDate() + 7);

    return new FormControl<Date>(nextWeek, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getStartDateTimeField(validators?: ValidatorFn[]) {
    const now = new Date();
    const startDateTime = this.formatLocalDateTime(now);

    return new FormControl<string>(startDateTime, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getEndDateTimeField(validators?: ValidatorFn[]) {
    const endDate = new Date();
    endDate.setDate(endDate.getDate() + 7);
    const endDateTime = this.formatLocalDateTime(endDate);

    return new FormControl<string>(endDateTime, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getStartTimeStampField(validators?: ValidatorFn[]) {
    const now = new Date();
    const startTimestamp = now.toISOString();

    return new FormControl<string>(startTimestamp, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getEndTimestampField(validators?: ValidatorFn[]) {
    const endDate = new Date();
    endDate.setDate(endDate.getDate() + 7);
    const endTimestamp = endDate.toISOString();

    return new FormControl<string>(endTimestamp, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getStepField(validators?: ValidatorFn[]): FormControl<number> {
    return new FormControl<number>(1, {
      nonNullable: true,
      validators: [
        Validators.required,
        Validators.pattern(/^-?(?!0(?:\.0+)?$)\d*\.?\d+$/),
        ...(validators || []),
      ],
    });
  }

  public getDurationField(
    duration: string = '1 days',
    validators?: ValidatorFn[],
  ): FormControl<string> {
    return new FormControl<string>(duration, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getDescriptionField(validators?: ValidatorFn[]): FormControl<string> {
    return new FormControl<string>('', {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getTimePatternField(validators?: ValidatorFn[]): FormControl<string> {
    return new FormControl<string>('HH:mm:ss', {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getDatePatternField(
    pattern: string = 'yyyy-MM-dd',
    validators?: ValidatorFn[],
  ): FormControl<string> {
    return new FormControl<string>(pattern, {
      nonNullable: true,
      validators: [Validators.required, ...(validators || [])],
    });
  }

  public getSchemaConfigForm(): FormGroup<SchemaConfigForm> {
    return new FormGroup({
      generatorsArray: new FormArray([this.getGeneratorArray()]),
    });
  }

  public getGeneratorArray(): FormGroup<SchemaFieldFormGroup> {
    return new FormGroup<SchemaFieldFormGroup>({
      name: new FormControl('', {
        nonNullable: true,
        validators: [Validators.required],
      }),
      type: new FormControl<FieldTypes | null>(null, {
        validators: [Validators.required],
      }),
      schema: new FormControl<Record<string, any> | null>({}),
      arrayLength: new FormControl<number>(0, {
        nonNullable: true,
        validators: [Validators.required, Validators.min(0)],
      }),
    });
  }

  private formatLocalDateTime(date: Date): string {
    const pad = (n: number) => (n < 10 ? '0' + n : n.toString());

    const year = date.getFullYear();
    const month = pad(date.getMonth() + 1);
    const day = pad(date.getDate());
    const hours = pad(date.getHours());
    const minutes = pad(date.getMinutes());
    const seconds = pad(date.getSeconds());
    return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
  }
}


Do you have any idea how i can make that form Validation work even for nestedSchemas? For example in schema-config.component.ts i tried to log the updatedSchema dn that's an example of how the form looks with an int and an array with a nested int inside "[

    {

        "name": "test",

        "type": "int_col",

        "schema": {

            "type": "uniform_generator",

            "min": 1,

            "max": 100

        },

        "arrayLength": 0

    },

    {

        "name": "test_array",

        "type": "array_col",

        "schema": {

            "type": "array_col",

            "nestedSchema": [

                {

                    "name": "",

                    "type": "int_col",

                    "schema": {

                        "type": "uniform_generator",

                        "min": 1,

                        "max": 100

                    },

                    "arrayLength": 0

                }

            ]

        },

        "arrayLength": 10

    }

]"