Skip to content

Commit efbdc88

Browse files
sanny-ioymc9claude
authored
feat: primitive custom types (#2852)
Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 2c81cbd commit efbdc88

41 files changed

Lines changed: 1829 additions & 193 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/cli/test/prisma-schema-gen.test.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,47 @@ model User {
9191
expect(prismaSchemaText.includes('@ds.JsonB')).toBe(true);
9292
expect(prismaSchemaText.includes('@ds.ByteA')).toBe(true);
9393
});
94+
95+
it('renames primitive type defs to match the base type', async () => {
96+
const model = await loadSchema(`
97+
model User {
98+
id String @id
99+
name UserName
100+
}
101+
102+
type UserName with String {
103+
this String
104+
}
105+
`);
106+
107+
const generator = new PrismaSchemaGenerator(model);
108+
const prismaSchemaText = await generator.generate();
109+
110+
expect(prismaSchemaText.includes('name UserName')).toBe(false);
111+
expect(prismaSchemaText.includes('name String')).toBe(true);
112+
});
113+
114+
it('renames primitive type defs to match the base type when used with lists', async () => {
115+
const model = await loadSchema(`
116+
datasource db {
117+
provider = 'postgresql'
118+
url = env('DATABASE_URL')
119+
}
120+
121+
model User {
122+
id String @id
123+
name UserName[]
124+
}
125+
126+
type UserName with String {
127+
this String
128+
}
129+
`);
130+
131+
const generator = new PrismaSchemaGenerator(model);
132+
const prismaSchemaText = await generator.generate();
133+
134+
expect(prismaSchemaText.includes('name UserName[]')).toBe(false);
135+
expect(prismaSchemaText.includes('name String[]')).toBe(true);
136+
});
94137
});

‎packages/cli/test/ts-schema-gen.test.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,4 +1015,61 @@ type Profile with Strict {
10151015
},
10161016
});
10171017
});
1018+
1019+
it('supports primitive type defs', async () => {
1020+
const { schema } = await generateTsSchema(`
1021+
model User {
1022+
id Int @id
1023+
name UserName
1024+
}
1025+
1026+
type UserName with String {
1027+
this String
1028+
}
1029+
`);
1030+
1031+
expect(schema).toMatchObject({
1032+
models: {
1033+
User: {
1034+
name: 'User',
1035+
fields: {
1036+
id: {
1037+
name: 'id',
1038+
type: 'Int',
1039+
id: true,
1040+
attributes: [
1041+
{
1042+
name: '@id',
1043+
},
1044+
],
1045+
},
1046+
name: {
1047+
name: 'name',
1048+
type: 'UserName',
1049+
},
1050+
},
1051+
idFields: ['id'],
1052+
uniqueFields: {
1053+
id: {
1054+
type: 'Int',
1055+
},
1056+
},
1057+
},
1058+
},
1059+
typeDefs: {
1060+
UserName: {
1061+
name: 'UserName',
1062+
base: 'String',
1063+
fields: {
1064+
this: {
1065+
name: 'this',
1066+
type: 'String',
1067+
},
1068+
},
1069+
},
1070+
},
1071+
authType: 'User',
1072+
plugins: {},
1073+
});
1074+
});
10181075
});

‎packages/language/src/generated/ast.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ export interface DataModel extends langium.AstNode {
435435
readonly $container: Model;
436436
readonly $type: 'DataModel';
437437
attributes: Array<DataModelAttribute>;
438+
base?: BuiltinType;
438439
baseModel?: langium.Reference<DataModel>;
439440
comments: Array<string>;
440441
fields: Array<DataField>;
@@ -448,6 +449,7 @@ export interface DataModel extends langium.AstNode {
448449
export const DataModel = {
449450
$type: 'DataModel',
450451
attributes: 'attributes',
452+
base: 'base',
451453
baseModel: 'baseModel',
452454
comments: 'comments',
453455
fields: 'fields',
@@ -912,10 +914,10 @@ export function isReferenceTarget(item: unknown): item is ReferenceTarget {
912914
return reflection.isInstance(item, ReferenceTarget.$type);
913915
}
914916

915-
export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'type' | 'view' | string;
917+
export type RegularID = 'abstract' | 'attribute' | 'datasource' | 'enum' | 'import' | 'in' | 'model' | 'plugin' | 'this' | 'type' | 'view' | string;
916918

917919
export function isRegularID(item: unknown): item is RegularID {
918-
return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || item === 'type' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item)));
920+
return item === 'model' || item === 'enum' || item === 'attribute' || item === 'datasource' || item === 'plugin' || item === 'abstract' || item === 'in' || item === 'view' || item === 'import' || item === 'type' || item === 'this' || (typeof item === 'string' && (/[_a-zA-Z][\w_]*/.test(item)));
919921
}
920922

