1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 09:24:35 +00:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Yury Shkoda
d8467c24b1 Merge pull request #112 from sasjs/cli-issue-73
feat(folder-management): made folder related methods public
2020-09-28 15:16:33 +03:00
Yury Shkoda
fc9056c1ac chore(folder-management): made 'moveFolder' method public and fixed 'createFolder' method 2020-09-28 14:59:27 +03:00
Yury Shkoda
9b1d295b82 feat(folder): made 'deleteFolder' method public 2020-09-26 11:41:18 +03:00
Allan Bowe
99d0b01a24 Update example.html 2020-09-24 23:00:37 +02:00
Yury Shkoda
131c672020 Merge pull request #110 from sasjs/cli-issue-105
fix(context): fixed 'ContextAllAttributes' interface
2020-09-24 17:10:14 +03:00
Yury Shkoda
338f2fb2dd Merge branch 'master' into cli-issue-105 2020-09-24 17:08:04 +03:00
Yury Shkoda
4552a9a856 fix(context): fixed 'ContextAllAttributes' interface 2020-09-24 16:51:50 +03:00
Yury Shkoda
daeb753f9e Merge pull request #109 from sasjs/cli-issue-105
feat(context): added getComputeContextById method
2020-09-24 16:10:02 +03:00
Yury Shkoda
f50a99d0b8 Merge branch 'master' into cli-issue-105 2020-09-24 16:08:09 +03:00
Yury Shkoda
e6d0d3efd5 docs(context): add docs for 'getComputeContextByName' method 2020-09-24 16:05:42 +03:00
Yury Shkoda
057460467c feat(context): added getComputeContextById method 2020-09-24 15:53:07 +03:00
Yury Shkoda
5aee9d955e Merge pull request #106 from sasjs/cli-issue-105
feat(context): made getContextByName function public
2020-09-24 08:38:47 +03:00
Yury Shkoda
7fb1da31e4 Merge branch 'master' into cli-issue-105 2020-09-24 08:35:28 +03:00
Allan Bowe
1aa92c0a69 Merge pull request #107 from sasjs/access-token-missed
Access token missed
2020-09-23 21:26:46 +02:00
Mihajlo Medjedovic
4c097a69fd style: lint 2020-09-23 20:43:41 +02:00
Mihajlo Medjedovic
2634933e84 fix: accessToken not passed in function calls 2020-09-23 20:41:31 +02:00
Yury Shkoda
d60c0850c2 docs(context): update docs related to getComputeContextByName function 2020-09-23 17:21:27 +03:00
Yury Shkoda
491bc3371c feat(context): made getContextByName function public 2020-09-23 16:38:21 +03:00
Krishna Acondy
c1bab07b08 Merge pull request #105 from sasjs/file-upload-csrf
fix(jes-job-execution): prevent file upload requests failing with invalid CSRF token
2020-09-23 08:04:18 +01:00
Krishna Acondy
95f3ebd51d chore(dx): add pull request template 2020-09-22 12:40:11 +01:00
Krishna Acondy
0e5b72b54f Merge branch 'master' into file-upload-csrf 2020-09-22 12:29:48 +01:00
Krishna Acondy
9f6591d7e3 chore(sasjs-tests): fix failing tests, bump adapter version 2020-09-22 09:32:45 +01:00
Krishna Acondy
5343ca00d8 fix(file-upload): maintain separate CSRF token for file uploads 2020-09-22 09:26:16 +01:00
57 changed files with 16125 additions and 93 deletions

