Repository: novuhq/novu
File: apps/api/src/app/workflows-v2/usecases/create-variables-object/create-variables-object.usecase.ts (TypeScript)

Code before:
    const { userId, environmentId, organizationId } = command;
    const isEnhancedDigestEnabled = await this.featureFlagService.getFlag({
      user: { _id: userId } as UserEntity,
      environment: { _id: environmentId } as EnvironmentEntity,
      organization: { _id: organizationId } as OrganizationEntity,
      key: FeatureFlagsKeysEnum.IS_ENHANCED_DIGEST_ENABLED,
      defaultValue: false,
    });
    const controlValues = await this.getControlValues(command);

    const variables = this.extractAllVariables(controlValues);
    const arrayVariables = [
      ...this.extractMailyAttribute(controlValues, MailyAttrsEnum.EACH_KEY),
      ...this.extractMailyAttribute(controlValues, MailyAttrsEnum.ID, this.extractArrayPath),
    ];
    const showIfVariables = this.extractMailyAttribute(controlValues, MailyAttrsEnum.SHOW_IF_KEY);

    const variablesObject = keysToObject(variables, arrayVariables, showIfVariables);
    if (isEnhancedDigestEnabled) {
      return this.ensureEventsArray(variablesObject);
    }

    return variablesObject;
  }

  private ensureEventsArray(variablesObject: Record<string, unknown>) {
    const stepsObject = (variablesObject.steps as Record<string, unknown>) ?? {};

    Object.keys(stepsObject)
      .filter((stepId) => typeof stepsObject[stepId] === 'object')
      .forEach((stepId) => {
        const step = stepsObject[stepId] as Record<string, unknown>;
        const hasUsedEventCount = !!step.eventCount;
        const hasUsedEventsLength = !!(
          step.events &&
          typeof step.events === 'object' &&
          !Array.isArray(step.events) &&
          'length' in step.events
        );
        const hasUsedEvents = !!(step.events && typeof step.events === 'string');
        if (hasUsedEventCount || hasUsedEventsLength || hasUsedEvents) {
          step.events = [];
        }
      });

    return variablesObject;
  }

  private async getControlValues(command: CreateVariablesObjectCommand) {
    if (command.controlValues) {
      return [command.controlValues].flat().flatMap((obj) => Object.values(obj));

Review comment (suggestion):
Whenever the `steps.digest-step.events, steps.digest-step.events.length, steps.digest-step.eventCount` variables are used we need to ensure that the `events` array variable is generated. Because the "count" and "events" variables are "connected" to each other.

Code after:
    const { userId, environmentId, organizationId } = command;
    const isEnhancedDigestEnabled = await this.featureFlagService.getFlag({
      user: { _id: userId } as UserEntity,
      environment: { _id: environmentId } as EnvironmentEntity,
      organization: { _id: organizationId } as OrganizationEntity,
      key: FeatureFlagsKeysEnum.IS_ENHANCED_DIGEST_ENABLED,
      defaultValue: false,
    });
    const controlValues = await this.getControlValues(command);

    const variables = this.extractAllVariables(controlValues);
    const arrayVariables = [
      ...this.extractMailyAttribute(controlValues, MailyAttrsEnum.EACH_KEY),
      ...this.extractMailyAttribute(controlValues, MailyAttrsEnum.ID, this.extractArrayPath),
    ];
    const showIfVariables = this.extractMailyAttribute(controlValues, MailyAttrsEnum.SHOW_IF_KEY);

    const variablesObject = keysToObject(variables, arrayVariables, showIfVariables);
    if (isEnhancedDigestEnabled) {
      return this.ensureEventsVariableIsAnArray(variablesObject);
    }

    return variablesObject;
  }

  private ensureEventsVariableIsAnArray(variablesObject: Record<string, unknown>) {
    const stepsObject = (variablesObject.steps as Record<string, unknown>) ?? {};

    Object.keys(stepsObject).forEach((stepId) => {
      const step = stepsObject[stepId] as Record<string, unknown>;
      const hasUsedEventCount = !!step.eventCount;
      const hasUsedEventsLength = !!(
        step.events &&
        typeof step.events === 'object' &&
        !Array.isArray(step.events) &&
        'length' in step.events
      );
      const hasUsedEvents = !!(step.events && typeof step.events === 'string');
      if (hasUsedEventCount || hasUsedEventsLength || hasUsedEvents) {
        step.events = [];
      }
    });

    return variablesObject;
  }

  private async getControlValues(command: CreateVariablesObjectCommand) {
    if (command.controlValues) {
      return [command.controlValues].flat().flatMap((obj) => Object.values(obj));
    }