1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-11 03:34:35 +00:00

chore: restructure repo into sub folders

This commit is contained in:
2021-10-20 11:43:10 +00:00
parent 99d55775aa
commit d530e0801e
28 changed files with 18 additions and 2 deletions

15
api/jest.config.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'node',
// FIXME: improve test coverage and uncomment below lines
// coverageThreshold: {
// global: {
// branches: 80,
// functions: 80,
// lines: 80,
// statements: -10
// }
// },
collectCoverageFrom: ['src/**/{!(index),}.ts'],
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/build/']
}

9673
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
api/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "server",
"version": "0.0.1",
"description": "SASjs server",
"main": "./src/server.ts",
"scripts": {
"start": "nodemon ./src/server.ts",
"start:prod": "nodemon ./src/prod-server.ts",
"build": "rimraf build && tsc",
"semantic-release": "semantic-release -d",
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
"test": "mkdir -p tmp && jest --coverage",
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack"
},
"release": {
"branches": [
"master"
]
},
"author": "Analytium Ltd",
"dependencies": {
"@sasjs/utils": "^2.23.3",
"express": "^4.17.1",
"multer": "^1.4.3"
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/jest": "^26.0.24",
"@types/multer": "^1.4.7",
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"jest": "^27.0.6",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.3",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas",
"sasJsPort": 5005
}
}

10
api/src/app.ts Normal file
View File

@@ -0,0 +1,10 @@
import express from 'express'
import indexRouter from './routes'
import path from 'path'
const app = express()
app.use(express.json({ limit: '50mb' }))
app.use('/', indexRouter)
app.use(express.static(path.join(__dirname, '..', '..', 'web', 'build')))
export default app

View File

@@ -0,0 +1,108 @@
import { getSessionController } from './'
import { readFile, fileExists, createFile } from '@sasjs/utils'
import path from 'path'
import { configuration } from '../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
import { Session } from '../types'
import { generateFileUploadSasCode } from '../utils'
const execFilePromise = promisify(execFile)
export class ExecutionController {
async execute(
program = '',
autoExec?: string,
session?: Session,
vars?: any,
otherArgs?: any
) {
if (program) {
if (!(await fileExists(program))) {
throw 'ExecutionController: SAS file does not exist.'
}
program = await readFile(program)
if (vars) {
Object.keys(vars).forEach(
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
)
}
}
const sessionController = getSessionController()
if (!session) {
session = await sessionController.getSession()
session.inUse = true
}
let log = path.join(session.path, 'log.log')
let webout = path.join(session.path, 'webout.txt')
await createFile(webout, '')
program = `
%let sasjsprocessmode=Stored Program;
filename _webout "${webout}";
${program}`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs && otherArgs.filesNamesMap) {
const uploadSasCode = await generateFileUploadSasCode(
otherArgs.filesNamesMap,
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}
}
const code = path.join(session.path, 'code.sas')
if (!(await fileExists(code))) {
await createFile(code, program)
}
let additionalArgs: string[] = []
if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec]
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
'-SYSIN',
code,
'-LOG',
log,
'-WORK',
session.path,
...additionalArgs,
process.platform === 'win32' ? '-nosplash' : ''
]).catch((err) => ({ stderr: err, stdout: '' }))
if (await fileExists(log)) log = await readFile(log)
else log = ''
if (await fileExists(webout)) webout = await readFile(webout)
else webout = ''
const debug = Object.keys(vars).find(
(key: string) => key.toLowerCase() === '_debug'
)
if ((debug && vars[debug] >= 131) || stderr) {
webout = `<html><body>
${webout}
<div style="text-align:left">
<hr /><h2>SAS Log</h2>
<pre>${log}</pre>
</div>
</body></html>`
}
session.inUse = false
sessionController.deleteSession(session)
return Promise.resolve(webout)
}
}

View File

