diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 758d5eca3..eed3c050f 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -176,7 +176,7 @@ export function isDataModel(item: unknown): item is DataModel { } export interface DataModelAttribute extends AstNode { - readonly $container: DataModel; + readonly $container: DataModel | Enum; readonly $type: 'DataModelAttribute'; args: Array decl: Reference @@ -204,7 +204,7 @@ export function isDataModelField(item: unknown): item is DataModelField { } export interface DataModelFieldAttribute extends AstNode { - readonly $container: DataModelField; + readonly $container: DataModelField | EnumField; readonly $type: 'DataModelFieldAttribute'; args: Array decl: Reference @@ -260,6 +260,8 @@ export function isDataSourceField(item: unknown): item is DataSourceField { export interface Enum extends AstNode { readonly $container: Model; readonly $type: 'Enum'; + attributes: Array + comments: Array fields: Array name: string } @@ -273,6 +275,8 @@ export function isEnum(item: unknown): item is Enum { export interface EnumField extends AstNode { readonly $container: DataModel | Enum | FunctionDecl; readonly $type: 'EnumField'; + attributes: Array + comments: Array name: string } @@ -697,10 +701,21 @@ export class ZModelAstReflection extends AbstractAstReflection { return { name: 'Enum', mandatory: [ + { name: 'attributes', type: 'array' }, + { name: 'comments', type: 'array' }, { name: 'fields', type: 'array' } ] }; } + case 'EnumField': { + return { + name: 'EnumField', + mandatory: [ + { name: 'attributes', type: 'array' }, + { name: 'comments', type: 'array' } + ] + }; + } case 'FunctionDecl': { return { name: 'FunctionDecl', diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 989c7f1da..52caebc1b 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -1603,13 +1603,22 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel ] }, { - "$type": "Assignment", - "feature": "array", - "operator": "?=", - "terminal": { - "$type": "Keyword", - "value": "[]" - }, + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "array", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "[" + } + }, + { + "$type": "Keyword", + "value": "]" + } + ], "cardinality": "?" }, { @@ -1638,11 +1647,16 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@56" + "$type": "Assignment", + "feature": "comments", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [] }, - "arguments": [], "cardinality": "*" }, { @@ -1666,16 +1680,33 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "value": "{" }, { - "$type": "Assignment", - "feature": "fields", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@30" + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "fields", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@30" + }, + "arguments": [] + } }, - "arguments": [] - }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@43" + }, + "arguments": [] + } + } + ], "cardinality": "+" }, { @@ -1698,11 +1729,16 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@56" + "$type": "Assignment", + "feature": "comments", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@56" + }, + "arguments": [] }, - "arguments": [], "cardinality": "*" }, { @@ -1716,6 +1752,19 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel }, "arguments": [] } + }, + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@42" + }, + "arguments": [] + }, + "cardinality": "*" } ] }, @@ -1937,13 +1986,22 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel ] }, { - "$type": "Assignment", - "feature": "array", - "operator": "?=", - "terminal": { - "$type": "Keyword", - "value": "[]" - }, + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "array", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "[" + } + }, + { + "$type": "Keyword", + "value": "]" + } + ], "cardinality": "?" } ] @@ -2334,13 +2392,22 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel ] }, { - "$type": "Assignment", - "feature": "array", - "operator": "?=", - "terminal": { - "$type": "Keyword", - "value": "[]" - }, + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "array", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "[" + } + }, + { + "$type": "Keyword", + "value": "]" + } + ], "cardinality": "?" }, { diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index d6feb0179..267483ec0 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -54,7 +54,7 @@ fragment ReferenceArgList: args+=ReferenceArg (',' args+=ReferenceArg)*; ReferenceArg: - name=('sort') ':' value=('Asc'| 'Desc'); + name=('sort') ':' value=('Asc' | 'Desc'); InvocationExpr: function=[FunctionDecl] '(' ArgumentList? ')'; @@ -145,14 +145,20 @@ DataModelField: name=ID type=DataModelFieldType (attributes+=DataModelFieldAttribute)*; DataModelFieldType: - (type=BuiltinType | reference=[TypeDeclaration:ID]) (array?='[]')? (optional?='?')?; + (type=BuiltinType | reference=[TypeDeclaration:ID]) (array?='[' ']')? (optional?='?')?; // enum Enum: - TRIPLE_SLASH_COMMENT* 'enum' name=ID '{' (fields+=EnumField)+ '}'; + (comments+=TRIPLE_SLASH_COMMENT)* + 'enum' name=ID '{' ( + fields+=EnumField + | attributes+=DataModelAttribute + )+ + '}'; EnumField: - TRIPLE_SLASH_COMMENT* name=ID; + (comments+=TRIPLE_SLASH_COMMENT)* + name=ID (attributes+=DataModelFieldAttribute)*; // function FunctionDecl: @@ -162,7 +168,7 @@ FunctionParam: TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType; FunctionParamType: - (type=ExpressionType | reference=[TypeDeclaration]) (array?='[]')?; + (type=ExpressionType | reference=[TypeDeclaration]) (array?='[' ']')?; QualifiedName returns string: ID ('.' ID)*; @@ -192,7 +198,7 @@ AttributeParam: // FieldReference refers to fields declared in the current model // TransitiveFieldReference refers to fields declared in the model type of the current field AttributeParamType: - (type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:ID]) (array?='[]')? (optional?='?')?; + (type=(ExpressionType | 'FieldReference' | 'TransitiveFieldReference' | 'ContextType') | reference=[TypeDeclaration:ID]) (array?='[' ']')? (optional?='?')?; type TypeDeclaration = DataModel | Enum; diff --git a/packages/schema/src/language-server/validator/datamodel-validator.ts b/packages/schema/src/language-server/validator/datamodel-validator.ts index 828e40c26..c7cc74a29 100644 --- a/packages/schema/src/language-server/validator/datamodel-validator.ts +++ b/packages/schema/src/language-server/validator/datamodel-validator.ts @@ -1,23 +1,16 @@ import { ArrayExpr, - Attribute, - AttributeParam, DataModel, - DataModelAttribute, DataModelField, - DataModelFieldAttribute, - isAttribute, isDataModel, - isDataModelField, isLiteralExpr, ReferenceExpr, } from '@zenstackhq/language/ast'; import { ValidationAcceptor } from 'langium'; -import pluralize from 'pluralize'; import { analyzePolicies } from '../../utils/ast-utils'; import { IssueCodes, SCALAR_TYPES } from '../constants'; import { AstValidator } from '../types'; -import { assignableToAttributeParam, validateDuplicatedDeclarations } from './utils'; +import { validateAttributeApplication, validateDuplicatedDeclarations } from './utils'; /** * Validates data model declarations. @@ -62,7 +55,7 @@ export default class DataModelValidator implements AstValidator { accept('error', 'Optional lists are not supported. Use either `Type[]` or `Type?`', { node: field.type }); } - field.attributes.forEach((attr) => this.validateAttributeApplication(attr, accept)); + field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); if (isDataModel(field.type.reference?.ref)) { this.validateRelationField(field, accept); @@ -71,133 +64,10 @@ export default class DataModelValidator implements AstValidator { private validateAttributes(dm: DataModel, accept: ValidationAcceptor) { dm.attributes.forEach((attr) => { - this.validateAttributeApplication(attr, accept); + validateAttributeApplication(attr, accept); }); } - private validateAttributeApplication( - attr: DataModelAttribute | DataModelFieldAttribute, - accept: ValidationAcceptor - ) { - const decl = attr.decl.ref; - if (!decl) { - return; - } - - const targetDecl = attr.$container; - if (decl.name === '@@@targetField' && !isAttribute(targetDecl)) { - accept('error', `attribute "${decl.name}" can only be used on attribute declarations`, { node: attr }); - return; - } - - if (isDataModelField(targetDecl) && !this.isValidAttributeTarget(decl, targetDecl)) { - accept('error', `attribute "${decl.name}" cannot be used on this type of field`, { node: attr }); - } - - const filledParams = new Set(); - - for (const arg of attr.args) { - let paramDecl: AttributeParam | undefined; - if (!arg.name) { - paramDecl = decl.params.find((p) => p.default && !filledParams.has(p)); - if (!paramDecl) { - accept('error', `Unexpected unnamed argument`, { - node: arg, - }); - return false; - } - } else { - paramDecl = decl.params.find((p) => p.name === arg.name); - if (!paramDecl) { - accept('error', `Attribute "${decl.name}" doesn't have a parameter named "${arg.name}"`, { - node: arg, - }); - return false; - } - } - - if (!assignableToAttributeParam(arg, paramDecl, attr)) { - accept('error', `Value is not assignable to parameter`, { - node: arg, - }); - return false; - } - - if (filledParams.has(paramDecl)) { - accept('error', `Parameter "${paramDecl.name}" is already provided`, { node: arg }); - return false; - } - filledParams.add(paramDecl); - arg.$resolvedParam = paramDecl; - } - - const missingParams = decl.params.filter((p) => !p.type.optional && !filledParams.has(p)); - if (missingParams.length > 0) { - accept( - 'error', - `Required ${pluralize('parameter', missingParams.length)} not provided: ${missingParams - .map((p) => p.name) - .join(', ')}`, - { node: attr } - ); - return false; - } - - return true; - } - - private isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField) { - const targetField = attrDecl.attributes.find((attr) => attr.decl.ref?.name === '@@@targetField'); - if (!targetField) { - // no field type constraint - return true; - } - - const fieldTypes = (targetField.args[0].value as ArrayExpr).items.map( - (item) => (item as ReferenceExpr).target.ref?.name - ); - - let allowed = false; - for (const allowedType of fieldTypes) { - switch (allowedType) { - case 'StringField': - allowed = allowed || targetDecl.type.type === 'String'; - break; - case 'IntField': - allowed = allowed || targetDecl.type.type === 'Int'; - break; - case 'FloatField': - allowed = allowed || targetDecl.type.type === 'Float'; - break; - case 'DecimalField': - allowed = allowed || targetDecl.type.type === 'Decimal'; - break; - case 'BooleanField': - allowed = allowed || targetDecl.type.type === 'Boolean'; - break; - case 'DateTimeField': - allowed = allowed || targetDecl.type.type === 'DateTime'; - break; - case 'JsonField': - allowed = allowed || targetDecl.type.type === 'Json'; - break; - case 'BytesField': - allowed = allowed || targetDecl.type.type === 'Bytes'; - break; - case 'ModelField': - allowed = allowed || isDataModel(targetDecl.type.reference?.ref); - break; - default: - break; - } - if (allowed) { - break; - } - } - - return allowed; - } - private parseRelation(field: DataModelField, accept?: ValidationAcceptor) { const relAttr = field.attributes.find((attr) => attr.decl.ref?.name === '@relation'); diff --git a/packages/schema/src/language-server/validator/enum-validator.ts b/packages/schema/src/language-server/validator/enum-validator.ts index ecf0828ed..4453b2c12 100644 --- a/packages/schema/src/language-server/validator/enum-validator.ts +++ b/packages/schema/src/language-server/validator/enum-validator.ts @@ -1,7 +1,7 @@ -import { Enum } from '@zenstackhq/language/ast'; -import { AstValidator } from '../types'; +import { Enum, EnumField } from '@zenstackhq/language/ast'; import { ValidationAcceptor } from 'langium'; -import { validateDuplicatedDeclarations } from './utils'; +import { AstValidator } from '../types'; +import { validateAttributeApplication, validateDuplicatedDeclarations } from './utils'; /** * Validates enum declarations. @@ -10,5 +10,21 @@ export default class EnumValidator implements AstValidator { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types validate(_enum: Enum, accept: ValidationAcceptor) { validateDuplicatedDeclarations(_enum.fields, accept); + this.validateAttributes(_enum, accept); + _enum.fields.forEach((field) => { + this.validateField(field, accept); + }); + } + + private validateAttributes(_enum: Enum, accept: ValidationAcceptor) { + _enum.attributes.forEach((attr) => { + validateAttributeApplication(attr, accept); + }); + } + + private validateField(field: EnumField, accept: ValidationAcceptor) { + field.attributes.forEach((attr) => { + validateAttributeApplication(attr, accept); + }); } } diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 4dfaff554..a4668fb3d 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -1,18 +1,25 @@ import { + ArrayExpr, + Attribute, AttributeArg, AttributeParam, BuiltinType, DataModelAttribute, + DataModelField, DataModelFieldAttribute, ExpressionType, isArrayExpr, + isAttribute, + isDataModel, isDataModelField, isEnum, isLiteralExpr, isReferenceExpr, + ReferenceExpr, } from '@zenstackhq/language/ast'; import { resolved } from '@zenstackhq/sdk'; import { AstNode, ValidationAcceptor } from 'langium'; +import pluralize from 'pluralize'; /** * Checks if the given declarations have duplicated names @@ -98,7 +105,7 @@ export function assignableToAttributeParam( } let dstType = param.type.type; - const dstIsArray = param.type.array; + let dstIsArray = param.type.array; const dstRef = param.type.reference; if (isEnum(argResolvedType.decl)) { @@ -108,7 +115,8 @@ export function assignableToAttributeParam( if (dstType === 'ContextType' && isDataModelField(attr.$container) && attr.$container?.type?.reference) { // attribute parameter type is ContextType, need to infer type from // the attribute's container - attrArgDeclType = resolved(attr.$container?.type?.reference); + attrArgDeclType = resolved(attr.$container.type.reference); + dstIsArray = attr.$container.type.array; } return attrArgDeclType === argResolvedType.decl && dstIsArray === argResolvedType.array; } else if (dstType) { @@ -136,6 +144,7 @@ export function assignableToAttributeParam( return false; } dstType = mapBuiltinTypeToExpressionType(attr.$container.type.type); + dstIsArray = attr.$container.type.array; } else { dstType = 'Any'; } @@ -149,3 +158,126 @@ export function assignableToAttributeParam( return dstRef?.ref === argResolvedType.decl && dstIsArray === argResolvedType.array; } } + +export function validateAttributeApplication( + attr: DataModelAttribute | DataModelFieldAttribute, + accept: ValidationAcceptor +) { + const decl = attr.decl.ref; + if (!decl) { + return; + } + + const targetDecl = attr.$container; + if (decl.name === '@@@targetField' && !isAttribute(targetDecl)) { + accept('error', `attribute "${decl.name}" can only be used on attribute declarations`, { node: attr }); + return; + } + + if (isDataModelField(targetDecl) && !isValidAttributeTarget(decl, targetDecl)) { + accept('error', `attribute "${decl.name}" cannot be used on this type of field`, { node: attr }); + } + + const filledParams = new Set(); + + for (const arg of attr.args) { + let paramDecl: AttributeParam | undefined; + if (!arg.name) { + paramDecl = decl.params.find((p) => p.default && !filledParams.has(p)); + if (!paramDecl) { + accept('error', `Unexpected unnamed argument`, { + node: arg, + }); + return false; + } + } else { + paramDecl = decl.params.find((p) => p.name === arg.name); + if (!paramDecl) { + accept('error', `Attribute "${decl.name}" doesn't have a parameter named "${arg.name}"`, { + node: arg, + }); + return false; + } + } + + if (!assignableToAttributeParam(arg, paramDecl, attr)) { + accept('error', `Value is not assignable to parameter`, { + node: arg, + }); + return false; + } + + if (filledParams.has(paramDecl)) { + accept('error', `Parameter "${paramDecl.name}" is already provided`, { node: arg }); + return false; + } + filledParams.add(paramDecl); + arg.$resolvedParam = paramDecl; + } + + const missingParams = decl.params.filter((p) => !p.type.optional && !filledParams.has(p)); + if (missingParams.length > 0) { + accept( + 'error', + `Required ${pluralize('parameter', missingParams.length)} not provided: ${missingParams + .map((p) => p.name) + .join(', ')}`, + { node: attr } + ); + return false; + } + + return true; +} + +function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataModelField) { + const targetField = attrDecl.attributes.find((attr) => attr.decl.ref?.name === '@@@targetField'); + if (!targetField) { + // no field type constraint + return true; + } + + const fieldTypes = (targetField.args[0].value as ArrayExpr).items.map( + (item) => (item as ReferenceExpr).target.ref?.name + ); + + let allowed = false; + for (const allowedType of fieldTypes) { + switch (allowedType) { + case 'StringField': + allowed = allowed || targetDecl.type.type === 'String'; + break; + case 'IntField': + allowed = allowed || targetDecl.type.type === 'Int'; + break; + case 'FloatField': + allowed = allowed || targetDecl.type.type === 'Float'; + break; + case 'DecimalField': + allowed = allowed || targetDecl.type.type === 'Decimal'; + break; + case 'BooleanField': + allowed = allowed || targetDecl.type.type === 'Boolean'; + break; + case 'DateTimeField': + allowed = allowed || targetDecl.type.type === 'DateTime'; + break; + case 'JsonField': + allowed = allowed || targetDecl.type.type === 'Json'; + break; + case 'BytesField': + allowed = allowed || targetDecl.type.type === 'Bytes'; + break; + case 'ModelField': + allowed = allowed || isDataModel(targetDecl.type.reference?.ref); + break; + default: + break; + } + if (allowed) { + break; + } + } + + return allowed; +} diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index 83d3c7710..0265892e9 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -217,9 +217,13 @@ export class ZModelLinker extends DefaultLinker { private resolveArray(node: ArrayExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { node.items.forEach((item) => this.resolve(item, document, extraScopes)); - const itemType = node.items[0].$resolvedType; - if (itemType?.decl) { - this.resolveToBuiltinTypeOrDecl(node, itemType.decl, true); + if (node.items.length > 0) { + const itemType = node.items[0].$resolvedType; + if (itemType?.decl) { + this.resolveToBuiltinTypeOrDecl(node, itemType.decl, true); + } + } else { + this.resolveToBuiltinTypeOrDecl(node, 'Any', true); } } diff --git a/packages/schema/src/plugins/prisma/prisma-builder.ts b/packages/schema/src/plugins/prisma/prisma-builder.ts index 6c3da1096..f7edcd671 100644 --- a/packages/schema/src/plugins/prisma/prisma-builder.ts +++ b/packages/schema/src/plugins/prisma/prisma-builder.ts @@ -27,8 +27,8 @@ export class PrismaModel { return model; } - addEnum(name: string, fields: string[]): Enum { - const e = new Enum(name, fields); + addEnum(name: string): Enum { + const e = new Enum(name); this.enums.push(e); return e; } @@ -281,12 +281,57 @@ export class FunctionCallArg { } } -export class Enum { - constructor(public name: string, public fields: EnumField[]) {} +export class Enum extends DeclarationBase { + public fields: EnumField[] = []; + public attributes: ModelAttribute[] = []; + + constructor(public name: string, public documentations: string[] = []) { + super(); + } + + addField(name: string, attributes: FieldAttribute[] = [], documentations: string[] = []): EnumField { + const field = new EnumField(name, attributes, documentations); + this.fields.push(field); + return field; + } + + addAttribute(name: string, args: AttributeArg[] = []): ModelAttribute { + const attr = new ModelAttribute(name, args); + this.attributes.push(attr); + return attr; + } + + addComment(name: string): string { + this.documentations.push(name); + return name; + } toString(): string { - return `enum ${this.name} {\n` + indentString(this.fields.join('\n')) + '\n}'; + return ( + super.toString() + + `enum ${this.name} {\n` + + indentString([...this.fields, ...this.attributes].map((d) => d.toString()).join('\n')) + + '\n}' + ); } } -type EnumField = string; +export class EnumField extends DeclarationBase { + constructor(public name: string, public attributes: FieldAttribute[] = [], public documentations: string[] = []) { + super(); + } + + addAttribute(name: string, args: AttributeArg[] = []): FieldAttribute { + const attr = new FieldAttribute(name, args); + this.attributes.push(attr); + return attr; + } + + toString(): string { + return ( + super.toString() + + this.name + + (this.attributes.length > 0 ? ' ' + this.attributes.map((a) => a.toString()).join(' ') : '') + ); + } +} diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 6bd8ba94b..0be856429 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -8,45 +8,47 @@ import { DataModelFieldAttribute, DataSource, Enum, + EnumField, Expression, GeneratorDecl, InvocationExpr, - LiteralExpr, - Model, isArrayExpr, isInvocationExpr, isLiteralExpr, isReferenceExpr, + LiteralExpr, + Model, } from '@zenstackhq/language/ast'; import { + getLiteral, + getLiteralArray, GUARD_FIELD_NAME, PluginError, PluginOptions, - TRANSACTION_FIELD_NAME, - getLiteral, - getLiteralArray, resolved, + TRANSACTION_FIELD_NAME, } from '@zenstackhq/sdk'; import fs from 'fs'; import { writeFile } from 'fs/promises'; import path from 'path'; import { analyzePolicies } from '../../utils/ast-utils'; import { execSync } from '../../utils/exec-utils'; -import ZModelCodeGenerator from './zmodel-code-generator'; import { - ModelFieldType, AttributeArg as PrismaAttributeArg, AttributeArgValue as PrismaAttributeArgValue, - Model as PrismaDataModel, DataSourceUrl as PrismaDataSourceUrl, + Enum as PrismaEnum, FieldAttribute as PrismaFieldAttribute, FieldReference as PrismaFieldReference, FieldReferenceArg as PrismaFieldReferenceArg, FunctionCall as PrismaFunctionCall, FunctionCallArg as PrismaFunctionCallArg, - PrismaModel, + Model as PrismaDataModel, ModelAttribute as PrismaModelAttribute, + ModelFieldType, + PrismaModel, } from './prisma-builder'; +import ZModelCodeGenerator from './zmodel-code-generator'; /** * Generates Prisma schema file @@ -293,7 +295,7 @@ export default class PrismaSchemaGenerator { ); } - private generateModelAttribute(model: PrismaDataModel, attr: DataModelAttribute) { + private generateModelAttribute(model: PrismaDataModel | PrismaEnum, attr: DataModelAttribute) { model.attributes.push( new PrismaModelAttribute( resolved(attr.decl).name, @@ -303,10 +305,35 @@ export default class PrismaSchemaGenerator { } private generateEnum(prisma: PrismaModel, decl: Enum) { - prisma.addEnum( - decl.name, - decl.fields.map((f) => f.name) + const _enum = prisma.addEnum(decl.name); + + for (const field of decl.fields) { + this.generateEnumField(_enum, field); + } + + for (const attr of decl.attributes.filter((attr) => attr.decl.ref && this.isPrismaAttribute(attr.decl.ref))) { + this.generateModelAttribute(_enum, attr); + } + + decl.attributes + .filter((attr) => attr.decl.ref && !this.isPrismaAttribute(attr.decl.ref)) + .forEach((attr) => _enum.addComment('/// ' + this.zModelGenerator.generateAttribute(attr))); + + // user defined comments pass-through + decl.comments.forEach((c) => _enum.addComment(c)); + } + + private generateEnumField(_enum: PrismaEnum, field: EnumField) { + const attributes = field.attributes + .filter((attr) => attr.decl.ref && this.isPrismaAttribute(attr.decl.ref)) + .map((attr) => this.makeFieldAttribute(attr)); + + const nonPrismaAttributes = field.attributes.filter( + (attr) => !attr.decl.ref || !this.isPrismaAttribute(attr.decl.ref) ); + + const documentations = nonPrismaAttributes.map((attr) => '/// ' + this.zModelGenerator.generateAttribute(attr)); + _enum.addField(field.name, attributes, documentations); } private isStringLiteral(node: AstNode): node is LiteralExpr { diff --git a/packages/schema/tests/generator/prisma-builder.test.ts b/packages/schema/tests/generator/prisma-builder.test.ts index a8d1276b2..2e1341167 100644 --- a/packages/schema/tests/generator/prisma-builder.test.ts +++ b/packages/schema/tests/generator/prisma-builder.test.ts @@ -38,7 +38,9 @@ describe('Prisma Builder Tests', () => { it('enum', async () => { const model = new PrismaModel(); - model.addEnum('UserRole', ['USER', 'ADMIN']); + const _enum = model.addEnum('UserRole'); + _enum.addField('USER'); + _enum.addField('ADMIN'); await validate(model); }); diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 55bde0023..5dc064721 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -61,4 +61,36 @@ describe('Prisma generator test', () => { expect(content).toContain(`/// @TypeGraphQL.omit(input: ['update', 'where', 'orderBy'])`); expect(content).toContain(`/// @TypeGraphQL.field(name: 'bar')`); }); + + it('enum mapping', async () => { + const model = await loadModel(` + datasource db { + provider = 'postgresql' + url = env('DATABASE_URL') + } + + enum Role { + ADMIN @map('admin') + CUSTOMER @map('customer') + @@map('_Role') + } + + model User { + id Int @id + role Role @default(CUSTOMER) + } + `); + + const { name } = tmp.fileSync({ postfix: '.prisma' }); + await new PrismaSchemaGenerator().generate(model, { + provider: '@zenstack/prisma', + schemaPath: 'schema.zmodel', + output: name, + }); + + const content = fs.readFileSync(name, 'utf-8'); + expect(content).toContain(`@@map("_Role")`); + expect(content).toContain(`@map("admin")`); + expect(content).toContain(`@map("customer")`); + }); }); diff --git a/packages/schema/tests/schema/parser.test.ts b/packages/schema/tests/schema/parser.test.ts index 83daa8af3..322d3b5bf 100644 --- a/packages/schema/tests/schema/parser.test.ts +++ b/packages/schema/tests/schema/parser.test.ts @@ -101,6 +101,7 @@ describe('Parsing Tests', () => { id String @id name String? tags String[] + tagsWithDefault String[] @default([]) } `; const doc = await loadModel(content, false); @@ -330,10 +331,10 @@ describe('Parsing Tests', () => { x Int } - function foo(a Int, b Int) Boolean { + function foo(a: Int, b: Int): Boolean { } - function bar(items N[]) Boolean { + function bar(items: N[]): Boolean { } `; const doc = await loadModel(content, false); @@ -370,7 +371,7 @@ describe('Parsing Tests', () => { y Int } - function foo(n N) Boolean { + function foo(n: N): Boolean { n.x < 0 } `; diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 0d49e050f..430bf3216 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -177,6 +177,15 @@ describe('Attribute tests', () => { @@map("__A") } `); + + await loadModel(` + ${prelude} + enum Role { + ADMIN @map("admin") + CUSTOMER @map("customer") + @@map("_Role") + } + `); }); it('attribute function coverage', async () => { diff --git a/packages/schema/tests/schema/validation/datamodel-validation.test.ts b/packages/schema/tests/schema/validation/datamodel-validation.test.ts index a5fb148f5..87bb49e21 100644 --- a/packages/schema/tests/schema/validation/datamodel-validation.test.ts +++ b/packages/schema/tests/schema/validation/datamodel-validation.test.ts @@ -28,7 +28,8 @@ describe('Data Model Validation Tests', () => { id String @id a String b Boolean? - c Int[] + c Int[] @default([]) + c1 Int[] @default([1, 2, 3]) d BigInt e Float f Decimal diff --git a/packages/schema/tests/utils.ts b/packages/schema/tests/utils.ts index 8f63c091b..91eb81cde 100644 --- a/packages/schema/tests/utils.ts +++ b/packages/schema/tests/utils.ts @@ -8,7 +8,7 @@ import { createZModelServices } from '../src/language-server/zmodel-module'; export class SchemaLoadingError extends Error { constructor(public readonly errors: string[]) { - super('Schema error'); + super('Schema error:\n' + errors.join('\n')); } } @@ -20,6 +20,15 @@ export async function loadModel(content: string, validate = true, verbose = true URI.file(path.resolve('src/res/stdlib.zmodel')) ); const doc = shared.workspace.LangiumDocuments.getOrCreateDocument(URI.file(docPath)); + + if (doc.parseResult.lexerErrors.length > 0) { + throw new SchemaLoadingError(doc.parseResult.lexerErrors.map((e) => e.message)); + } + + if (doc.parseResult.parserErrors.length > 0) { + throw new SchemaLoadingError(doc.parseResult.parserErrors.map((e) => e.message)); + } + await shared.workspace.DocumentBuilder.build([stdLib, doc], { validationChecks: validate ? 'all' : 'none', }); diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index 7d60d335a..6aacdb493 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -152,10 +152,7 @@ "typescript": "^4.9.3" }, "peerDependencies": { - "@prisma/client": "^4.0.0", - "next": "^12.3.1 || ^13", - "react": "^17.0.2 || ^18", - "react-dom": "^17.0.2 || ^18" + "@prisma/client": "^4.0.0" } }, "../../../packages/schema/dist": {