From 45fbf2df465dff795e6db1f6bb771acd2f56c803 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Wed, 10 Nov 2021 21:59:46 +0500 Subject: [PATCH] chore: drive all endpoints docs generated --- api/public/swagger.yaml | 153 ++++++++++++++++++++++++++++++++ api/src/controllers/drive.ts | 165 ++++++++++++++++++++++++++++++----- api/src/routes/api/drive.ts | 78 ++++++++--------- api/src/utils/validation.ts | 11 +++ package.json | 5 +- 5 files changed, 349 insertions(+), 63 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index d2d6b53..502bc02 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -84,6 +84,73 @@ components: - fileTree type: object additionalProperties: false + GetFileResponse: + properties: + status: + type: string + fileContent: + type: string + message: + type: string + required: + - status + type: object + additionalProperties: false + UpdateFileResponse: + properties: + status: + type: string + message: + type: string + required: + - status + type: object + additionalProperties: false + FilePayload: + properties: + filePath: + type: string + description: 'Path of the file' + example: /Public/somefolder/some.file + fileContent: + type: string + description: 'Contents of the file' + example: 'Contents of the File' + required: + - filePath + - fileContent + type: object + additionalProperties: false + TreeNode: + properties: + name: + type: string + relativePath: + type: string + absolutePath: + type: string + children: + items: + $ref: '#/components/schemas/TreeNode' + type: array + required: + - name + - relativePath + - absolutePath + - children + type: object + additionalProperties: false + GetFileTreeResponse: + properties: + status: + type: string + tree: + $ref: '#/components/schemas/TreeNode' + required: + - status + - tree + type: object + additionalProperties: false UserResponse: properties: id: @@ -348,6 +415,92 @@ paths: application/json: schema: $ref: '#/components/schemas/DeployPayload' + /SASjsApi/drive/file: + get: + operationId: GetFile + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileResponse' + examples: + 'Example 1': + value: {status: success, fileContent: 'Contents of the File'} + '400': + description: 'Unable to get File' + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileResponse' + examples: + 'Example 1': + value: {status: failure, message: 'File request failed.'} + description: 'Get file from SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: + - + in: query + name: filePath + required: true + schema: + type: string + patch: + operationId: UpdateFile + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFileResponse' + examples: + 'Example 1': + value: {status: success} + '400': + description: 'Unable to get File' + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFileResponse' + examples: + 'Example 1': + value: {status: failure, message: 'File request failed.'} + description: 'Modify a file in SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FilePayload' + /SASjsApi/drive/filetree: + get: + operationId: GetFileTree + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/GetFileTreeResponse' + description: 'Fetch file tree within SASjs Drive.' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: [] /SASjsApi/user: get: operationId: GetAllUsers diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index adb3f4e..f3971d2 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -1,13 +1,38 @@ -import { Security, Route, Tags, Example, Post, Body, Response } from 'tsoa' +import { + Security, + Route, + Tags, + Example, + Post, + Body, + Response, + Query, + Get, + Patch +} from 'tsoa' import { fileExists, readFile, createFile } from '@sasjs/utils' -import { createFileTree, getTreeExample } from '.' +import { createFileTree, ExecutionController, getTreeExample } from '.' -import { FileTree, isFileTree } from '../types' +import { FileTree, isFileQuery, isFileTree, TreeNode } from '../types' +import path from 'path' +import { getTmpFilesFolderPath } from '../utils' interface DeployPayload { appLoc?: string fileTree: FileTree } +interface FilePayload { + /** + * Path of the file + * @example "/Public/somefolder/some.file" + */ + filePath: string + /** + * Contents of the file + * @example "Contents of the File" + */ + fileContent: string +} interface DeployResponse { status: string @@ -15,18 +40,34 @@ interface DeployResponse { example?: FileTree } +interface GetFileResponse { + status: string + fileContent?: string + message?: string +} + +interface GetFileTreeResponse { + status: string + tree: TreeNode +} + +interface UpdateFileResponse { + status: string + message?: string +} + const fileTreeExample = getTreeExample() -const successResponse: DeployResponse = { +const successDeployResponse: DeployResponse = { status: 'success', message: 'Files deployed successfully to @sasjs/server.' } -const invalidFormatResponse: DeployResponse = { +const invalidDeployFormatResponse: DeployResponse = { status: 'failure', message: 'Provided not supported data format.', example: fileTreeExample } -const execErrorResponse: DeployResponse = { +const execDeployErrorResponse: DeployResponse = { status: 'failure', message: 'Deployment failed!' } @@ -39,42 +80,122 @@ export class DriveController { * Creates/updates files within SASjs Drive using provided payload. * */ - @Example(successResponse) - @Response(400, 'Invalid Format', invalidFormatResponse) - @Response(500, 'Execution Error', execErrorResponse) + @Example(successDeployResponse) + @Response(400, 'Invalid Format', invalidDeployFormatResponse) + @Response(500, 'Execution Error', execDeployErrorResponse) @Post('/deploy') public async deploy(@Body() body: DeployPayload): Promise { return deploy(body) } - async readFile(filePath: string) { - await this.validateFilePath(filePath) - return await readFile(filePath) + /** + * Get file from SASjs Drive + * + */ + @Example({ + status: 'success', + fileContent: 'Contents of the File' + }) + @Response(400, 'Unable to get File', { + status: 'failure', + message: 'File request failed.' + }) + @Get('/file') + public async getFile(@Query() filePath: string): Promise { + return getFile(filePath) } - async updateFile(filePath: string, fileContent: string) { - await this.validateFilePath(filePath) - return await createFile(filePath, fileContent) + /** + * Modify a file in SASjs Drive + * + */ + @Example({ + status: 'success' + }) + @Response(400, 'Unable to get File', { + status: 'failure', + message: 'File request failed.' + }) + @Patch('/file') + public async updateFile( + @Body() body: FilePayload + ): Promise { + return updateFile(body) } - private async validateFilePath(filePath: string) { - if (!(await fileExists(filePath))) { - throw 'DriveController: File does not exists.' - } + /** + * Fetch file tree within SASjs Drive. + * + */ + @Get('/filetree') + public async getFileTree(): Promise { + return getFileTree() } } +const getFileTree = () => { + const tree = new ExecutionController().buildDirectorytree() + return { status: 'success', tree } +} + const deploy = async (data: DeployPayload) => { if (!isFileTree(data.fileTree)) { - throw { code: 400, ...invalidFormatResponse } + throw { code: 400, ...invalidDeployFormatResponse } } await createFileTree( data.fileTree.members, data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : [] ).catch((err) => { - throw { code: 500, ...execErrorResponse, ...err } + throw { code: 500, ...execDeployErrorResponse, ...err } }) - return successResponse + return successDeployResponse +} + +const getFile = async (filePath: string): Promise => { + try { + const filePathFull = path + .join(getTmpFilesFolderPath(), filePath) + .replace(new RegExp('/', 'g'), path.sep) + + await validateFilePath(filePathFull) + const fileContent = await readFile(filePathFull) + + return { status: 'success', fileContent: fileContent } + } catch (err) { + throw { + code: 400, + status: 'failure', + message: 'File request failed.', + ...(typeof err === 'object' ? err : { details: err }) + } + } +} + +const updateFile = async (body: FilePayload): Promise => { + const { filePath, fileContent } = body + try { + const filePathFull = path + .join(getTmpFilesFolderPath(), filePath) + .replace(new RegExp('/', 'g'), path.sep) + + await validateFilePath(filePathFull) + await createFile(filePathFull, fileContent) + + return { status: 'success' } + } catch (err) { + throw { + code: 400, + status: 'failure', + message: 'File request failed.', + ...(typeof err === 'object' ? err : { details: err }) + } + } +} + +const validateFilePath = async (filePath: string) => { + if (!(await fileExists(filePath))) { + throw 'DriveController: File does not exists.' + } } diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 391e5b1..ef739f0 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -3,7 +3,11 @@ import path from 'path' import { ExecutionController } from '../../controllers' import { DriveController } from '../../controllers/drive' import { isFileQuery } from '../../types' -import { getTmpFilesFolderPath } from '../../utils' +import { + getFileDriveValidation, + getTmpFilesFolderPath, + updateFileDriveValidation +} from '../../utils' const driveRouter = express.Router() @@ -22,51 +26,47 @@ driveRouter.post('/deploy', async (req, res) => { }) driveRouter.get('/file', async (req, res) => { - if (isFileQuery(req.query)) { - const filePath = path - .join(getTmpFilesFolderPath(), req.query.filePath) - .replace(new RegExp('/', 'g'), path.sep) - await new DriveController() - .readFile(filePath) - .then((fileContent) => { - res.status(200).send({ status: 'success', fileContent: fileContent }) - }) - .catch((err) => { - res.status(400).send({ - status: 'failure', - message: 'File request failed.', - ...(typeof err === 'object' ? err : { details: err }) - }) - }) - } else { - res.status(400).send({ - status: 'failure', - message: 'Invalid Request: Expected parameter filePath was not provided' - }) + const { error, value: query } = getFileDriveValidation(req.query) + if (error) return res.status(400).send(error.details[0].message) + + const controller = new DriveController() + try { + const response = await controller.getFile(query.filePath) + res.send(response) + } catch (err: any) { + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err) } }) driveRouter.patch('/file', async (req, res) => { - const filePath = path - .join(getTmpFilesFolderPath(), req.body.filePath) - .replace(new RegExp('/', 'g'), path.sep) - await new DriveController() - .updateFile(filePath, req.body.fileContent) - .then(() => { - res.status(200).send({ status: 'success' }) - }) - .catch((err) => { - res.status(400).send({ - status: 'failure', - message: 'File request failed.', - ...(typeof err === 'object' ? err : { details: err }) - }) - }) + const { error, value: body } = updateFileDriveValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + const controller = new DriveController() + try { + const response = await controller.updateFile(body) + res.send(response) + } catch (err: any) { + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err) + } }) driveRouter.get('/fileTree', async (req, res) => { - const tree = new ExecutionController().buildDirectorytree() - res.status(200).send({ status: 'success', tree }) + const controller = new DriveController() + try { + const response = await controller.getFileTree() + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } }) export default driveRouter diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index de78fad..7113ed1 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -65,3 +65,14 @@ export const registerClientValidation = (data: any): Joi.ValidationResult => clientId: Joi.string().required(), clientSecret: Joi.string().required() }).validate(data) + +export const getFileDriveValidation = (data: any): Joi.ValidationResult => + Joi.object({ + filePath: Joi.string().required() + }).validate(data) + +export const updateFileDriveValidation = (data: any): Joi.ValidationResult => + Joi.object({ + filePath: Joi.string().required(), + fileContent: Joi.string().required() + }).validate(data) diff --git a/package.json b/package.json index 744067c..81942e0 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "0.0.1", "description": "NodeJS wrapper for calling the SAS binary executable", "scripts": { - "server:install": "cd web && npm ci && cd ../api && npm ci", - "server": "cd web && npm run build && cd ../api && npm run start:prod", + "server": "npm run server:prepare && npm run server:start", + "server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..", + "server:start": "cd api && npm run start:prod", "lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", "lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", "lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",