Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import {
getAllAttributes,
getStringLiteral,
hasAttribute,
isAuthOrAuthMemberAccess,
isBeforeInvocation,
isCollectionPredicate,
Expand Down Expand Up @@ -364,6 +365,11 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
if (dstType === 'ContextType') {
// ContextType is inferred from the attribute's container's type
if (isDataField(attr.$container)) {
// If the field is Typed JSON, and the param is @default, the argument must be a string
const dstIsTypedJson = hasAttribute(attr.$container, '@json');
if (dstIsTypedJson && param.default) {
return argResolvedType.decl === 'String';
}
dstIsArray = attr.$container.type.array;
}
}
Expand Down
21 changes: 5 additions & 16 deletions packages/runtime/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -991,15 +991,14 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {

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

// join conditions
const m2m = getManyToManyRelation(this.schema, model, field);
if (m2m) {
// many-to-many relation, count the join table
fieldCountQuery = eb
.selectFrom(fieldModel)
fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false)
.innerJoin(m2m.joinTable, (join) =>
join
.onRef(`${m2m.joinTable}.${m2m.otherFkName}`, '=', `${fieldModel}.${m2m.otherPKName}`)
Expand All @@ -1008,7 +1007,9 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
.select(eb.fn.countAll().as(`_count$${field}`));
} else {
// build a nested query to count the number of records in the relation
fieldCountQuery = eb.selectFrom(fieldModel).select(eb.fn.countAll().as(`_count$${field}`));
fieldCountQuery = this.buildModelSelect(fieldModel, fieldModel, value as any, false).select(
eb.fn.countAll().as(`_count$${field}`),
);

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

// merge _count filter
if (
value &&
typeof value === 'object' &&
'where' in value &&
value.where &&
typeof value.where === 'object'
) {
const filter = this.buildFilter(fieldModel, fieldModel, value.where);
fieldCountQuery = fieldCountQuery.where(filter);
}

jsonObject[field] = fieldCountQuery;
}

Expand Down
20 changes: 12 additions & 8 deletions packages/runtime/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,16 +822,20 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
continue;
}
if (!(field in data)) {
if (typeof fields[field]?.default === 'object' && 'kind' in fields[field].default) {
const generated = this.evalGenerator(fields[field].default);
if (typeof fieldDef?.default === 'object' && 'kind' in fieldDef.default) {
const generated = this.evalGenerator(fieldDef.default);
if (generated !== undefined) {
values[field] = generated;
values[field] = this.dialect.transformPrimitive(
generated,
fieldDef.type as BuiltinType,
!!fieldDef.array,
);
}
} else if (fields[field]?.updatedAt) {
} else if (fieldDef?.updatedAt) {
// TODO: should this work at kysely level instead?
values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
} else if (fields[field]?.default !== undefined) {
let value = fields[field].default;
} else if (fieldDef?.default !== undefined) {
let value = fieldDef.default;
if (fieldDef.type === 'Json') {
// Schema uses JSON string for default value of Json fields
if (fieldDef.array && Array.isArray(value)) {
Expand All @@ -842,8 +846,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}
values[field] = this.dialect.transformPrimitive(
value,
fields[field].type as BuiltinType,
!!fields[field].array,
fieldDef.type as BuiltinType,
!!fieldDef.array,
);
}
}
Expand Down
14 changes: 13 additions & 1 deletion packages/runtime/src/client/helpers/schema-db-pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,21 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
if (fieldDef.unique) {
continue;
}
if (fieldDef.originModel && fieldDef.originModel !== modelDef.name) {
// field is inherited from a base model, skip
continue;
}
table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, [this.getColumnName(fieldDef)]);
} else {
// multi-field constraint
// multi-field constraint, if any field is inherited from base model, skip
if (
Object.keys(value).some((f) => {
const fDef = modelDef.fields[f]!;
return fDef.originModel && fDef.originModel !== modelDef.name;
})
) {
continue;
}
table = table.addUniqueConstraint(
`unique_${modelDef.name}_${key}`,
Object.keys(value).map((f) => this.getColumnName(modelDef.fields[f]!)),
Expand Down
90 changes: 90 additions & 0 deletions tests/regression/test/v2-migrated/issue-2028.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createTestClient } from '@zenstackhq/testtools';
import { expect, it } from 'vitest';

it('verifies issue 2028', async () => {
const db = await createTestClient(
`
enum FooType {
Bar
Baz
}

model User {
id String @id @default(cuid())
userFolders UserFolder[]
@@allow('all', true)
}

model Foo {
id String @id @default(cuid())
type FooType

userFolders UserFolder[]

@@delegate(type)
@@allow('all', true)
}

model Bar extends Foo {
name String
}

model Baz extends Foo {
age Int
}

model UserFolder {
id String @id @default(cuid())
userId String
fooId String

user User @relation(fields: [userId], references: [id])
foo Foo @relation(fields: [fooId], references: [id])

@@unique([userId, fooId])
@@allow('all', true)
}
`,
);

// Ensure we can query by the CompoundUniqueInput
const user = await db.user.create({ data: {} });
const bar = await db.bar.create({ data: { name: 'bar' } });
const baz = await db.baz.create({ data: { age: 1 } });

const userFolderA = await db.userFolder.create({
data: {
userId: user.id,
fooId: bar.id,
},
});

const userFolderB = await db.userFolder.create({
data: {
userId: user.id,
fooId: baz.id,
},
});

await expect(
db.userFolder.findUnique({
where: {
userId_fooId: {
userId: user.id,
fooId: bar.id,
},
},
}),
).resolves.toMatchObject(userFolderA);

await expect(
db.userFolder.findUnique({
where: {
userId_fooId: {
userId: user.id,
fooId: baz.id,
},
},
}),
).resolves.toMatchObject(userFolderB);
});
25 changes: 25 additions & 0 deletions tests/regression/test/v2-migrated/issue-2038.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createTestClient } from '@zenstackhq/testtools';
import { expect, it } from 'vitest';

it('verifies issue 2038', async () => {
const db = await createTestClient(
`
model User {
id Int @id @default(autoincrement())
flag Boolean
@@allow('all', true)
}

model Post {
id Int @id @default(autoincrement())
published Boolean @default(auth().flag)
@@allow('all', true)
}
`,
);

const authDb = db.$setAuth({ id: 1, flag: true });
await expect(authDb.post.create({ data: {} })).resolves.toMatchObject({
published: true,
});
});
27 changes: 27 additions & 0 deletions tests/regression/test/v2-migrated/issue-2039.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createTestClient } from '@zenstackhq/testtools';
import { expect, it } from 'vitest';

