VoxaAI/voxa-cli

View on GitHub
src/InteractionBuilder.ts

Summary

Maintainability
B
4 hrs
Test Coverage
/*
 * Copyright (c) 2018 Rain Agency <contact@rain.agency>
 * Author: Rain Agency <contact@rain.agency>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
/* tslint:disable:no-console no-submodule-imports */
import fsExtra from "fs-extra";
import _ from "lodash";
import path from "path";
import { AlexaSchema } from "./AlexaSchema";
import { transform } from "./connectors/Spreadsheet";
import { DialogflowSchema } from "./DialogflowSchema";
import { downloadDirs } from "./Drive";
import { IFileContent, IInvocation, Schema } from "./Schema";
import { IPlatformSheets, IVoxaSheet } from "./VoxaSheet";
const fs = Promise.promisifyAll(fsExtra);

export type ISupportedPlatforms = "alexa" | "dialogflow";

export interface IInteractionOptions {
  rootPath?: string;
  speechPath?: string;
  platforms?: ISupportedPlatforms[] | ISupportedPlatforms;
  contentPath?: string;
  viewsPath?: string;
  synonymPath?: string;
  spreadsheets: string | string[];
  alexaSpreadsheets: string | string[];
  dialogflowSpreadsheets: string | string[];
  assets?: string[];
  assetsPath?: string;
}

export interface IDefinedInteractionOptions {
  rootPath: string;
  speechPath: string;
  platforms: ISupportedPlatforms[];
  contentPath: string;
  viewsPath: string;
  synonymPath: string;
  spreadsheets: string[];
  alexaSpreadsheets: string | string[];
  dialogflowSpreadsheets: string | string[];
  assets: string[];
  assetsPath: string;
}

export const DEFAULT_INTERACTION_OPTIONS = {
  speechPath: "speech-assets",
  platforms: ["alexa"] as ISupportedPlatforms[],
  contentPath: "content",
  viewsPath: "/",
  synonymPath: "synonyms",
  assets: [],
  assetsPath: "assets"
};

function defaultOptions(interactionOptions: IInteractionOptions): IDefinedInteractionOptions {
  const rootPath: string = interactionOptions.rootPath || "";
  const speechPath: string =
    interactionOptions.speechPath || DEFAULT_INTERACTION_OPTIONS.speechPath;
  const synonymPath: string =
    interactionOptions.synonymPath || DEFAULT_INTERACTION_OPTIONS.synonymPath;
  const viewsPath: string = interactionOptions.viewsPath || DEFAULT_INTERACTION_OPTIONS.viewsPath;
  const contentPath: string =
    interactionOptions.contentPath || DEFAULT_INTERACTION_OPTIONS.contentPath;
  const assetsPath: string =
    interactionOptions.assetsPath || DEFAULT_INTERACTION_OPTIONS.assetsPath;

  const assets: string[] = interactionOptions.assets || DEFAULT_INTERACTION_OPTIONS.assets;

  const spreadsheets: string[] = arrayify(interactionOptions.spreadsheets) as string[];

  const alexaSpreadsheets: string[] = arrayify(interactionOptions.alexaSpreadsheets) as string[];

  const dialogflowSpreadsheets: string[] = arrayify(
    interactionOptions.dialogflowSpreadsheets
  ) as string[];

  let platforms: ISupportedPlatforms[] = arrayify(
    interactionOptions.platforms
  ) as ISupportedPlatforms[];

  const spreadsheetMapping = {
    alexa: alexaSpreadsheets,
    dialogflow: dialogflowSpreadsheets
  };

  // If the user didn't declare anything in the platorm key,
  // then we should inspect the spreadshseet in alexaSpreadsheets and dialogflowSpreadsheets
  // to find out what platform the user wants to use.

  platforms = _.chain(spreadsheetMapping)
    .toPairs()
    .reduce(
      (acc: string[], next: any[]) => {
        const key = next[0];
        const value = next[1];
        if (value) {
          acc.push(key);
        }
        return acc;
      },
      [] as ISupportedPlatforms[]
    )
    .concat(platforms)
    .filter()
    .uniq()
    .value() as ISupportedPlatforms[];

  if (_.isEmpty(spreadsheets) && _.isEmpty(platforms)) {
    throw Error("Spreadsheet were not specified in the right format");
  }

  platforms = _.isEmpty(platforms) ? DEFAULT_INTERACTION_OPTIONS.platforms : platforms;

  return {
    rootPath,
    spreadsheets,
    alexaSpreadsheets,
    dialogflowSpreadsheets,
    speechPath,
    synonymPath,
    viewsPath,
    assetsPath,
    contentPath,
    platforms,
    assets
  };
}

