Skip to content

Commit 721c938

Browse files
Gabrolayoussef-suhylymc9
authored
fix: don't set default value in nested writes when set through FK (#1989)
Co-authored-by: Youssef Gaber <[email protected]> Co-authored-by: ymc9 <[email protected]>
1 parent 0e379f8 commit 721c938

File tree

2 files changed

+209
-36
lines changed

2 files changed

+209
-36
lines changed

packages/runtime/src/enhancements/node/default-auth.ts

+78-36
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { ACTIONS_WITH_WRITE_PAYLOAD } from '../../constants';
55
import {
66
FieldInfo,
77
NestedWriteVisitor,
8+
NestedWriteVisitorContext,
89
PrismaWriteActionType,
910
clone,
1011
enumerate,
1112
getFields,
13+
getModelInfo,
1214
getTypeDefInfo,
1315
requireField,
1416
} from '../../cross';
@@ -61,7 +63,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
6163
private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) {
6264
const newArgs = clone(args);
6365

64-
const processCreatePayload = (model: string, data: any) => {
66+
const processCreatePayload = (model: string, data: any, context: NestedWriteVisitorContext) => {
6567
const fields = getFields(this.options.modelMeta, model);
6668
for (const fieldInfo of Object.values(fields)) {
6769
if (fieldInfo.isTypeDef) {
@@ -82,24 +84,24 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
8284
const defaultValue = this.getDefaultValue(fieldInfo);
8385
if (defaultValue !== undefined) {
8486
// set field value extracted from `auth()`
85-
this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue);
87+
this.setDefaultValueForModelData(fieldInfo, model, data, defaultValue, context);
8688
}
8789
}
8890
};
8991

9092
// visit create payload and set default value to fields using `auth()` in `@default()`
9193
const visitor = new NestedWriteVisitor(this.options.modelMeta, {
92-
create: (model, data) => {
93-
processCreatePayload(model, data);
94+
create: (model, data, context) => {
95+
processCreatePayload(model, data, context);
9496
},
9597

96-
upsert: (model, data) => {
97-
processCreatePayload(model, data.create);
98+
upsert: (model, data, context) => {
99+
processCreatePayload(model, data.create, context);
98100
},
99101

100-
createMany: (model, args) => {
102+
createMany: (model, args, context) => {
101103
for (const item of enumerate(args.data)) {
102-
processCreatePayload(model, item);
104+
processCreatePayload(model, item, context);
103105
}
104106
},
105107
});
@@ -108,42 +110,82 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
108110
return newArgs;
109111
}
110112

111-
private setDefaultValueForModelData(fieldInfo: FieldInfo, model: string, data: any, authDefaultValue: unknown) {
112-
if (fieldInfo.isForeignKey && fieldInfo.relationField && fieldInfo.relationField in data) {
113+
private setDefaultValueForModelData(
114+
fieldInfo: FieldInfo,
115+
model: string,
116+
data: any,
117+
authDefaultValue: unknown,
118+
context: NestedWriteVisitorContext
119+
) {
120+
if (fieldInfo.isForeignKey) {
121+
// if the field being inspected is a fk field, there are several cases we should not
122+
// set the default value or should not set directly
123+
113124
// if the field is a fk, and the relation field is already set, we should not override it
114-
return;
115-
}
125+
if (fieldInfo.relationField && fieldInfo.relationField in data) {
126+
return;
127+
}
116128

117-
if (fieldInfo.isForeignKey && !isUnsafeMutate(model, data, this.options.modelMeta)) {
118-
// if the field is a fk, and the create payload is not unsafe, we need to translate
119-
// the fk field setting to a `connect` of the corresponding relation field
120-
const relFieldName = fieldInfo.relationField;
121-
if (!relFieldName) {
122-
throw new Error(
123-
`Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found`
129+
if (context.field?.backLink && context.nestingPath.length > 1) {
130+
// if the fk field is in a creation context where its implied by the parent,
131+
// we should not set the default value, e.g.:
132+
//
133+
// ```
134+
// parent.create({ data: { child: { create: {} } } })
135+
// ```
136+
//
137+
// event if child's fk to parent has a default value, we should not set default
138+
// value here
139+
140+
// fetch parent model from the parent context
141+
const parentModel = getModelInfo(
142+
this.options.modelMeta,
143+
context.nestingPath[context.nestingPath.length - 2].model
124144
);
125-
}
126-
const relationField = requireField(this.options.modelMeta, model, relFieldName);
127145

128-
// construct a `{ connect: { ... } }` payload
129-
let connect = data[relationField.name]?.connect;
130-
if (!connect) {
131-
connect = {};
132-
data[relationField.name] = { connect };
146+
if (parentModel) {
147+
// get the opposite side of the relation for the current create context
148+
const oppositeRelationField = requireField(this.options.modelMeta, model, context.field.backLink);
149+
if (parentModel.name === oppositeRelationField.type) {
150+
// if the opposite side matches the parent model, it means we currently in a creation context
151+
// that implicitly sets this fk field
152+
return;
153+
}
154+
}
133155
}
134156

135-
// sets the opposite fk field to value `authDefaultValue`
136-
const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo);
137-
if (!oppositeFkFieldName) {
138-
throw new Error(
139-
`Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\``
140-
);
157+
if (!isUnsafeMutate(model, data, this.options.modelMeta)) {
158+
// if the field is a fk, and the create payload is not unsafe, we need to translate
159+
// the fk field setting to a `connect` of the corresponding relation field
160+
const relFieldName = fieldInfo.relationField;
161+
if (!relFieldName) {
162+
throw new Error(
163+
`Field \`${fieldInfo.name}\` is a foreign key field but no corresponding relation field is found`
164+
);
165+
}
166+
const relationField = requireField(this.options.modelMeta, model, relFieldName);
167+
168+
// construct a `{ connect: { ... } }` payload
169+
let connect = data[relationField.name]?.connect;
170+
if (!connect) {
171+
connect = {};
172+
data[relationField.name] = { connect };
173+
}
174+
175+
// sets the opposite fk field to value `authDefaultValue`
176+
const oppositeFkFieldName = this.getOppositeFkFieldName(relationField, fieldInfo);
177+
if (!oppositeFkFieldName) {
178+
throw new Error(
179+
`Cannot find opposite foreign key field for \`${fieldInfo.name}\` in relation field \`${relFieldName}\``
180+
);
181+
}
182+
connect[oppositeFkFieldName] = authDefaultValue;
183+
return;
141184
}
142-
connect[oppositeFkFieldName] = authDefaultValue;
143-
} else {
144-
// set default value directly
145-
data[fieldInfo.name] = authDefaultValue;
146185
}
186+
187+
// set default value directly
188+
data[fieldInfo.name] = authDefaultValue;
147189
}
148190

149191
private getOppositeFkFieldName(relationField: FieldInfo, fieldInfo: FieldInfo) {
+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 1997', () => {
4+
it('regression', async () => {
5+
const { prisma, enhance } = await loadSchema(
6+
`
7+
model Tenant {
8+
id String @id @default(uuid())
9+
10+
users User[]
11+
posts Post[]
12+
comments Comment[]
13+
postUserLikes PostUserLikes[]
14+
}
15+
16+
model User {
17+
id String @id @default(uuid())
18+
tenantId String @default(auth().tenantId)
19+
tenant Tenant @relation(fields: [tenantId], references: [id])
20+
posts Post[]
21+
likes PostUserLikes[]
22+
23+
@@allow('all', true)
24+
}
25+
26+
model Post {
27+
tenantId String @default(auth().tenantId)
28+
tenant Tenant @relation(fields: [tenantId], references: [id])
29+
id String @default(uuid())
30+
author User @relation(fields: [authorId], references: [id])
31+
authorId String @default(auth().id)
32+
33+
comments Comment[]
34+
likes PostUserLikes[]
35+
36+
@@id([tenantId, id])
37+
38+
@@allow('all', true)
39+
}
40+
41+
model PostUserLikes {
42+
tenantId String @default(auth().tenantId)
43+
tenant Tenant @relation(fields: [tenantId], references: [id])
44+
id String @default(uuid())
45+
46+
userId String
47+
user User @relation(fields: [userId], references: [id])
48+
49+
postId String
50+
post Post @relation(fields: [tenantId, postId], references: [tenantId, id])
51+
52+
@@id([tenantId, id])
53+
@@unique([tenantId, userId, postId])
54+
55+
@@allow('all', true)
56+
}
57+
58+
model Comment {
59+
tenantId String @default(auth().tenantId)
60+
tenant Tenant @relation(fields: [tenantId], references: [id])
61+
id String @default(uuid())
62+
postId String
63+
post Post @relation(fields: [tenantId, postId], references: [tenantId, id])
64+
65+
@@id([tenantId, id])
66+
67+
@@allow('all', true)
68+
}
69+
`,
70+
{ logPrismaQuery: true }
71+
);
72+
73+
const tenant = await prisma.tenant.create({
74+
data: {},
75+
});
76+
const user = await prisma.user.create({
77+
data: { tenantId: tenant.id },
78+
});
79+
80+
const db = enhance({ id: user.id, tenantId: tenant.id });
81+
82+
await expect(
83+
db.post.create({
84+
data: {
85+
likes: {
86+
createMany: {
87+
data: [
88+
{
89+
userId: user.id,
90+
},
91+
],
92+
},
93+
},
94+
},
95+
include: {
96+
likes: true,
97+
},
98+
})
99+
).resolves.toMatchObject({
100+
authorId: user.id,
101+
likes: [
102+
{
103+
tenantId: tenant.id,
104+
userId: user.id,
105+
},
106+
],
107+
});
108+
109+
await expect(
110+
db.post.create({
111+
data: {
112+
comments: {
113+
createMany: {
114+
data: [{}],
115+
},
116+
},
117+
},
118+
include: {
119+
comments: true,
120+
},
121+
})
122+
).resolves.toMatchObject({
123+
authorId: user.id,
124+
comments: [
125+
{
126+
tenantId: tenant.id,
127+
},
128+
],
129+
});
130+
});
131+
});

0 commit comments

Comments
 (0)