Skip to content

Commit f896eaf

Browse files
authored
fix: delegate count relation issue, default boolean value issue (#302)
* fix: delegate count relation issue, default boolean value issue * address pr comments
1 parent d3a5606 commit f896eaf

File tree

10 files changed

+334
-25
lines changed

10 files changed

+334
-25
lines changed

packages/language/src/validators/attribute-application-validator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {
2323
getAllAttributes,
2424
getStringLiteral,
25+
hasAttribute,
2526
isAuthOrAuthMemberAccess,
2627
isBeforeInvocation,
2728
isCollectionPredicate,
@@ -364,6 +365,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
364365
if (dstType === 'ContextType') {
365366
// ContextType is inferred from the attribute's container's type
366367
if (isDataField(attr.$container)) {
368+
// If the field is Typed JSON, and the attribute is @default, the argument must be a string
369+
const dstIsTypedJson = hasAttribute(attr.$container, '@json');
370+
if (dstIsTypedJson && attr.decl.ref?.name === '@default') {
371+
return argResolvedType.decl === 'String';
372+
}
367373
dstIsArray = attr.$container.type.array;
368374
}
369375
}

packages/runtime/src/client/crud/dialects/base-dialect.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -991,15 +991,14 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
991991

992992
for (const [field, value] of Object.entries(selections.select)) {
993993
const fieldDef = requireField(this.schema, model, field);
994-
const fieldModel = fieldDef.type;
994+
const fieldModel = fieldDef.type as GetModels<Schema>;
995995
let fieldCountQuery: SelectQueryBuilder<any, any, any>;
996996

997997
// join conditions
998998
const m2m = getManyToManyRelation(this.schema, model, field);
999999
if (m2m) {
10001000
// many-to-many relation, count the join table
1001-
fieldCountQuery = eb
1002-
.selectFrom(fieldModel)
1001+
fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false)
10031002
.innerJoin(m2m.joinTable, (join) =>
10041003
join
10051004
.onRef(`${m2m.joinTable}.${m2m.otherFkName}`, '=', `${fieldModel}.${m2m.otherPKName}`)
@@ -1008,7 +1007,9 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10081007
.select(eb.fn.countAll().as(`_count$${field}`));
10091008
} else {
10101009
// build a nested query to count the number of records in the relation
1011-
fieldCountQuery = eb.selectFrom(fieldModel).select(eb.fn.countAll().as(`_count$${field}`));
1010+
fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false).select(
1011+
eb.fn.countAll().as(`_count$${field}`),
1012+
);
10121013

10131014
// join conditions
10141015
const joinPairs = buildJoinPairs(this.schema, model, parentAlias, field, fieldModel);
@@ -1017,18 +1018,6 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
10171018
}
10181019
}
10191020

1020-
// merge _count filter
1021-
if (
1022-
value &&
1023-
typeof value === 'object' &&
1024-
'where' in value &&
1025-
value.where &&
1026-
typeof value.where === 'object'
1027-
) {
1028-
const filter = this.buildFilter(fieldModel, fieldModel, value.where);
1029-
fieldCountQuery = fieldCountQuery.where(filter);
1030-
}
1031-
10321021
jsonObject[field] = fieldCountQuery;
10331022
}
10341023

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -822,16 +822,20 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
822822
continue;
823823
}
824824
if (!(field in data)) {
825-
if (typeof fields[field]?.default === 'object' && 'kind' in fields[field].default) {
826-
const generated = this.evalGenerator(fields[field].default);
825+
if (typeof fieldDef?.default === 'object' && 'kind' in fieldDef.default) {
826+
const generated = this.evalGenerator(fieldDef.default);
827827
if (generated !== undefined) {
828-
values[field] = generated;
828+
values[field] = this.dialect.transformPrimitive(
829+
generated,
830+
fieldDef.type as BuiltinType,
831+
!!fieldDef.array,
832+
);
829833
}
830-
} else if (fields[field]?.updatedAt) {
834+
} else if (fieldDef?.updatedAt) {
831835
// TODO: should this work at kysely level instead?
832836
values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
833-
} else if (fields[field]?.default !== undefined) {
834-
let value = fields[field].default;
837+
} else if (fieldDef?.default !== undefined) {
838+
let value = fieldDef.default;
835839
if (fieldDef.type === 'Json') {
836840
// Schema uses JSON string for default value of Json fields
837841
if (fieldDef.array && Array.isArray(value)) {
@@ -842,8 +846,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
842846
}
843847
values[field] = this.dialect.transformPrimitive(
844848
value,
845-
fields[field].type as BuiltinType,
846-
!!fields[field].array,
849+
fieldDef.type as BuiltinType,
850+
!!fieldDef.array,
847851
);
848852
}
849853
}

packages/runtime/src/client/helpers/schema-db-pusher.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,21 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
162162
if (fieldDef.unique) {
163163
continue;
164164
}
165+
if (fieldDef.originModel && fieldDef.originModel !== modelDef.name) {
166+
// field is inherited from a base model, skip
167+
continue;
168+
}
165169
table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, [this.getColumnName(fieldDef)]);
166170
} else {
167-
// multi-field constraint
171+
// multi-field constraint, if any field is inherited from base model, skip
172+
if (
173+
Object.keys(value).some((f) => {
174+
const fDef = modelDef.fields[f]!;
175+
return fDef.originModel && fDef.originModel !== modelDef.name;
176+
})
177+
) {
178+
continue;
179+
}
168180
table = table.addUniqueConstraint(
169181
`unique_${modelDef.name}_${key}`,
170182
Object.keys(value).map((f) => this.getColumnName(modelDef.fields[f]!)),
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 2028', async () => {
5+
const db = await createTestClient(
6+
`
7+
enum FooType {
8+
Bar
9+
Baz
10+
}
11+
12+
model User {
13+
id String @id @default(cuid())
14+
userFolders UserFolder[]
15+
@@allow('all', true)
16+
}
17+
18+
model Foo {
19+
id String @id @default(cuid())
20+
type FooType
21+
22+
userFolders UserFolder[]
23+
24+
@@delegate(type)
25+
@@allow('all', true)
26+
}
27+
28+
model Bar extends Foo {
29+
name String
30+
}
31+
32+
model Baz extends Foo {
33+
age Int
34+
}
35+
36+
model UserFolder {
37+
id String @id @default(cuid())
38+
userId String
39+
fooId String
40+
41+
user User @relation(fields: [userId], references: [id])
42+
foo Foo @relation(fields: [fooId], references: [id])
43+
44+
@@unique([userId, fooId])
45+
@@allow('all', true)
46+
}
47+
`,
48+
);
49+
50+
// Ensure we can query by the CompoundUniqueInput
51+
const user = await db.user.create({ data: {} });
52+
const bar = await db.bar.create({ data: { name: 'bar' } });
53+
const baz = await db.baz.create({ data: { age: 1 } });
54+
55+
const userFolderA = await db.userFolder.create({
56+
data: {
57+
userId: user.id,
58+
fooId: bar.id,
59+
},
60+
});
61+
62+
const userFolderB = await db.userFolder.create({
63+
data: {
64+
userId: user.id,
65+
fooId: baz.id,
66+
},
67+
});
68+
69+
await expect(
70+
db.userFolder.findUnique({
71+
where: {
72+
userId_fooId: {
73+
userId: user.id,
74+
fooId: bar.id,
75+
},
76+
},
77+
}),
78+
).resolves.toMatchObject(userFolderA);
79+
80+
await expect(
81+
db.userFolder.findUnique({
82+
where: {
83+
userId_fooId: {
84+
userId: user.id,
85+
fooId: baz.id,
86+
},
87+
},
88+
}),
89+
).resolves.toMatchObject(userFolderB);
90+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 2038', async () => {
5+
const db = await createTestClient(
6+
`
7+
model User {
8+
id Int @id @default(autoincrement())
9+
flag Boolean
10+
@@allow('all', true)
11+
}
12+
13+
model Post {
14+
id Int @id @default(autoincrement())
15+
published Boolean @default(auth().flag)
16+
@@allow('all', true)
17+
}
18+
`,
19+
);
20+
21+
const authDb = db.$setAuth({ id: 1, flag: true });
22+
await expect(authDb.post.create({ data: {} })).resolves.toMatchObject({
23+
published: true,
24+
});
25+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 2039', async () => {
5+
const db = await createTestClient(
6+
`
7+
type Foo {
8+
a String
9+
}
10+
11+
model Bar {
12+
id String @id @default(cuid())
13+
foo Foo @json @default("{ \\"a\\": \\"a\\" }")
14+
fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]")
15+
@@allow('all', true)
16+
}
17+
`,
18+
{ provider: 'postgresql' },
19+
);
20+
21+
// Ensure default values are correctly set
22+
await expect(db.bar.create({ data: {} })).resolves.toMatchObject({
23+
id: expect.any(String),
24+
foo: { a: 'a' },
25+
fooList: [{ a: 'b' }],
26+
});
27+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 2106', async () => {
5+
const db = await createTestClient(
6+
`
7+
model User {
8+
id Int @id
9+
age BigInt
10+
@@allow('all', true)
11+
}
12+
`,
13+
);
14+
15+
await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy();
16+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 2246', async () => {
5+
const db = await createTestClient(
6+
`
7+
model Media {
8+
id Int @id @default(autoincrement())
9+
title String
10+
mediaType String
11+
12+
@@delegate(mediaType)
13+
@@allow('all', true)
14+
}
15+
16+
model Movie extends Media {
17+
director Director @relation(fields: [directorId], references: [id])
18+
directorId Int
19+
duration Int
20+
rating String
21+
}
22+
23+
model Director {
24+
id Int @id @default(autoincrement())
25+
name String
26+
email String
27+
movies Movie[]
28+
29+
@@allow('all', true)
30+
}
31+
`,
32+
);
33+
34+
await db.director.create({
35+
data: {
36+
name: 'Christopher Nolan',
37+
38+
movies: {
39+
create: {
40+
title: 'Inception',
41+
duration: 148,
42+
rating: 'PG-13',
43+
},
44+
},
45+
},
46+
});
47+
48+
await expect(
49+
db.director.findMany({
50+
include: {
51+
movies: {
52+
where: { title: 'Inception' },
53+
},
54+
},
55+
}),
56+
).resolves.toHaveLength(1);
57+
58+
await expect(
59+
db.director.findFirst({
60+
include: {
61+
_count: { select: { movies: { where: { title: 'Inception' } } } },
62+
},
63+
}),
64+
).resolves.toMatchObject({ _count: { movies: 1 } });
65+
66+
await expect(
67+
db.movie.findMany({
68+
where: { title: 'Interstellar' },
69+
}),
70+
).resolves.toHaveLength(0);
71+
72+
await expect(
73+
db.director.findFirst({
74+
include: {
75+
_count: { select: { movies: { where: { title: 'Interstellar' } } } },
76+
},
77+
}),
78+
).resolves.toMatchObject({ _count: { movies: 0 } });
79+
});

0 commit comments

Comments
 (0)