Skip to content

Commit 02df86a

Browse files
committed
test: migrate more migration cases, a few minor fixes
1 parent 8fbe27d commit 02df86a

28 files changed

+1308
-27
lines changed

packages/language/res/stdlib.zmodel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,7 @@ attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "
676676
* @param condition: a boolean expression that controls if the operation should be allowed.
677677
* @param override: a boolean value that controls if the field-level policy should override the model-level policy.
678678
*/
679-
attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?)
679+
// attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean, _ override: Boolean?)
680680

681681
/**
682682
* Defines an access policy that denies a set of operations when the given condition is true.
@@ -692,7 +692,7 @@ attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'
692692
* @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations.
693693
* @param condition: a boolean expression that controls if the operation should be denied.
694694
*/
695-
attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
695+
// attribute @deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean)
696696

697697
/**
698698
* Checks if the current user can perform the given operation on the given field.

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

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -939,17 +939,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
939939
combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere;
940940
}
941941

942-
// fill in automatically updated fields
943942
const modelDef = this.requireModel(model);
944943
let finalData = data;
945-
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
946-
if (fieldDef.updatedAt) {
947-
if (finalData === data) {
948-
finalData = clone(data);
949-
}
950-
finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
951-
}
952-
}
953944

954945
if (Object.keys(finalData).length === 0) {
955946
// nothing to update, return the original filter so that caller can identify the entity
@@ -1027,6 +1018,19 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
10271018
}
10281019
}
10291020

1021+
// fill in automatically updated fields
1022+
const scalarFields = Object.values(modelDef.fields)
1023+
.filter((f) => !f.relation)
1024+
.map((f) => f.name);
1025+
if (Object.keys(updateFields).some((f) => scalarFields.includes(f))) {
1026+
// if any scalar fields are being updated, also update the `updatedAt` fields
1027+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1028+
if (fieldDef.updatedAt) {
1029+
updateFields[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
1030+
}
1031+
}
1032+
}
1033+
10301034
if (Object.keys(updateFields).length === 0) {
10311035
// nothing to update, return the filter so that the caller can identify the entity
10321036
return combinedWhere;
@@ -2073,22 +2077,11 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
20732077
}
20742078
}
20752079

2076-
// Given a unique filter of a model, return the entity ids by trying to
2077-
// reused the filter if it's a complete id filter (without extra fields)
2078-
// otherwise, read the entity by the filter
2080+
// Given a unique filter of a model, load the entity and return its id fields
20792081
private getEntityIds(kysely: ToKysely<Schema>, model: GetModels<Schema>, uniqueFilter: any) {
2080-
const idFields: string[] = requireIdFields(this.schema, model);
2081-
if (
2082-
// all id fields are provided
2083-
idFields.every((f) => f in uniqueFilter && uniqueFilter[f] !== undefined) &&
2084-
// no non-id filter exists
2085-
Object.keys(uniqueFilter).every((k) => idFields.includes(k))
2086-
) {
2087-
return uniqueFilter;
2088-
}
2089-
20902082
return this.readUnique(kysely, model, {
20912083
where: uniqueFilter,
2084+
select: this.makeIdSelect(model),
20922085
});
20932086
}
20942087

packages/runtime/src/client/crud/validator/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,9 +976,14 @@ export class InputValidator<Schema extends SchemaDef> {
976976
])
977977
.optional();
978978

979+
let upsertWhere = this.makeWhereSchema(fieldType, true);
980+
if (!fieldDef.array) {
981+
// to-one relation, can upsert without where clause
982+
upsertWhere = upsertWhere.optional();
983+
}
979984
fields['upsert'] = this.orArray(
980985
z.strictObject({
981-
where: this.makeWhereSchema(fieldType, true),
986+
where: upsertWhere,
982987
create: this.makeCreateDataSchema(fieldType, false, withoutFields),
983988
update: this.makeUpdateDataSchema(fieldType, withoutFields),
984989
}),

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/e2e/orm/client-api/update.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,21 @@ describe('Client update tests', () => {
114114
).resolves.toMatchObject({ id: 'user2' });
115115
});
116116

117+
it('does not update updatedAt if no other scalar fields are updated', async () => {
118+
const user = await createUser(client, '[email protected]');
119+
const originalUpdatedAt = user.updatedAt;
120+
121+
await client.user.update({
122+
where: { id: user.id },
123+
data: {
124+
posts: { create: { title: 'Post1' } },
125+
},
126+
});
127+
128+
const updatedUser = await client.user.findUnique({ where: { id: user.id } });
129+
expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt);
130+
});
131+
117132
it('works with numeric incremental update', async () => {
118133
await createUser(client, '[email protected]', {
119134
profile: { create: { id: '1', bio: 'bio' } },

tests/regression/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
"test": "pnpm generate && tsc && vitest run"
99
},
1010
"dependencies": {
11-
"@zenstackhq/testtools": "workspace:*"
11+
"@zenstackhq/testtools": "workspace:*",
12+
"decimal.js": "^10.4.3"
1213
},
1314
"devDependencies": {
1415
"@zenstackhq/cli": "workspace:*",
15-
"@zenstackhq/sdk": "workspace:*",
1616
"@zenstackhq/language": "workspace:*",
1717
"@zenstackhq/runtime": "workspace:*",
18+
"@zenstackhq/sdk": "workspace:*",
1819
"@zenstackhq/typescript-config": "workspace:*",
1920
"@zenstackhq/vitest-config": "workspace:*"
2021
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import Decimal from 'decimal.js';
3+
import { expect, it } from 'vitest';
4+
5+
// TODO: zod support
6+
it.skip('verifies issue 657', async () => {
7+
const { zodSchemas } = await createTestClient(`
8+
model Foo {
9+
id Int @id @default(autoincrement())
10+
intNumber Int @gt(0)
11+
floatNumber Float @gt(0)
12+
decimalNumber Decimal @gt(0.1) @lte(10)
13+
}
14+
`);
15+
16+
const schema = zodSchemas.models.FooUpdateSchema;
17+
expect(schema.safeParse({ intNumber: 0 }).success).toBeFalsy();
18+
expect(schema.safeParse({ intNumber: 1 }).success).toBeTruthy();
19+
expect(schema.safeParse({ floatNumber: 0 }).success).toBeFalsy();
20+
expect(schema.safeParse({ floatNumber: 1.1 }).success).toBeTruthy();
21+
expect(schema.safeParse({ decimalNumber: 0 }).success).toBeFalsy();
22+
expect(schema.safeParse({ decimalNumber: '0' }).success).toBeFalsy();
23+
expect(schema.safeParse({ decimalNumber: new Decimal(0) }).success).toBeFalsy();
24+
expect(schema.safeParse({ decimalNumber: 11 }).success).toBeFalsy();
25+
expect(schema.safeParse({ decimalNumber: '11.123456789' }).success).toBeFalsy();
26+
expect(schema.safeParse({ decimalNumber: new Decimal('11.123456789') }).success).toBeFalsy();
27+
expect(schema.safeParse({ decimalNumber: 10 }).success).toBeTruthy();
28+
expect(schema.safeParse({ decimalNumber: '10' }).success).toBeTruthy();
29+
expect(schema.safeParse({ decimalNumber: new Decimal('10') }).success).toBeTruthy();
30+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
// TODO: field-level policy support
5+
it.skip('verifies issue 665', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id Int @id @default(autoincrement())
10+
admin Boolean @default(false)
11+
username String @unique @allow("all", auth() == this) @allow("all", auth().admin)
12+
password String @password @default("") @allow("all", auth() == this) @allow("all", auth().admin)
13+
firstName String @default("")
14+
lastName String @default("")
15+
16+
@@allow('all', true)
17+
}
18+
`,
19+
);
20+
21+
await db.$unuseAll.user.create({ data: { id: 1, username: 'test', password: 'test', admin: true } });
22+
23+
// admin
24+
let r = await db.$setAuth({ id: 1, admin: true }).user.findFirst();
25+
expect(r.username).toEqual('test');
26+
27+
// owner
28+
r = await db.$setAuth({ id: 1 }).user.findFirst();
29+
expect(r.username).toEqual('test');
30+
31+
// anonymous
32+
r = await db.$setAuth({ id: 0 }).user.findFirst();
33+
expect(r.username).toBeUndefined();
34+
35+
// non-owner
36+
r = await db.$setAuth({ id: 2 }).user.findFirst();
37+
expect(r.username).toBeUndefined();
38+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
import { it } from 'vitest';
3+
4+
it('verifies issue 674', async () => {
5+
await loadSchema(
6+
`
7+
model Foo {
8+
id Int @id
9+
}
10+
11+
enum MyUnUsedEnum { ABC CDE @@map('my_unused_enum') }
12+
`,
13+
);
14+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
it('verifies issue 689', async () => {
5+
const db = await createPolicyTestClient(
6+
`
7+
model UserRole {
8+
id Int @id @default(autoincrement())
9+
user User @relation(fields: [userId], references: [id])
10+
userId Int
11+
role String
12+
13+
@@allow('all', true)
14+
}
15+
16+
model User {
17+
id Int @id @default(autoincrement())
18+
userRole UserRole[]
19+
deleted Boolean @default(false)
20+
21+
@@allow('create,read', true)
22+
@@allow('read', auth() == this)
23+
@@allow('read', userRole?[user == auth() && 'Admin' == role])
24+
@@allow('read', userRole?[user == auth()])
25+
}
26+
`,
27+
);
28+
29+
const rawDb = db.$unuseAll();
30+
31+
await rawDb.user.create({
32+
data: {
33+
id: 1,
34+
userRole: {
35+
create: [
36+
{ id: 1, role: 'Admin' },
37+
{ id: 2, role: 'Student' },
38+
],
39+
},
40+
},
41+
});
42+
43+
await rawDb.user.create({
44+
data: {
45+
id: 2,
46+
userRole: {
47+
connect: { id: 1 },
48+
},
49+
},
50+
});
51+
52+
const c1 = await rawDb.user.count({
53+
where: {
54+
userRole: {
55+
some: { role: 'Student' },
56+
},
57+
NOT: { deleted: true },
58+
},
59+
});
60+
61+
const c2 = await db.user.count({
62+
where: {
63+
userRole: {
64+
some: { role: 'Student' },
65+
},
66+
NOT: { deleted: true },
67+
},
68+
});
69+
70+
expect(c1).toEqual(c2);
71+
});

0 commit comments

Comments
 (0)