Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14,220 changes: 10,035 additions & 4,185 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"form-data": "^4.0.2",
"fs-extra": "^11.3.0",
"get-port": "^7.1.0",
"is-in-ci": "^2.0.0",
"livereload": "^0.9.3",
"neverthrow": "^8.2.0",
"open": "^8.4.0",
Expand Down
3 changes: 2 additions & 1 deletion src/actions/portal/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { withDirPath } from "../../infrastructure/tmp-extensions.js";
import { LauncherService } from "../../infrastructure/launcher-service.js";
import { TempContext } from "../../types/temp-context.js";
import { CommandMetadata } from "../../types/common/command-metadata.js";
import { isServiceError } from "../../infrastructure/api-utils.js";

export class GenerateAction {
private readonly prompts: PortalGeneratePrompts = new PortalGeneratePrompts();
Expand Down Expand Up @@ -57,7 +58,7 @@ export class GenerateAction {

if (response.isErr()) {
const error = response.error;
if (typeof error === "string") {
if (isServiceError(error)) {
this.prompts.portalGenerationError(error);
} else {
const errorZipPath = await tempContext.save(error);
Expand Down
4 changes: 4 additions & 0 deletions src/infrastructure/api-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export const enum ServiceError {
UnAuthorized = "UNAUTHORIZED"
}

export function isServiceError(e: unknown): e is ServiceError {
return typeof e === "string";
}

export function getErrorMessage(error: ServiceError): string {
switch (error) {
case ServiceError.NetworkError:
Expand Down
9 changes: 8 additions & 1 deletion src/infrastructure/launcher-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import os from "os";
import { spawn } from "child_process";
import open from "open";
import { UrlPath } from "../types/file/urlPath.js";
import isInCi from "is-in-ci";

export class LauncherService {

public async openInEditor(filePath: FilePath): Promise<void> {
if (!isInCi) return;
try {
await execa("code", ["--wait", filePath.toString()]);
} catch {
Expand Down Expand Up @@ -51,6 +53,11 @@ export class LauncherService {
}

public openUrlInBrowser(url: UrlPath) {
return open(url.toString());
if (!isInCi) return;
try{
return open(url.toString());
} catch {
// Silently ignore errors
}
}
}
34 changes: 34 additions & 0 deletions src/infrastructure/services/api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SubscriptionInfo } from "../../types/api/account.js";
import { envInfo } from "../env-info.js";
import { err, ok, Result } from "neverthrow";
import { handleServiceError, ServiceError } from "../api-utils.js";
import { PortalGenerationStatusResponse } from "@apimatic/sdk";

export class ApiService {
private readonly apiBaseUrl = "https://api.apimatic.io" as const;
Expand Down Expand Up @@ -32,6 +33,39 @@ export class ApiService {
}
}

public async getPortalGenerationStatus(
requestId: string,
configDir: DirectoryPath,
shell: string,
authKey: string | null
): Promise<Result<PortalGenerationStatusResponse, ServiceError>> {
const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString());
if (authInfo === null && !authKey) {
return err(ServiceError.UnAuthorized);
}

try {
const token = authKey || authInfo?.authKey;
const response = await this.axiosInstance(shell, token).get(`/portal/v2/${requestId}/status`, {
headers: { Accept: "application/json" },
maxRedirects: 0,
validateStatus: () => true
});

if (response.status === 200) {
return ok(response.data as PortalGenerationStatusResponse);
}

if (response.status === 302) {
return ok({ status: "Completed" } as PortalGenerationStatusResponse);
}

return err(ServiceError.InvalidResponse);
} catch (error: unknown) {
return err(handleServiceError(error));
}
}

public async sendTelemetry(payload: string, authKey: string, shell: string): Promise<Result<string, string>> {
try {
const response = await this.axiosInstance(shell, authKey).post("/telemetry/track", payload, {
Expand Down
52 changes: 41 additions & 11 deletions src/infrastructure/services/portal-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
BadRequestResponseSdkError,
CodeGenerationExternalApisController,
ContentType,
DocsPortalManagementController,
DocsPortalGenerationAsyncController,
UnauthorizedResponseError,
ProblemDetailsError,
FileWrapper,
Expand All @@ -28,37 +28,67 @@ import { CommandMetadata } from "../../types/common/command-metadata.js";
import { err, ok, Result } from "neverthrow";
import { Language } from "../../types/sdk/generate.js";
import { handleServiceError, ServiceError } from "../api-utils.js";
import { ApiService } from "./api-service.js";

export class PortalService {
private readonly CONTENT_TYPE = ContentType.EnumMultipartformdata;
private readonly fileService = new FileService();
private readonly apiService = new ApiService();

// TODO: Pass stream as parameter instead of file path.
public async generatePortal(
buildPath: FilePath,
configDir: DirectoryPath,
eventMetadata: CommandMetadata,
commandMetadata: CommandMetadata,
authKey: string | null
): Promise<Result<NodeJS.ReadableStream, string | NodeJS.ReadableStream>> {
): Promise<Result<NodeJS.ReadableStream, ServiceError | NodeJS.ReadableStream>> {
const buildFileStream = await this.fileService.getStream(buildPath);
const file = new FileWrapper(buildFileStream);

const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString());
const authorizationHeader = this.createAuthorizationHeader(authInfo, authKey);
const client = apiClientFactory.createApiClient(authorizationHeader, eventMetadata.shell);
const docsPortalManagementController = new DocsPortalManagementController(client);
const client = apiClientFactory.createApiClient(authorizationHeader, commandMetadata.shell);
const docsPortalAsyncController = new DocsPortalGenerationAsyncController(client);

let generationId: string;
try {
const response = await docsPortalManagementController.generateOnPremPortalViaBuildInput(
const portalInstance = await docsPortalAsyncController.generateOnPremPortalViaBuildInputAsync(
this.CONTENT_TYPE,
file,
this.createOriginQueryParameter(eventMetadata.commandName)
file
);
return ok(response.result as NodeJS.ReadableStream);
} catch (error) {
return err(await PortalService.handlePortalGenerationErrors(error));
generationId = portalInstance.result.id;
} catch (error: unknown) {
return err(handleServiceError(error));
} finally {
buildFileStream.close();
}

let statusResult;
do {
await new Promise((resolve) => setTimeout(resolve, 3000));
statusResult = await this.apiService.getPortalGenerationStatus(
generationId,
configDir,
commandMetadata.shell,
authKey
);
if (statusResult.isErr()) {
return err(statusResult.error);
}
if (statusResult.value.status === "Failed") {
return err(ServiceError.ServerError);
}
} while (statusResult.value.status !== "Completed");

try {
const portalDownloadResponse = await docsPortalAsyncController.downloadGeneratedPortal(generationId);
return ok(portalDownloadResponse.result as NodeJS.ReadableStream);
} catch (error) {
if (error instanceof ApiError && error.statusCode === 422) {
return err(error.body as NodeJS.ReadableStream);
}
return err(handleServiceError(error));
}
}

// TODO: Pass stream as parameter instead of file path.
Expand Down
7 changes: 4 additions & 3 deletions src/prompts/portal/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DirectoryPath } from "../../types/file/directoryPath.js";
import { format as f, withSpinner } from "../format.js";
import { Result } from "neverthrow";
import { FilePath } from "../../types/file/filePath.js";
import { getErrorMessage, ServiceError } from "../../infrastructure/api-utils.js";

export class PortalGeneratePrompts {
public async overwritePortal(directory: DirectoryPath): Promise<boolean> {
Expand Down Expand Up @@ -35,12 +36,12 @@ export class PortalGeneratePrompts {
log.error(message);
}

public generatePortal(fn: Promise<Result<NodeJS.ReadableStream, string | NodeJS.ReadableStream>>) {
public generatePortal(fn: Promise<Result<NodeJS.ReadableStream, ServiceError | NodeJS.ReadableStream>>) {
return withSpinner("Generating portal", "Portal generated successfully.", "Portal Generation failed.", fn);
}

public portalGenerationError(error: string) {
log.error(error);
public portalGenerationError(error: ServiceError) {
log.error(getErrorMessage(error));
}

portalGenerationErrorWithReport(reportPath: FilePath) {
Expand Down