From feeec4eb149e9a47e5a52320d1fc95243bf5eb15 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 28 Feb 2022 22:34:18 +0500 Subject: [PATCH 1/3] fix(upload): added query param as well for filepath --- api/public/swagger.yaml | 13 +++++++++---- api/src/controllers/drive.ts | 7 ++++--- api/src/routes/api/drive.ts | 21 ++++++++++++++++----- api/src/routes/api/spec/drive.spec.ts | 15 ++++++++++++++- api/src/utils/validation.ts | 13 +++++++++++++ 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index ad4dde2..ff8c054 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -669,7 +669,13 @@ paths: security: - bearerAuth: [] - parameters: [] + parameters: + - + in: query + name: _filePath + required: false + schema: + type: string requestBody: required: true content: @@ -677,13 +683,12 @@ paths: schema: type: object properties: - filePath: - type: string file: type: string format: binary + filePath: + type: string required: - - filePath - file patch: operationId: UpdateFile diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index bab541b..a59b90f 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -129,10 +129,11 @@ export class DriveController { }) @Post('/file') public async saveFile( - @FormField() filePath: string, - @UploadedFile() file: Express.Multer.File + @UploadedFile() file: Express.Multer.File, + @Query() _filePath?: string, + @FormField() filePath?: string ): Promise { - return saveFile(filePath, file) + return saveFile((_filePath ?? filePath)!, file) } /** diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index fb4bc7c..2a533f9 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -3,7 +3,12 @@ import { deleteFile } from '@sasjs/utils' import { multerSingle } from '../../middlewares/multer' import { DriveController } from '../../controllers/' -import { getFileDriveValidation, updateFileDriveValidation } from '../../utils' +import { + getFileDriveValidation, + updateFileDriveValidation, + uploadFileBodyValidation, + uploadFileParamValidation +} from '../../utils' const controller = new DriveController() @@ -42,16 +47,22 @@ driveRouter.post( '/file', (...arg) => multerSingle('file', arg), async (req, res) => { - const { error, value: body } = updateFileDriveValidation(req.body) - if (error) { + const { error: errQ, value: query } = uploadFileParamValidation(req.query) + const { error: errB, value: body } = uploadFileBodyValidation(req.body) + + if (errQ && errB) { if (req.file) await deleteFile(req.file.path) - return res.status(400).send(error.details[0].message) + return res.status(400).send(errB.details[0].message) } if (!req.file) return res.status(400).send('"file" is not present.') try { - const response = await controller.saveFile(body.filePath, req.file) + const response = await controller.saveFile( + req.file, + query._filePath, + body.filePath + ) res.send(response) } catch (err: any) { await deleteFile(req.file.path) diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index aa6d9b1..1f70a56 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -172,7 +172,7 @@ describe('files', () => { describe('file', () => { describe('create', () => { - it('should create a SAS file on drive', async () => { + it('should create a SAS file on drive having filePath as form field', async () => { const res = await request(app) .post('/SASjsApi/drive/file') .auth(accessToken, { type: 'bearer' }) @@ -185,6 +185,19 @@ describe('files', () => { }) }) + it('should create a SAS file on drive having _filePath as query param', async () => { + const res = await request(app) + .post('/SASjsApi/drive/file') + .auth(accessToken, { type: 'bearer' }) + .query({ _filePath: '/my/path/code1.sas' }) + .attach('file', path.join(__dirname, 'files', 'sample.sas')) + + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual({ + status: 'success' + }) + }) + it('should respond with Unauthorized if access token is not present', async () => { const res = await request(app) .post('/SASjsApi/drive/file') diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 8f1f0cb..1bd1c31 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -72,12 +72,25 @@ export const getFileDriveValidation = (data: any): Joi.ValidationResult => }).validate(data) export const updateFileDriveValidation = (data: any): Joi.ValidationResult => + Joi.object({ + filePath: Joi.string().required(), + fileContent: Joi.string().required() + }).validate(data) + +export const uploadFileBodyValidation = (data: any): Joi.ValidationResult => Joi.object({ filePath: Joi.string().pattern(/.sas$/).required().messages({ 'string.pattern.base': `Valid extensions for filePath: .sas` }) }).validate(data) +export const uploadFileParamValidation = (data: any): Joi.ValidationResult => + Joi.object({ + _filePath: Joi.string().pattern(/.sas$/).required().messages({ + 'string.pattern.base': `Valid extensions for filePath: .sas` + }) + }).validate(data) + export const runSASValidation = (data: any): Joi.ValidationResult => Joi.object({ code: Joi.string().required() From 5fce7d8f71466ea78e5eb6ba562796bafbc34696 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 28 Feb 2022 22:54:49 +0500 Subject: [PATCH 2/3] chore: added some docs to file upload --- api/public/swagger.yaml | 3 +++ api/src/controllers/drive.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index ff8c054..6adc771 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -663,6 +663,7 @@ paths: examples: 'Example 1': value: {status: failure, message: 'File request failed.'} + description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provided else API will respond with Bad Request." summary: 'Create a file in SASjs Drive' tags: - Drive @@ -671,11 +672,13 @@ paths: bearerAuth: [] parameters: - + description: 'Location of SAS program' in: query name: _filePath required: false schema: type: string + example: /Public/somefolder/some.file.sas requestBody: required: true content: diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index a59b90f..6bbb813 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -117,7 +117,13 @@ export class DriveController { } /** + * It's optional to either provide `_filePath` in url as query parameter + * Or provide `filePath` in body as form field. + * But it's required to provided else API will respond with Bad Request. + * * @summary Create a file in SASjs Drive + * @param _filePath Location of SAS program + * @example _filePath "/Public/somefolder/some.file.sas" * */ @Example({ From ed5c58e10e87e647a919a143fc6abe90199778bc Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Mon, 28 Feb 2022 23:39:03 +0500 Subject: [PATCH 3/3] chore: added restclient sample requests --- restClient/auth.rest | 22 +++++++++++++ restClient/drive.rest | 45 +++++++++++++++++++++++++++ routes.rest => restClient/routes.rest | 37 +++++++++++++++++++++- restClient/sample.sas | 1 + restClient/session.rest | 2 ++ restClient/users.rest | 10 ++++++ 6 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 restClient/auth.rest create mode 100644 restClient/drive.rest rename routes.rest => restClient/routes.rest (62%) create mode 100644 restClient/sample.sas create mode 100644 restClient/session.rest create mode 100644 restClient/users.rest diff --git a/restClient/auth.rest b/restClient/auth.rest new file mode 100644 index 0000000..6eed756 --- /dev/null +++ b/restClient/auth.rest @@ -0,0 +1,22 @@ +### Get Auth Code +POST http://localhost:5000/SASjsApi/auth/authorize +Content-Type: application/json + +{ + "username": "secretuser", + "password": "secretpassword", + "client_id": "clientID1" +} + +### Exchange AuthCode with Access/Refresh Tokens +POST http://localhost:5000/SASjsApi/auth/token +Content-Type: application/json + +{ + "client_id": "clientID1", + "client_secret": "clientID1secret", + "code": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDYxLCJleHAiOjE2MzU4MDQwOTF9.jV7DpBWG7XAGODs22zAW_kWOqVLZvOxmmYJGpSNQ-KM" +} + +### Perform logout to deactivate access token instantly +DELETE http://localhost:5000/SASjsApi/auth/logout diff --git a/restClient/drive.rest b/restClient/drive.rest new file mode 100644 index 0000000..75315f1 --- /dev/null +++ b/restClient/drive.rest @@ -0,0 +1,45 @@ +### +POST http://localhost:5000/SASjsApi/drive/deploy +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I + +### multipart upload to sas server file +POST http://localhost:5000/SASjsApi/drive/file +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="filePath" + +/saad/files/new.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="sample_new.sas" +Content-Type: application/octet-stream + +< ./sample.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + +### multipart upload to sas server file text +POST http://localhost:5000/SASjsApi/drive/file +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW \n +Content-Disposition: form-data; name="filePath" + +/saad/files/new2.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="sample_new.sas" +Content-Type: text/plain + +SOME CONTENTS OF SAS FILE IN REQUEST + +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + + +Users + "username": "username1", + "password": "some password", + + "username": "username2", + "password": "some password", +Admins + "username": "secretuser", + "password": "secretpassword", \ No newline at end of file diff --git a/routes.rest b/restClient/routes.rest similarity index 62% rename from routes.rest rename to restClient/routes.rest index 91d8954..bfae700 100644 --- a/routes.rest +++ b/restClient/routes.rest @@ -23,7 +23,7 @@ Content-Type: application/json "client_secret": "newClientSecret" } ### -POST https://sas.analytium.co.uk:5002/SASjsApi/auth/authorize +POST http://localhost:5000/SASjsApi/auth/authorize Content-Type: application/json { @@ -45,6 +45,41 @@ Content-Type: application/json ### DELETE http://localhost:5000/SASjsApi/auth/logout +### +GET http://localhost:5000/SASjsApi/session + + +### multipart upload to sas server file +POST http://localhost:5000/SASjsApi/drive/file +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="filePath" + +/saad/files/new.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="sample_new.sas" +Content-Type: application/octet-stream + +< ./sample.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + +### multipart upload to sas server file text +POST http://localhost:5000/SASjsApi/drive/file +Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW + +------WebKitFormBoundary7MA4YWxkTrZu0gW \n +Content-Disposition: form-data; name="filePath" + +/saad/files/new2.sas +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="file"; filename="sample_new.sas" +Content-Type: text/plain + +SOME CONTENTS OF SAS FILE IN REQUEST + +------WebKitFormBoundary7MA4YWxkTrZu0gW-- + Users "username": "username1", diff --git a/restClient/sample.sas b/restClient/sample.sas new file mode 100644 index 0000000..8556487 --- /dev/null +++ b/restClient/sample.sas @@ -0,0 +1 @@ +some code of sas \ No newline at end of file diff --git a/restClient/session.rest b/restClient/session.rest new file mode 100644 index 0000000..31b96fc --- /dev/null +++ b/restClient/session.rest @@ -0,0 +1,2 @@ +### Get current user's info via access token +GET http://localhost:5000/SASjsApi/session diff --git a/restClient/users.rest b/restClient/users.rest new file mode 100644 index 0000000..8e7d51e --- /dev/null +++ b/restClient/users.rest @@ -0,0 +1,10 @@ +### Create User +POST http://localhost:5000/SASjsApi/user +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InNlY3JldHVzZXIiLCJpc2FkbWluIjp0cnVlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODAzOTc3LCJleHAiOjE2MzU4OTAzNzd9.f-FLgLwryKvB5XrihdzaGZajO3d5E5OHEEuJI_03GRI +Content-Type: application/json + +{ + "displayname": "User 2", + "username": "username2", + "password": "some password" +}