From 14c2def459e3fb1545ddc5634bbfc320bbaa3be8 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Sat, 6 Nov 2021 00:36:51 +0500 Subject: [PATCH] chore: drive auto-generated swagger --- .gitignore | 1 - public/swagger.yaml | 553 ++++++++++++++++++++++++++++++ src/controllers/deploy.ts | 14 +- src/controllers/drive.ts | 63 ++++ src/routes/api/drive.ts | 35 +- src/routes/api/index.ts | 2 +- src/routes/api/spec/drive.spec.ts | 16 +- src/types/FileTree.ts | 4 +- tsoa.json | 5 + 9 files changed, 648 insertions(+), 45 deletions(-) create mode 100644 public/swagger.yaml create mode 100644 src/controllers/drive.ts diff --git a/.gitignore b/.gitignore index 80afe14..ca8a456 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,5 @@ node_modules/ sas/ tmp/ build/ -public/ certificates/ .env diff --git a/public/swagger.yaml b/public/swagger.yaml new file mode 100644 index 0000000..09135db --- /dev/null +++ b/public/swagger.yaml @@ -0,0 +1,553 @@ +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + MemberType.folder: + enum: + - folder + type: string + FolderMember: + properties: + name: + type: string + type: + $ref: '#/components/schemas/MemberType.folder' + members: + items: + anyOf: + - + $ref: '#/components/schemas/FolderMember' + - + $ref: '#/components/schemas/ServiceMember' + type: array + required: + - name + - type + - members + type: object + additionalProperties: false + MemberType.service: + enum: + - service + type: string + ServiceMember: + properties: + name: + type: string + type: + $ref: '#/components/schemas/MemberType.service' + code: + type: string + required: + - name + - type + - code + type: object + additionalProperties: false + FileTree: + properties: + members: + items: + anyOf: + - + $ref: '#/components/schemas/FolderMember' + - + $ref: '#/components/schemas/ServiceMember' + type: array + required: + - members + type: object + additionalProperties: false + DeployResponse: + properties: + status: + type: string + message: + type: string + example: + $ref: '#/components/schemas/FileTree' + required: + - status + - message + type: object + additionalProperties: false + DeployPayload: + properties: + appLoc: + type: string + fileTree: + $ref: '#/components/schemas/FileTree' + required: + - fileTree + type: object + additionalProperties: false + UserResponse: + properties: + id: + type: number + format: double + username: + type: string + displayName: + type: string + required: + - id + - username + - displayName + type: object + additionalProperties: false + UserDetailsResponse: + properties: + id: + type: number + format: double + displayName: + type: string + username: + type: string + isActive: + type: boolean + isAdmin: + type: boolean + required: + - id + - displayName + - username + - isActive + - isAdmin + type: object + additionalProperties: false + UserPayload: + properties: + displayName: + type: string + description: 'Display name for user' + example: 'John Snow' + username: + type: string + description: 'Username for user' + example: johnSnow01 + password: + type: string + description: 'Password for user' + isAdmin: + type: boolean + description: 'Account should be admin or not, defaults to false' + example: 'false' + isActive: + type: boolean + description: 'Account should be active or not, defaults to true' + example: 'true' + required: + - displayName + - username + - password + type: object + additionalProperties: false + ClientPayload: + properties: + clientId: + type: string + description: 'Client ID' + example: someFormattedClientID1234 + clientSecret: + type: string + description: 'Client Secret' + example: someRandomCryptoString + required: + - clientId + - clientSecret + type: object + additionalProperties: false + AuthorizeResponse: + properties: + code: + type: string + description: 'Authorization code' + example: someRandomCryptoString + required: + - code + type: object + additionalProperties: false + AuthorizePayload: + properties: + username: + type: string + description: 'Username for user' + example: johnSnow01 + password: + type: string + description: 'Password for user' + example: secretpassword + clientId: + type: string + description: 'Client ID' + example: someFormattedClientID1234 + required: + - username + - password + - clientId + type: object + additionalProperties: false + TokenResponse: + properties: + accessToken: + type: string + description: 'Access Token' + example: someRandomCryptoString + refreshToken: + type: string + description: 'Refresh Token' + example: someRandomCryptoString + required: + - accessToken + - refreshToken + type: object + additionalProperties: false + TokenPayload: + properties: + clientId: + type: string + description: 'Client ID' + example: someFormattedClientID1234 + code: + type: string + description: 'Authorization code' + example: someRandomCryptoString + required: + - clientId + - code + type: object + additionalProperties: false + InfoJWT: + properties: + clientId: + type: string + userId: + type: number + format: double + required: + - clientId + - userId + type: object + additionalProperties: false + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +info: + title: server + version: 0.0.1 + description: 'SASjs server' + contact: + name: 'Analytium Ltd' +openapi: 3.0.0 +paths: + /SASjsApi/drive/deploy: + post: + operationId: Deploy + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/DeployResponse' + examples: + 'Example 1': + value: {status: success, message: 'Files deployed successfully to @sasjs/server.'} + '400': + description: 'Invalid Format' + content: + application/json: + schema: + $ref: '#/components/schemas/DeployResponse' + examples: + 'Example 1': + value: {status: failure, message: 'Provided not supported data format.'} + '500': + description: 'Execution Error' + content: + application/json: + schema: + $ref: '#/components/schemas/DeployResponse' + examples: + 'Example 1': + value: {status: failure, message: 'Deployment failed!'} + description: 'Creates/updates files within SASjs Drive using provided payload.' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DeployPayload' + /SASjsApi/user: + get: + operationId: GetAllUsers + responses: + '200': + description: Ok + content: + application/json: + schema: + items: + $ref: '#/components/schemas/UserResponse' + type: array + examples: + 'Example 1': + value: [{id: 123, username: johnusername, displayName: John}, {id: 456, username: starkusername, displayName: Stark}] + description: 'Get list of all users (username, displayname). All users can request this.' + tags: + - User + security: + - + bearerAuth: [] + parameters: [] + post: + operationId: CreateUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + description: 'Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.' + tags: + - User + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + '/SASjsApi/user/{userId}': + get: + operationId: GetUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + description: 'Get user properties - such as group memberships, userName, displayName.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: 1234 + patch: + operationId: UpdateUser + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserDetailsResponse' + examples: + 'Example 1': + value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true} + description: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: '1234' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserPayload' + delete: + operationId: DeleteUser + responses: + '204': + description: 'No content' + description: 'Delete a user. Can be performed either by admins, or the user in question.' + tags: + - User + security: + - + bearerAuth: [] + parameters: + - + description: 'The user''s identifier' + in: path + name: userId + required: true + schema: + format: double + type: number + example: 1234 + requestBody: + required: true + content: + application/json: + schema: + properties: + password: + type: string + type: object + /SASjsApi/client: + post: + operationId: CreateClient + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/ClientPayload' + examples: + 'Example 1': + value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString} + description: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.' + tags: + - Client + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ClientPayload' + /SASjsApi/auth/authorize: + post: + operationId: Authorize + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizeResponse' + examples: + 'Example 1': + value: {code: someRandomCryptoString} + description: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE' + tags: + - Auth + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizePayload' + /SASjsApi/auth/token: + post: + operationId: Token + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + examples: + 'Example 1': + value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString} + description: 'Accepts client/auth code and returns access/refresh tokens' + tags: + - Auth + security: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TokenPayload' + /SASjsApi/auth/refresh: + post: + operationId: Refresh + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + examples: + 'Example 1': + value: {accessToken: someRandomCryptoString, refreshToken: someRandomCryptoString} + description: 'Returns new access/refresh tokens' + tags: + - Auth + security: + - + bearerAuth: [] + parameters: [] + /SASjsApi/auth/logout: + post: + operationId: Logout + responses: + '204': + description: 'No content' + description: 'Logout terminate access/refresh tokens and returns nothing' + tags: + - Auth + security: + - + bearerAuth: [] + parameters: [] +servers: + - + url: / +tags: + - + name: User + description: 'Operations about users' + - + name: Client + description: 'Operations about clients' + - + name: Auth + description: 'Operations about auth' + - + name: Drive + description: 'Operations about drive' diff --git a/src/controllers/deploy.ts b/src/controllers/deploy.ts index a3fb302..2852bc8 100644 --- a/src/controllers/deploy.ts +++ b/src/controllers/deploy.ts @@ -1,11 +1,11 @@ -import { MemberType, FolderMember, ServiceMember } from '../types' +import { MemberType, FolderMember, ServiceMember, FileTree } from '../types' import { getTmpFilesFolderPath } from '../utils/file' import { createFolder, createFile, asyncForEach } from '@sasjs/utils' import path from 'path' // REFACTOR: export FileTreeCpntroller export const createFileTree = async ( - members: [FolderMember, ServiceMember], + members: (FolderMember | ServiceMember)[], parentFolders: string[] = [] ) => { const destinationPath = path.join( @@ -16,7 +16,7 @@ export const createFileTree = async ( await asyncForEach(members, async (member: FolderMember | ServiceMember) => { let name = member.name - if (member.type === 'service') name += '.sas' + if (member.type === MemberType.service) name += '.sas' if (member.type === MemberType.folder) { await createFolder(path.join(destinationPath, name)).catch((err) => @@ -36,19 +36,19 @@ export const createFileTree = async ( return Promise.resolve() } -export const getTreeExample = () => ({ +export const getTreeExample = (): FileTree => ({ members: [ { name: 'jobs', - type: 'folder', + type: MemberType.folder, members: [ { name: 'extract', - type: 'folder', + type: MemberType.folder, members: [ { name: 'makedata1', - type: 'service', + type: MemberType.service, code: '%put Hello World!;' } ] diff --git a/src/controllers/drive.ts b/src/controllers/drive.ts new file mode 100644 index 0000000..1f40251 --- /dev/null +++ b/src/controllers/drive.ts @@ -0,0 +1,63 @@ +import { Security, Route, Tags, Example, Post, Body, Response } from 'tsoa' +import { createFileTree, getTreeExample } from '.' + +import { FileTree, isFileTree } from '../types' + +interface DeployPayload { + appLoc?: string + fileTree: FileTree +} + +interface DeployResponse { + status: string + message: string + example?: FileTree +} + +const fileTreeExample = getTreeExample() + +const successResponse: DeployResponse = { + status: 'success', + message: 'Files deployed successfully to @sasjs/server.' +} +const invalidFormatResponse: DeployResponse = { + status: 'failure', + message: 'Provided not supported data format.', + example: fileTreeExample +} +const execErrorResponse: DeployResponse = { + status: 'failure', + message: 'Deployment failed!' +} + +@Security('bearerAuth') +@Route('SASjsApi/drive') +@Tags('Drive') +export default class DriveController { + /** + * Creates/updates files within SASjs Drive using provided payload. + * + */ + @Example(successResponse) + @Response(400, 'Invalid Format', invalidFormatResponse) + @Response(500, 'Execution Error', execErrorResponse) + @Post('/deploy') + public async deploy(@Body() body: DeployPayload): Promise { + return deploy(body) + } +} + +const deploy = async (data: DeployPayload) => { + if (!isFileTree(data.fileTree)) { + throw { code: 400, ...invalidFormatResponse } + } + + await createFileTree( + data.fileTree.members, + data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : [] + ).catch((err) => { + throw { code: 500, ...execErrorResponse, ...err } + }) + + return successResponse +} diff --git a/src/routes/api/drive.ts b/src/routes/api/drive.ts index c3724ab..f5e8c0a 100644 --- a/src/routes/api/drive.ts +++ b/src/routes/api/drive.ts @@ -1,35 +1,18 @@ import express from 'express' -import { createFileTree, getTreeExample } from '../../controllers' -import { isFileTree } from '../../types' +import DriveController from '../../controllers/drive' const driveRouter = express.Router() driveRouter.post('/deploy', async (req, res) => { - if (!isFileTree(req.body.fileTree)) { - res.status(400).send({ - status: 'failure', - message: 'Provided not supported data format.', - example: getTreeExample() - }) - - return + const controller = new DriveController() + try { + const response = await controller.deploy(req.body) + res.send(response) + } catch (err: any) { + const statusCode = err.code + delete err.code + res.status(statusCode).send(err) } - - await createFileTree( - req.body.fileTree.members, - req.body.appLoc ? req.body.appLoc.replace(/^\//, '').split('/') : [] - ) - .then(() => { - res.status(200).send({ - status: 'success', - message: 'Files deployed successfully to @sasjs/server.' - }) - }) - .catch((err) => { - res - .status(500) - .send({ status: 'failure', message: 'Deployment failed!', ...err }) - }) }) export default driveRouter diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index 7ecc1e5..8b9c3ac 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -25,7 +25,7 @@ router.use( swaggerUi.serve, swaggerUi.setup(undefined, { swaggerOptions: { - url: '/swagger.json' + url: '/swagger.yaml' } }) ) diff --git a/src/routes/api/spec/drive.spec.ts b/src/routes/api/spec/drive.spec.ts index 8f1231f..4432284 100644 --- a/src/routes/api/spec/drive.spec.ts +++ b/src/routes/api/spec/drive.spec.ts @@ -9,6 +9,7 @@ import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils' import path from 'path' import { generateAccessToken } from '../../../controllers/auth' import { saveTokensInDB } from '../../../utils' +import { FolderMember, ServiceMember } from '../../../types' const clientId = 'someclientID' const user = { @@ -135,21 +136,20 @@ describe('files', () => { ) await expect(folderExists(testJobFolder)).resolves.toEqual(true) - const testJobFile = - path.join( - testJobFolder, - getTreeExample().members[0].members[0].members[0].name - ) + '.sas' + const exampleService = getExampleService() + const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas' console.log(`[testJobFile]`, testJobFile) await expect(fileExists(testJobFile)).resolves.toEqual(true) - await expect(readFile(testJobFile)).resolves.toEqual( - getTreeExample().members[0].members[0].members[0].code - ) + await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code) await deleteFolder(getTmpFilesFolderPath()) }) }) }) + +const getExampleService = (): ServiceMember => + ((getTreeExample().members[0] as FolderMember).members[0] as FolderMember) + .members[0] as ServiceMember diff --git a/src/types/FileTree.ts b/src/types/FileTree.ts index 548bc5a..2058736 100644 --- a/src/types/FileTree.ts +++ b/src/types/FileTree.ts @@ -1,5 +1,5 @@ export interface FileTree { - members: [FolderMember, ServiceMember] + members: (FolderMember | ServiceMember)[] } export enum MemberType { @@ -10,7 +10,7 @@ export enum MemberType { export interface FolderMember { name: string type: MemberType.folder - members: [FolderMember, ServiceMember] + members: (FolderMember | ServiceMember)[] } export interface ServiceMember { diff --git a/tsoa.json b/tsoa.json index e3b95da..83fb1d9 100644 --- a/tsoa.json +++ b/tsoa.json @@ -22,8 +22,13 @@ { "name": "Auth", "description": "Operations about auth" + }, + { + "name": "Drive", + "description": "Operations about drive" } ], + "yaml": true, "specVersion": 3 } }