function arrayify(value: string | any[] | undefined): any[] | undefined {
  return _.isString(value) ? [value] : value;
}

async function getSheetsByPlatform(
  interactionOptions: IInteractionOptions,
  authKeys: any
): Promise<IPlatformSheets> {
  const sheets = await transform(interactionOptions, authKeys);

  const AVAILABLE_PLATFORM_KEYS = ["alexaSpreadsheets", "dialogflowSpreadsheets"];
  const transformByPlatfromPromises = AVAILABLE_PLATFORM_KEYS.map(spreadsheetKey =>
    transform(interactionOptions, authKeys, spreadsheetKey)
  );

  const sheetsByPlatformPromise: IVoxaSheet[][] = await Promise.all(transformByPlatfromPromises);
  const sheetsByPlatform: IPlatformSheets = AVAILABLE_PLATFORM_KEYS.reduce((acc, next, index) => {
    const sheetByPlatform = _.chain(sheets)
      .cloneDeep()
      .concat(sheetsByPlatformPromise[index])
      .uniq()
      .value();

    _.set(acc, next, sheetByPlatform);
    return acc;
  }, {}) as IPlatformSheets; // concat all the sheets from the `spreadsheets` key with platform specific sheets

  return sheetsByPlatform;
}
export const buildInteraction = async (interactionOptions: IInteractionOptions, authKeys: any) => {
  const definedInteractionOptions = defaultOptions(interactionOptions);
  console.time("all");
  console.time("timeframe");

  const { alexaSpreadsheets, dialogflowSpreadsheets } = await getSheetsByPlatform(
    definedInteractionOptions,
    authKeys
  );

  console.timeEnd("timeframe");
  const platforms = definedInteractionOptions.platforms;
  const schemas = [];

  if (platforms.includes("alexa")) {
    const schema = new AlexaSchema(alexaSpreadsheets, definedInteractionOptions);
    schemas.push(schema);
  }

  if (platforms.includes("dialogflow")) {
    const schema = new DialogflowSchema(dialogflowSpreadsheets, definedInteractionOptions);
    schemas.push(schema);
  }

  await Promise.all(
    schemas.map(schema =>
      fs.remove(
        path.join(
          definedInteractionOptions.rootPath,
          definedInteractionOptions.speechPath,
          schema.NAMESPACE
        )
      )
    )
  );

  const fileContentsProcess = _.chain(schemas)
    .reduce(
      (acc, schema, index) => {
        if (index === 0) {
          // We only want to execute this file once
          schema.buildDownloads();
          schema.buildViews();
          schema.buildViewsMapping();
          schema.buildSynonyms();
        }

        schema.invocations.map((invoc: IInvocation) => {
          schema.build(invoc.locale, invoc.environment);
        });
        acc = acc.concat(schema.fileContent);
        return acc;
      },
      [] as IFileContent[]
    )
    .uniqBy("path")
    .map((file: IFileContent) =>
      fs.outputFile(file.path, JSON.stringify(file.content, null, 2), { flag: "w" })
    )
    .value();

  await Promise.all(fileContentsProcess);
  await downloadDirs(
    definedInteractionOptions.assets,
    path.join(definedInteractionOptions.rootPath, definedInteractionOptions.assetsPath),
    authKeys
  );

  console.timeEnd("all");
};