diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 1d75f3488c..5d19bc4e92 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -38,7 +38,9 @@ jobs: SESSION_SECRET: "secret" MAGIC_LINK_SECRET: "secret" ENCRYPTION_KEY: "secret" - + + - name: 🧪 Run Package Unit Tests + run: pnpm run test --filter "@trigger.dev/*" - name: 🧪 Run Internal Unit Tests run: pnpm run test --filter "@internal/*" diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 71fd691ecb..7ef0a20d12 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -95,36 +95,47 @@ export function unflattenAttributes( const result: Record = {}; for (const [key, value] of Object.entries(obj)) { - const parts = key.split(".").reduce((acc, part) => { - if (part.includes("[")) { - // Handling nested array indices - const subparts = part.split(/\[|\]/).filter((p) => p !== ""); - acc.push(...subparts); - } else { - acc.push(part); - } - return acc; - }, [] as string[]); + const parts = key.split(".").reduce( + (acc, part) => { + if (part.startsWith("[") && part.endsWith("]")) { + // Handle array indices more precisely + const match = part.match(/^\[(\d+)\]$/); + if (match && match[1]) { + acc.push(parseInt(match[1])); + } else { + // Remove brackets for non-numeric array keys + acc.push(part.slice(1, -1)); + } + } else { + acc.push(part); + } + return acc; + }, + [] as (string | number)[] + ); let current: any = result; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; + const nextPart = parts[i + 1]; - if (!part) { + if (!part && part !== 0) { continue; } - const nextPart = parts[i + 1]; - const isArray = nextPart && /^\d+$/.test(nextPart); - if (isArray && !Array.isArray(current[part])) { - current[part] = []; - } else if (!isArray && current[part] === undefined) { + if (typeof nextPart === "number") { + // Ensure we create an array for numeric indices + current[part] = Array.isArray(current[part]) ? current[part] : []; + } else if (current[part] === undefined) { + // Create an object for non-numeric paths current[part] = {}; } + current = current[part]; } + const lastPart = parts[parts.length - 1]; - if (lastPart) { + if (lastPart !== undefined) { current[lastPart] = rehydrateNull(value); } } diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 4758f174bf..139d2311e3 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -1,6 +1,20 @@ import { flattenAttributes, unflattenAttributes } from "../src/v3/utils/flattenAttributes.js"; describe("flattenAttributes", () => { + it("handles number keys correctl", () => { + expect(flattenAttributes({ bar: { "25": "foo" } })).toEqual({ "bar.25": "foo" }); + expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { "25": "foo" } }); + expect(flattenAttributes({ bar: ["foo", "baz"] })).toEqual({ + "bar.[0]": "foo", + "bar.[1]": "baz", + }); + expect(unflattenAttributes({ "bar.[0]": "foo", "bar.[1]": "baz" })).toEqual({ + bar: ["foo", "baz"], + }); + expect(flattenAttributes({ bar: { 25: "foo" } })).toEqual({ "bar.25": "foo" }); + expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { 25: "foo" } }); + }); + it("handles null correctly", () => { expect(flattenAttributes(null)).toEqual({ "": "$@null((" }); expect(unflattenAttributes({ "": "$@null((" })).toEqual(null);