Skip to content

Commit 2bccb9d

Browse files
committed
more fixes
1 parent adcbf4b commit 2bccb9d

File tree

5 files changed

+139
-10
lines changed

5 files changed

+139
-10
lines changed

packages/runtime/src/enhancements/node/delegate.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
587587
let curr = args;
588588
let base = this.getBaseModel(model);
589589
let sub = this.getModelInfo(model);
590+
const hasDelegateBase = !!base;
590591

591592
while (base) {
592593
const baseRelationName = this.makeAuxRelationName(base);
@@ -615,6 +616,50 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
615616
sub = base;
616617
base = this.getBaseModel(base.name);
617618
}
619+
620+
if (hasDelegateBase) {
621+
// A delegate base model creation is added, this can be incompatible if
622+
// the user-provided payload assigns foreign keys directly, because Prisma
623+
// doesn't permit mixed "checked" and "unchecked" fields in a payload.
624+
//
625+
// {
626+
// delegate_aux_base: { ... },
627+
// [fkField]: value // <- this is not compatible
628+
// }
629+
//
630+
// We need to convert foreign key assignments to `connect`.
631+
this.fkAssignmentToConnect(model, args);
632+
}
633+
}
634+
635+
// convert foreign key assignments to `connect` payload
636+
// e.g.: { authorId: value } -> { author: { connect: { id: value } } }
637+
private fkAssignmentToConnect(model: string, args: any) {
638+
for (const key of Object.keys(args)) {
639+
const value = args[key];
640+
if (value === undefined) {
641+
continue;
642+
}
643+
644+
const fieldInfo = this.queryUtils.getModelField(model, key);
645+
if (
646+
!fieldInfo?.inheritedFrom && // fields from delegate base are handled outside
647+
fieldInfo?.isForeignKey
648+
) {
649+
const relationInfo = this.queryUtils.getRelationForForeignKey(model, key);
650+
if (relationInfo) {
651+
// turn { [fk]: value } into { [relation]: { connect: { [id]: value } } }
652+
if (!args[relationInfo.relation.name]) {
653+
args[relationInfo.relation.name] = {};
654+
}
655+
if (!args[relationInfo.relation.name].connect) {
656+
args[relationInfo.relation.name].connect = {};
657+
}
658+
args[relationInfo.relation.name].connect[relationInfo.idField] = value;
659+
delete args[key];
660+
}
661+
}
662+
}
618663
}
619664

620665
// inject field data that belongs to base type into proper nesting structure

packages/runtime/src/enhancements/node/query-utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,25 @@ export class QueryUtils {
232232

233233
return model;
234234
}
235+
236+
/**
237+
* Gets relation info for a foreign key field.
238+
*/
239+
getRelationForForeignKey(model: string, fkField: string) {
240+
const modelInfo = getModelInfo(this.options.modelMeta, model);
241+
if (!modelInfo) {
242+
return undefined;
243+
}
244+
245+
for (const field of Object.values(modelInfo.fields)) {
246+
if (field.foreignKeyMapping) {
247+
const entry = Object.entries(field.foreignKeyMapping).find(([, v]) => v === fkField);
248+
if (entry) {
249+
return { relation: field, idField: entry[0], fkField: entry[1] };
250+
}
251+
}
252+
}
253+
254+
return undefined;
255+
}
235256
}

packages/schema/src/plugins/zod/transformer.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
2-
import { indentString, isDiscriminatorField, type PluginOptions } from '@zenstackhq/sdk';
3-
import { DataModel, Enum, isDataModel, isEnum, isTypeDef, type Model } from '@zenstackhq/sdk/ast';
2+
import {
3+
getForeignKeyFields,
4+
hasAttribute,
5+
indentString,
6+
isDiscriminatorField,
7+
type PluginOptions,
8+
} from '@zenstackhq/sdk';
9+
import { DataModel, DataModelField, Enum, isDataModel, isEnum, isTypeDef, type Model } from '@zenstackhq/sdk/ast';
410
import { checkModelHasModelRelation, findModelByName, isAggregateInputType } from '@zenstackhq/sdk/dmmf-helpers';
511
import { supportCreateMany, type DMMF as PrismaDMMF } from '@zenstackhq/sdk/prisma';
612
import path from 'path';
@@ -241,7 +247,8 @@ export default class Transformer {
241247
this.addSchemaImport(inputType.type);
242248
}
243249

244-
result.push(this.generatePrismaStringLine(field, inputType, lines.length));
250+
const contextField = contextDataModel?.fields.find((f) => f.name === field.name);
251+
result.push(this.generatePrismaStringLine(field, inputType, lines.length, contextField));
245252
}
246253
}
247254