921923
export type RegularIDWithTypeNames = 'Any' | 'BigInt' | 'Boolean' | 'Bytes' | 'DateTime' | 'Decimal' | 'Float' | 'Int' | 'Json' | 'Null' | 'Object' | 'String' | 'Unsupported' | 'Void' | RegularID;
@@ -968,6 +970,7 @@ export interface TypeDef extends langium.AstNode {
968970
readonly $container: Model;
969971
readonly $type: 'TypeDef';
970972
attributes: Array<DataModelAttribute>;
973+
base?: BuiltinType;
971974
comments: Array<string>;
972975
fields: Array<DataField>;
973976
mixins: Array<langium.Reference<TypeDef>>;
@@ -977,6 +980,7 @@ export interface TypeDef extends langium.AstNode {
977980
export const TypeDef = {
978981
$type: 'TypeDef',
979982
attributes: 'attributes',
983+
base: 'base',
980984
comments: 'comments',
981985
fields: 'fields',
982986
mixins: 'mixins',
@@ -1354,6 +1358,9 @@ export class ZModelAstReflection extends langium.AbstractAstReflection {
13541358
name: DataModel.attributes,
13551359
defaultValue: []
13561360
},
1361+
base: {
1362+
name: DataModel.base
1363+
},
13571364
baseModel: {
13581365
name: DataModel.baseModel,
13591366
referenceType: DataModel.$type
@@ -1765,6 +1772,9 @@ export class ZModelAstReflection extends langium.AbstractAstReflection {
17651772
name: TypeDef.attributes,
17661773
defaultValue: []
17671774
},
1775+
base: {
1776+
name: TypeDef.base
1777+
},
17681778
comments: {
17691779
name: TypeDef.comments,
17701780
defaultValue: []

‎packages/language/src/generated/grammar.ts‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,17 +2013,34 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
20132013
"value": "with"
20142014
},
20152015
{
2016-
"$type": "Assignment",
2017-
"feature": "mixins",
2018-
"operator": "+=",
2019-
"terminal": {
2020-
"$type": "CrossReference",
2021-
"type": {
2022-
"$ref": "#/rules@44"
2016+
"$type": "Alternatives",
2017+
"elements": [
2018+
{
2019+
"$type": "Assignment",
2020+
"feature": "mixins",
2021+
"operator": "+=",
2022+
"terminal": {
2023+
"$type": "CrossReference",
2024+
"type": {
2025+
"$ref": "#/rules@44"
2026+
},
2027+
"deprecatedSyntax": false,
2028+
"isMulti": false
2029+
}
20232030
},
2024-
"deprecatedSyntax": false,
2025-
"isMulti": false
2026-
}
2031+
{
2032+
"$type": "Assignment",
2033+
"feature": "base",
2034+
"operator": "=",
2035+
"terminal": {
2036+
"$type": "RuleCall",
2037+
"rule": {
2038+
"$ref": "#/rules@64"
2039+
},
2040+
"arguments": []
2041+
}
2042+
}
2043+
]
20272044
},
20282045
{
20292046
"$type": "Group",
@@ -3087,6 +3104,10 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel
30873104
{
30883105
"$type": "Keyword",
30893106
"value": "type"
3107+
},
3108+
{
3109+
"$type": "Keyword",
3110+
"value": "this"
30903111
}
30913112
]
30923113
},

‎packages/language/src/utils.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,13 @@ export function isLiteAttribute(node: AstNode): node is Attribute {
249249
return isAttribute(node) && hasAttribute(node, '@@@lite');
250250
}
251251

252+
/**
253+
* Returns if the given node is primitive type def.
254+
*/
255+
export function isPrimitiveTypeDef(node: AstNode): node is TypeDef {
256+
return isTypeDef(node) && !!node.base;
257+
}
258+
252259
/**
253260
* Returns the datasource provider literal (e.g. `'postgresql'`) declared in the schema, or undefined
254261
* if no datasource is found or its provider is not a literal.
@@ -737,6 +744,10 @@ export function getAllFields(
737744
return fields;
738745
}
739746

747+
export function getPrimitiveTypeDefThisField(td: TypeDef) {
748+
return td.fields.find((f) => f.name === 'this');
749+
}
750+
740751
/**
741752
* Gets all attributes of a data model or type def, including inherited attributes
742753
* from base models and mixins.

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isModel,
2323
isReferenceExpr,
2424
isStringLiteral,
25+
isThisExpr,
2526
isTypeDef,
2627
} from '../generated/ast';
2728
import {
@@ -30,6 +31,7 @@ import {
3031
getContainingDataModel,
3132
getDataSourceProvider,
3233
getNumberLiteral,
34+
getPrimitiveTypeDefThisField,
3335
getStringLiteral,
3436
hasAttribute,
3537
isAuthOrAuthMemberAccess,
@@ -40,6 +42,7 @@ import {
4042
isDelegateModel,
4143
isManyToManyField,
4244
isNativeTypeMappingAttribute,
45+
isPrimitiveTypeDef,
4346
isRelationshipField,
4447
mapBuiltinTypeToExpressionType,
4548
resolved,
@@ -142,6 +145,14 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
142145
}
143146

144147
const targetDecl = attr.$container;
148+
if (
149+
(isPrimitiveTypeDef(targetDecl) || isPrimitiveTypeDef(targetDecl.$container)) &&
150+
!hasAttribute(attr.decl.ref!, '@@@validation')
151+
) {
152+
accept('error', `attribute "${decl.name}" cannot be used with primitive type defs`, { node: attr });
153+
return;
154+
}
155+
145156
if (decl.name === '@@@targetField' && !isAttribute(targetDecl)) {
146157
accept('error', `attribute "${decl.name}" can only be used on attribute declarations`, { node: attr });
147158
return;
@@ -381,13 +392,24 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
381392
@check('@@validate')
382393
private _checkValidate(attr: AttributeApplication, accept: ValidationAcceptor) {
383394
const condition = attr.args[0]?.value;
384-
if (
385-
condition &&
386-
AstUtils.streamAst(condition).some(
387-
(node) => isDataFieldReference(node) && isDataModel(node.$resolvedType?.decl),
388-
)
389-
) {
390-
accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition });
395+
if (condition) {
396+
if (
397+
AstUtils.streamAst(condition).some(
398+
(node) => isDataFieldReference(node) && isDataModel(node.$resolvedType?.decl),
399+
)
400+
) {
401+
accept('error', `\`@@validate\` condition cannot use relation fields`, { node: condition });
402+
return;
403+
}
404+
405+
if (isPrimitiveTypeDef(attr.$container) && AstUtils.streamAst(condition).some((node) => isThisExpr(node))) {
406+
const thisField = getPrimitiveTypeDefThisField(attr.$container);
407+
if (!thisField) {
408+
accept('error', 'primitive type def cannot reference `this` field without declaring it', {
409+
node: condition,
410+
});
411+
}
412+
}
391413
}
392414
}
393415

@@ -664,10 +686,16 @@ function assignableToAttributeParam(
664686
// attribute parameter type is ContextType, need to infer type from
665687
// the attribute's container
666688
if (isDataField(attr.$container)) {
667-
if (!attr.$container?.type?.type) {
668-
return genericError;
689+
if (attr.$container.type.reference?.ref && isPrimitiveTypeDef(attr.$container.type.reference.ref)) {
690+
dstType = mapBuiltinTypeToExpressionType(attr.$container.type.reference.ref.base!);
691+
} else {
692+
if (!attr.$container?.type?.type) {
693+
return genericError;
694+
}
695+
696+
dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type);
669697
}
670-
dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type);
698+
671699
dstIsArray = attr.$container.type.array;
672700
} else {
673701
dstType = 'Any';
@@ -708,34 +736,35 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataField) {
708736
.filter((name): name is string => !!name);
709737

710738
let allowed = false;
739+
const targetDeclType = targetDecl.$resolvedType?.decl ?? targetDecl.type.type;
711740
for (const allowedType of fieldTypes) {
712741
switch (allowedType) {
713742
case 'StringField':
714-
allowed = allowed || targetDecl.type.type === 'String';
743+
allowed = allowed || targetDeclType === 'String';
715744
break;
716745
case 'IntField':
717-
allowed = allowed || targetDecl.type.type === 'Int';
746+
allowed = allowed || targetDeclType === 'Int';
718747
break;
719748
case 'BigIntField':
720-
allowed = allowed || targetDecl.type.type === 'BigInt';
749+
allowed = allowed || targetDeclType === 'BigInt';
721750
break;
722751
case 'FloatField':
723-
allowed = allowed || targetDecl.type.type === 'Float';
752+
allowed = allowed || targetDeclType === 'Float';
724753
break;
725754
case 'DecimalField':
726-
allowed = allowed || targetDecl.type.type === 'Decimal';
755+
allowed = allowed || targetDeclType === 'Decimal';
727756
break;
728757
case 'BooleanField':
729-
allowed = allowed || targetDecl.type.type === 'Boolean';
758+
allowed = allowed || targetDeclType === 'Boolean';
730759
break;
731760
case 'DateTimeField':
732-
allowed = allowed || targetDecl.type.type === 'DateTime';
761+
allowed = allowed || targetDeclType === 'DateTime';
733762
break;
734763
case 'JsonField':
735-
allowed = allowed || targetDecl.type.type === 'Json';
764+
allowed = allowed || targetDeclType === 'Json';
736765
break;
737766
case 'BytesField':
738-
allowed = allowed || targetDecl.type.type === 'Bytes';
767+
allowed = allowed || targetDeclType === 'Bytes';
739768
break;
740769
case 'ModelField':
741770
allowed = allowed || isDataModel(targetDecl.type.reference?.ref);

0 commit comments

Comments
 (0)