diff --git a/src/app.ts b/src/app.ts index 93db9fe..646ad88 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,10 +1,12 @@ import express from 'express' -import indexRouter from './routes' +import webRouter from './routes/web' +import apiRouter from './routes/api' const app = express() app.use(express.json({ limit: '50mb' })) -app.use('/', indexRouter) +app.use('/', webRouter) +app.use('/SASjsApi', apiRouter) export default app diff --git a/src/controllers/Execution.ts b/src/controllers/Execution.ts index 0410594..d57ffc0 100644 --- a/src/controllers/Execution.ts +++ b/src/controllers/Execution.ts @@ -14,7 +14,8 @@ export class ExecutionController { autoExec?: string, session?: Session, vars?: any, - otherArgs?: any + otherArgs?: any, + returnJson?: boolean ) { if (program) { if (!(await fileExists(program))) { @@ -89,6 +90,7 @@ ${program}` (key: string) => key.toLowerCase() === '_debug' ) + let jsonResult if ((debug && vars[debug] >= 131) || stderr) { webout = ` ${webout} @@ -97,12 +99,14 @@ ${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) } } diff --git a/src/controllers/deploy.ts b/src/controllers/deploy.ts index 2c1f1db..a3fb302 100644 --- a/src/controllers/deploy.ts +++ b/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/src/routes/api/drive.ts b/src/routes/api/drive.ts new file mode 100644 index 0000000..c3724ab --- /dev/null +++ b/src/routes/api/drive.ts @@ -0,0 +1,35 @@ +import express from 'express' +import { createFileTree, getTreeExample } from '../../controllers' +import { isFileTree } from '../../types' + +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 }) + }) +}) + +export default driveRouter diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts new file mode 100644 index 0000000..afaa0d6 --- /dev/null +++ b/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/src/routes/api/spec/drive.spec.ts b/src/routes/api/spec/drive.spec.ts new file mode 100644 index 0000000..191db5b --- /dev/null +++ b/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/src/routes/api/stp.ts b/src/routes/api/stp.ts new file mode 100644 index 0000000..45ae3a1 --- /dev/null +++ b/src/routes/api/stp.ts @@ -0,0 +1,59 @@ +import express from 'express' +import { ExecutionResult, isRequestQuery, isFileTree } 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 (isRequestQuery(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/src/routes/spec/routes.spec.ts b/src/routes/spec/routes.spec.ts deleted file mode 100644 index 56d2e69..0000000 --- a/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/src/routes/web/index.ts b/src/routes/web/index.ts new file mode 100644 index 0000000..21fcb3e --- /dev/null +++ b/src/routes/web/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import webRouter from './web' + +const router = express.Router() + +router.use('/web', webRouter) + +export default router diff --git a/src/routes/web/web.ts b/src/routes/web/web.ts new file mode 100644 index 0000000..c45f67b --- /dev/null +++ b/src/routes/web/web.ts @@ -0,0 +1,55 @@ +import express from 'express' +import { + createFileTree, + getSessionController, + getTreeExample +} from '../../controllers' +import { ExecutionResult, isRequestQuery, isFileTree } from '../../types' +import path from 'path' +import { + getTmpFilesFolderPath, + getTmpFolderPath, + makeFilesNamesMap +} from '../../utils' +import { ExecutionController, FileUploadController } from '../../controllers' +import { uuidv4 } from '@sasjs/utils' + +const webRouter = express.Router() + +webRouter.get('/', async (_, res) => { + res.status(200).send('Welcome to @sasjs/server API') +}) + +// TODO: respond with HTML page including file tree +webRouter.get('/SASjsExecutor', async (req, res) => { + res.status(200).send({ status: 'success', tree: {} }) +}) + +webRouter.get('/SASjsExecutor/do', async (req, res) => { + if (isRequestQuery(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