Skip to content

Commit

Permalink
Add initial file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
moz-gh committed Sep 16, 2024
1 parent f7061af commit 3f8ae54
Showing 1 changed file with 158 additions and 79 deletions.
237 changes: 158 additions & 79 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
const FORMULAIC_BASE_URL = "https://formulaic.app";
const FORMULA_CACHE_TTL = 600000; // 10 minutes in milliseconds

const fs = require("fs");
const { Blob } = require("buffer");
class HttpClient {
async request(url, method = "GET", data = null, headers = {}) {
constructor(apiKey) {
this.apiKey = apiKey;
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
Accept: "application/json",
};
}

async request(url, method = "GET", data = null, customHeaders = {}) {
const options = {
method,
headers,
body: data ? JSON.stringify(data) : undefined,
headers: { ...this.headers, ...customHeaders },
body: data && !(data instanceof FormData) ? JSON.stringify(data) : data,
};

const response = await fetch(url, options);

if (!response.ok) {
Expand All @@ -16,137 +26,206 @@ class HttpClient {
);
}

return response.json(); // Assuming the response is always JSON
return response.json();
}

get(url, headers) {
get(url, headers = {}) {
return this.request(url, "GET", null, headers);
}

post(url, data, headers) {
post(url, data, headers = {}) {
return this.request(url, "POST", data, headers);
}

patch(url, data, headers = {}) {
return this.request(url, "PATCH", data, headers);
}

delete(url, headers = {}) {
return this.request(url, "DELETE", null, headers);
}
}

class FormulaicCache {
constructor(ttl = FORMULA_CACHE_TTL) {
this.cache = new Map();
this.ttl = ttl;
}

get(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
this.cache.delete(key); // Remove expired cache
return null;
}

set(key, value) {
this.cache.set(key, { data: value, timestamp: Date.now() });
}

clear() {
this.cache.clear();
}
}

class Formulaic {
/**
* Create a new Formulaic instance.
* @param {string} apiKey - The API key for the Formulaic API.
* @param {object} [options] - Additional options for the Formulaic instance.
* @param {string} [options.baseURL] - The base URL for the Formulaic API.
* @param {object} [options.httpClient] - An HTTP client to use for requests.
* @param {boolean} [options.debug] - Whether to log debug messages.
* @returns {Formulaic} A new Formulaic instance.
* @throws {Error} If the API key is not provided.
* @throws {Error} If the API key is not a string.
* @throws {Error} If the API key is an empty string.
* @throws {Error} If the base URL is not a string.
* @throws {Error} If the HTTP client is not an object.
* @throws {Error} If the debug option is not a boolean.
*
**/
constructor(apiKey, options = {}) {
this.apiKey = apiKey;
this.baseURL = options.baseURL || FORMULAIC_BASE_URL;
this.headers = {
Authorization: `Bearer ${this.apiKey}`,
Accept: "application/json", // Changed to application/json for more specific Accept header
"Content-Type": "application/json",
};
this.httpClient = options.httpClient || new HttpClient();
this.httpClient = options.httpClient || new HttpClient(apiKey);
this.debug = options.debug || false;
this.formulaCache = {};
this.formulaCache = new FormulaicCache();
}

logDebug(...messages) {
if (this.debug) {
console.log("Formulaic:", ...messages);
console.log("Formulaic Debug:", ...messages);
}
}

async getModels() {
const url = `${this.baseURL}/api/models`;

this.logDebug("Sending request to:", url);
this.logDebug("Fetching models from:", url);

try {
const models = await this.httpClient.get(url, this.headers);
this.logDebug("Received models:", models);
return models;
return await this.httpClient.get(url);
} catch (error) {
throw new Error(`Failed to get models: ${error.message}`);
}
}

async getFormula(formulaId) {
if (!formulaId) {
throw new Error("Formula ID is required");
}
if (!formulaId) throw new Error("Formula ID is required");

// Check the cache
const cachedFormula = this.formulaCache[formulaId];
if (
cachedFormula &&
Date.now() - cachedFormula.timestamp < FORMULA_CACHE_TTL
) {
const cachedFormula = this.formulaCache.get(formulaId);
if (cachedFormula) {
this.logDebug("Returning formula from cache:", formulaId);
return cachedFormula.data;
return cachedFormula;
}

const url = `${this.baseURL}/api/recipes/${formulaId}/scripts`;

this.logDebug("Sending request to:", url);
const url = `${this.baseURL}/api/recipes/${formulaId}`;
this.logDebug("Fetching formula from:", url);

try {
const formulaData = await this.httpClient.get(url, this.headers);
this.formulaCache[formulaId] = {
timestamp: Date.now(),
data: formulaData,
};
this.logDebug("Updating formula cache:", formulaId);
const formulaData = await this.httpClient.get(url);
this.formulaCache.set(formulaId, formulaData);
return formulaData;
} catch (error) {
throw new Error(`Failed to get formula: ${error.message}`);
}
}

async createFormula(data) {
const url = `${this.baseURL}/api/recipes`;

this.logDebug("Creating new formula:", url);

try {
return await this.httpClient.post(url, data);
} catch (error) {
throw new Error(`Failed to create formula: ${error.message}`);
}
}

async createCompletion(formulaId, data = {}) {
// Ensure `models` and `variables` are arrays, defaulting to empty arrays if not provided
const models = Array.isArray(data.models) ? data.models : [];
const variables = Array.isArray(data.variables) ? data.variables : [];

// Throw an error only if models array is explicitly provided but is empty
if (data.models && models.length === 0) {
throw new Error(
"Data must include at least one model in the 'models' array."
);
if (!models.length) throw new Error("At least one model is required.");
if (!variables.length)
throw new Error("At least one variable is required.");

try {
const formula = await this.getFormula(formulaId);
const url = `${this.baseURL}/api/recipes/${formulaId}/scripts/${formula.id}/artifacts`;
this.logDebug("Creating completion for formula:", formulaId);

return await this.httpClient.post(url, { ...data, models, variables });
} catch (error) {
throw new Error(`Failed to create completion: ${error.message}`);
}
}

// Similar check for variables if needed
if (data.variables && variables.length === 0) {
throw new Error(
"Data must include at least one variable in the 'variables' array."
);
async uploadFile(formulaId, file, fileName) {
const url = `${this.baseURL}/api/recipes/${formulaId}/files`;
const formData = new FormData();

if (Buffer.isBuffer(file)) {
// Convert Buffer to Blob for native FormData compatibility
const blob = new Blob([file]);
formData.append("file", blob, fileName);
} else if (typeof file === "string") {
// If it's a file path (Node.js), read it from the filesystem as a stream
const fileStream = fs.createReadStream(file);
formData.append("file", fileStream, fileName);
} else {
throw new Error("Invalid file type, must be Buffer or file path");
}

this.logDebug("Sending request with data:", { ...data, models, variables });
this.logDebug("Uploading file to:", url, "with file name:", fileName);

try {
const formula = await this.getFormula(formulaId);
const scriptId = formula.id;
const url = `${this.baseURL}/api/recipes/${formulaId}/scripts/${scriptId}/artifacts`;
const headers = formData.getHeaders(); // Get headers for form-data
const response = await this.httpClient.post(url, formData, headers);
this.logDebug("File upload response:", response);
return response;
} catch (error) {
console.log(error);
throw new Error(`Failed to upload file: ${error.message}`);
}
}

this.logDebug("Sending request to:", url);
async getFiles(formulaId) {
const url = `${this.baseURL}/api/recipes/${formulaId}/files`;
this.logDebug("Fetching files for formula:", formulaId);

const completionResponse = await this.httpClient.post(
url,
{ ...data, models, variables },
this.headers
);
this.logDebug("Received completion response:", completionResponse);
return completionResponse;
try {
const response = await this.httpClient.get(url);
this.logDebug("Fetched files:", response);
return response;
} catch (error) {
throw new Error(`Failed to create completion: ${error.message}`);
throw new Error(`Failed to get files: ${error.message}`);
}
}

async getFile(formulaId, fileId) {
const url = `${this.baseURL}/api/recipes/${formulaId}/files/${fileId}`;
this.logDebug("Fetching file:", fileId, "for formula:", formulaId);

try {
const response = await this.httpClient.get(url);
this.logDebug("Fetched file:", response);
return response;
} catch (error) {
throw new Error(`Failed to get file: ${error.message}`);
}
}

async updateFile(formulaId, fileId, data) {
const url = `${this.baseURL}/api/recipes/${formulaId}/files/${fileId}`;
this.logDebug("Updating file:", fileId, "for formula:", formulaId);

try {
const response = await this.httpClient.patch(url, data);
this.logDebug("File update response:", response);
return response;
} catch (error) {
throw new Error(`Failed to update file: ${error.message}`);
}
}

async deleteFile(formulaId, fileId) {
const url = `${this.baseURL}/api/recipes/${formulaId}/files/${fileId}`;
this.logDebug("Deleting file:", fileId, "from formula:", formulaId);

try {
const response = await this.httpClient.delete(url);
this.logDebug("File deletion response:", response);
return response;
} catch (error) {
throw new Error(`Failed to delete file: ${error.message}`);
}
}
}
Expand Down

0 comments on commit 3f8ae54

Please sign in to comment.