Skip to content

Commit

Permalink
chore: check if object is converted to string and stringify it (#3726)
Browse files Browse the repository at this point in the history
  • Loading branch information
BonapartePC authored Sep 13, 2024
2 parents 8dc04ed + 37b02f0 commit 19ba875
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 1 deletion.
71 changes: 71 additions & 0 deletions src/warehouse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,23 @@ function setDataFromInputAndComputeColumnTypes(
level = 0,
) {
if (!input || !isObject(input)) return;
if (
(completePrefix.endsWith('context_traits_') || completePrefix === 'group_traits_') &&
isStringLikeObject(input)
) {
if (prefix === 'context_traits_') {
appendColumnNameAndType(
utils,
eventType,
`${prefix}`,
stringLikeObjectToString(input),
output,
columnTypes,
options,
);
}
return;
}
Object.keys(input).forEach((key) => {
const isValidLegacyJSONPath = isValidLegacyJsonPathKey(
eventType,
Expand Down Expand Up @@ -272,6 +289,60 @@ function setDataFromInputAndComputeColumnTypes(
});
}

function isNonNegativeInteger(str) {
if (str.length === 0) return false;
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
if (charCode < 48 || charCode > 57) return false;
}
return true;
}

function isStringLikeObject(obj) {
if (typeof obj !== 'object' || obj === null) return false;

const keys = Object.keys(obj);
if (keys.length === 0) return false;

let minKey = Infinity;
let maxKey = -Infinity;

for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = obj[key];

if (!isNonNegativeInteger(key)) return false;
if (typeof value !== 'string' || value.length !== 1) return false;

const numKey = parseInt(key, 10);
if (numKey < minKey) minKey = numKey;
if (numKey > maxKey) maxKey = numKey;
}

for (let i = minKey; i <= maxKey; i++) {
if (!keys.includes(i.toString())) return false;
}

return (minKey === 0 || minKey === 1) && maxKey - minKey + 1 === keys.length;
}

function stringLikeObjectToString(obj) {
if (!isStringLikeObject(obj)) {
return obj; // Return the original input if it's not a valid string-like object
}

const keys = Object.keys(obj)
.map(Number)
.sort((a, b) => a - b);
let result = '';

for (let i = 0; i < keys.length; i++) {
result += obj[keys[i].toString()];
}

return result;
}

/*
* uuid_ts and loaded_at datatypes are passed from here to create appropriate columns.
* Corresponding values are inserted when loading into the warehouse
Expand Down
189 changes: 188 additions & 1 deletion test/__tests__/warehouse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1233,4 +1233,191 @@ describe("isBlank", () => {
expect(isBlank(testCase.input)).toEqual(testCase.expected);
});
}
});
});

describe("context traits", () => {
const testCases = [
{
name: "traits with string like object",
input: {"1":"f","2":"o", "3":"o"},
expectedData: "foo",
expectedMetadata: "string",
expectedColumns: ["context_traits"],
},
{
name: "traits with string like object with missing keys",
input: {"1":"a","3":"a"},
expectedData: "a",
expectedMetadata: "string",
expectedColumns: ["context_traits_1", "context_traits_3"],
},
{
name: "traits with empty object",
input: {},
expectedData: {},
expectedColumns: [],
},
{
name: "traits with empty array",
input: [],
expectedData: [],
expectedMetadata: "array",
expectedColumns: [],
},
{
name: "traits with null",
input: null,
expectedData: null,
expectedMetadata: "null",
expectedColumns: [],
},
{
name: "traits with undefined",
input: undefined,
expectedData: undefined,
expectedMetadata: "undefined",
expectedColumns: [],
groupTypeColumns: []
},
{
name: "traits with string",
input: "already a string",
expectedData: "already a string",
expectedMetadata: "string",
expectedColumns: ["context_traits"],
},
{
name: "traits with number",
input: 42,
expectedData: 42,
expectedMetadata: "int",
expectedColumns: ["context_traits"],
},
{
name: "traits with boolean",
input: true,
expectedData: true,
expectedMetadata: "boolean",
expectedColumns: ["context_traits"],
},
{
name: "traits with array",
input: ["a", "b", "cd"],
expectedData: ["a", "b", "cd"],
expectedMetadata: "string",
expectedColumns: ["context_traits"],
}
];
for (const t of testCases) {
it(`should return ${t.expectedData} for ${t.name}`, () => {
for (const e of eventTypes) {
let i = input(e);
i.message.context = {"traits": t.input};
if (i.metadata) delete i.metadata.sourceCategory;
transformers.forEach((transformer, index) => {
const received = transformer.process(i);
if(t.expectedColumns.length === 0) {
expect(Object.keys(received[0].metadata.columns).join()).not.toMatch(/context_traits/g);
expect(Object.keys(received[0].data).join()).not.toMatch(/context_traits/g);
}
for (const column of t.expectedColumns) {
expect(received[0].metadata.columns[integrationCasedString(integrations[index], column)]).toEqual(t.expectedMetadata);
expect(received[0].data[integrationCasedString(integrations[index], column)]).toEqual(t.expectedData);
}
});
}
});
}
})

describe("group traits", () => {
const testCases = [
{
name: "traits with string like object",
input: {"1":"f","2":"o", "3":"o"},
expectedData: "foo",
expectedMetadata: "string",
expectedColumns: [],
},
{
name: "traits with string like object with missing keys",
input: {"1":"a","3":"a"},
expectedData: "a",
expectedMetadata: "string",
expectedColumns: ["_1", "_3"],
},
{
name: "traits with empty object",
input: {},
expectedData: {},
expectedColumns: [],
},
{
name: "traits with empty array",
input: [],
expectedData: [],
expectedMetadata: "array",
expectedColumns: [],
},
{
name: "traits with null",
input: null,
expectedData: null,
expectedMetadata: "null",
expectedColumns: [],
},
{
name: "traits with undefined",
input: undefined,
expectedData: undefined,
expectedMetadata: "undefined",
expectedColumns: [],
},
{
name: "traits with string",
input: "already a string",
expectedData: "already a string",
expectedMetadata: "string",
expectedColumns: [],
},
{
name: "traits with number",
input: 42,
expectedData: 42,
expectedMetadata: "int",
expectedColumns: [],
},
{
name: "traits with boolean",
input: true,
expectedData: true,
expectedMetadata: "boolean",
expectedColumns: [],
},
{
name: "traits with array",
input: ["a", "b", "cd"],
expectedData: ["a", "b", "cd"],
expectedMetadata: "string",
expectedColumns: [],
}
];
for (const t of testCases){
it(`should return ${t.expectedData} for ${t.name}`, () => {
let i = input("group");
i.message.traits = t.input;
if (i.metadata) delete i.metadata.sourceCategory;
transformers.forEach((transformer, index) => {
const received = transformer.process(i);
if(t.expectedColumns.length === 0) {
expect(Object.keys(received[0].metadata.columns).join()).not.toMatch(/group_traits/g);
expect(Object.keys(received[0].data).join()).not.toMatch(/group_traits/g);
}
for (const column of t.expectedColumns) {
expect(received[0].metadata.columns[integrationCasedString(integrations[index], column)]).toEqual(t.expectedMetadata);
expect(received[0].data[integrationCasedString(integrations[index], column)]).toEqual(t.expectedData);
}
});
});
}
})

0 comments on commit 19ba875

Please sign in to comment.