1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-05 03:30:05 +00:00

Compare commits

..

17 Commits

Author SHA1 Message Date
0bd156141c chore(git): Merge branch 'master' into fixing-sas9-tests 2021-07-26 14:53:52 +02:00
Allan Bowe
0ea66f6d37 Merge pull request #494 from sasjs/fix-browser-issue
fix(browser): only import file I/O functions when running in Node.js environments
2021-07-25 10:00:51 +03:00
a615c5fdb6 style: lint 2021-07-24 18:07:17 +02:00
ca7ee83f7f chore: fixing multiple login attempts by adding pause between calling functions 2021-07-24 18:06:15 +02:00
Krishna Acondy
eac9da22bf chore(test): fix assertion 2021-07-24 10:27:31 +01:00
Krishna Acondy
626fc2e15f fix(path): make log file path platform-agnostic 2021-07-24 09:53:39 +01:00
Krishna Acondy
87e2edbd6c chore(test): fix long poll count 2021-07-24 00:12:11 +01:00
Krishna Acondy
7cf681bea3 chore(tests): fix tests 2021-07-23 22:24:48 +01:00
Krishna Acondy
281a145bef fix(node): only create and write file stream if running in node 2021-07-23 22:24:41 +01:00
Krishna Acondy
15d5f9ec91 chore(paths): fix import paths 2021-07-23 22:24:21 +01:00
Krishna Acondy
0a6c5a0ec4 fix(fs): replace fs imports with locally defined WriteStream interface 2021-07-23 22:24:04 +01:00
Krishna Acondy
2a9526d056 fix(node): add util to check if running in node 2021-07-23 22:23:05 +01:00
Allan Bowe
c2ff28c323 Update PULL_REQUEST_TEMPLATE.md 2021-07-23 13:04:38 +03:00
97a530cc66 style: lint 2021-07-22 14:44:13 +02:00
317c8c81a0 chore: JES test disable on SAS9 2021-07-22 13:48:11 +02:00
c87776ca1b chore(git): Merge branch 'master' into fixing-sas9-tests 2021-07-22 13:44:23 +02:00
04032831c3 fix: debug on test & make error and parse log test 2021-07-22 13:43:50 +02:00
16 changed files with 153 additions and 69 deletions

View File

