@sasjs/adapter 4.15.0 → 4.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/SASViyaApiClient.d.ts +2 -0
- package/cypress.config.js +18 -0
- package/index-dev.js +74 -4
- package/index.js +74 -4
- package/minified_sas9.js +3 -3
- package/node/SASViyaApiClient.d.ts +2 -0
- package/node/index.js +74 -4
- package/node/request/RequestClient.d.ts +3 -1
- package/package.json +5 -8
- package/request/RequestClient.d.ts +3 -1
- package/src/SASViyaApiClient.ts +153 -9
- package/src/request/RequestClient.ts +6 -2
|
@@ -67,7 +67,9 @@ export declare class RequestClient implements HttpClient {
|
|
|
67
67
|
result: T;
|
|
68
68
|
etag: string;
|
|
69
69
|
}>;
|
|
70
|
-
patch<T>(url: string, data?: any, accessToken?: string
|
|
70
|
+
patch<T>(url: string, data?: any, accessToken?: string, overrideHeaders?: {
|
|
71
|
+
[key: string]: string | number;
|
|
72
|
+
}): Promise<{
|
|
71
73
|
result: T;
|
|
72
74
|
etag: string;
|
|
73
75
|
}>;
|
package/package.json
CHANGED
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
"homepage": "https://adapter.sasjs.io",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"nodeVersionMessage": "echo \u001b[33m make sure you are running node lts version \u001b[0m",
|
|
7
|
-
"
|
|
8
|
-
"prebuild": "npm run nodeVersionMessage",
|
|
9
|
-
"build": "npx rimraf build && npx rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && npx rimraf build/src && npx rimraf node",
|
|
7
|
+
"build": "npm run nodeVersionMessage && npx rimraf build && npx rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && npx rimraf build/src && npx rimraf node",
|
|
10
8
|
"package:lib": "npm run build && copyfiles ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
|
11
9
|
"publish:lib": "npm run build && cd build && npm publish",
|
|
12
10
|
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
13
11
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
14
12
|
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"cypress/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
|
|
15
13
|
"test": "jest --silent --coverage",
|
|
16
|
-
"
|
|
17
|
-
"postpublish": "git clean -fd",
|
|
14
|
+
"publishInit": "cp -r ./build/* . && rm -rf ./build",
|
|
18
15
|
"semantic-release": "semantic-release",
|
|
19
16
|
"typedoc": "node createTSDocs",
|
|
20
17
|
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true",
|
|
@@ -54,7 +51,7 @@
|
|
|
54
51
|
"copyfiles": "2.4.1",
|
|
55
52
|
"cors": "^2.8.5",
|
|
56
53
|
"cp": "0.2.0",
|
|
57
|
-
"cypress": "
|
|
54
|
+
"cypress": "^15.7.1",
|
|
58
55
|
"dotenv": "16.0.0",
|
|
59
56
|
"express": "4.17.3",
|
|
60
57
|
"jest": "29.7.0",
|
|
@@ -79,12 +76,12 @@
|
|
|
79
76
|
},
|
|
80
77
|
"main": "index.js",
|
|
81
78
|
"dependencies": {
|
|
82
|
-
"@sasjs/utils": "3.5.
|
|
79
|
+
"@sasjs/utils": "3.5.6",
|
|
83
80
|
"axios": "1.12.2",
|
|
84
81
|
"axios-cookiejar-support": "5.0.5",
|
|
85
82
|
"form-data": "4.0.4",
|
|
86
83
|
"https": "1.0.0",
|
|
87
84
|
"tough-cookie": "4.1.3"
|
|
88
85
|
},
|
|
89
|
-
"version": "4.
|
|
86
|
+
"version": "4.16.1"
|
|
90
87
|
}
|
|
@@ -67,7 +67,9 @@ export declare class RequestClient implements HttpClient {
|
|
|
67
67
|
result: T;
|
|
68
68
|
etag: string;
|
|
69
69
|
}>;
|
|
70
|
-
patch<T>(url: string, data?: any, accessToken?: string
|
|
70
|
+
patch<T>(url: string, data?: any, accessToken?: string, overrideHeaders?: {
|
|
71
|
+
[key: string]: string | number;
|
|
72
|
+
}): Promise<{
|
|
71
73
|
result: T;
|
|
72
74
|
etag: string;
|
|
73
75
|
}>;
|
package/src/SASViyaApiClient.ts
CHANGED
|
@@ -35,6 +35,60 @@ interface JobExecutionResult {
|
|
|
35
35
|
log?: string
|
|
36
36
|
error?: object
|
|
37
37
|
}
|
|
38
|
+
/* Viya /types/types?limit=999999 response structure */
|
|
39
|
+
interface IViyaTypesResponse {
|
|
40
|
+
accept: string
|
|
41
|
+
count: number
|
|
42
|
+
items: IViyaTypesItem[]
|
|
43
|
+
limit: number
|
|
44
|
+
links: IViyaTypesLink[]
|
|
45
|
+
name: string
|
|
46
|
+
start: number
|
|
47
|
+
version: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Item element within the Viya types response */
|
|
51
|
+
interface IViyaTypesItem {
|
|
52
|
+
description?: string
|
|
53
|
+
extensions?: string[]
|
|
54
|
+
iconUri?: string
|
|
55
|
+
label: string
|
|
56
|
+
links: IViyaTypesLink[]
|
|
57
|
+
mappedTypes?: string[]
|
|
58
|
+
mediaType?: string
|
|
59
|
+
mediaTypes?: string[]
|
|
60
|
+
name?: string | undefined
|
|
61
|
+
pluralLabel?: string
|
|
62
|
+
properties?: IViyaTypesProperties
|
|
63
|
+
resourceUri?: string
|
|
64
|
+
serviceRootUri?: string
|
|
65
|
+
tags?: string[]
|
|
66
|
+
version: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generic structure for a link
|
|
71
|
+
* in the links array of a Viya
|
|
72
|
+
* types/types api response
|
|
73
|
+
*/
|
|
74
|
+
type IViyaTypesLink = Record<string, string>
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generic structure for a type's
|
|
78
|
+
* 'properties' object from the Viya
|
|
79
|
+
* types/types api response
|
|
80
|
+
*/
|
|
81
|
+
type IViyaTypesProperties = Record<string, string>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Arbitrary interface for storing
|
|
85
|
+
* sufficient additional detail to
|
|
86
|
+
* create and patch a new file.
|
|
87
|
+
*/
|
|
88
|
+
interface IViyaTypesExtensionInfo {
|
|
89
|
+
typeDefName: string | undefined
|
|
90
|
+
properties: IViyaTypesProperties | undefined
|
|
91
|
+
}
|
|
38
92
|
|
|
39
93
|
/**
|
|
40
94
|
* A client for interfacing with the SAS Viya REST API.
|
|
@@ -61,6 +115,9 @@ export class SASViyaApiClient {
|
|
|
61
115
|
this.requestClient
|
|
62
116
|
)
|
|
63
117
|
private folderMap = new Map<string, Job[]>()
|
|
118
|
+
private fileExtensionMap = new Map<string, IViyaTypesExtensionInfo>()
|
|
119
|
+
private boolExtensionMap = false // required in case the map has zero entries
|
|
120
|
+
// after an attempt to populate it.
|
|
64
121
|
|
|
65
122
|
/**
|
|
66
123
|
* A helper method used to call appendRequest method of RequestClient
|
|
@@ -434,15 +491,102 @@ export class SASViyaApiClient {
|
|
|
434
491
|
const formData = new NodeFormData()
|
|
435
492
|
formData.append('file', contentBuffer, fileName)
|
|
436
493
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
494
|
+
/** Query Viya for file metadata based on extension type. */
|
|
495
|
+
|
|
496
|
+
// typeDefName - Viya accepts this property during the file creation
|
|
497
|
+
let typeDefName: string | undefined = undefined
|
|
498
|
+
// Additional properties are supplied by a patch.
|
|
499
|
+
let filePatch:
|
|
500
|
+
| {
|
|
501
|
+
name: string
|
|
502
|
+
properties: IViyaTypesProperties | undefined
|
|
503
|
+
}
|
|
504
|
+
| undefined = undefined
|
|
505
|
+
|
|
506
|
+
// The patching process requires properties related to the file-extension
|
|
507
|
+
const fileExtension: string | undefined = fileName
|
|
508
|
+
.split('.')
|
|
509
|
+
.pop()
|
|
510
|
+
?.toLowerCase()
|
|
511
|
+
|
|
512
|
+
if (fileExtension) {
|
|
513
|
+
if (!this.boolExtensionMap) {
|
|
514
|
+
// Populate the file extension map
|
|
515
|
+
// 1. Get Viya's response to this api call
|
|
516
|
+
const typesQueryUrl = `/types/types?limit=999999`
|
|
517
|
+
const response = (
|
|
518
|
+
await this.requestClient.get(typesQueryUrl, accessToken)
|
|
519
|
+
).result as IViyaTypesResponse
|
|
520
|
+
// 2. Filter the returned items that have file extensions into a map
|
|
521
|
+
// using forEach as an item may relate to multiple file extensions.
|
|
522
|
+
response.items
|
|
523
|
+
.filter((e) => e.extensions)
|
|
524
|
+
.forEach((e) => {
|
|
525
|
+
e.extensions?.forEach((ext) => {
|
|
526
|
+
this.fileExtensionMap.set(ext, {
|
|
527
|
+
// `name` becomes the typeDefName value at file creation time.
|
|
528
|
+
// `name` is ignored here if it is not populated in the map, or
|
|
529
|
+
// has a blank/empty value.
|
|
530
|
+
typeDefName: e.name
|
|
531
|
+
? e.name.trim().length
|
|
532
|
+
? e.name.trim()
|
|
533
|
+
: undefined
|
|
534
|
+
: undefined,
|
|
535
|
+
properties: e.properties
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
// 3. Toggle the flag to avoid repeating this step
|
|
540
|
+
this.boolExtensionMap = true
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Query the map for the current file extension
|
|
544
|
+
const fileExtInfo = this.fileExtensionMap.get(fileExtension)
|
|
545
|
+
if (fileExtInfo) {
|
|
546
|
+
// If the extension was found in the map, record the typeDefName and
|
|
547
|
+
// create a patch if a properties object was returned.
|
|
548
|
+
typeDefName = fileExtInfo.typeDefName
|
|
549
|
+
if (fileExtInfo.properties)
|
|
550
|
+
filePatch = { name: fileName, properties: fileExtInfo.properties }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Create the file
|
|
555
|
+
const createFileResponse = await this.requestClient.post<File>(
|
|
556
|
+
`/files/files?parentFolderUri=${parentFolderUri}&typeDefName=${
|
|
557
|
+
typeDefName ?? 'file'
|
|
558
|
+
}#rawUpload`,
|
|
559
|
+
formData,
|
|
560
|
+
accessToken,
|
|
561
|
+
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
|
562
|
+
headers
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
// If a patch was created...
|
|
566
|
+
if (filePatch) {
|
|
567
|
+
try {
|
|
568
|
+
const patchHeaders = {
|
|
569
|
+
Accept: 'application/json',
|
|
570
|
+
'If-Match': '*'
|
|
571
|
+
}
|
|
572
|
+
// Get the URI of the newly created file
|
|
573
|
+
const fileUri = createFileResponse.result.links.filter(
|
|
574
|
+
(e) => e.method == 'PATCH' && e.rel == 'patch'
|
|
575
|
+
)[0].uri
|
|
576
|
+
// and apply the patch
|
|
577
|
+
return (
|
|
578
|
+
await this.requestClient.patch<File>(
|
|
579
|
+
`${fileUri}`,
|
|
580
|
+
filePatch,
|
|
581
|
+
accessToken,
|
|
582
|
+
patchHeaders
|
|
583
|
+
)
|
|
584
|
+
).result
|
|
585
|
+
} catch (e: any) {
|
|
586
|
+
throw new Error(`Error patching file ${fileName}.\n${e.message}`)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return createFileResponse.result
|
|
446
590
|
}
|
|
447
591
|
|
|
448
592
|
/**
|
|
@@ -273,9 +273,13 @@ export class RequestClient implements HttpClient {
|
|
|
273
273
|
public async patch<T>(
|
|
274
274
|
url: string,
|
|
275
275
|
data: any = {},
|
|
276
|
-
accessToken?: string
|
|
276
|
+
accessToken?: string,
|
|
277
|
+
overrideHeaders: { [key: string]: string | number } = {}
|
|
277
278
|
): Promise<{ result: T; etag: string }> {
|
|
278
|
-
const headers =
|
|
279
|
+
const headers = {
|
|
280
|
+
...this.getHeaders(accessToken, 'application/json'),
|
|
281
|
+
...overrideHeaders
|
|
282
|
+
}
|
|
279
283
|
|
|
280
284
|
return this.httpClient
|
|
281
285
|
.patch<T>(url, data, { headers, withXSRFToken: true })
|