mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-16 16:40:06 +00:00
Merge pull request #494 from sasjs/fix-browser-issue
fix(browser): only import file I/O functions when running in Node.js environments
This commit is contained in:
17
src/api/viya/getFileStream.ts
Normal file
17
src/api/viya/getFileStream.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
41
src/api/viya/spec/getFileStream.spec.ts
Normal file
41
src/api/viya/spec/getFileStream.spec.ts
Normal 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))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
25
src/api/viya/spec/writeStream.spec.ts
Normal file
25
src/api/viya/spec/writeStream.spec.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
4
src/types/WriteStream.ts
Normal file
4
src/types/WriteStream.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface WriteStream {
|
||||||
|
write: (content: string, callback: (err?: Error) => any) => void
|
||||||
|
path: string
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
4
src/utils/isNode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const isNode = () =>
|
||||||
|
typeof process !== 'undefined' &&
|
||||||
|
process.versions != null &&
|
||||||
|
process.versions.node != null
|
||||||
Reference in New Issue
Block a user