From 4ed9f87434c1d66336108f8a50169cdab6c9c863 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 28 Jul 2022 14:20:41 +0500 Subject: [PATCH 1/3] fix: moved validateInput method to separate file and added some additional validation --- src/SASjs.ts | 72 +----------------------------- src/utils/index.ts | 13 +++--- src/utils/validateInput.ts | 90 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 76 deletions(-) create mode 100644 src/utils/validateInput.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 01c00b5..8fd5cde 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -1,4 +1,4 @@ -import { compareTimestamps, asyncForEach } from './utils' +import { compareTimestamps, asyncForEach, validateInput } from './utils' import { SASjsConfig, UploadFile, @@ -686,7 +686,7 @@ export default class SASjs { ...config } - const validationResult = this.validateInput(data) + const validationResult = validateInput(data) // status is true if the data passes validation checks above 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`. * @param serviceJson - the JSON specifying the folders and services to be created. diff --git a/src/utils/index.ts b/src/utils/index.ts index 065dd87..62ceb97 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,20 +1,21 @@ +export * from './appendExtraResponseAttributes' export * from './asyncForEach' export * from './compareTimestamps' export * from './convertToCsv' export * from './createAxiosInstance' export * from './delay' +export * from './fetchLogByChunks' +export * from './getValidJson' export * from './isNode' export * from './isRelativePath' export * from './isUri' export * from './isUrl' export * from './needsRetry' export * from './parseGeneratedCode' -export * from './parseSourceCode' export * from './parseSasViyaLog' +export * from './parseSourceCode' +export * from './parseViyaDebugResponse' +export * from './parseWeboutResponse' export * from './serialize' export * from './splitChunks' -export * from './parseWeboutResponse' -export * from './fetchLogByChunks' -export * from './getValidJson' -export * from './parseViyaDebugResponse' -export * from './appendExtraResponseAttributes' +export * from './validateInput' diff --git a/src/utils/validateInput.ts b/src/utils/validateInput.ts new file mode 100644 index 0000000..6f1774d --- /dev/null +++ b/src/utils/validateInput.ts @@ -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 + } +} From 88eadd27aa4928e77f3dd3cae6dae367ef104615 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 28 Jul 2022 14:22:16 +0500 Subject: [PATCH 2/3] chore: moved utils specs to spec folder --- src/utils/{ => spec}/convertToCsv.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/utils/{ => spec}/convertToCsv.spec.ts (99%) diff --git a/src/utils/convertToCsv.spec.ts b/src/utils/spec/convertToCsv.spec.ts similarity index 99% rename from src/utils/convertToCsv.spec.ts rename to src/utils/spec/convertToCsv.spec.ts index ddd2c40..9ab58fc 100644 --- a/src/utils/convertToCsv.spec.ts +++ b/src/utils/spec/convertToCsv.spec.ts @@ -1,4 +1,4 @@ -import { convertToCSV, isFormatsTable } from './convertToCsv' +import { convertToCSV, isFormatsTable } from '../convertToCsv' describe('convertToCsv', () => { const tableName = 'testTable' From 706cbe55133fbdb0d311f2e0f797dc5c462a727a Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 28 Jul 2022 14:23:00 +0500 Subject: [PATCH 3/3] chore: add unit tests for validateInput --- src/utils/spec/validateInput.spec.ts | 84 ++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/utils/spec/validateInput.spec.ts diff --git a/src/utils/spec/validateInput.spec.ts b/src/utils/spec/validateInput.spec.ts new file mode 100644 index 0000000..2ddcdd6 --- /dev/null +++ b/src/utils/spec/validateInput.spec.ts @@ -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.` + }) + }) +})