mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-03 18:50:05 +00:00
Merge pull request #615 from sasjs/issue-607
Support special missing values
This commit is contained in:
11
src/SASjs.ts
11
src/SASjs.ts
@@ -736,15 +736,19 @@ export default class SASjs {
|
||||
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_]/)) {
|
||||
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_]*$/)) {
|
||||
if (!key.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) && !isSasFormatsTable(key)) {
|
||||
return { status: false, msg: 'Table name should be alphanumeric.' }
|
||||
}
|
||||
|
||||
@@ -755,7 +759,7 @@ export default class SASjs {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getType(data[key]) !== 'Array') {
|
||||
if (this.getType(data[key]) !== 'Array' && !isSasFormatsTable(key)) {
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Parameter data contains invalid table structure.'
|
||||
@@ -771,6 +775,7 @@ export default class SASjs {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { status: true, msg: '' }
|
||||
}
|
||||
|
||||
|
||||
95
src/test/utils/formatDataForRequest.spec.ts
Normal file
95
src/test/utils/formatDataForRequest.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { formatDataForRequest } from '../../utils/formatDataForRequest'
|
||||
|
||||
describe('formatDataForRequest', () => {
|
||||
const testTable = 'sometable'
|
||||
|
||||
it('should format table with special missing values', () => {
|
||||
const tableWithMissingValues = {
|
||||
[testTable]: [
|
||||
{ var1: 'string', var2: 232, nullvar: 'A' },
|
||||
{ var1: 'string', var2: 232, nullvar: 'B' },
|
||||
{ var1: 'string', var2: 232, nullvar: '_' },
|
||||
{ var1: 'string', var2: 232, nullvar: 0 },
|
||||
{ var1: 'string', var2: 232, nullvar: 'z' },
|
||||
{ var1: 'string', var2: 232, nullvar: null }
|
||||
],
|
||||
[`$${testTable}`]: { formats: { var1: '$char12.', nullvar: 'best.' } }
|
||||
}
|
||||
|
||||
const expectedOutput = {
|
||||
sasjs1data: `var1:$char12. var2:best. nullvar:best.\r\nstring,232,.a\r\nstring,232,.b\r\nstring,232,._\r\nstring,232,0\r\nstring,232,.z\r\nstring,232,.`,
|
||||
sasjs_tables: testTable
|
||||
}
|
||||
|
||||
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should return error if string is more than 32765 characters', () => {
|
||||
const data = { testTable: [{ var1: 'z'.repeat(32765 + 1) }] }
|
||||
|
||||
expect(() => formatDataForRequest(data)).toThrow(
|
||||
new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error if string is more than 32765 characters', () => {
|
||||
const charsCount = 16 * 1000 + 1
|
||||
const allChars = 'z'.repeat(charsCount)
|
||||
const data = { [testTable]: [{ var1: allChars }] }
|
||||
const firstChunk = `var1:$char${charsCount}.\r\n`
|
||||
const firstChunkChars = 'z'.repeat(16000 - firstChunk.length)
|
||||
const secondChunkChars = 'z'.repeat(
|
||||
charsCount - (16000 - firstChunk.length)
|
||||
)
|
||||
|
||||
const expectedOutput = {
|
||||
sasjs1data0: 2,
|
||||
sasjs1data1: `${firstChunk}${firstChunkChars}`,
|
||||
sasjs1data2: secondChunkChars,
|
||||
sasjs_tables: testTable
|
||||
}
|
||||
|
||||
expect(formatDataForRequest(data)).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should throw an error if special missing values is not valid', () => {
|
||||
let tableWithMissingValues = {
|
||||
[testTable]: [{ var: 'AA' }, { var: 0 }],
|
||||
[`$${testTable}`]: { formats: { var: 'best.' } }
|
||||
}
|
||||
|
||||
expect(() => formatDataForRequest(tableWithMissingValues)).toThrow(
|
||||
new Error(
|
||||
'Special missing value can only be a single character from A to Z or _'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should auto-detect special missing values type as best.', () => {
|
||||
const tableWithMissingValues = {
|
||||
[testTable]: [{ var: 'a' }, { var: 'A' }, { var: '_' }, { var: 0 }]
|
||||
}
|
||||
|
||||
const expectedOutput = {
|
||||
sasjs1data: `var:best.\r\n.a\r\n.a\r\n._\r\n0`,
|
||||
sasjs_tables: testTable
|
||||
}
|
||||
|
||||
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should auto-detect values type as $char1.', () => {
|
||||
const tableWithMissingValues = {
|
||||
[testTable]: [{ var: 'a' }, { var: 'A' }, { var: '_' }]
|
||||
}
|
||||
|
||||
const expectedOutput = {
|
||||
sasjs1data: `var:$char1.\r\na\r\nA\r\n_`,
|
||||
sasjs_tables: testTable
|
||||
}
|
||||
|
||||
expect(formatDataForRequest(tableWithMissingValues)).toEqual(expectedOutput)
|
||||
})
|
||||
})
|
||||
@@ -167,4 +167,17 @@ describe('convertToCsv', () => {
|
||||
convertToCSV([{ slashWithSpecialExtra: '\\\ts\tl\ta\ts\t\th\t' }])
|
||||
).toEqual(`slashWithSpecialExtra:$char13.\r\n\"\\\ts\tl\ta\ts\t\th\t\"`)
|
||||
})
|
||||
|
||||
it('should console log error if data has mixed types', () => {
|
||||
const colName = 'var1'
|
||||
const data = [{ [colName]: 'string' }, { [colName]: 232 }]
|
||||
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
convertToCSV(data)
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
`Row (2), Column (${colName}) has mixed types: ERROR`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,70 +2,115 @@
|
||||
* Converts the given JSON object array to a CSV string.
|
||||
* @param data - the array of JSON objects to convert.
|
||||
*/
|
||||
export const convertToCSV = (data: any) => {
|
||||
const replacer = (key: any, value: any) => (value === null ? '' : value)
|
||||
const headerFields = Object.keys(data[0])
|
||||
export const convertToCSV = (
|
||||
data: any,
|
||||
sasFormats?: { formats: { [key: string]: string } }
|
||||
) => {
|
||||
let formats = sasFormats?.formats
|
||||
let headers: string[] = []
|
||||
let csvTest
|
||||
let invalidString = false
|
||||
const headers = headerFields.map((field) => {
|
||||
let firstFoundType: string | null = null
|
||||
let hasMixedTypes: boolean = false
|
||||
let rowNumError: number = -1
|
||||
const specialMissingValueRegExp = /^[a-z_]{1}$/i
|
||||
|
||||
const longestValueForField = data
|
||||
.map((row: any, index: number) => {
|
||||
if (row[field] || row[field] === '') {
|
||||
if (firstFoundType) {
|
||||
let currentFieldType =
|
||||
row[field] === '' || typeof row[field] === 'string'
|
||||
? 'chars'
|
||||
: 'number'
|
||||
if (formats) {
|
||||
headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
|
||||
}
|
||||
|
||||
if (!hasMixedTypes) {
|
||||
hasMixedTypes = currentFieldType !== firstFoundType
|
||||
rowNumError = hasMixedTypes ? index + 1 : -1
|
||||
}
|
||||
} else {
|
||||
if (row[field] === '') {
|
||||
firstFoundType = 'chars'
|
||||
} else {
|
||||
firstFoundType =
|
||||
typeof row[field] === 'string' ? 'chars' : 'number'
|
||||
}
|
||||
}
|
||||
const headerFields = Object.keys(data[0])
|
||||
|
||||
let byteSize
|
||||
headerFields.forEach((field) => {
|
||||
if (!formats || !Object.keys(formats).includes(field)) {
|
||||
let hasNullOrNumber = false
|
||||
let hasSpecialMissingString = false
|
||||
|
||||
if (typeof row[field] === 'string') {
|
||||
byteSize = getByteSize(row[field])
|
||||
}
|
||||
|
||||
return byteSize
|
||||
data.forEach((row: { [key: string]: any }) => {
|
||||
if (row[field] === null || typeof row[field] === 'number') {
|
||||
hasNullOrNumber = true
|
||||
} else if (
|
||||
typeof row[field] === 'string' &&
|
||||
specialMissingValueRegExp.test(row[field])
|
||||
) {
|
||||
hasSpecialMissingString = true
|
||||
}
|
||||
})
|
||||
.sort((a: number, b: number) => b - a)[0]
|
||||
if (longestValueForField && longestValueForField > 32765) {
|
||||
invalidString = true
|
||||
}
|
||||
if (hasMixedTypes) {
|
||||
console.error(
|
||||
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
||||
)
|
||||
}
|
||||
|
||||
return `${field}:${firstFoundType === 'chars' ? '$char' : ''}${
|
||||
longestValueForField
|
||||
? longestValueForField
|
||||
: firstFoundType === 'chars'
|
||||
? '1'
|
||||
: 'best'
|
||||
}.`
|
||||
if (hasNullOrNumber && hasSpecialMissingString) {
|
||||
headers.push(`${field}:best.`)
|
||||
|
||||
if (!formats) formats = {}
|
||||
|
||||
formats[field] = 'best.'
|
||||
} else {
|
||||
let firstFoundType: string | null = null
|
||||
let hasMixedTypes: boolean = false
|
||||
let rowNumError: number = -1
|
||||
|
||||
const longestValueForField = data
|
||||
.map((row: any, index: number) => {
|
||||
if (row[field] || row[field] === '') {
|
||||
if (firstFoundType) {
|
||||
let currentFieldType =
|
||||
row[field] === '' || typeof row[field] === 'string'
|
||||
? 'chars'
|
||||
: 'number'
|
||||
|
||||
if (!hasMixedTypes) {
|
||||
hasMixedTypes = currentFieldType !== firstFoundType
|
||||
rowNumError = hasMixedTypes ? index + 1 : -1
|
||||
}
|
||||
} else {
|
||||
if (row[field] === '') {
|
||||
firstFoundType = 'chars'
|
||||
} else {
|
||||
firstFoundType =
|
||||
typeof row[field] === 'string' ? 'chars' : 'number'
|
||||
}
|
||||
}
|
||||
|
||||
let byteSize
|
||||
|
||||
if (typeof row[field] === 'string') {
|
||||
byteSize = getByteSize(row[field])
|
||||
}
|
||||
|
||||
return byteSize
|
||||
}
|
||||
})
|
||||
.sort((a: number, b: number) => b - a)[0]
|
||||
|
||||
if (longestValueForField && longestValueForField > 32765) {
|
||||
invalidString = true
|
||||
}
|
||||
|
||||
if (hasMixedTypes) {
|
||||
console.error(
|
||||
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
||||
)
|
||||
}
|
||||
|
||||
headers.push(
|
||||
`${field}:${firstFoundType === 'chars' ? '$char' : ''}${
|
||||
longestValueForField
|
||||
? longestValueForField
|
||||
: firstFoundType === 'chars'
|
||||
? '1'
|
||||
: 'best'
|
||||
}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (invalidString) {
|
||||
return 'ERROR: LARGE STRING LENGTH'
|
||||
if (sasFormats) {
|
||||
headers = headers.sort(
|
||||
(a, b) =>
|
||||
headerFields.indexOf(a.replace(/:.*/, '')) -
|
||||
headerFields.indexOf(b.replace(/:.*/, ''))
|
||||
)
|
||||
}
|
||||
|
||||
if (invalidString) return 'ERROR: LARGE STRING LENGTH'
|
||||
|
||||
csvTest = data.map((row: any) => {
|
||||
const fields = Object.keys(row).map((fieldName, index) => {
|
||||
let value
|
||||
@@ -76,6 +121,17 @@ export const convertToCSV = (data: any) => {
|
||||
// stringify with replacer converts null values to empty strings
|
||||
value = currentCell === null ? '' : currentCell
|
||||
|
||||
if (formats && formats[fieldName] === 'best.') {
|
||||
if (value && !specialMissingValueRegExp.test(value)) {
|
||||
console.log(`🤖[value]🤖`, value)
|
||||
throw new Error(
|
||||
'Special missing value can only be a single character from A to Z or _'
|
||||
)
|
||||
}
|
||||
|
||||
return `.${value.toLowerCase()}`
|
||||
}
|
||||
|
||||
// if there any present, it should have preceding (") for escaping
|
||||
value = value.replace(/"/g, `""`)
|
||||
|
||||
|
||||
@@ -7,9 +7,17 @@ export const formatDataForRequest = (data: any) => {
|
||||
const result: any = {}
|
||||
|
||||
for (const tableName in data) {
|
||||
if (
|
||||
tableName.match(/^\$.*/) &&
|
||||
Object.keys(data).includes(tableName.replace(/^\$/, ''))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
tableCounter++
|
||||
sasjsTables.push(tableName)
|
||||
const csv = convertToCSV(data[tableName])
|
||||
const csv = convertToCSV(data[tableName], data[`$${tableName}`])
|
||||
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
@@ -27,6 +35,7 @@ export const formatDataForRequest = (data: any) => {
|
||||
result[`sasjs${tableCounter}data`] = csv
|
||||
}
|
||||
}
|
||||
|
||||
result['sasjs_tables'] = sasjsTables.join(' ')
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user