@@ -0,0 +1,36 @@
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
const multer = require('multer')
export class FileUploadController {
private storage = multer.diskStorage({
destination: function (req: any, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder
cb(null, req.sasSession.path)
},
filename: function (req: any, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
}
})
private upload = multer({ storage: this.storage })
//It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded
public preuploadMiddleware = async (req: any, res: any, next: any) => {
let session
const sessionController = getSessionController()
session = await sessionController.getSession()
session.inUse = true
req.sasSession = session
next()
}
public getMulterUploadObject() {
return this.upload
}
}

View File

@@ -0,0 +1,127 @@
import { Session } from '../types'
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils'
import {
deleteFolder,
createFile,
fileExists,
deleteFile,
generateTimestamp
} from '@sasjs/utils'
import path from 'path'
import { ExecutionController } from './Execution'
export class SessionController {
private sessions: Session[] = []
private executionController: ExecutionController
constructor() {
this.executionController = new ExecutionController()
}
public async getSession() {
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const session = readySessions.length
? readySessions[0]
: await this.createSession()
if (readySessions.length < 2) this.createSession()
return session
}
private async createSession() {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(await getTmpSessionsFolderPath(), sessionId)
const autoExecContent = `data _null_;
/* remove the dummy SYSIN */
length fname $8;
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
slept=slept+sleep(0.01,1);
end;
run;
EOL`
const autoExec = path.join(sessionFolder, 'autoexec.sas')
await createFile(autoExec, autoExecContent)
await createFile(path.join(sessionFolder, 'code.sas'), '')
const creationTimeStamp = sessionId.split('-').pop() as string
const session: Session = {
id: sessionId,
ready: false,
creationTimeStamp: creationTimeStamp,
deathTimeStamp: (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString(),
path: sessionFolder,
inUse: false
}
this.scheduleSessionDestroy(session)
this.executionController.execute('', autoExec, session).catch(() => {})
this.sessions.push(session)
await this.waitForSession(session)
return session
}
public async waitForSession(session: Session) {
if (await fileExists(path.join(session.path, 'code.sas'))) {
while (await fileExists(path.join(session.path, 'code.sas'))) {}
await deleteFile(path.join(session.path, 'log.log'))
session.ready = true
return Promise.resolve(session)
} else {
session.ready = true
return Promise.resolve(session)
}
}
public async deleteSession(session: Session) {
await deleteFolder(session.path)
if (session.ready) {
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
export const getSessionController = () => {
if (process.sessionController) return process.sessionController
process.sessionController = new SessionController()
return process.sessionController
}

View File

@@ -0,0 +1,57 @@
import { MemberType, FolderMember, ServiceMember } 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],
parentFolders: string[] = []
) => {
const destinationPath = path.join(
getTmpFilesFolderPath(),
path.join(...parentFolders)
)
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
const name = member.name
if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name })
)
await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
} else {
await createFile(path.join(destinationPath, name), member.code).catch(
(err) => Promise.reject({ error: err, failedToCreate: name })
)
}
})
return Promise.resolve()
}
export const getTreeExample = () => ({
members: [
{
name: 'jobs',
type: 'folder',
members: [
{
name: 'extract',
type: 'folder',
members: [
{
name: 'makedata1',
type: 'service',
code: '%put Hello World!;'
}
]
}
]
}
]
})

View File

@@ -0,0 +1,6 @@
export * from './deploy'
export * from './sasjsExecutor'
export * from './sasjsDrive'
export * from './Session'
export * from './Execution'
export * from './FileUploadController'

View File

@@ -0,0 +1,15 @@
import { fileExists, readFile, createFile } from '@sasjs/utils'
export class SASjsDriveController {
async readFile(filePath: string) {
if (await fileExists(filePath)) {
return await readFile(filePath)
}
}
async updateFile(filePath: string, fileContent: string) {
if (await fileExists(filePath)) {
return await createFile(filePath, fileContent)
}
}
}

