diff --git a/api/src/app.ts b/api/src/app.ts index aebf051..28c31d1 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,11 +1,16 @@ import express from 'express' -import indexRouter from './routes' -import path from 'path' +import webRouter from './routes/web' +import apiRouter from './routes/api' import { getWebBuildFolderPath } from './utils' + const app = express() app.use(express.json({ limit: '50mb' })) -app.use('/', indexRouter) + +app.use('/', webRouter) +app.use('/SASjsApi', apiRouter) +app.use(express.json({ limit: '50mb' })) + app.use(express.static(getWebBuildFolderPath())) export default app diff --git a/api/src/controllers/Execution.ts b/api/src/controllers/Execution.ts index a025dd4..76378b2 100644 --- a/api/src/controllers/Execution.ts +++ b/api/src/controllers/Execution.ts @@ -16,7 +16,8 @@ export class ExecutionController { autoExec?: string, session?: Session, vars?: any, - otherArgs?: any + otherArgs?: any, + returnJson?: boolean ) { if (program) { if (!(await fileExists(program))) { @@ -91,6 +92,7 @@ ${program}` (key: string) => key.toLowerCase() === '_debug' ) + let jsonResult if ((debug && vars[debug] >= 131) || stderr) { webout = ` ${webout} @@ -99,13 +101,15 @@ ${webout}
${log}
` + } else if (returnJson) { + jsonResult = { result: webout, log: log } } session.inUse = false sessionController.deleteSession(session) - return Promise.resolve(webout) + return Promise.resolve(jsonResult || webout) } buildDirectorytree() { diff --git a/api/src/controllers/deploy.ts b/api/src/controllers/deploy.ts index 2c1f1db..a3fb302 100644 --- a/api/src/controllers/deploy.ts +++ b/api/src/controllers/deploy.ts @@ -14,7 +14,9 @@ export const createFileTree = async ( ) await asyncForEach(members, async (member: FolderMember | ServiceMember) => { - const name = member.name + let name = member.name + + if (member.type === 'service') name += '.sas' if (member.type === MemberType.folder) { await createFolder(path.join(destinationPath, name)).catch((err) => diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts new file mode 100644 index 0000000..986c677 --- /dev/null +++ b/api/src/routes/api/drive.ts @@ -0,0 +1,85 @@ +import express from 'express' +import path from 'path' +import { createFileTree, getTreeExample, DriveController, ExecutionController } from '../../controllers' +import { isFileTree, isFileQuery } from '../../types' +import { getTmpFilesFolderPath } from '../../utils' + +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 + } + + 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 }) + }) +}) + +driveRouter.get('/files', 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' + }) + } +}) + +driveRouter.patch('/files', 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 }) + }) + }) +}) + +driveRouter.get('/fileTree', async (req, res) => { + const tree = new ExecutionController().buildDirectorytree() + res.status(200).send({ status: 'success', tree }) +}) + +export default driveRouter diff --git a/api/src/routes/api/index.ts b/api/src/routes/api/index.ts new file mode 100644 index 0000000..afaa0d6 --- /dev/null +++ b/api/src/routes/api/index.ts @@ -0,0 +1,10 @@ +import express from 'express' +import driveRouter from './drive' +import stpRouter from './stp' + +const router = express.Router() + +router.use('/drive', driveRouter) +router.use('/stp', stpRouter) + +export default router diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts new file mode 100644 index 0000000..191db5b --- /dev/null +++ b/api/src/routes/api/spec/drive.spec.ts @@ -0,0 +1,114 @@ +import request from 'supertest' +import app from '../../../app' +import { getTreeExample } from '../../../controllers/deploy' +import { getTmpFilesFolderPath } from '../../../utils/file' +import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils' +import path from 'path' + +describe('files', () => { + describe('deploy', () => { + const shouldFailAssertion = async (payload: any) => { + const res = await request(app) + .post('/SASjsApi/drive/deploy') + .send(payload) + + expect(res.statusCode).toEqual(400) + expect(res.body).toEqual({ + status: 'failure', + message: 'Provided not supported data format.', + example: getTreeExample() + }) + } + + it('should respond with payload example if valid payload was not provided', async () => { + await shouldFailAssertion(null) + await shouldFailAssertion(undefined) + await shouldFailAssertion('data') + await shouldFailAssertion({}) + await shouldFailAssertion({ + userId: 1, + title: 'test is cool' + }) + await shouldFailAssertion({ + membersWRONG: [] + }) + await shouldFailAssertion({ + members: {} + }) + await shouldFailAssertion({ + members: [ + { + nameWRONG: 'jobs', + type: 'folder', + members: [] + } + ] + }) + await shouldFailAssertion({ + members: [ + { + name: 'jobs', + type: 'WRONG', + members: [] + } + ] + }) + await shouldFailAssertion({ + members: [ + { + name: 'jobs', + type: 'folder', + members: [ + { + name: 'extract', + type: 'folder', + members: [ + { + name: 'makedata1', + type: 'service', + codeWRONG: '%put Hello World!;' + } + ] + } + ] + } + ] + }) + }) + + it('should respond with payload example if valid payload was not provided', async () => { + const res = await request(app) + .post('/SASjsApi/drive/deploy') + .send({ fileTree: getTreeExample() }) + + expect(res.statusCode).toEqual(200) + expect(res.text).toEqual( + '{"status":"success","message":"Files deployed successfully to @sasjs/server."}' + ) + await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true) + + const testJobFolder = path.join( + getTmpFilesFolderPath(), + 'jobs', + 'extract' + ) + await expect(folderExists(testJobFolder)).resolves.toEqual(true) + + const testJobFile = + path.join( + testJobFolder, + getTreeExample().members[0].members[0].members[0].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 deleteFolder(getTmpFilesFolderPath()) + }) + }) +}) diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts new file mode 100644 index 0000000..3c8e597 --- /dev/null +++ b/api/src/routes/api/stp.ts @@ -0,0 +1,59 @@ +import express from 'express' +import { isExecutionQuery } from '../../types' +import path from 'path' +import { getTmpFilesFolderPath, makeFilesNamesMap } from '../../utils' +import { ExecutionController, FileUploadController } from '../../controllers' + +const stpRouter = express.Router() + +const fileUploadController = new FileUploadController() + +stpRouter.post( + '/execute', + fileUploadController.preuploadMiddleware, + fileUploadController.getMulterUploadObject().any(), + async (req: any, res: any) => { + if (isExecutionQuery(req.body)) { + let sasCodePath = + path + .join(getTmpFilesFolderPath(), req.body._program) + .replace(new RegExp('/', 'g'), path.sep) + '.sas' + + let filesNamesMap = null + + if (req.files && req.files.length > 0) { + filesNamesMap = makeFilesNamesMap(req.files) + } + + await new ExecutionController() + .execute( + sasCodePath, + undefined, + req.sasSession, + { ...req.query, ...req.body }, + { filesNamesMap: filesNamesMap }, + true + ) + .then((result: {}) => { + res.status(200).send({ + status: 'success', + ...result + }) + }) + .catch((err: {} | string) => { + res.status(400).send({ + status: 'failure', + message: 'Job execution failed.', + ...(typeof err === 'object' ? err : { details: err }) + }) + }) + } else { + res.status(400).send({ + status: 'failure', + message: `Please provide the location of SAS code` + }) + } + } +) + +export default stpRouter diff --git a/api/src/routes/index.ts b/api/src/routes/index.ts deleted file mode 100644 index dab165e..0000000 --- a/api/src/routes/index.ts +++ /dev/null @@ -1,176 +0,0 @@ -import express from 'express' -import path from 'path' -import { - createFileTree, - getTreeExample, - DriveController, - ExecutionController, - FileUploadController -} from '../controllers' -import { isExecutionQuery, isFileQuery, isFileTree } from '../types' -import { - getTmpFilesFolderPath, - getWebBuildFolderPath, - makeFilesNamesMap -} from '../utils' - -const router = express.Router() - -const fileUploadController = new FileUploadController() - -router.get('/', async (_, res) => { - res.sendFile(path.join(getWebBuildFolderPath(), 'index.html')) -}) - -router.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 - } - - 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 }) - }) -}) - -router.get('/SASjsApi/files', 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' - }) - } -}) - -router.post('/SASjsApi/files', 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 }) - }) - }) -}) - -router.get('/SASjsApi/executor', async (req, res) => { - const tree = new ExecutionController().buildDirectorytree() - res.status(200).send({ status: 'success', tree }) -}) - -router.get('/SASjsExecutor/do', async (req, res) => { - if (isExecutionQuery(req.query)) { - let sasCodePath = path - .join(getTmpFilesFolderPath(), req.query._program) - .replace(new RegExp('/', 'g'), path.sep) - - // If no extension provided, add .sas extension - sasCodePath += '.sas' - - await new ExecutionController() - .execute(sasCodePath, undefined, undefined, { ...req.query }) - .then((result: {}) => { - res.status(200).send(result) - }) - .catch((err: {} | string) => { - res.status(400).send({ - status: 'failure', - message: 'Job execution failed.', - ...(typeof err === 'object' ? err : { details: err }) - }) - }) - } else { - res.status(400).send({ - status: 'failure', - message: `Please provide the location of SAS code` - }) - } -}) - -router.post( - '/SASjsExecutor/do', - fileUploadController.preuploadMiddleware, - fileUploadController.getMulterUploadObject().any(), - async (req: any, res: any) => { - if (isExecutionQuery(req.query)) { - let sasCodePath = path - .join(getTmpFilesFolderPath(), req.query._program) - .replace(new RegExp('/', 'g'), path.sep) - - // If no extension provided, add .sas extension - sasCodePath += '.sas' - - let filesNamesMap = null - - if (req.files && req.files.length > 0) { - filesNamesMap = makeFilesNamesMap(req.files) - } - - await new ExecutionController() - .execute( - sasCodePath, - undefined, - req.sasSession, - { ...req.query, ...req.body }, - { filesNamesMap: filesNamesMap } - ) - .then((result: {}) => { - res.status(200).send(result) - }) - .catch((err: {} | string) => { - res.status(400).send({ - status: 'failure', - message: 'Job execution failed.', - ...(typeof err === 'object' ? err : { details: err }) - }) - }) - } else { - res.status(400).send({ - status: 'failure', - message: `Please provide the location of SAS code` - }) - } - } -) - -export default router diff --git a/api/src/routes/spec/routes.spec.ts b/api/src/routes/spec/routes.spec.ts deleted file mode 100644 index 56d2e69..0000000 --- a/api/src/routes/spec/routes.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import request from 'supertest' -import app from '../../app' -import { getTreeExample } from '../../controllers/deploy' -import { getTmpFilesFolderPath } from '../../utils/file' -import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils' -import path from 'path' - -describe('deploy', () => { - const shouldFailAssertion = async (payload: any) => { - const res = await request(app).post('/deploy').send(payload) - - expect(res.statusCode).toEqual(400) - expect(res.body).toEqual({ - status: 'failure', - message: 'Provided not supported data format.', - example: getTreeExample() - }) - } - - it('should respond with payload example if valid payload was not provided', async () => { - await shouldFailAssertion(null) - await shouldFailAssertion(undefined) - await shouldFailAssertion('data') - await shouldFailAssertion({}) - await shouldFailAssertion({ - userId: 1, - title: 'test is cool' - }) - await shouldFailAssertion({ - membersWRONG: [] - }) - await shouldFailAssertion({ - members: {} - }) - await shouldFailAssertion({ - members: [ - { - nameWRONG: 'jobs', - type: 'folder', - members: [] - } - ] - }) - await shouldFailAssertion({ - members: [ - { - name: 'jobs', - type: 'WRONG', - members: [] - } - ] - }) - await shouldFailAssertion({ - members: [ - { - name: 'jobs', - type: 'folder', - members: [ - { - name: 'extract', - type: 'folder', - members: [ - { - name: 'makedata1', - type: 'service', - codeWRONG: '%put Hello World!;' - } - ] - } - ] - } - ] - }) - }) - - it('should respond with payload example if valid payload was not provided', async () => { - const res = await request(app) - .post('/deploy') - .send({ fileTree: getTreeExample() }) - - expect(res.statusCode).toEqual(200) - expect(res.text).toEqual( - '{"status":"success","message":"Files deployed successfully to @sasjs/server."}' - ) - await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true) - - const testJobFolder = path.join(getTmpFilesFolderPath(), 'jobs', 'extract') - await expect(folderExists(testJobFolder)).resolves.toEqual(true) - - const testJobFile = path.join( - testJobFolder, - getTreeExample().members[0].members[0].members[0].name - ) - - await expect(fileExists(testJobFile)).resolves.toEqual(true) - - await expect(readFile(testJobFile)).resolves.toEqual( - getTreeExample().members[0].members[0].members[0].code - ) - - await deleteFolder(getTmpFilesFolderPath()) - }) -}) diff --git a/api/src/routes/web/index.ts b/api/src/routes/web/index.ts new file mode 100644 index 0000000..6f75c11 --- /dev/null +++ b/api/src/routes/web/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import webRouter from './web' + +const router = express.Router() + +router.use('/', webRouter) + +export default router diff --git a/api/src/routes/web/web.ts b/api/src/routes/web/web.ts new file mode 100644 index 0000000..7f90ea6 --- /dev/null +++ b/api/src/routes/web/web.ts @@ -0,0 +1,43 @@ +import express from 'express' +import { isExecutionQuery } from '../../types' +import path from 'path' +import { + getTmpFilesFolderPath, + getWebBuildFolderPath, +} from '../../utils' +import { ExecutionController } from '../../controllers' + +const webRouter = express.Router() + +webRouter.get('/', async (_, res) => { + res.sendFile(path.join(getWebBuildFolderPath(), 'index.html')) +}) + +webRouter.get('/SASjsExecutor/do', async (req, res) => { + if (isExecutionQuery(req.query)) { + let sasCodePath = + path + .join(getTmpFilesFolderPath(), req.query._program) + .replace(new RegExp('/', 'g'), path.sep) + '.sas' + + await new ExecutionController() + .execute(sasCodePath, undefined, undefined, { ...req.query }) + .then((result: {}) => { + res.status(200).send(result) + }) + .catch((err: {} | string) => { + res.status(400).send({ + status: 'failure', + message: 'Job execution failed.', + ...(typeof err === 'object' ? err : { details: err }) + }) + }) + } else { + res.status(400).send({ + status: 'failure', + message: `Please provide the location of SAS code` + }) + } +}) + +export default webRouter