@grouparoo/core
Version:
The Grouparoo Core
173 lines (152 loc) • 4.37 kB
text/typescript
import os from "os";
import path from "path";
import tar from "tar";
import fs from "fs";
import FormData from "form-data";
import fetch from "isomorphic-fetch";
import { mkdtemp, copy, remove } from "fs-extra";
import { utils } from "actionhero";
export class CloudError extends Error {
code: string;
constructor({ code, message }: { code: string; message: string }) {
super(message);
this.code = code;
}
}
export async function packageConfig(
projectPath: string,
configDirPath: string,
tarballPath: string
) {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "grouparoo-pack-"));
await copy(
path.join(projectPath, "package.json"),
path.join(tempDir, "package.json")
);
await copy(configDirPath, path.join(tempDir, "config"), {
filter: (src) => {
const ext = path.extname(src);
return !ext || [".json", ".js"].includes(ext);
},
});
await tar.create(
{
file: tarballPath,
cwd: tempDir,
gzip: true,
strict: true,
},
["."]
);
await remove(tempDir);
return tarballPath;
}
const maxAttempts = 5;
export interface ConfigurationApiData {
id: string;
state: string;
projectId: string;
toApply: boolean;
errorMessage?: string;
applyJobId?: string;
validateJobId?: string;
coreVersion: string;
processedAt: string;
validatedAt: string;
appliedAt: string;
finishedAt: string;
createdAt: string;
updatedAt: string;
}
export interface JobApiData {
id: string;
type: string;
state: string;
configurationId: string;
logs: string;
completedAt: string;
createdAt: string;
updatedAt: string;
}
export class CloudClient {
baseUrl: string;
token: string;
projectId: string;
constructor(projectId: string, token: string) {
this.projectId = projectId;
this.token = token;
this.baseUrl =
process.env.GROUPAROO_CLOUD_API_URL ?? "https://cloud.grouparoo.com";
}
async request<T>(
url: string,
options?: RequestInit & { _buildFormData?: () => FormData },
attempts = 0
): Promise<T> {
const fetchUrl = new URL(url, this.baseUrl);
fetchUrl.searchParams.append("apiToken", this.token);
// when retrying a form based on a readStream, we need to restart the stream on every attempt
if (typeof options?._buildFormData === "function") {
const formData = options._buildFormData.bind(this)();
//@ts-ignore typings are incorrectly not allowing FormData to be set as body
options.body = formData;
options.headers = formData.getHeaders();
}
try {
const optionsToSend = {
...options,
_buildFormData: undefined as Function,
};
const res = await fetch(fetchUrl.toString(), optionsToSend);
const data = (await res.json()) as T & { error?: any };
if (res.status !== 200) {
if (data.error) throw new CloudError(data.error);
throw new Error(await res.text());
}
return data;
} catch (error) {
if (
(error?.code === "ETIMEDOUT" || error.toString().match("ETIMEDOUT")) &&
attempts < maxAttempts - 1
) {
await utils.sleep(100);
return this.request<T>(url, options, attempts + 1);
} else throw error;
}
}
async createConfiguration(
tarballPath: string,
toApply: boolean,
message?: string,
externalUrl?: string
) {
function _buildFormData() {
const formData = new FormData();
formData.append("_file", fs.createReadStream(tarballPath));
formData.append("projectId", this.projectId);
formData.append("toApply", toApply.toString());
if (message) formData.append("message", message);
if (externalUrl) formData.append("externalUrl", externalUrl);
return formData;
}
const { configuration } = await this.request<{
configuration: ConfigurationApiData;
}>("/api/v1/configuration", {
method: "POST",
_buildFormData,
});
return configuration;
}
async getConfiguration(configurationId: string) {
const { configuration } = await this.request<{
configuration: ConfigurationApiData;
}>(`/api/v1/configuration/${configurationId}`);
return configuration;
}
async getJob(jobId: string) {
const { job } = await this.request<{ job: JobApiData }>(
`/api/v1/job/${jobId}`
);
return job;
}
}