@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.
@@ -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): Promise<{
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
- "preinstall": "npm run nodeVersionMessage",
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
- "prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
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": "7.7.0",
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.2",
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.15.0"
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): Promise<{
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
  }>;
@@ -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
- return (
438
- await this.requestClient.post<File>(
439
- `/files/files?parentFolderUri=${parentFolderUri}&typeDefName=file#rawUpload`,
440
- formData,
441
- accessToken,
442
- 'multipart/form-data; boundary=' + (formData as any)._boundary,
443
- headers
444
- )
445
- ).result
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 = this.getHeaders(accessToken, 'application/json')
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 })