zenstackhq / zenstack

Fullstack TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
https://zenstack.dev
MIT License
2.07k stars 88 forks source link

Polymorphic relationship breaks Decimal values with Postgres #1487

Closed sslotnick closed 4 months ago

sslotnick commented 4 months ago

Description and expected behavior

Using delegates causes the response object to get decorated differently from when using the prisma client directly. This seems to affect all models with any relationship to the polymorphic model, even if it is not being queried directly.

Test Case

import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools';
import Decimal from 'decimal.js';

describe('new-issue', () => {
    it('regression1', async () => {
        const dbUrl = await createPostgresDb('new-issue-1');
        let prisma: any;

        try {
            const r = await loadSchema(
                `
                model LineItem {
                    id Int @id @default(autoincrement())
                    price Decimal

                    orderId Int
                    order Order @relation(fields: [orderId], references: [id])
                }
                model Order {
                    id Int @id @default(autoincrement())
                    total Decimal
                    lineItems LineItem[]
                }
                `,
                {
                    provider: 'postgresql',
                    dbUrl,
                    enhancements: ['omit', 'delegate'],
                }
            );

            prisma = r.prisma;
            const db = r.enhance();

            const create = await db.Order.create({
                data: {
                    total: new Decimal(100_100.99),
                    lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] },
                },
            });

            const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } });
            expect(Decimal.isDecimal(order.total)).toBe(true);
            expect(order.total.toString()).toEqual('100100.99');
            order.lineItems.forEach((item: any) => {
                expect(Decimal.isDecimal(item.price)).toBe(true);
                expect(item.price.toString()).not.toEqual('[object Object]');
            });
        } finally {
            if (prisma) {
                await prisma.$disconnect();
            }
            await dropPostgresDb('new-issue-1');
        }
    });

    it('regression2', async () => {
        const dbUrl = await createPostgresDb('new-issue-2');
        let prisma: any;

        try {
            const r = await loadSchema(
                `
                model LineItem {
                    id Int @id @default(autoincrement())
                    price Decimal

                    orderId Int
                    order Order @relation(fields: [orderId], references: [id])
                }
                model Order extends BaseType {
                    total Decimal
                    lineItems LineItem[]
                }
                model BaseType {
                    id Int @id @default(autoincrement())
                    entityType String

                    @@delegate(entityType)
                }
                `,
                {
                    provider: 'postgresql',
                    dbUrl,
                    enhancements: ['omit', 'delegate'],
                }
            );

            prisma = r.prisma;
            const db = r.enhance();

            const create = await db.Order.create({
                data: {
                    total: new Decimal(100_100.99),
                    lineItems: { create: [{ price: 90_000.66 }, { price: 20_100.33 }] },
                },
            });

            const order = await db.Order.findFirst({ where: { id: create.id }, include: { lineItems: true } });
            expect(Decimal.isDecimal(order.total)).toBe(true); // FAILS!
            expect(order.total.toString()).toEqual('100100.99'); // FAILS!
            order.lineItems.forEach((item: any) => {
                expect(Decimal.isDecimal(item.price)).toBe(true); // FAILS!
                expect(item.price.toString()).not.toEqual('[object Object]'); // ALSO FAILS!
            });

            const lineItems = await db.LineItem.findMany();
            lineItems.forEach((item: any) => {
                expect(Decimal.isDecimal(item.price)).toBe(true); // ALSO FAILS!
                expect(item.price.toString()).not.toEqual('[object Object]'); // ALSO FAILS!
            });
        } finally {
            if (prisma) {
                await prisma.$disconnect();
            }
            await dropPostgresDb('new-issue-2');
        }
    });
});

That the Order model exhibits the behavior made sense once I narrowed this down to polymorphic models. However, seeing the issue again when querying LineItem was definitely unexpected.

Environment (please complete the following information):

Additional context Add any other context about the problem here.

ymc9 commented 4 months ago

Fixed in v2.2.0