diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74796ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/dist/** +/node_modules +/package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index c5d529f..c6079eb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,46 @@ -# grepper-node -node client library +# Grepper Node.js client + +> **Work in progress** + +This library is a wrapper for the Grepper API. + +## Getting started + +### Authentication + +Replace `your_api_key` with [your actual API Key](https://www.grepper.com/app/settings-account.php) +```typescript +import Grepper from "./index.ts"; + +Grepper.apiKey = "your_api_key"; +``` + +### Search All Answers + +This endpoint searches all answers based on a query. + +```typescript +Grepper.search("strings in c").then((answers) => { + console.log(answers); +}); +``` + +### Retrieve an Answer + +This endpoint retrieves a specific answer. + +```typescript +Grepper.retrieve(12345).then((answer) => { + console.log(answer); +}); +``` + +### Update a specific answer + +This endpoint updates a specific answer. + +```typescript +Grepper.update(54321, "This answer will be updated").then((updateResult) => { + console.log(updateResult); +}); +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd82e64 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "grepper-node", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "querystring": "^0.2.1", + "typescript": "^5.6.2" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..539b2b2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,186 @@ +import axios from "axios"; +import querystring from "querystring"; + +const ANSWERS_ENDPOINT = "https://api.grepper.com/v1/answers"; + +class Errors { + public static API_KEY_MISSING = "Grepper API key not found"; + public static QUERY_EMPTY = "Query is empty"; + public static SIMILARITY_INVALID = "Similarity is invalid (1-100 where 1 is really loose matching and 100 is really strict/tight match)"; + public static ANSWER_ID_INVALID = "Answer ID is invalid"; +} + +export default class Grepper { + private static _apiKey: string; + + /** + * Setter for the API Key + * @example + * import {apiKey} from "grepper-node" + * apiKey = "your_api_key" + * @param {string} value - The API key + */ + public static set apiKey(value: string) { + this._apiKey = value; + } + + /** + * Throws an error if the API key is missing + * @private + */ + private static validateApiKey() { + if (!Grepper.apiKey) { + throw new Error(Errors.API_KEY_MISSING); + } + } + + /** + * Throws an error if the answer ID is missing or an invalid number + * @param {number} id - The answer ID + * @private + */ + private static validateAnswerId(id: number) { + if (!id || isNaN(id) || id <= 0) { + throw new Error(Errors.ANSWER_ID_INVALID); + } + } + + /** + * Throws an error if an HTTP status code other than 200 was returned by Grepper + * @param {number} statusCode - The HTTP response status code + * @private + */ + private static validateStatusCode(statusCode: number) { + switch (statusCode) { + case 200: + break; + case 400: + throw new Error("Bad Request -- Your request is invalid."); + case 401: + throw new Error("Unauthorized -- Your API key is wrong."); + case 403: + throw new Error("Forbidden -- You do not have access to the requested resource."); + case 404: + throw new Error("Not Found -- The specified enpoint could not be found."); + case 405: + throw new Error("Method Not Allowed -- You tried to access an enpoint with an invalid method."); + case 429: + throw new Error("Too Many Requests -- You're making too many requests! Slow down!"); + case 500: + throw new Error("Internal Server Error -- We had a problem with our server. Try again later."); + case 503: + throw new Error("Service Unavailable -- We're temporarily offline for maintenance. Please try again later."); + default: + throw new Error(`Grepper returned status code ${statusCode}`); + } + } + + /** + * This endpoint searches all answers based on a query. + * @param {string} query - query to search through answer titles ex: "Javascript loop array backwords" + * @param {number} [similarity=60] - How similar the query has to be to the answer title. 1-100 where 1 is really loose matching and 100 is really strict/tight match. + */ + public static async search(query: string, similarity: number | undefined = 60) { + Grepper.validateApiKey(); + + if (!query) { + throw new Error(Errors.QUERY_EMPTY); + } + + if (similarity < 1 || similarity > 100) { + throw new Error(Errors.SIMILARITY_INVALID); + } + + const config = { + url: `${ANSWERS_ENDPOINT}/search`, + method: "GET", + auth: { + username: Grepper._apiKey, + password: "", + }, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + params: { + query, + similarity, + }, + }; + + const response = await axios.request(config); + + Grepper.validateStatusCode(response.status); + + const answers = [ + ...response.data.data, + ]; + + for (const answer of answers) { + try { + answer.content = JSON.parse(answer.content); + } catch (error) { + } + } + + return answers; + } + + /** + * This endpoint retrieves a specific answer. + * @param {number} id - The answer id of the answer to retrieve + */ + public static async retrieve(id: number) { + Grepper.validateApiKey(); + Grepper.validateAnswerId(id); + + const config = { + url: `${ANSWERS_ENDPOINT}/${id}`, + method: "GET", + auth: { + username: Grepper._apiKey, + password: "", + }, + }; + + const response = await axios.request(config); + + Grepper.validateStatusCode(response.status); + + const answer = response.data; + + try { + answer.content = JSON.parse(answer.content); + } catch (error) { + } + + return answer; + } + + /** + * This endpoint updates a specific answer. + * @param {number} id - The answer id of the answer to update + * @param {string} content - The new content of the answer + */ + public static async update(id: number, content: string) { + Grepper.validateApiKey(); + Grepper.validateAnswerId(id); + + const config = { + url: `${ANSWERS_ENDPOINT}/${id}`, + method: "POST", + auth: { + username: Grepper._apiKey, + password: "", + }, + data: querystring.stringify({ + "answer[content]": content, + }), + }; + + const response = await axios.request(config); + + Grepper.validateStatusCode(response.status); + + return response.data; + } +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..37d51fe --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,20 @@ +interface GrepperAnswer { + id: number; + content: string; + author_name: string; + author_profile_url: string; + title: string; + upvotes: number; + object: string; + downvotes: number; +} + +interface GrepperSearchResponse { + object: string; + data: GrepperAnswer[]; +} + +interface GrepperUpdateResponse { + id: number; + success: `${boolean}`; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dfc34fe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist" + } +}