mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-05 03:30:05 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1df27fdf1 | ||
| eb739a83a4 | |||
| d8b686dd7e | |||
|
|
a0b8316d7c | ||
|
|
92be5a2dca | ||
|
|
f58f2eba97 | ||
|
|
e37bb182c3 | ||
|
|
504777603c | ||
| 706cbe5513 | |||
| 88eadd27aa | |||
| 4ed9f87434 | |||
|
|
f0f80a1c1f | ||
| d0d8d58945 |
@@ -237,7 +237,8 @@ run;
|
|||||||
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
|
%webout(OBJ,a) /* Rows in table `a` are objects (easy to use) */
|
||||||
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
|
%webout(ARR,b) /* Rows in table `b` are arrays (compact) */
|
||||||
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
|
%webout(OBJ,c,fmt=N) /* Table `c` is sent unformatted (raw) */
|
||||||
%webout(OBJ,c,label=d) /* Rename as `d` on JS side */
|
%webout(OBJ,c,label=d) /* Rename table as `d` in output JSON */
|
||||||
|
%webout(OBJ,c,label=e, maxobs=10) /* send only 10 rows back */
|
||||||
%webout(CLOSE) /* Close the JSON and add default variables */
|
%webout(CLOSE) /* Close the JSON and add default variables */
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
72
src/SASjs.ts
72
src/SASjs.ts
@@ -1,4 +1,4 @@
|
|||||||
import { compareTimestamps, asyncForEach } from './utils'
|
import { compareTimestamps, asyncForEach, validateInput } from './utils'
|
||||||
import {
|
import {
|
||||||
SASjsConfig,
|
SASjsConfig,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
@@ -686,7 +686,7 @@ export default class SASjs {
|
|||||||
...config
|
...config
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = this.validateInput(data)
|
const validationResult = validateInput(data)
|
||||||
|
|
||||||
// status is true if the data passes validation checks above
|
// status is true if the data passes validation checks above
|
||||||
if (validationResult.status) {
|
if (validationResult.status) {
|
||||||
@@ -748,74 +748,6 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: '' }
|
|
||||||
|
|
||||||
const isSasFormatsTable = (key: string) =>
|
|
||||||
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
|
||||||
|
|
||||||
for (const key in data) {
|
|
||||||
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
|
||||||
return {
|
|
||||||
status: false,
|
|
||||||
msg: 'First letter of table should be alphabet or underscore.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
|
||||||
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' && !isSasFormatsTable(key)) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
|
||||||
* @param serviceJson - the JSON specifying the folders and services to be created.
|
* @param serviceJson - the JSON specifying the folders and services to be created.
|
||||||
|
|||||||
@@ -223,9 +223,17 @@ export class AuthManager {
|
|||||||
|
|
||||||
private async getNewLoginForm() {
|
private async getNewLoginForm() {
|
||||||
if (this.serverType === ServerType.Sasjs) {
|
if (this.serverType === ServerType.Sasjs) {
|
||||||
// server will be sending CSRF cookie,
|
// server will be sending CSRF token in response,
|
||||||
|
// need to save in cookie so that,
|
||||||
// http client will use it automatically
|
// http client will use it automatically
|
||||||
return this.requestClient.get('/', undefined)
|
return this.requestClient.get('/', undefined).then(({ result }) => {
|
||||||
|
const cookie =
|
||||||
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||||
|
result as string
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (cookie) document.cookie = cookie
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: formResponse } = await this.requestClient.get<string>(
|
const { result: formResponse } = await this.requestClient.get<string>(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
parseSourceCode,
|
parseSourceCode,
|
||||||
createAxiosInstance
|
createAxiosInstance
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { InvalidCsrfError } from '../types/errors/InvalidCsrfError'
|
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||||||
|
|
||||||
export interface HttpClient {
|
export interface HttpClient {
|
||||||
get<T>(
|
get<T>(
|
||||||
@@ -154,8 +154,12 @@ export class RequestClient implements HttpClient {
|
|||||||
sasWork = log
|
sasWork = log
|
||||||
}
|
}
|
||||||
} else if (response?.result) {
|
} else if (response?.result) {
|
||||||
sourceCode = parseSourceCode(response.result)
|
// We parse only if it's a string, otherwise it would throw error
|
||||||
generatedCode = parseGeneratedCode(response.result)
|
if (typeof response.result === 'string') {
|
||||||
|
sourceCode = parseSourceCode(response.result)
|
||||||
|
generatedCode = parseGeneratedCode(response.result)
|
||||||
|
}
|
||||||
|
|
||||||
sasWork = response.result.WORK
|
sasWork = response.result.WORK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,12 +503,20 @@ export class RequestClient implements HttpClient {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e instanceof InvalidCsrfError) {
|
if (e instanceof InvalidSASjsCsrfError) {
|
||||||
// Fetching root will inject CSRF token in cookie
|
// Fetching root and creating CSRF cookie
|
||||||
await this.httpClient
|
await this.httpClient
|
||||||
.get('/', {
|
.get('/', {
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
})
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const cookie =
|
||||||
|
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||||
|
response.data
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (cookie) document.cookie = cookie
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
||||||
})
|
})
|
||||||
@@ -611,8 +623,11 @@ export const throwIfError = (response: AxiosResponse) => {
|
|||||||
throw new LoginRequiredError(response.data)
|
throw new LoginRequiredError(response.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.data.toLowerCase() === 'invalid csrf token!') {
|
if (
|
||||||
throw new InvalidCsrfError()
|
typeof response.data === 'string' &&
|
||||||
|
response.data.toLowerCase() === 'invalid csrf token!'
|
||||||
|
) {
|
||||||
|
throw new InvalidSASjsCsrfError()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 401:
|
case 401:
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export class InvalidCsrfError extends Error {
|
|
||||||
constructor() {
|
|
||||||
const message = 'Invalid CSRF token!'
|
|
||||||
|
|
||||||
super(`Auth error: ${message}`)
|
|
||||||
this.name = 'InvalidCsrfError'
|
|
||||||
Object.setPrototypeOf(this, InvalidCsrfError.prototype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
src/types/errors/InvalidSASjsCsrfError.ts
Normal file
9
src/types/errors/InvalidSASjsCsrfError.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class InvalidSASjsCsrfError extends Error {
|
||||||
|
constructor() {
|
||||||
|
const message = 'Invalid CSRF token!'
|
||||||
|
|
||||||
|
super(`Auth error: ${message}`)
|
||||||
|
this.name = 'InvalidSASjsCsrfError'
|
||||||
|
Object.setPrototypeOf(this, InvalidSASjsCsrfError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
|
export * from './appendExtraResponseAttributes'
|
||||||
export * from './asyncForEach'
|
export * from './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
export * from './createAxiosInstance'
|
export * from './createAxiosInstance'
|
||||||
export * from './delay'
|
export * from './delay'
|
||||||
|
export * from './fetchLogByChunks'
|
||||||
|
export * from './getValidJson'
|
||||||
export * from './isNode'
|
export * from './isNode'
|
||||||
export * from './isRelativePath'
|
export * from './isRelativePath'
|
||||||
export * from './isUri'
|
export * from './isUri'
|
||||||
export * from './isUrl'
|
export * from './isUrl'
|
||||||
export * from './needsRetry'
|
export * from './needsRetry'
|
||||||
export * from './parseGeneratedCode'
|
export * from './parseGeneratedCode'
|
||||||
export * from './parseSourceCode'
|
|
||||||
export * from './parseSasViyaLog'
|
export * from './parseSasViyaLog'
|
||||||
|
export * from './parseSourceCode'
|
||||||
|
export * from './parseViyaDebugResponse'
|
||||||
|
export * from './parseWeboutResponse'
|
||||||
export * from './serialize'
|
export * from './serialize'
|
||||||
export * from './splitChunks'
|
export * from './splitChunks'
|
||||||
export * from './parseWeboutResponse'
|
export * from './validateInput'
|
||||||
export * from './fetchLogByChunks'
|
|
||||||
export * from './getValidJson'
|
|
||||||
export * from './parseViyaDebugResponse'
|
|
||||||
export * from './appendExtraResponseAttributes'
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { convertToCSV, isFormatsTable } from './convertToCsv'
|
import { convertToCSV, isFormatsTable } from '../convertToCsv'
|
||||||
|
|
||||||
describe('convertToCsv', () => {
|
describe('convertToCsv', () => {
|
||||||
const tableName = 'testTable'
|
const tableName = 'testTable'
|
||||||
84
src/utils/spec/validateInput.spec.ts
Normal file
84
src/utils/spec/validateInput.spec.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
validateInput,
|
||||||
|
INVALID_TABLE_STRUCTURE,
|
||||||
|
MORE_INFO
|
||||||
|
} from '../validateInput'
|
||||||
|
|
||||||
|
const tableArray = [{ col1: 'first col value' }]
|
||||||
|
const stringData: any = { table1: tableArray }
|
||||||
|
|
||||||
|
describe('validateInput', () => {
|
||||||
|
it('should not return an error message if input data valid', () => {
|
||||||
|
const validationResult = validateInput(stringData)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: true,
|
||||||
|
msg: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return an error message if input data is null', () => {
|
||||||
|
const validationResult = validateInput(null)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: true,
|
||||||
|
msg: ''
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if input data is an array', () => {
|
||||||
|
const validationResult = validateInput(tableArray)
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if first letter of table is neither alphabet nor underscore', () => {
|
||||||
|
const validationResult = validateInput({ '1stTable': tableArray })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'First letter of table should be alphabet or underscore.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if table name contains a character other than alphanumeric or underscore', () => {
|
||||||
|
const validationResult = validateInput({ 'table!': tableArray })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'Table name should be alphanumeric.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if length of table name contains exceeds 32', () => {
|
||||||
|
const validationResult = validateInput({
|
||||||
|
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: tableArray
|
||||||
|
})
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: 'Maximum length for table name could be 32 characters.'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if table does not have array of objects', () => {
|
||||||
|
const validationResult = validateInput({ table: stringData })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if a table array has an item other than object', () => {
|
||||||
|
const validationResult = validateInput({ table1: ['invalid'] })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: `Table table1 contains invalid structure. ${MORE_INFO}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error message if a row in a table contains an column with undefined value', () => {
|
||||||
|
const validationResult = validateInput({ table1: [{ column: undefined }] })
|
||||||
|
expect(validationResult).toEqual({
|
||||||
|
status: false,
|
||||||
|
msg: `A row in table table1 contains invalid value. Can't assign undefined to column.`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
90
src/utils/validateInput.ts
Normal file
90
src/utils/validateInput.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export const MORE_INFO =
|
||||||
|
'For more info see https://sasjs.io/sasjs-adapter/#request-response'
|
||||||
|
export const INVALID_TABLE_STRUCTURE = `Parameter data contains invalid table structure. ${MORE_INFO}`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
export const validateInput = (
|
||||||
|
data: { [key: string]: any } | null
|
||||||
|
): {
|
||||||
|
status: boolean
|
||||||
|
msg: string
|
||||||
|
} => {
|
||||||
|
if (data === null) return { status: true, msg: '' }
|
||||||
|
|
||||||
|
if (getType(data) !== 'object') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSasFormatsTable = (key: string) =>
|
||||||
|
key.match(/^\$.*/) && Object.keys(data).includes(key.replace(/^\$/, ''))
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
if (!key.match(/^[a-zA-Z_]/) && !isSasFormatsTable(key)) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: 'First letter of table should be alphabet or underscore.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
||||||
|
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 (getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: INVALID_TABLE_STRUCTURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of data[key]) {
|
||||||
|
if (getType(item) !== 'object') {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `Table ${key} contains invalid structure. ${MORE_INFO}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const attributes = Object.keys(item)
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (item[attribute] === undefined) {
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
msg: `A row in table ${key} contains invalid value. Can't assign undefined to ${attribute}.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const getType = (data: any): string => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return 'Array'
|
||||||
|
} else {
|
||||||
|
return typeof data
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user