it('verifies issue 2039', async () => {
const db = await createTestClient(
`
type Foo {
a String
}

model Bar {
id String @id @default(cuid())
foo Foo @json @default("{ \\"a\\": \\"a\\" }")
fooList Foo[] @json @default("[{ \\"a\\": \\"b\\" }]")
@@allow('all', true)
}
`,
{ provider: 'postgresql' },
);

// Ensure default values are correctly set
await expect(db.bar.create({ data: {} })).resolves.toMatchObject({
id: expect.any(String),
foo: { a: 'a' },
fooList: [{ a: 'b' }],
});
});
16 changes: 16 additions & 0 deletions tests/regression/test/v2-migrated/issue-2106.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createTestClient } from '@zenstackhq/testtools';
import { expect, it } from 'vitest';

it('verifies issue 2106', async () => {
const db = await createTestClient(
`
model User {
id Int @id
age BigInt
@@allow('all', true)
}
`,
);

await expect(db.user.create({ data: { id: 1, age: 1n } })).toResolveTruthy();
});
79 changes: 79 additions & 0 deletions tests/regression/test/v2-migrated/issue-2246.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createTestClient } from '@zenstackhq/testtools';
import { expect, it } from 'vitest';

it('verifies issue 2246', async () => {
const db = await createTestClient(
`
model Media {
id Int @id @default(autoincrement())
title String
mediaType String

@@delegate(mediaType)
@@allow('all', true)
}

model Movie extends Media {
director Director @relation(fields: [directorId], references: [id])
directorId Int
duration Int
rating String
}

model Director {
id Int @id @default(autoincrement())
name String
email String
movies Movie[]

@@allow('all', true)
}
`,
);

await db.director.create({
data: {
name: 'Christopher Nolan',
email: '[email protected]',
movies: {
create: {
title: 'Inception',
duration: 148,
rating: 'PG-13',
},
},
},
});

await expect(
db.director.findMany({
include: {
movies: {
where: { title: 'Inception' },
},
},
}),
).resolves.toHaveLength(1);

await expect(
db.director.findFirst({
include: {
_count: { select: { movies: { where: { title: 'Inception' } } } },
},
}),
).resolves.toMatchObject({ _count: { movies: 1 } });

await expect(
db.movie.findMany({
where: { title: 'Interstellar' },
}),
).resolves.toHaveLength(0);

await expect(
db.director.findFirst({
include: {
_count: { select: { movies: { where: { title: 'Interstellar' } } } },
},
}),
).resolves.toMatchObject({ _count: { movies: 0 } });
});
Loading