Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"module": "ESNext",
"skipLibCheck": true
},
"include": ["/**/*.ts"],
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
40 changes: 40 additions & 0 deletions packages/next-admin/src/tests/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Decimal } from "@prisma/client/runtime/library";
import { describe, expect, it } from "vitest";
import { extractSerializable, formatLabel } from "../utils/tools";

Expand Down Expand Up @@ -34,6 +35,45 @@ describe("extractSerializable", () => {
const obj = { a: null, b: undefined };
expect(extractSerializable(obj)).toEqual({ a: null, b: null });
});
it("should convert BigInt values to strings to preserve precision", () => {
const largeBigInt = BigInt("6302764515981008896");
const obj = { id: largeBigInt, count: BigInt(123) };
expect(extractSerializable(obj)).toEqual({
id: "6302764515981008896",
count: "123",
});
});
it("should convert Prisma Decimal values to strings to preserve precision", () => {
const largeDecimal = new Decimal("6302764515981008896");
const smallDecimal = new Decimal("5.25");
const obj = { id: largeDecimal, rate: smallDecimal };
expect(extractSerializable(obj)).toEqual({
id: "6302764515981008896",
rate: "5.25",
});
});
it("should handle nested objects with BigInt and Decimal values", () => {
const obj = {
user: {
id: BigInt("9223372036854775807"),
balance: new Decimal("1234567890.123456789"),
},
items: [
{ id: BigInt(1), price: new Decimal("99.99") },
{ id: BigInt(2), price: new Decimal("149.99") },
],
};
expect(extractSerializable(obj)).toEqual({
user: {
id: "9223372036854775807",
balance: "1234567890.123456789",
},
items: [
{ id: "1", price: "99.99" },
{ id: "2", price: "149.99" },
],
});
});
});

describe("formatLabel", () => {
Expand Down
118 changes: 115 additions & 3 deletions packages/next-admin/src/utils/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import { describe, expect, it } from "vitest";
import { Decimal } from "@prisma/client/runtime/library";
import { describe, expect, it, vi } from "vitest";
import {
findRelationInData,
getParamsFromUrl,
getResourceFromParams,
getResourceIdFromParam,
transformData,
} from "./server";
import { getSchema } from "./globals";

// Mock the globals module
vi.mock("./globals", () => ({
getSchema: vi.fn(() => ({
definitions: {
User: {
properties: {
id: {
__nextadmin: {
type: "Decimal",
kind: "scalar",
primaryKey: true,
},
},
balance: {
__nextadmin: {
type: "Decimal",
kind: "scalar",
},
},
},
},
},
})),
}));

describe("Server utils", () => {
describe("getResourceFromParams", () => {
Expand All @@ -26,15 +55,17 @@ describe("Server utils", () => {

describe("getResourceIdFromParam", () => {
it("should get the id from /admin/User/1", () => {
expect(getResourceIdFromParam("1", "User")).toEqual(1);
// With our mock schema, User has Decimal id, so formatId returns string
expect(getResourceIdFromParam("1", "User")).toEqual("1");
});

it("should not return an id from /admin/User/new", () => {
expect(getResourceIdFromParam("new", "User")).toEqual(undefined);
});

it("should not return an id from /admin/Dummy/--__", () => {
expect(getResourceIdFromParam("--__", "User")).toEqual(undefined);
// With Decimal id type, formatId returns the string as-is
expect(getResourceIdFromParam("--__", "User")).toEqual("--__");
});
});

Expand Down Expand Up @@ -97,4 +128,85 @@ describe("Server utils", () => {
).toEqual(["User", "new"]);
});
});

describe("transformData", () => {
it("should convert large Decimal values to strings without precision loss", async () => {
const largeDecimalId = new Decimal("6302764515981008896");
const data = {
id: largeDecimalId,
balance: new Decimal("123456789012345678901234567890.50"),
};

const result = await transformData(data, "User", {}, undefined);

expect(result.id).toBe("6302764515981008896");
expect(result.balance).toBe("123456789012345678901234567890.5");
});

it("should handle null Decimal values", async () => {
const data = {
id: null,
balance: null,
};

const result = await transformData(data, "User", {}, undefined);

expect(result.id).toBe(null);
expect(result.balance).toBe(null);
});

it("should preserve precision for Decimal values that exceed Number.MAX_SAFE_INTEGER", async () => {
// Number.MAX_SAFE_INTEGER is 9007199254740991
const exceedsSafeInteger = new Decimal("9007199254740992");
const data = {
id: exceedsSafeInteger,
};

const result = await transformData(data, "User", {}, undefined);

// If converted to Number, this value would be correctly represented in this case
// but we still want to ensure we're using string representation
expect(result.id).toBe("9007199254740992");
expect(typeof result.id).toBe("string");
});
});

describe("findRelationInData", () => {
it("should convert Decimal values to strings in list data without precision loss", () => {
const schema = getSchema().definitions.User;
const largeDecimalId = new Decimal("6302764515981008896");
const data = [
{
id: largeDecimalId,
balance: new Decimal("999999999999999999.99"),
},
];

const result = findRelationInData(data, schema);

expect(result[0].id).toBe("6302764515981008896");
expect(result[0].balance).toBe("999999999999999999.99");
});

it("should handle multiple items with large Decimal values", () => {
const schema = getSchema().definitions.User;
const data = [
{
id: new Decimal("6302764515981008896"),
balance: new Decimal("100.50"),
},
{
id: new Decimal("6302764515981008897"),
balance: new Decimal("200.75"),
},
];

const result = findRelationInData(data, schema);

expect(result[0].id).toBe("6302764515981008896");
expect(result[0].balance).toBe("100.5");
expect(result[1].id).toBe("6302764515981008897");
expect(result[1].balance).toBe("200.75");
});
});
});
4 changes: 2 additions & 2 deletions packages/next-admin/src/utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ export const transformData = <M extends ModelName>(
} else if (fieldTypes === "Json") {
acc[key] = data[key] ? JSON.stringify(data[key]) : null;
} else if (fieldTypes === "Decimal") {
acc[key] = data[key] ? Number(data[key]) : null;
acc[key] = data[key] ? data[key].toFixed() : null;
} else if (fieldTypes === "BigInt") {
acc[key] = data[key] ? BigInt(data[key]).toString() : null;
} else {
Expand Down Expand Up @@ -531,7 +531,7 @@ export const findRelationInData = (
value: item[property].toISOString(),
};
} else if (propertyType === "Decimal") {
item[property] = Number(item[property]);
item[property] = item[property].toFixed();
} else if (propertyType === "BigInt") {
item[property] = BigInt(item[property]).toString();
}
Expand Down
12 changes: 12 additions & 0 deletions packages/next-admin/src/utils/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,19 @@ export const extractSerializable = <T>(obj: T, isAppDir?: boolean): T => {
return obj.toISOString() as unknown as T;
} else if (obj === null) {
return obj;
} else if (typeof obj === "bigint") {
return obj.toString() as unknown as T;
} else if (typeof obj === "object") {
// Handle Prisma Decimal objects BEFORE React elements check
// Decimal objects have properties 'd', 's', 'e' and a toFixed method
if (
"d" in obj &&
"s" in obj &&
"e" in obj &&
typeof (obj as any).toFixed === "function"
) {
return (obj as any).toFixed() as unknown as T;
}
if (isAppDir && React.isValidElement(obj)) {
return null as unknown as T;
}
Expand Down