Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/dist/**
/node_modules
/package-lock.json
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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);
});
```
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
186 changes: 186 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<GrepperSearchResponse>(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<GrepperAnswer>(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<GrepperUpdateResponse>(config);

Grepper.validateStatusCode(response.status);

return response.data;
}
}
20 changes: 20 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
11 changes: 11 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
}
}