View File

@@ -0,0 +1,40 @@
import fs from 'fs'
import { TreeNode } from '../types'
import { getTmpFilesFolderPath } from '../utils'
export const sasjsExecutor = () => {
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getTmpFilesFolderPath(),
children: []
}
const stack = [root]
while (stack.length) {
const currentNode = stack.pop()
if (currentNode) {
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
children: []
}
currentNode.children.push(childNode)
if (fs.statSync(childNode.absolutePath).isDirectory()) {
stack.push(childNode)
}
}
}
}
return root
}

19
api/src/prod-server.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from 'path'
import { readFileSync } from 'fs'
import * as https from 'https'
import { configuration } from '../package.json'
import app from './app'
const keyPath = path.join('certificates', 'privkey.pem')
const certPath = path.join('certificates', 'fullchain.pem')
const key = readFileSync(keyPath)
const cert = readFileSync(certPath)
const httpsServer = https.createServer({ key, cert }, app)
httpsServer.listen(configuration.sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}`
)
})

155
api/src/routes/index.ts Normal file
View File

@@ -0,0 +1,155 @@
import express from 'express'
import path from 'path'
import {
createFileTree,
getTreeExample,
sasjsExecutor,
SASjsDriveController,
ExecutionController,
FileUploadController
} from '../controllers'
import { isExecutionQuery, isFileQuery, isFileTree } from '../types'
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
const router = express.Router()
const fileUploadController = new FileUploadController()
router.get('/', async (_, res) => {
res.sendFile(
path.join(__dirname, '..', '..', '..', 'web', 'build', 'index.html')
)
})
router.post('/deploy', async (req, res) => {
if (!isFileTree({ members: req.body.members })) {
res.status(400).send({
status: 'failure',
message: 'Provided not supported data format.',
example: getTreeExample()
})
return
}
await createFileTree(
req.body.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)
const fileContent = await new SASjsDriveController().readFile(filePath)
res.status(200).send({ status: 'success', fileContent: fileContent })
} 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 SASjsDriveController().updateFile(filePath, req.body.fileContent)
res.status(200).send({ status: 'success' })
})
router.get('/SASjsApi/executor', async (req, res) => {
const tree = sasjsExecutor()
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

View File

@@ -0,0 +1,101 @@
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(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())
})
})

8
api/src/server.ts Normal file
View File

@@ -0,0 +1,8 @@
import app from './app'
import { configuration } from '../package.json'
app.listen(configuration.sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at http://localhost:${configuration.sasJsPort}`
)
})

View File

@@ -0,0 +1,5 @@
export interface ExecutionResult {
webout?: string
log?: string
logPath?: string
}

47
api/src/types/FileTree.ts Normal file
View File

@@ -0,0 +1,47 @@
export interface FileTree {
members: [FolderMember, ServiceMember]
}
export enum MemberType {
folder = 'folder',
service = 'service'
}
export interface FolderMember {
name: string
type: MemberType.folder
members: [FolderMember, ServiceMember]
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isFolderMember = (arg: any): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'

5
api/src/types/Process.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace NodeJS {
export interface Process {
sessionController?: import('../controllers/Session').SessionController
}
}

17
api/src/types/Request.ts Normal file
View File

@@ -0,0 +1,17 @@
import { MacroVars } from '@sasjs/utils'
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars
_debug?: boolean
}
export interface FileQuery {
filePath: string
}
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
arg && !Array.isArray(arg) && typeof arg._program === 'string'
export const isFileQuery = (arg: any): arg is FileQuery =>
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'

8
api/src/types/Session.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface Session {
id: string
ready: boolean
creationTimeStamp: string
deathTimeStamp: string
path: string
inUse: boolean
}

View File

@@ -0,0 +1,6 @@
export interface TreeNode {
name: string
relativePath: string
absolutePath: string
children: Array<TreeNode>
}