17
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,17 @@
## Issue
Link any related issue(s) in this section.
## Intent
What this PR intends to achieve.
## Implementation
What code changes have been made to achieve the intent.
## Checks
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All 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)).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,8 +30,8 @@
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
// make a request to a SAS service
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
// request data from an endpoint under your appLoc
sasJs.request("/common/getdata", {
// request data from an endpoint under your appLoc (missing opening slash implies relative path)
sasJs.request("common/getdata", {
// send data as an array of objects - each object is one row
fromjs: [{ type: type }]
}).then((response) => {

View File

@@ -1357,9 +1357,9 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@sasjs/adapter": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.3.13.tgz",
"integrity": "sha512-dWcDxgY3FB7Yx1I5dPpeQeyJDu4lezhIFrjn6lbdwRhV15aqOt4l9o9qZP+VbgOXqyi9gN0Y+p+vs2chBDFQqg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",

View File

@@ -4,7 +4,7 @@
"homepage": ".",
"private": true,
"dependencies": {
"@sasjs/adapter": "^1.3.13",
"@sasjs/adapter": "^1.12.0",
"@sasjs/test-framework": "^1.4.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",

View File

@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
return adapter.request("common/sendArr", data).catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
return !!error && !!error.body && !!error.body.message;
}
},
{
@@ -185,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
};
return adapter.request("common/sendObj", invalidData).catch((e) => e);
},
assertion: (error: any) => !!error && !!error.MESSAGE
assertion: (error: any) => !!error && !!error.body && !!error.body.message
},
{
title: "Single string value",
@@ -219,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
.catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
return !!error && !!error.body && !!error.body.message;
}
},
{

View File

@@ -12,6 +12,7 @@ import {
Job,
Session,
Context,
ContextAllAttributes,
Folder,
CsrfToken,
EditContextInput,
@@ -36,12 +37,12 @@ export class SASViyaApiClient {
}
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
)
private isForceDeploy: boolean = false
private folderMap = new Map<string, Job[]>()
/**
@@ -223,16 +224,16 @@ export class SASViyaApiClient {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken?: string
accessToken?: string,
authorizedUsers?: string[]
) {
if (!contextName) {
throw new Error('Context name is required.')
@@ -312,10 +313,22 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const originalContext = await this.getContextByName(
let originalContext
originalContext = await this.getComputeContextByName(
contextName,
accessToken
)
).catch((_) => {})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
originalContext = await this.getComputeContextById(
editedContext.id!,
accessToken
).catch((err) => {
throw err
})
}
const { result: context, etag } = await this.request<Context>(
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
@@ -371,7 +384,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const context = await this.getContextByName(contextName, accessToken)
const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = {
method: 'DELETE',
@@ -611,8 +624,6 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) {
if (isForced) this.isForceDeploy = true
console.log(
`Parent folder at path '${parentFolderPath}' is not present.`
)
@@ -638,37 +649,16 @@ export class SASViyaApiClient {
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
} else if (isForced && accessToken && !this.isForceDeploy) {
this.isForceDeploy = true
} else if (isForced && accessToken) {
const folderPath = parentFolderPath + '/' + folderName
const folderUri = await this.getFolderUri(folderPath, accessToken)
await this.deleteFolder(parentFolderPath, accessToken)
const newParentFolderPath = parentFolderPath.substring(
0,
parentFolderPath.lastIndexOf('/')
)
const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') {
throw new Error(`Root folder has to be present on the server.`)
}
console.log(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
)
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
if (folderUri) {
await this.deleteFolder(
parentFolderPath + '/' + folderName,
accessToken
)
console.log(
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
}
}
}
@@ -931,7 +921,10 @@ export class SASViyaApiClient {
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(`${this.rootFolderName}/${folderName}`)
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
@@ -1028,7 +1021,10 @@ export class SASViyaApiClient {
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(`${this.rootFolderName}/${folderName}`)
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
@@ -1335,7 +1331,9 @@ export class SASViyaApiClient {
const uploadResponse = await this.request<any>(
`${this.serverUrl}/files/files#rawUpload`,
createFileRequest
createFileRequest,
'json',
'fileUpload'
)
uploadedFiles.push({ tableName, file: uploadResponse.result })
@@ -1384,7 +1382,13 @@ export class SASViyaApiClient {
return `/folders/folders/${folder.id}`
}
private async getContextByName(
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
): Promise<Context> {
@@ -1399,9 +1403,7 @@ export class SASViyaApiClient {
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
{ headers }
).catch((err) => {
throw err
})
)
if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error(
@@ -1412,6 +1414,33 @@ export class SASViyaApiClient {
return contexts.items[0]
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(
contextId: string,
accessToken?: string
): Promise<ContextAllAttributes> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: context } = await this.request<ContextAllAttributes>(
`${this.serverUrl}/compute/contexts/${contextId}`,
{ headers }
).catch((err) => {
throw err
})
return context
}
/**
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
@@ -1455,6 +1484,16 @@ export class SASViyaApiClient {
`${this.serverUrl}${url}`,
requestInfo
).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
}
throw notFoundError
}
throw err
})
@@ -1490,22 +1529,36 @@ export class SASViyaApiClient {
this.setCsrfToken(csrfToken)
}
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
this.fileUploadCsrfToken = csrfToken
}
private async request<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
contentType: 'text' | 'json' = 'json',
type: 'fileUpload' | 'other' = 'other'
) {
const callback =
type === 'fileUpload'
? this.setFileUploadCsrfToken
: this.setCsrfTokenLocal
if (type === 'other') {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
this.setCsrfTokenLocal,
contentType
)
} else {
if (this.fileUploadCsrfToken) {
options.headers = {
...options.headers,
[this.fileUploadCsrfToken.headerName]: this.fileUploadCsrfToken.value
}
}
}
return await makeRequest<T>(url, options, callback, contentType)
}
}

View File

@@ -113,16 +113,16 @@ export default class SASjs {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context as.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken: string
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createContext', ServerType.SASViya)
@@ -131,8 +131,8 @@ export default class SASjs {
launchContextName,
sharedAccountId,
autoExecLines,
authorizedUsers,
accessToken
accessToken,
authorizedUsers
)
}
@@ -167,6 +167,38 @@ export default class SASjs {
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
}
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
) {
this.isMethodSupported('getComputeContextByName', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextByName(
contextName,
accessToken
)
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextById(
contextId,
accessToken
)
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SASViya)
@@ -211,8 +243,6 @@ export default class SASjs {
sasApiClient?: SASViyaApiClient,
isForced?: boolean
) {
this.isMethodSupported('createFolder', ServerType.SASViya)
if (sasApiClient)
return await sasApiClient.createFolder(
folderName,
@@ -229,6 +259,40 @@ export default class SASjs {
)
}
/**
* For performance (and in case of accidental error) the `deleteFolder` function does not actually delete the folder (and all its content and subfolder content). Instead the folder is simply moved to the recycle bin. Deletion time will be added to the folder name.
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
/**
* Moves folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
* @param targetParentFolder - the full path or URI of the _parent_ folder to which the `sourceFolder` will be moved (eg `/Public/newDestination`). To move a folder, a user has to have write permissions in targetParentFolder. Providing URI instead of path will save one extra request.
* @param targetFolderName - the name of the "moved" folder. If left blank, the original folder name will be used (eg `myFolder` in `/Public/newDestination/myFolder` for the example above). Optional field.
* @param accessToken - an access token for authorizing the request.
*/
public async moveFolder(
sourceFolder: string,
targetParentFolder: string,
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
targetParentFolder,
targetFolderName,
accessToken
)
}
public async createJobDefinition(
jobName: string,
code: string,

View File

@@ -14,4 +14,26 @@ export interface EditContextInput {
attributes?: any
authorizedUsers?: string[]
authorizeAllAuthenticatedUsers?: boolean
id?: string
}
export interface ContextAllAttributes {
attributes: {
reuseServerProcesses: boolean
runServerAs: string
}
modifiedTimeStamp: string
createdBy: string
creationTimeStamp: string
launchType: string
environment: {
autoExecLines: [string]
}
launchContext: {
contextName: string
}
modifiedBy: string
id: string
version: number
name: string
}