mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe5ac0ff7 | ||
|
|
d50f5a030a | ||
|
|
c320caec99 | ||
|
|
16a5b2b012 | ||
|
|
2951e0cc2d | ||
|
|
6bb4a7ea18 | ||
|
|
2827978fe5 | ||
|
|
541c19c1a4 | ||
|
|
c5e995f8d6 | ||
|
|
8bf36da566 | ||
| ccb4ec6e03 | |||
| 06ebb52bc9 | |||
|
|
b61cf34723 | ||
|
|
22445d1268 | ||
|
|
cba9dacb37 | ||
| 6419686269 | |||
|
|
4554c9100c | ||
| 919c83c143 | |||
|
|
1867658cde | ||
|
|
c5824a8a8d | ||
|
|
56a1960fff |
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1276,6 +1276,12 @@
|
||||
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
|
||||
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimist": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
|
||||
|
||||
@@ -39,11 +39,13 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"jest": "^27.0.4",
|
||||
"jest-extended": "^0.11.5",
|
||||
"mime": "^2.5.2",
|
||||
"path": "^0.12.7",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
@@ -176,11 +176,59 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
name: 'sendObj',
|
||||
tests: [
|
||||
{
|
||||
title: 'Invalid column name',
|
||||
title: 'Table name starts with numeric',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
'1 invalid table': [{ col1: 42 }]
|
||||
'1InvalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Table name contains a space',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
'an invalidTable': [{ col1: 42 }]
|
||||
}
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Table name contains a special character',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
'anInvalidTable#': [{ col1: 42 }]
|
||||
}
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: 'Table name exceeds max length of 32 characters',
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
|
||||
}
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Invalid data object's structure",
|
||||
description: 'Should throw an error',
|
||||
test: async () => {
|
||||
const invalidData: any = {
|
||||
inData: [[{ data: 'value' }]]
|
||||
}
|
||||
return adapter.request('common/sendObj', invalidData).catch((e) => e)
|
||||
},
|
||||
|
||||
@@ -45,7 +45,16 @@ export class SAS9ApiClient {
|
||||
) {
|
||||
await this.requestClient.login(userName, password, this.jobsPath)
|
||||
|
||||
const formData = generateFileUploadForm(linesOfCode.join('\n'))
|
||||
// This piece of code forces a webout to prevent Stored Process Errors.
|
||||
const forceOutputCode = [
|
||||
'data _null_;',
|
||||
'file _webout;',
|
||||
`put 'Executed sasjs run';`,
|
||||
'run;'
|
||||
]
|
||||
const formData = generateFileUploadForm(
|
||||
[...linesOfCode, ...forceOutputCode].join('\n')
|
||||
)
|
||||
|
||||
const codeInjectorPath = `/User Folders/${userName}/My Folder/sasjs/runner`
|
||||
const contentType =
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Context,
|
||||
ContextAllAttributes,
|
||||
Folder,
|
||||
File,
|
||||
EditContextInput,
|
||||
JobDefinition,
|
||||
PollOptions
|
||||
@@ -30,6 +31,7 @@ import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import * as mime from 'mime'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
@@ -536,6 +538,53 @@ export class SASViyaApiClient {
|
||||
.then((res) => res.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file. Path to or URI of the parent folder is required.
|
||||
* @param fileName - the name of the new file.
|
||||
* @param contentBuffer - the content of the new file in Buffer.
|
||||
* @param parentFolderPath - the full path to the parent folder. If not
|
||||
* provided, the parentFolderUri must be provided.
|
||||
* @param parentFolderUri - the URI (eg /folders/folders/UUID) of the parent
|
||||
* folder. If not provided, the parentFolderPath must be provided.
|
||||
* @param accessToken - an access token for authorizing the request.
|
||||
*/
|
||||
public async createFile(
|
||||
fileName: string,
|
||||
contentBuffer: Buffer,
|
||||
parentFolderPath?: string,
|
||||
parentFolderUri?: string,
|
||||
accessToken?: string
|
||||
): Promise<File> {
|
||||
if (!parentFolderPath && !parentFolderUri) {
|
||||
throw new Error('Path or URI of the parent folder is required.')
|
||||
}
|
||||
|
||||
if (!parentFolderUri && parentFolderPath) {
|
||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/vnd.sas.file+json',
|
||||
'Content-Disposition': `filename="${fileName}";`
|
||||
}
|
||||
|
||||
const formData = new NodeFormData()
|
||||
formData.append('file', contentBuffer, fileName)
|
||||
|
||||
const mimeType =
|
||||
mime.getType(fileName.match(/\.[0-9a-z]+$/i)?.[0] || '') ?? 'text/plain'
|
||||
|
||||
return (
|
||||
await this.requestClient.post<File>(
|
||||
`/files/files?parentFolderUri=${parentFolderUri}&typeDefName=file#rawUpload`,
|
||||
formData,
|
||||
accessToken,
|
||||
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
||||
headers
|
||||
)
|
||||
).result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a folder. Path to or URI of the parent folder is required.
|
||||
* @param folderName - the name of the new folder.
|
||||
|
||||
163
src/SASjs.ts
163
src/SASjs.ts
@@ -268,7 +268,7 @@ export default class SASjs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a folder at SAS file system.
|
||||
* Creates a folder in the logical SAS folder tree
|
||||
* @param folderName - name of the folder to be created.
|
||||
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
|
||||
* @param parentFolderUri - the URI of the parent folder.
|
||||
@@ -300,6 +300,40 @@ export default class SASjs {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file in the logical SAS folder tree
|
||||
* @param fileName - name of the file to be created.
|
||||
* @param content - content of the file to be created.
|
||||
* @param parentFolderPath - the full path (eg `/Public/example/myFolder`) of the parent folder.
|
||||
* @param parentFolderUri - the URI of the parent folder.
|
||||
* @param accessToken - the access token to authorizing the request.
|
||||
* @param sasApiClient - a client for interfacing with SAS API.
|
||||
*/
|
||||
public async createFile(
|
||||
fileName: string,
|
||||
content: Buffer,
|
||||
parentFolderPath: string,
|
||||
parentFolderUri?: string,
|
||||
accessToken?: string,
|
||||
sasApiClient?: SASViyaApiClient
|
||||
) {
|
||||
if (sasApiClient)
|
||||
return await sasApiClient.createFile(
|
||||
fileName,
|
||||
content,
|
||||
parentFolderPath,
|
||||
parentFolderUri,
|
||||
accessToken
|
||||
)
|
||||
return await this.sasViyaApiClient!.createFile(
|
||||
fileName,
|
||||
content,
|
||||
parentFolderPath,
|
||||
parentFolderUri,
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a folder from the SAS file system.
|
||||
* @param folderPath - path of the folder to be fetched.
|
||||
@@ -547,7 +581,7 @@ export default class SASjs {
|
||||
*/
|
||||
public async request(
|
||||
sasJob: string,
|
||||
data: { [key: string]: any },
|
||||
data: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any,
|
||||
accessToken?: string,
|
||||
@@ -558,17 +592,36 @@ export default class SASjs {
|
||||
...config
|
||||
}
|
||||
|
||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
||||
if (config.useComputeApi) {
|
||||
return await this.computeJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken
|
||||
)
|
||||
const validationResult = this.validateInput(data)
|
||||
|
||||
if (validationResult.status) {
|
||||
if (config.serverType === ServerType.SasViya && config.contextName) {
|
||||
if (config.useComputeApi) {
|
||||
return await this.computeJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken
|
||||
)
|
||||
} else {
|
||||
return await this.jesJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken,
|
||||
extraResponseAttributes
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
config.serverType === ServerType.Sas9 &&
|
||||
config.username &&
|
||||
config.password
|
||||
) {
|
||||
return await this.sas9JobExecutor!.execute(sasJob, data, config)
|
||||
} else {
|
||||
return await this.jesJobExecutor!.execute(
|
||||
return await this.webJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
@@ -577,19 +630,71 @@ export default class SASjs {
|
||||
extraResponseAttributes
|
||||
)
|
||||
}
|
||||
} else if (
|
||||
config.serverType === ServerType.Sas9 &&
|
||||
config.username &&
|
||||
config.password
|
||||
) {
|
||||
return await this.sas9JobExecutor!.execute(sasJob, data, config)
|
||||
} else {
|
||||
return await this.webJobExecutor!.execute(
|
||||
sasJob,
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback
|
||||
)
|
||||
return Promise.reject(new ErrorResponse(validationResult.msg))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function validates the input data structure and table naming convention
|
||||
*
|
||||
* @param data A json object that contains one or more tables, it can also be null
|
||||
* @returns An object which contains two attributes: 1) status: boolean, 2) msg: string
|
||||
*/
|
||||
private validateInput(data: { [key: string]: any } | null): {
|
||||
status: boolean
|
||||
msg: string
|
||||
} {
|
||||
if (data === null) return { status: true, msg: '' }
|
||||
for (const key in data) {
|
||||
if (!key.match(/^[a-zA-Z_]/)) {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'First letter of table should be alphabet or underscore.'
|
||||
}
|
||||
}
|
||||
|
||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) {
|
||||
return { status: false, msg: 'Table name should be alphanumeric.' }
|
||||
}
|
||||
|
||||
if (key.length > 32) {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Maximum length for table name could be 32 characters.'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getType(data[key]) !== 'Array') {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Parameter data contains invalid table structure.'
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < data[key].length; i++) {
|
||||
if (this.getType(data[key][i]) !== 'object') {
|
||||
return {
|
||||
status: false,
|
||||
msg: `Table ${key} contains invalid structure.`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { status: true, msg: '' }
|
||||
}
|
||||
|
||||
/**
|
||||
* this function returns the type of variable
|
||||
*
|
||||
* @param data it could be anything, like string, array, object etc.
|
||||
* @returns a string which tells the type of input parameter
|
||||
*/
|
||||
private getType(data: any): string {
|
||||
if (Array.isArray(data)) {
|
||||
return 'Array'
|
||||
} else {
|
||||
return typeof data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -880,6 +985,16 @@ export default class SASjs {
|
||||
isForced
|
||||
)
|
||||
break
|
||||
case 'file':
|
||||
await this.createFile(
|
||||
member.name,
|
||||
member.code,
|
||||
parentFolder,
|
||||
undefined,
|
||||
accessToken,
|
||||
sasApiClient
|
||||
)
|
||||
break
|
||||
case 'service':
|
||||
await this.createJobDefinition(
|
||||
member.name,
|
||||
|
||||
8
src/types/File.ts
Normal file
8
src/types/File.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Link } from './Link'
|
||||
|
||||
export interface File {
|
||||
id: string
|
||||
name: string
|
||||
parentUri: string
|
||||
links: Link[]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './Context'
|
||||
export * from './CsrfToken'
|
||||
export * from './Folder'
|
||||
export * from './File'
|
||||
export * from './Job'
|
||||
export * from './JobDefinition'
|
||||
export * from './JobResult'
|
||||
|
||||
Reference in New Issue
Block a user