10
api/src/types/Upload.ts Normal file
View File

@@ -0,0 +1,10 @@
export interface MulterFile {
fieldname: string
originalname: string
encoding: string
mimetype: string
destination: string
filename: string
path: string
size: number
}

6
api/src/types/index.ts Normal file
View File

@@ -0,0 +1,6 @@
// TODO: uppercase types
export * from './Execution'
export * from './Request'
export * from './FileTree'
export * from './Session'
export * from './TreeNode'

26
api/src/utils/file.ts Normal file
View File

@@ -0,0 +1,26 @@
import path from 'path'
import { getRealPath, generateTimestamp } from '@sasjs/utils'
export const getTmpFolderPath = () =>
getRealPath(path.join(__dirname, '..', '..', 'tmp'))
export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files')
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
export const getTmpWeboutFolderPath = () =>
path.join(getTmpFolderPath(), 'webouts')
export const getTmpSessionsFolderPath = () =>
path.join(getTmpFolderPath(), 'sessions')
export const generateUniqueFileName = (fileName: string, extension = '') =>
[
fileName,
'-',
Math.round(Math.random() * 100000),
'-',
new Date().getTime(),
extension
].join('')

3
api/src/utils/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './file'
export * from './sleep'
export * from './upload'

3
api/src/utils/sleep.ts Normal file
View File

@@ -0,0 +1,3 @@
export const sleep = async (delay: number) => {
await new Promise((resolve) => setTimeout(resolve, delay))
}

89
api/src/utils/upload.ts Normal file
View File

@@ -0,0 +1,89 @@
import path from 'path'
import fs from 'fs'
import { getTmpSessionsFolderPath } from '.'
import { MulterFile } from '../types/Upload'
import { listFilesInFolder } from '@sasjs/utils'
/**
* It will create an object that maps hashed file names to the original names
* @param files array of files to be mapped
* @returns object
*/
export const makeFilesNamesMap = (files: MulterFile[]) => {
if (!files) return null
const filesNamesMap: { [key: string]: string } = {}
for (let file of files) {
filesNamesMap[file.filename] = file.fieldname
}
return filesNamesMap
}
/**
* Generates the sas code that references uploaded files in the concurrent request
* @param filesNamesMap object that maps hashed file names and original file names
* @param sasUploadFolder name of the folder that is created for the purpose of files in concurrent request
* @returns generated sas code
*/
export const generateFileUploadSasCode = async (
filesNamesMap: any,
sasSessionFolder: string
): Promise<string> => {
let uploadSasCode = ''
let fileCount = 0
let uploadedFilesMap: {
fileref: string
filepath: string
filename: string
count: number
}[] = []
const sasSessionFolderList: string[] = await listFilesInFolder(
sasSessionFolder
)
sasSessionFolderList.forEach((fileName) => {
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount
if (fileName.includes('req_file')) {
fileCount++
uploadedFilesMap.push({
fileref: `_sjs${fileCountString}`,
filepath: `${sasSessionFolder}/${fileName}`,
filename: filesNamesMap[fileName],
count: fileCount
})
}
})
for (let uploadedMap of uploadedFilesMap) {
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
}
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
for (let uploadedMap of uploadedFilesMap) {
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filepath};`
}
for (let uploadedMap of uploadedFilesMap) {
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
}
for (let uploadedMap of uploadedFilesMap) {
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filename};`
}
if (fileCount > 0) {
uploadSasCode += `\n%let _WEBIN_NAME=&_WEBIN_NAME1;`
uploadSasCode += `\n%let _WEBIN_FILEREF=&_WEBIN_FILEREF1;`
uploadSasCode += `\n%let _WEBIN_FILENAME=&_WEBIN_FILENAME1;`
}
uploadSasCode += `\n`
return uploadSasCode
}

14
api/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"rootDir": "./",
"outDir": "./build",
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true
},
"ts-node": {
"files": true
}
}