export default()

in cli/src/commands/router/commands/compose.ts [50:327]


export default (opts: BaseCommandOptions) => {
  const command = new Command('compose');
  command.description(
    'Generates a router config from a local composition file. This makes it easy to test your router without a control-plane connection. For production, please use the "router fetch" command',
  );
  command.requiredOption('-i, --input <path-to-input>', 'The yaml file with data about graph and subgraphs.');
  command.option('-o, --out [string]', 'Destination file for the router config.');
  command.option('--suppress-warnings', 'This flag suppresses any warnings produced by composition.');
  command.action(async (options) => {
    const inputFile = resolve(options.input);
    const inputFileLocation = dirname(inputFile);

    if (!existsSync(inputFile)) {
      program.error(
        pc.red(pc.bold(`The input file '${pc.bold(inputFile)}' does not exist. Please check the path and try again.`)),
      );
    }

    const fileContent = (await readFile(inputFile)).toString();
    const config = yaml.load(fileContent) as Config;

    const subgraphSDLs = new Map<string, string>();
    for (const s of config.subgraphs) {
      if (s.schema?.file) {
        const schemaFile = resolve(inputFileLocation, s.schema.file);
        const sdl = (await readFile(schemaFile)).toString();
        subgraphSDLs.set(s.name, sdl);
        continue;
      }

      const url = s.introspection?.url ?? s.routing_url;

      try {
        const result = await introspectSubgraph({
          subgraphURL: url,
          additionalHeaders: Object.entries(s.introspection?.headers ?? {}).map(([key, value]) => ({
            key,
            value,
          })),
          rawIntrospection: s.introspection?.raw,
        });

        if (!result.success) {
          program.error(`Could not introspect subgraph ${s.name}, URL: ${url}: ${result.errorMessage ?? 'failed'}`);
        }

        subgraphSDLs.set(s.name, result.sdl);
      } catch (e: any) {
        program.error(`Could not introspect subgraph ${s.name}, URL: ${url}: ${e.message}`);
      }
    }

    const result = composeSubgraphs(
      config.subgraphs.map((s, index) => ({
        name: s.name,
        url: normalizeURL(s.routing_url),
        definitions: parse(subgraphSDLs.get(s.name) ?? ''),
      })),
    );

    if (!result.success) {
      const compositionErrorsTable = new Table({
        head: [pc.bold(pc.white('ERROR_MESSAGE'))],
        colWidths: [120],
        wordWrap: true,
      });

      console.log(
        pc.red(`We found composition errors, while composing.\n${pc.bold('Please check the errors below:')}`),
      );
      for (const compositionError of result.errors) {
        compositionErrorsTable.push([compositionError.message]);
      }
      console.log(compositionErrorsTable.toString());
      process.exitCode = 1;
      return;
    }

    if (!options.suppressWarnings && result.warnings.length > 0) {
      const compositionWarningsTable = new Table({
        head: [pc.bold(pc.white('WARNING_MESSAGE'))],
        colWidths: [120],
        wordWrap: true,
      });

      console.log(pc.yellow(`The following warnings were produced while composing:`));
      for (const warning of result.warnings) {
        compositionWarningsTable.push([warning.message]);
      }
      console.log(compositionWarningsTable.toString());
    }

    const federatedClientSDL = result.shouldIncludeClientSchema ? printSchema(result.federatedGraphClientSchema) : '';
    const routerConfig = buildRouterConfig({
      federatedClientSDL,
      federatedSDL: printSchemaWithDirectives(result.federatedGraphSchema),
      fieldConfigurations: result.fieldConfigurations,
      // @TODO get router compatibility version programmatically
      routerCompatibilityVersion: ROUTER_COMPATIBILITY_VERSION_ONE,
      schemaVersionId: 'static',
      subgraphs: config.subgraphs.map((s, index) => {
        const subgraphConfig = result.subgraphConfigBySubgraphName.get(s.name);
        const schema = subgraphConfig?.schema;
        const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName;
        return {
          id: `${index}`,
          name: s.name,
          url: normalizeURL(s.routing_url),
          sdl: subgraphSDLs.get(s.name) ?? '',
          subscriptionUrl: s.subscription?.url || s.routing_url,
          subscriptionProtocol: s.subscription?.protocol || 'ws',
          websocketSubprotocol:
            s.subscription?.protocol === 'ws' ? s.subscription?.websocketSubprotocol || 'auto' : undefined,
          schema,
          configurationDataByTypeName,
        };
      }),
    });

    routerConfig.version = randomUUID();

    if (config.feature_flags && config.feature_flags.length > 0) {
      const ffConfigs: FeatureFlagRouterExecutionConfigs = new FeatureFlagRouterExecutionConfigs();

      // @TODO This logic should exist only once in the shared package and reused across
      // control-plane and cli

      for (const ff of config.feature_flags) {
        const subgraphs: Subgraph[] = [];

        // Replace base subgraphs with feature flag subgraphs
        for (const s of config.subgraphs) {
          const featureSubgraph = ff.feature_graphs.find((ffs) => ffs.subgraph_name === s.name);
          if (featureSubgraph) {
            if (featureSubgraph?.schema?.file) {
              const schemaFile = resolve(inputFileLocation, featureSubgraph.schema.file);
              const sdl = (await readFile(schemaFile)).toString();
              // Replace feature subgraph sdl with the base subgraph sdl
              subgraphSDLs.set(featureSubgraph.name, sdl);
            } else {
              const url = featureSubgraph.introspection?.url ?? featureSubgraph.routing_url;
              try {
                const result = await introspectSubgraph({
                  subgraphURL: url,
                  additionalHeaders: Object.entries(featureSubgraph.introspection?.headers ?? {}).map(
                    ([key, value]) => ({
                      key,
                      value,
                    }),
                  ),
                  rawIntrospection: featureSubgraph.introspection?.raw,
                });

                if (!result.success) {
                  program.error(
                    `Could not introspect feature-graph subgraph ${featureSubgraph.name}, URL: ${url}: ${
                      result.errorMessage ?? 'failed'
                    }`,
                  );
                }

                // Replace feature subgraph sdl with the base subgraph sdl
                subgraphSDLs.set(s.name, result.sdl);
              } catch (e: any) {
                program.error(
                  `Could not introspect feature-graph subgraph ${featureSubgraph.name}, URL: ${url}: ${e.message}`,
                );
              }
            }

            subgraphs.push({
              name: featureSubgraph.name,
              routing_url: featureSubgraph.routing_url,
              schema: featureSubgraph.schema,
              subscription: featureSubgraph.subscription,
              introspection: featureSubgraph.introspection,
            });
          } else {
            subgraphs.push(s);
          }
        }

        const result = composeSubgraphs(
          subgraphs.map((s, index) => ({
            name: s.name,
            url: normalizeURL(s.routing_url),
            definitions: parse(subgraphSDLs.get(s.name) ?? ''),
          })),
        );

        if (!result.success) {
          const compositionErrorsTable = new Table({
            head: [pc.bold(pc.white('ERROR_MESSAGE'))],
            colWidths: [120],
            wordWrap: true,
          });

          console.log(
            pc.red(
              `We found composition errors, while composing the feature flag ${pc.italic(ff.name)}.\n${pc.bold(
                'Please check the errors below:',
              )}`,
            ),
          );
          for (const compositionError of result.errors) {
            compositionErrorsTable.push([compositionError.message]);
          }
          console.log(compositionErrorsTable.toString());
          continue;
        }

        if (!options.suppressWarnings && result.warnings.length > 0) {
          const compositionWarningsTable = new Table({
            head: [pc.bold(pc.white('WARNING_MESSAGE'))],
            colWidths: [120],
            wordWrap: true,
          });

          console.log(
            pc.yellow(`The following warnings were produced while composing the feature flag ${pc.italic(ff.name)}:`),
          );
          for (const warning of result.warnings) {
            compositionWarningsTable.push([warning.message]);
          }
          console.log(compositionWarningsTable.toString());
        }

        const federatedClientSDL = result.shouldIncludeClientSchema
          ? printSchema(result.federatedGraphClientSchema)
          : '';
        const routerConfig = buildRouterConfig({
          federatedClientSDL,
          federatedSDL: printSchemaWithDirectives(result.federatedGraphSchema),
          fieldConfigurations: result.fieldConfigurations,
          // @TODO get router compatibility version programmatically
          routerCompatibilityVersion: ROUTER_COMPATIBILITY_VERSION_ONE,
          schemaVersionId: `static`,
          subgraphs: subgraphs.map((s, index) => {
            const subgraphConfig = result.subgraphConfigBySubgraphName.get(s.name);
            const schema = subgraphConfig?.schema;
            const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName;
            return {
              id: `${index}`,
              name: s.name,
              url: normalizeURL(s.routing_url),
              sdl: subgraphSDLs.get(s.name) ?? '',
              subscriptionUrl: s.subscription?.url || s.routing_url,
              subscriptionProtocol: s.subscription?.protocol || 'ws',
              websocketSubprotocol:
                s.subscription?.protocol === 'ws' ? s.subscription?.websocketSubprotocol || 'auto' : undefined,
              schema,
              configurationDataByTypeName,
            };
          }),
        });

        ffConfigs.configByFeatureFlagName[ff.name] = new FeatureFlagRouterExecutionConfig({
          version: routerConfig.version,
          subgraphs: routerConfig.subgraphs,
          engineConfig: routerConfig.engineConfig,
        });

        ffConfigs.configByFeatureFlagName[ff.name].version = randomUUID();
      }

      routerConfig.featureFlagConfigs = ffConfigs;
    }

    if (options.out) {
      await writeFile(options.out, routerConfig.toJsonString());
      console.log(pc.green(`Router config successfully written to ${pc.bold(options.out)}`));
    } else {
      console.log(routerConfig.toJsonString());
    }
  });

  return command;
};