|
| 1 | +import { createTestClient } from '@zenstackhq/testtools'; |
| 2 | +import { describe, expect, it } from 'vitest'; |
| 3 | + |
| 4 | +// Bug: when a model with @computed fields inherited from a mixin type (the `with` |
| 5 | +// keyword) is fetched as a nested relation via an explicit `include`, ZenStack |
| 6 | +// emits the computed field name as a raw column reference inside `jsonb_build_object` |
| 7 | +// while the inner subquery SELECT only contains the real DB columns. This causes |
| 8 | +// PostgreSQL to fail with: |
| 9 | +// "column $$tN.field_name does not exist" |
| 10 | +// |
| 11 | +// Root cause: `buildSelectField` uses `fieldDef.originModel` (the mixin type name) |
| 12 | +// as the table alias when selecting the computed field into the inner subquery. |
| 13 | +// But the inner subquery aliases the actual table under the model name, not the |
| 14 | +// mixin type name, so the correlated subquery is never emitted and `parentCode` |
| 15 | +// is absent from the subquery's SELECT list. The outer `jsonb_build_object` then |
| 16 | +// references `$$tN.parentCode` which does not exist. |
| 17 | +// |
| 18 | +// The bug does NOT occur when: |
| 19 | +// - the @computed field is declared directly on the model (not via a mixin type) |
| 20 | +// - the relation is not explicitly included |
| 21 | +// - the model is queried directly (not as a nested include) |
| 22 | + |
| 23 | +describe('Computed fields with nested include', () => { |
| 24 | + it('includes computed fields inherited from a mixin type when the model is explicitly included', async () => { |
| 25 | + const db = await createTestClient( |
| 26 | + ` |
| 27 | +type ParentRelated { |
| 28 | + parentCode String? @computed |
| 29 | +} |
| 30 | +
|
| 31 | +model Parent { |
| 32 | + id Int @id @default(autoincrement()) |
| 33 | + code String |
| 34 | + children Child[] |
| 35 | +} |
| 36 | +
|
| 37 | +model Child with ParentRelated { |
| 38 | + id Int @id @default(autoincrement()) |
| 39 | + name String |
| 40 | + parentId Int |
| 41 | + parent Parent @relation(fields: [parentId], references: [id]) |
| 42 | +} |
| 43 | + `, |
| 44 | + { |
| 45 | + provider: 'postgresql', |
| 46 | + computedFields: { |
| 47 | + Child: { |
| 48 | + // Correlated subquery — looks up Parent.code via the FK. |
| 49 | + // The computed field is inherited from the `ParentRelated` mixin, |
| 50 | + // so fieldDef.originModel === 'ParentRelated', which is the |
| 51 | + // alias incorrectly used by buildSelectField. |
| 52 | + parentCode: (eb: any) => |
| 53 | + eb |
| 54 | + .selectFrom('Parent') |
| 55 | + .select('Parent.code') |
| 56 | + .whereRef('Parent.id', '=', 'parentId') |
| 57 | + .limit(1), |
| 58 | + }, |
| 59 | + }, |
| 60 | + } as any, |
| 61 | + ); |
| 62 | + |
| 63 | + const parent = await db.parent.create({ |
| 64 | + data: { code: 'P-001', children: { create: [{ name: 'Alice' }, { name: 'Bob' }] } }, |
| 65 | + }); |
| 66 | + |
| 67 | + // Direct query on Child works fine |
| 68 | + await expect(db.child.findFirst({ where: { parentId: parent.id } })).resolves.toMatchObject({ |
| 69 | + parentCode: 'P-001', |
| 70 | + }); |
| 71 | + |
| 72 | + // Querying Parent with include: { children: true } should also work, |
| 73 | + // but currently fails with "column $$tN.parentCode does not exist" |
| 74 | + await expect( |
| 75 | + db.parent.findFirst({ |
| 76 | + where: { id: parent.id }, |
| 77 | + include: { children: true }, |
| 78 | + }), |
| 79 | + ).resolves.toMatchObject({ |
| 80 | + code: 'P-001', |
| 81 | + children: expect.arrayContaining([ |
| 82 | + expect.objectContaining({ name: 'Alice', parentCode: 'P-001' }), |
| 83 | + expect.objectContaining({ name: 'Bob', parentCode: 'P-001' }), |
| 84 | + ]), |
| 85 | + }); |
| 86 | + }); |
| 87 | +}); |
0 commit comments