Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 6bf2b51

Browse files
authored
fix(zmodel): improve attribute argument assignability check (#588)
* fix(zmodel): improve attribute argument assignability check fixes #584 * fix JSON array check * update * update ts schema generator
1 parent 9a62051 commit 6bf2b51

3 files changed

Lines changed: 255 additions & 27 deletions

File tree

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

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isLiteralExpr,
2323
isModel,
2424
isReferenceExpr,
25+
isStringLiteral,
2526
isTypeDef,
2627
} from '../generated/ast';
2728
import {
@@ -103,10 +104,9 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
103104
}
104105
}
105106

106-
if (!assignableToAttributeParam(arg, paramDecl, attr)) {
107-
accept('error', `Value is not assignable to parameter`, {
108-
node: arg,
109-
});
107+
const argAssignable = assignableToAttributeParam(arg, paramDecl, attr);
108+
if (!argAssignable.result) {
109+
accept('error', argAssignable.error, { node: arg });
110110
return;
111111
}
112112

@@ -393,10 +393,21 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
393393
}
394394
}
395395

396-
function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, attr: AttributeApplication): boolean {
396+
function assignableToAttributeParam(
397+
arg: AttributeArg,
398+
param: AttributeParam,
399+
attr: AttributeApplication,
400+
):
401+
| {
402+
result: true;
403+
}
404+
| { result: false; error: string } {
405+
const genericError = { result: false, error: 'invalid argument type' } as const;
406+
const success = { result: true } as const;
407+
397408
const argResolvedType = arg.$resolvedType;
398409
if (!argResolvedType) {
399-
return false;
410+
return { result: false, error: 'unable to resolve argument type' };
400411
}
401412

402413
let dstType = param.type.type;
@@ -405,10 +416,30 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
405416
if (dstType === 'ContextType') {
406417
// ContextType is inferred from the attribute's container's type
407418
if (isDataField(attr.$container)) {
408-
// If the field is Typed JSON, and the attribute is @default, the argument must be a string
409-
const dstIsTypedJson = hasAttribute(attr.$container, '@json');
410-
if (dstIsTypedJson && attr.decl.ref?.name === '@default') {
411-
return argResolvedType.decl === 'String';
419+
// If the field is JSON, and the attribute is @default, the argument must be a JSON string
420+
// (design inherited from Prisma)
421+
const dstIsJson = attr.$container.type.type === 'Json' || hasAttribute(attr.$container, '@json');
422+
if (dstIsJson && attr.decl.ref?.name === '@default') {
423+
if (attr.$container.type.array && attr.$container.type.type === 'Json') {
424+
// Json[] default value, must be array of JSON strings
425+
if (isArrayExpr(arg.value) && arg.value.items.every((item) => isLiteralJsonString(item))) {
426+
return success;
427+
} else {
428+
return {
429+
result: false,
430+
error: 'expected an array of JSON string literals',
431+
};
432+
}
433+
} else {
434+
if (isLiteralJsonString(arg.value)) {
435+
return success;
436+
} else {
437+
return {
438+
result: false,
439+
error: 'expected a JSON string literal',
440+
};
441+
}
442+
}
412443
}
413444
dstIsArray = attr.$container.type.array;
414445
}
@@ -417,30 +448,45 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
417448
const dstRef = param.type.reference;
418449

419450
if (dstType === 'Any' && !dstIsArray) {
420-
return true;
451+
return success;
421452
}
422453

423454
if (argResolvedType.decl === 'Any') {
424455
// arg is any type
425456
if (!argResolvedType.array) {
426457
// if it's not an array, it's assignable to any type
427-
return true;
458+
return success;
428459
} else {
429460
// otherwise it's assignable to any array type
430-
return argResolvedType.array === dstIsArray;
461+
if (argResolvedType.array === dstIsArray) {
462+
return success;
463+
} else {
464+
return {
465+
result: false,
466+
error: `expected ${dstIsArray ? 'array' : 'non-array'}`,
467+
};
468+
}
431469
}
432470
}
433471

434472
// destination is field reference or transitive field reference, check if
435473
// argument is reference or array or reference
436474
if (dstType === 'FieldReference' || dstType === 'TransitiveFieldReference') {
437475
if (dstIsArray) {
438-
return (
476+
if (
439477
isArrayExpr(arg.value) &&
440-
!arg.value.items.find((item) => !isReferenceExpr(item) || !isDataField(item.target.ref))
441-
);
478+
!arg.value.items.some((item) => !isReferenceExpr(item) || !isDataField(item.target.ref))
479+
) {
480+
return success;
481+
} else {
482+
return { result: false, error: 'expected an array of field references' };
483+
}
442484
} else {
443-
return isReferenceExpr(arg.value) && isDataField(arg.value.target.ref);
485+
if (isReferenceExpr(arg.value) && isDataField(arg.value.target.ref)) {
486+
return success;
487+
} else {
488+
return { result: false, error: 'expected a field reference' };
489+
}
444490
}
445491
}
446492

@@ -454,21 +500,30 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
454500
attrArgDeclType = resolved(attr.$container.type.reference);
455501
dstIsArray = attr.$container.type.array;
456502
}
457-
return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array;
503+
504+
if (attrArgDeclType !== argResolvedType.decl) {
505+
return genericError;
506+
}
507+
508+
if (dstIsArray !== argResolvedType.array) {
509+
return { result: false, error: `expected ${dstIsArray ? 'array' : 'non-array'}` };
510+
}
511+
512+
return success;
458513
} else if (dstType) {
459514
// scalar type
460515

461516
if (typeof argResolvedType?.decl !== 'string') {
462517
// destination type is not a reference, so argument type must be a plain expression
463-
return false;
518+
return genericError;
464519
}
465520

466521
if (dstType === 'ContextType') {
467522
// attribute parameter type is ContextType, need to infer type from
468523
// the attribute's container
469524
if (isDataField(attr.$container)) {
470525
if (!attr.$container?.type?.type) {
471-
return false;
526+
return genericError;
472527
}
473528
dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type);
474529
dstIsArray = attr.$container.type.array;
@@ -477,10 +532,18 @@ function assignableToAttributeParam(arg: AttributeArg, param: AttributeParam, at
477532
}
478533
}
479534

480-
return typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array;
535+
if (typeAssignable(dstType, argResolvedType.decl, arg.value) && dstIsArray === argResolvedType.array) {
536+
return success;
537+
} else {
538+
return genericError;
539+
}
481540
} else {
482541
// reference type
483-
return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array;
542+
if ((dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array) {
543+
return success;
544+
} else {
545+
return genericError;
546+
}
484547
}
485548
}
486549

@@ -552,3 +615,15 @@ export function validateAttributeApplication(
552615
) {
553616
new AttributeApplicationValidator().validate(attr, accept, contextDataModel);
554617
}
618+
619+
function isLiteralJsonString(value: Expression) {
620+
if (!isStringLiteral(value)) {
621+
return false;
622+
}
623+
try {
624+
JSON.parse(value.value);
625+
return true;
626+
} catch {
627+
return false;
628+
}
629+
}

packages/sdk/src/ts-schema-generator.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,12 +607,15 @@ export class TsSchemaGenerator {
607607

608608
const defaultValue = this.getFieldMappedDefault(field);
609609
if (defaultValue !== undefined) {
610-
if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
610+
if (defaultValue === null) {
611+
objectFields.push(
612+
ts.factory.createPropertyAssignment('default', this.createExpressionUtilsCall('_null')),
613+
);
614+
} else if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
611615
if ('call' in defaultValue) {
612616
objectFields.push(
613617
ts.factory.createPropertyAssignment(
614618
'default',
615-
616619
this.createExpressionUtilsCall('call', [
617620
ts.factory.createStringLiteral(defaultValue.call),
618621
...(defaultValue.args.length > 0
@@ -725,7 +728,15 @@ export class TsSchemaGenerator {
725728

726729
private getFieldMappedDefault(
727730
field: DataField,
728-
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
731+
):
732+
| string
733+
| number
734+
| boolean
735+
| unknown[]
736+
| { call: string; args: any[] }
737+
| { authMember: string[] }
738+
| null
739+
| undefined {
729740
const defaultAttr = getAttribute(field, '@default');
730741
if (!defaultAttr) {
731742
return undefined;
@@ -738,7 +749,7 @@ export class TsSchemaGenerator {
738749
private getMappedValue(
739750
expr: Expression,
740751
fieldType: DataFieldType,
741-
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined {
752+
): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | null {
742753
if (isLiteralExpr(expr)) {
743754
const lit = (expr as LiteralExpr).value;
744755
return fieldType.type === 'Boolean'
@@ -759,8 +770,10 @@ export class TsSchemaGenerator {
759770
return {
760771
authMember: this.getMemberAccessChain(expr),
761772
};
773+
} else if (isNullExpr(expr)) {
774+
return null;
762775
} else {
763-
throw new Error(`Unsupported default value type for ${expr.$type}`);
776+
throw new Error(`Unsupported expression type: ${expr.$type}`);
764777
}
765778
}
766779

0 commit comments

Comments
 (0)