@@ -315,7 +322,12 @@ export default class Transformer {
315322
this.schemaImports.add(upperCaseFirst(name));
316323
}
317324

318-
generatePrismaStringLine(field: PrismaDMMF.SchemaArg, inputType: PrismaDMMF.InputTypeRef, inputsLength: number) {
325+
generatePrismaStringLine(
326+
field: PrismaDMMF.SchemaArg,
327+
inputType: PrismaDMMF.InputTypeRef,
328+
inputsLength: number,
329+
contextField: DataModelField | undefined
330+
) {
319331
const isEnum = inputType.location === 'enumTypes';
320332

321333
const { isModelQueryType, modelName, queryName } = this.checkIsModelQueryType(inputType.type as string);
@@ -330,11 +342,36 @@ export default class Transformer {
330342

331343
const arr = inputType.isList ? '.array()' : '';
332344

333-
const opt = !field.isRequired ? '.optional()' : '';
345+
const optional =
346+
!field.isRequired ||
347+
// also check if the zmodel field infers the field as optional
348+
(contextField && this.isFieldOptional(contextField));
334349

335350
return inputsLength === 1
336-
? ` ${field.name}: z.lazy(() => ${schema})${arr}${opt}`
337-
: `z.lazy(() => ${schema})${arr}${opt}`;
351+
? ` ${field.name}: z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}`
352+
: `z.lazy(() => ${schema})${arr}${optional ? '.optional()' : ''}`;
353+
}
354+
355+
private isFieldOptional(dmField: DataModelField) {
356+
if (hasAttribute(dmField, '@default')) {
357+
// it's possible that ZModel field has a default but it's transformed away
358+
// when generating Prisma schema, e.g.: `@default(auth().id)`
359+
return true;
360+
}
361+
362+
if (isDataModel(dmField.type.reference?.ref)) {
363+
// if field is a relation, we need to check if the corresponding fk field has a default
364+
// {
365+
// authorId Int @default(auth().id)
366+
// author User @relation(...) // <- author should be optional
367+
// }
368+
const fkFields = getForeignKeyFields(dmField);
369+
if (fkFields.every((fkField) => hasAttribute(fkField, '@default'))) {
370+
return true;
371+
}
372+
}
373+
374+
return false;
338375
}
339376

340377
generateFieldValidators(zodStringWithMainType: string, field: PrismaDMMF.SchemaArg) {

packages/sdk/src/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,13 @@ export function getRelationField(fkField: DataModelField) {
381381
});
382382
}
383383

384+
/**
385+
* Gets the foreign key fields of the given relation field.
386+
*/
387+
export function getForeignKeyFields(relationField: DataModelField) {
388+
return getRelationKeyPairs(relationField).map((pair) => pair.foreignKey);
389+
}
390+
384391
export function resolvePath(_path: string, options: Pick<PluginOptions, 'schemaPath'>) {
385392
if (path.isAbsolute(_path)) {
386393
return _path;

tests/regression/tests/issue-1843.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { loadSchema } from '@zenstackhq/testtools';
22

33
describe('issue 1843', () => {
44
it('regression', async () => {
5-
await loadSchema(
5+
const { zodSchemas, enhance, prisma } = await loadSchema(
66
`
77
model User {
88
id String @id @default(cuid())
@@ -43,9 +43,9 @@ describe('issue 1843', () => {
4343
coauthorId String
4444
4545
@@allow('all', true)
46-
}
46+
}
4747
48-
model Post extends Content {
48+
model Post extends Content {
4949
title String
5050
5151
@@allow('all', true)
@@ -85,5 +85,24 @@ describe('issue 1843', () => {
8585
],
8686
}
8787
);
88+
89+
const user = await prisma.user.create({ data: { email: 'abc', password: '123' } });
90+
const db = enhance({ id: user.id });
91+
92+
// connect
93+
await expect(
94+
db.postWithCoauthor.create({ data: { title: 'new post', coauthor: { connect: { id: user.id } } } })
95+
).toResolveTruthy();
96+
97+
// fk setting
98+
await expect(
99+
db.postWithCoauthor.create({ data: { title: 'new post', coauthorId: user.id } })
100+
).toResolveTruthy();
101+
102+
// zod validation
103+
zodSchemas.models.PostWithCoauthorCreateSchema.parse({
104+
title: 'new post',
105+
coauthorId: '1',
106+
});
88107
});
89108
});

0 commit comments

Comments
 (0)