@@ -12,9 +12,9 @@ What code changes have been made to achieve the intent.
## Checks ## Checks
No PR (that involves a non-trivial code change) should be merged, unless all four of the items below are confirmed! If an urgent fix is needed - use a tar file. No PR (that involves a non-trivial code change) should be merged, unless all items below are confirmed! If an urgent fix is needed - use a tar file.
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-cli` unit tests are passing (`npm test`). - [ ] All `sasjs-cli` unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)). - [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).
- [ ] [Data Controller](https://datacontroller.io) builds and is functional on both SAS 9 and Viya

View File

@@ -47,7 +47,9 @@ export const basicTests = (
'Should fail on first attempt and should log the user in on second attempt', 'Should fail on first attempt and should log the user in on second attempt',
test: async () => { test: async () => {
await adapter.logOut() await adapter.logOut()
await sleep(1000)
await adapter.logIn('invalid', 'invalid') await adapter.logIn('invalid', 'invalid')
await sleep(1000)
return adapter.logIn(userName, password) return adapter.logIn(userName, password)
}, },
assertion: (response: any) => assertion: (response: any) =>
@@ -151,6 +153,9 @@ export const basicTests = (
description: description:
'Should complete successful request with extra attributes present in response', 'Should complete successful request with extra attributes present in response',
test: async () => { test: async () => {
if (adapter.getSasjsConfig().serverType !== 'SASVIYA')
return Promise.resolve('skip')
const config = { const config = {
useComputeApi: false useComputeApi: false
} }
@@ -165,9 +170,15 @@ export const basicTests = (
) )
}, },
assertion: (response: any) => { assertion: (response: any) => {
if (response === 'skip') return true
const responseKeys: any = Object.keys(response) const responseKeys: any = Object.keys(response)
return responseKeys.includes('file') && responseKeys.includes('data') return responseKeys.includes('file') && responseKeys.includes('data')
} }
} }
] ]
}) })
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms))
}

View File

@@ -0,0 +1,17 @@
import { isFolder } from '@sasjs/utils/file'
import { generateTimestamp } from '@sasjs/utils/time'
import { Job } from '../../types'
export const getFileStream = async (job: Job, filePath?: string) => {
const { createWriteStream } = require('@sasjs/utils/file')
const logPath = filePath || process.cwd()
const isFolderPath = await isFolder(logPath)
if (isFolderPath) {
const logFileName = `${job.name || 'job'}-${generateTimestamp()}.log`
const path = require('path')
const logFilePath = path.join(filePath || process.cwd(), logFileName)
return await createWriteStream(logFilePath)
} else {
return await createWriteStream(logPath)
}
}

View File

@@ -3,11 +3,8 @@ import { Job, PollOptions } from '../..'
import { getTokens } from '../../auth/getTokens' import { getTokens } from '../../auth/getTokens'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { JobStatePollError } from '../../types/errors' import { JobStatePollError } from '../../types/errors'
import { generateTimestamp } from '@sasjs/utils/time' import { Link, WriteStream } from '../../types'
import { saveLog } from './saveLog' import { isNode } from '../../utils'
import { createWriteStream, isFolder } from '@sasjs/utils/file'
import { WriteStream } from 'fs'
import { Link } from '../../types'
export async function pollJobState( export async function pollJobState(
requestClient: RequestClient, requestClient: RequestClient,
@@ -55,21 +52,9 @@ export async function pollJobState(
} }
let logFileStream let logFileStream
if (pollOptions.streamLog) { if (pollOptions.streamLog && isNode()) {
const logPath = pollOptions?.logFolderPath || process.cwd() const { getFileStream } = require('./getFileStream')
const isFolderPath = await isFolder(logPath) logFileStream = await getFileStream(postedJob, pollOptions.logFolderPath)
if (isFolderPath) {
const logFileName = `${
postedJob.name || 'job'
}-${generateTimestamp()}.log`
const logFilePath = `${
pollOptions?.logFolderPath || process.cwd()
}/${logFileName}`
logFileStream = await createWriteStream(logFilePath)
} else {
logFileStream = await createWriteStream(logPath)
}
} }
// Poll up to the first 100 times with the specified poll interval // Poll up to the first 100 times with the specified poll interval
@@ -230,14 +215,17 @@ const doPoll = async (
const endLogLine = job.logStatistics?.lineCount ?? 1000000 const endLogLine = job.logStatistics?.lineCount ?? 1000000
await saveLog( const { saveLog } = isNode() ? require('./saveLog') : { saveLog: null }
postedJob, if (saveLog) {
requestClient, await saveLog(
startLogLine, postedJob,
endLogLine, requestClient,
logStream, startLogLine,
authConfig?.access_token endLogLine,
) logStream,
authConfig?.access_token
)
}
startLogLine += endLogLine startLogLine += endLogLine
} }

View File

@@ -1,7 +1,7 @@
import { Job } from '../..' import { Job } from '../..'
import { RequestClient } from '../../request/RequestClient' import { RequestClient } from '../../request/RequestClient'
import { fetchLog } from '../../utils' import { fetchLog } from '../../utils'
import { WriteStream } from 'fs' import { WriteStream } from '../../types'
import { writeStream } from './writeStream' import { writeStream } from './writeStream'
/** /**

View File

@@ -0,0 +1,41 @@
import { Logger, LogLevel } from '@sasjs/utils/logger'
import * as path from 'path'
import * as fileModule from '@sasjs/utils/file'
import { getFileStream } from '../getFileStream'
import { mockJob } from './mockResponses'
import { WriteStream } from '../../../types'
describe('getFileStream', () => {
beforeEach(() => {
;(process as any).logger = new Logger(LogLevel.Off)
setupMocks()
})
it('should use the given log path if it points to a file', async () => {
const { createWriteStream } = require('@sasjs/utils/file')
await getFileStream(mockJob, path.join(__dirname, 'test.log'))
expect(createWriteStream).toHaveBeenCalledWith(
path.join(__dirname, 'test.log')
)
})
it('should generate a log file path with a timestamp if it points to a folder', async () => {
const { createWriteStream } = require('@sasjs/utils/file')
await getFileStream(mockJob, __dirname)
expect(createWriteStream).not.toHaveBeenCalledWith(__dirname)
expect(createWriteStream).toHaveBeenCalledWith(
expect.stringContaining(path.join(__dirname, 'test job-20'))
)
})
})
const setupMocks = () => {
jest.restoreAllMocks()
jest.mock('@sasjs/utils/file/file')
jest
.spyOn(fileModule, 'createWriteStream')
.mockImplementation(() => Promise.resolve({} as unknown as WriteStream))
}

View File

@@ -1,11 +1,11 @@
import { Logger, LogLevel } from '@sasjs/utils' import { Logger, LogLevel } from '@sasjs/utils'
import * as path from 'path'
import * as fileModule from '@sasjs/utils/file'
import { RequestClient } from '../../../request/RequestClient' import { RequestClient } from '../../../request/RequestClient'
import { mockAuthConfig, mockJob } from './mockResponses' import { mockAuthConfig, mockJob } from './mockResponses'
import { pollJobState } from '../pollJobState' import { pollJobState } from '../pollJobState'
import * as getTokensModule from '../../../auth/getTokens' import * as getTokensModule from '../../../auth/getTokens'
import * as saveLogModule from '../saveLog' import * as saveLogModule from '../saveLog'
import * as getFileStreamModule from '../getFileStream'
import * as isNodeModule from '../../../utils/isNode'
import { PollOptions } from '../../../types' import { PollOptions } from '../../../types'
import { WriteStream } from 'fs' import { WriteStream } from 'fs'
@@ -77,42 +77,43 @@ describe('pollJobState', () => {
it('should attempt to fetch and save the log after each poll when streamLog is true', async () => { it('should attempt to fetch and save the log after each poll when streamLog is true', async () => {
mockSimplePoll() mockSimplePoll()
const { saveLog } = require('../saveLog')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollOptions,
streamLog: true streamLog: true
}) })
expect(saveLogModule.saveLog).toHaveBeenCalledTimes(2) expect(saveLog).toHaveBeenCalledTimes(2)
}) })
it('should use the given log path if it points to a file', async () => { it('should create a write stream in Node.js environment when streamLog is true', async () => {
mockSimplePoll() mockSimplePoll()
const { getFileStream } = require('../getFileStream')
const { saveLog } = require('../saveLog')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollOptions,
streamLog: true, streamLog: true
logFolderPath: path.join(__dirname, 'test.log')
}) })
expect(fileModule.createWriteStream).toHaveBeenCalledWith( expect(getFileStream).toHaveBeenCalled()
path.join(__dirname, 'test.log') expect(saveLog).toHaveBeenCalledTimes(2)
)
}) })
it('should generate a log file path with a timestamp if it points to a folder', async () => { it('should not create a write stream in a non-Node.js environment', async () => {
mockSimplePoll() mockSimplePoll()
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => false)
const { saveLog } = require('../saveLog')
const { getFileStream } = require('../getFileStream')
await pollJobState(requestClient, mockJob, false, mockAuthConfig, { await pollJobState(requestClient, mockJob, false, mockAuthConfig, {
...defaultPollOptions, ...defaultPollOptions,
streamLog: true, streamLog: true
logFolderPath: path.join(__dirname)
}) })
expect(fileModule.createWriteStream).not.toHaveBeenCalledWith(__dirname) expect(getFileStream).not.toHaveBeenCalled()
expect(fileModule.createWriteStream).toHaveBeenCalledWith( expect(saveLog).not.toHaveBeenCalled()
expect.stringContaining(__dirname + '/test job-20')
)
}) })
it('should not attempt to fetch and save the log after each poll when streamLog is false', async () => { it('should not attempt to fetch and save the log after each poll when streamLog is false', async () => {
@@ -247,7 +248,8 @@ const setupMocks = () => {
jest.mock('../../../request/RequestClient') jest.mock('../../../request/RequestClient')
jest.mock('../../../auth/getTokens') jest.mock('../../../auth/getTokens')
jest.mock('../saveLog') jest.mock('../saveLog')
jest.mock('@sasjs/utils/file') jest.mock('../getFileStream')
jest.mock('../../../utils/isNode')
jest jest
.spyOn(requestClient, 'get') .spyOn(requestClient, 'get')
@@ -261,8 +263,9 @@ const setupMocks = () => {
.spyOn(saveLogModule, 'saveLog') .spyOn(saveLogModule, 'saveLog')
.mockImplementation(() => Promise.resolve()) .mockImplementation(() => Promise.resolve())
jest jest
.spyOn(fileModule, 'createWriteStream') .spyOn(getFileStreamModule, 'getFileStream')
.mockImplementation(() => Promise.resolve({} as unknown as WriteStream)) .mockImplementation(() => Promise.resolve({} as unknown as WriteStream))
jest.spyOn(isNodeModule, 'isNode').mockImplementation(() => true)
} }
const mockSimplePoll = (runningCount = 2) => { const mockSimplePoll = (runningCount = 2) => {
@@ -308,7 +311,7 @@ const mockLongPoll = () => {
return Promise.resolve({ result: mockJob, etag: '', status: 200 }) return Promise.resolve({ result: mockJob, etag: '', status: 200 })
} }
return Promise.resolve({ return Promise.resolve({
result: count <= 101 ? 'running' : 'completed', result: count <= 102 ? 'running' : 'completed',
etag: '', etag: '',
status: 200 status: 200
}) })

View File

@@ -4,7 +4,7 @@ import * as fetchLogsModule from '../../../utils/fetchLogByChunks'
import * as writeStreamModule from '../writeStream' import * as writeStreamModule from '../writeStream'
import { saveLog } from '../saveLog' import { saveLog } from '../saveLog'
import { mockJob } from './mockResponses' import { mockJob } from './mockResponses'
import { WriteStream } from 'fs' import { WriteStream } from '../../../types'
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)() const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
const stream = {} as unknown as WriteStream const stream = {} as unknown as WriteStream

View File

@@ -0,0 +1,25 @@
import { WriteStream } from '../../../types'
import { writeStream } from '../writeStream'
import 'jest-extended'
describe('writeStream', () => {
const stream: WriteStream = {
write: jest.fn(),
path: 'test'
}
it('should resolve when the stream is written successfully', async () => {
expect(writeStream(stream, 'test')).toResolve()
expect(stream.write).toHaveBeenCalledWith('test\n', expect.anything())
})
it('should reject when the write errors out', async () => {
jest
.spyOn(stream, 'write')
.mockImplementation((_, callback) => callback(new Error('Test Error')))
const error = await writeStream(stream, 'test').catch((e) => e)
expect(error.message).toEqual('Test Error')
})
})

View File

@@ -1,11 +1,11 @@
import { WriteStream } from 'fs' import { WriteStream } from '../../types'
export const writeStream = async ( export const writeStream = async (
stream: WriteStream, stream: WriteStream,
content: string content: string
): Promise<void> => { ): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stream.write(content + '\n\nnext chunk\n\n', (e) => { stream.write(content + '\n', (e) => {
if (e) { if (e) {
return reject(e) return reject(e)
} }

View File

@@ -1,9 +1,9 @@
import { import {
AuthConfig,
isAccessTokenExpiring, isAccessTokenExpiring,
isRefreshTokenExpiring, isRefreshTokenExpiring,
hasTokenExpired hasTokenExpired
} from '@sasjs/utils' } from '@sasjs/utils/auth'
import { AuthConfig } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { refreshTokens } from './refreshTokens' import { refreshTokens } from './refreshTokens'

View File

@@ -107,20 +107,9 @@ export class WebJobExecutor extends BaseJobExecutor {
this.appendRequest(res, sasJob, config.debug) this.appendRequest(res, sasJob, config.debug)
resolve(jsonResponse) resolve(jsonResponse)
} }
if (this.serverType === ServerType.Sas9 && config.debug) {
const jsonResponse = parseWeboutResponse(res.result as string)
if (jsonResponse === '') {
throw new Error(
'Valid JSON could not be extracted from response.'
)
}
getValidJson(jsonResponse)
this.appendRequest(res, sasJob, config.debug)
resolve(res.result)
}
getValidJson(res.result as string)
this.appendRequest(res, sasJob, config.debug) this.appendRequest(res, sasJob, config.debug)
getValidJson(res.result as string)
resolve(res.result) resolve(res.result)
}) })
.catch(async (e: Error) => { .catch(async (e: Error) => {

4
src/types/WriteStream.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface WriteStream {
write: (content: string, callback: (err?: Error) => any) => void
path: string
}

View File

@@ -11,3 +11,4 @@ export * from './SASjsRequest'
export * from './Session' export * from './Session'
export * from './UploadFile' export * from './UploadFile'
export * from './PollOptions' export * from './PollOptions'
export * from './WriteStream'

View File

@@ -1,6 +1,7 @@
export * from './asyncForEach' export * from './asyncForEach'
export * from './compareTimestamps' export * from './compareTimestamps'
export * from './convertToCsv' export * from './convertToCsv'
export * from './isNode'
export * from './isRelativePath' export * from './isRelativePath'
export * from './isUri' export * from './isUri'
export * from './isUrl' export * from './isUrl'

4
src/utils/isNode.ts Normal file
View File

@@ -0,0 +1,4 @@
export const isNode = () =>
typeof process !== 'undefined' &&
process.versions != null &&
process.versions.node != null