Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add help endpoint. #311

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
78f3dfd
Initial Draft for Help Endpoint
princerajpoot20 Jun 11, 2023
70d474d
[Update]: Change in the structure, and split will work with space
princerajpoot20 Jun 12, 2023
3d0c0c0
[feat] : Added functionality of fetching data from github repo and ma…
princerajpoot20 Jun 14, 2023
58fe75c
Update src/controllers/help.controller.ts
princerajpoot20 Jun 18, 2023
d6879d5
fix : Improved error handling, added commands.md file and corrected t…
princerajpoot20 Jun 18, 2023
bb28c77
fix: changed the parameter passing from request body to url and file …
princerajpoot20 Jun 20, 2023
acc3cd1
added axios dependency and updated openapi.yml
princerajpoot20 Jul 8, 2023
993d7ce
change response to json formate
princerajpoot20 Jul 28, 2023
3849fb1
Update response and added relative link
princerajpoot20 Aug 5, 2023
867496d
Added unit test and update responses
princerajpoot20 Aug 8, 2023
8cbc884
Used optional chaining to remove code smell
princerajpoot20 Aug 11, 2023
b44ee3d
Refactored code for clarity
princerajpoot20 Aug 11, 2023
5a005f6
Minor changes
princerajpoot20 Aug 11, 2023
ed228cd
refactor: Apply Problem schema, update schema : HelpListResponse, Hel…
princerajpoot20 Aug 19, 2023
457bfc9
Update version of axios
princerajpoot20 Aug 19, 2023
17f05f8
Minor changes
princerajpoot20 Aug 19, 2023
e747130
Used openapi spec from project itself
princerajpoot20 Sep 4, 2023
29bdfca
Fix failing checks: lint fix
princerajpoot20 Sep 8, 2023
f381db7
Fix Warning: unsafe regular expression and Object Injection Sink
princerajpoot20 Sep 9, 2023
7af427c
Fix Warnings: Generic Object Injection Sink
princerajpoot20 Sep 9, 2023
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 jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ module.exports = {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.ts?$': 'ts-jest',
},
moduleNameMapper: {
'^axios$': require.resolve('axios'),
},

// Test spec file resolution pattern
// Matches parent folder `tests` or `__tests__` and filename
Expand Down
62 changes: 62 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,49 @@ paths:
schema:
$ref: '#/components/schemas/Problem'

/help:
get:
summary: Retrieve help information for the given command.
operationId: help
tags:
- help
parameters:
- name: command
in: query
style: form
explode: true
description: The command for which help information is needed.
required: true
schema:
type: string
responses:
"200":
description: Help information retrieved successfully.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/HelpListResponse'
- $ref: '#/components/schemas/HelpCommandResponse'
"400":
BOLT04 marked this conversation as resolved.
Show resolved Hide resolved
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Problem'
"404":
description: Command not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
default:
description: Unexpected problem.
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"

/diff:
post:
summary: Compare the given AsyncAPI documents.
Expand Down Expand Up @@ -415,6 +458,25 @@ components:
type: [object, string]
description: The diff between the two AsyncAPI documents.

HelpListResponse:
type: object
properties:
commands:
type: array
items:
type: string
description: A list of all available commands.
HelpCommandResponse:
type: object
description: Detailed help information for a specific command.
properties:
command:
type: string
description: The name of the command.
description:
type: string
description: Detailed description of the command.

Problem:
type: object
properties:
Expand Down
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"ajv": "^8.8.2",
"ajv-formats": "^2.1.1",
"archiver": "^5.3.0",
"axios": "^1.4.0",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"config": "^3.3.6",
Expand Down
134 changes: 134 additions & 0 deletions src/controllers/help.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Controller } from '../interfaces';
import axios from 'axios';
import yaml from 'js-yaml';
import { ProblemException } from '../exceptions/problem.exception';

export const fetchCommands = async (user, repo) => {
try {
const url = `https://api.github.com/repos/${user}/${repo}/contents/openapi.yaml`;
const response = await axios.get(url, {
headers: {
Accept: 'application/vnd.github.v3.raw'
}
});
return yaml.load(response.data);
} catch (error) {
console.error(`Error fetching commands: ${error}`);
BOLT04 marked this conversation as resolved.
Show resolved Hide resolved
throw new ProblemException({
type: 'fetch-commands-error',
title: 'Error Fetching Commands',
status: 500,
detail: error.message
});
}
};

const resolveRefs = (obj, openapiSpec) => {
if (obj instanceof Object) {
for (let key in obj) {
if (obj[key]?.["$ref"]) {
const componentKey = obj[key].$ref.replace('#/components/schemas/', '');
if (componentKey === 'AsyncAPIDocument') {
obj[key] = {
"$ref": "https://github.com/asyncapi/spec/blob/master/spec/asyncapi.md#asyncapi-object"
};
} else {
obj[key] = openapiSpec.components.schemas[componentKey];
}
} else {
resolveRefs(obj[key], openapiSpec);
}
}
}
};

const getCommandsFromRequest = (req: Request): string[] => {
return req.params[0] ? req.params[0].split('/').filter(cmd => cmd.trim()) : [];
}

const getPathKeysMatchingCommands = (commands: string[], pathKeys: string[]): string | undefined => {
return pathKeys.find(pathKey => {
const pathParts = pathKey.split('/').filter(part => part !== '');
if (pathParts.length !== commands.length) return false;
for (let i = 0; i < pathParts.length; i++) {
const pathPart = pathParts[i];
const command = commands[i];
if (pathPart !== command && !pathPart.startsWith('{')) {
return false;
}
}
return true;
});
}

const buildResponseObject = (matchedPathKey: string, method: string, operationDetails: any, requestBodyComponent: any) => {
return {
command: matchedPathKey,
method: method.toUpperCase(),
summary: operationDetails.summary || '',
requestBody: requestBodyComponent
};
}

export class HelpController implements Controller {
public basepath = '/help';

public async boot(): Promise<Router> {
const router: Router = Router();

router.get(/^\/help(\/.*)?$/, async (req: Request, res: Response, next: NextFunction) => {
const commands = getCommandsFromRequest(req);
let openapiSpec: any;

try {
openapiSpec = await fetchCommands('asyncapi', 'server-api');
} catch (err) {
return next(err);
}

if (commands.length === 0) {
const routes = Object.keys(openapiSpec.paths).map(path => ({ command: path.replace(/^\//, ''), url: `${this.basepath}${path}` }));
return res.json(routes);
}

const pathKeys = Object.keys(openapiSpec.paths);
const matchedPathKey = getPathKeysMatchingCommands(commands, pathKeys);
if (!matchedPathKey) {
return next(new ProblemException({
type: 'invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
}));
}

const pathInfo = openapiSpec.paths[matchedPathKey];
const method = commands.length > 1 ? 'get' : 'post';
const operationDetails = pathInfo[method];
if (!operationDetails) {
return next(new ProblemException({
type: 'invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
}));
}

const { requestBody } = operationDetails;
let requestBodyComponent: any = {};
if (requestBody?.content?.['application/json']) {
const { $ref } = requestBody.content['application/json'].schema;
if ($ref) {
const componentKey = $ref.replace('#/components/schemas/', '');
requestBodyComponent = openapiSpec.components.schemas[componentKey];
}
}

resolveRefs(requestBodyComponent, openapiSpec);
return res.json(buildResponseObject(matchedPathKey, method, operationDetails, requestBodyComponent));
});

return router;
}
}
Loading