diff --git a/package.json b/package.json index 5737e5f25..bdee97c10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 08d2703cf..7a998e716 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 8e00e671e..8a928ad08 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -44,6 +44,8 @@ export function isReferenceTarget(item: unknown): item is ReferenceTarget { return reflection.isInstance(item, ReferenceTarget); } +export type RegularID = 'in' | string; + export type TypeDeclaration = DataModel | Enum; export const TypeDeclaration = 'TypeDeclaration'; @@ -55,7 +57,7 @@ export function isTypeDeclaration(item: unknown): item is TypeDeclaration { export interface Argument extends AstNode { readonly $container: InvocationExpr; readonly $type: 'Argument'; - name?: string + name?: RegularID value: Expression } @@ -94,7 +96,7 @@ export function isAttribute(item: unknown): item is Attribute { export interface AttributeArg extends AstNode { readonly $container: AttributeAttribute | DataModelAttribute | DataModelFieldAttribute; readonly $type: 'AttributeArg'; - name?: string + name?: RegularID value: Expression } @@ -121,7 +123,7 @@ export interface AttributeParam extends AstNode { readonly $container: Attribute; readonly $type: 'AttributeParam'; default: boolean - name: string + name: RegularID type: AttributeParamType } @@ -166,7 +168,7 @@ export interface DataModel extends AstNode { attributes: Array comments: Array fields: Array - name: string + name: RegularID } export const DataModel = 'DataModel'; @@ -193,7 +195,7 @@ export interface DataModelField extends AstNode { readonly $type: 'DataModelField'; attributes: Array comments: Array - name: string + name: RegularID type: DataModelFieldType } @@ -235,7 +237,7 @@ export interface DataSource extends AstNode { readonly $container: Model; readonly $type: 'DataSource'; fields: Array - name: string + name: RegularID } export const DataSource = 'DataSource'; @@ -247,7 +249,7 @@ export function isDataSource(item: unknown): item is DataSource { export interface DataSourceField extends AstNode { readonly $container: DataSource; readonly $type: 'DataSourceField'; - name: string + name: RegularID value: ArrayExpr | InvocationExpr | LiteralExpr } @@ -263,7 +265,7 @@ export interface Enum extends AstNode { attributes: Array comments: Array fields: Array - name: string + name: RegularID } export const Enum = 'Enum'; @@ -277,7 +279,7 @@ export interface EnumField extends AstNode { readonly $type: 'EnumField'; attributes: Array comments: Array - name: string + name: RegularID } export const EnumField = 'EnumField'; @@ -289,7 +291,7 @@ export function isEnumField(item: unknown): item is EnumField { export interface FieldInitializer extends AstNode { readonly $container: ObjectExpr; readonly $type: 'FieldInitializer'; - name: string + name: RegularID value: Expression } @@ -303,7 +305,7 @@ export interface FunctionDecl extends AstNode { readonly $container: Model; readonly $type: 'FunctionDecl'; expression?: Expression - name: string + name: RegularID params: Array returnType: FunctionParamType } @@ -317,7 +319,7 @@ export function isFunctionDecl(item: unknown): item is FunctionDecl { export interface FunctionParam extends AstNode { readonly $container: DataModel | Enum | FunctionDecl; readonly $type: 'FunctionParam'; - name: string + name: RegularID optional: boolean type: FunctionParamType } @@ -346,7 +348,7 @@ export interface GeneratorDecl extends AstNode { readonly $container: Model; readonly $type: 'GeneratorDecl'; fields: Array - name: string + name: RegularID } export const GeneratorDecl = 'GeneratorDecl'; @@ -358,7 +360,7 @@ export function isGeneratorDecl(item: unknown): item is GeneratorDecl { export interface GeneratorField extends AstNode { readonly $container: GeneratorDecl; readonly $type: 'GeneratorField'; - name: string + name: RegularID value: ArrayExpr | LiteralExpr } @@ -445,7 +447,7 @@ export interface Plugin extends AstNode { readonly $container: Model; readonly $type: 'Plugin'; fields: Array - name: string + name: RegularID } export const Plugin = 'Plugin'; @@ -457,8 +459,8 @@ export function isPlugin(item: unknown): item is Plugin { export interface PluginField extends AstNode { readonly $container: Plugin; readonly $type: 'PluginField'; - name: string - value: ArrayExpr | LiteralExpr + name: RegularID + value: ArrayExpr | LiteralExpr | ObjectExpr } export const PluginField = 'PluginField'; diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 14d2c86d4..decee5369 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -85,7 +85,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "arguments": [] } @@ -107,7 +107,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -123,7 +123,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -167,7 +167,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -179,7 +179,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -205,7 +205,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@18" }, "arguments": [] }, @@ -237,7 +237,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -253,7 +253,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -297,7 +297,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -309,7 +309,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -360,7 +360,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -376,7 +376,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -420,7 +420,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -432,7 +432,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -461,6 +461,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$ref": "#/rules@10" }, "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [] } ] } @@ -480,7 +487,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@25" }, "arguments": [] }, @@ -504,21 +511,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [] } @@ -605,7 +612,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -627,7 +634,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] } @@ -657,7 +664,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] }, @@ -789,6 +796,117 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, + { + "$type": "ParserRule", + "name": "ObjectExpr", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "fields", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "fields", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": ",", + "cardinality": "?" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, + { + "$type": "ParserRule", + "name": "FieldInitializer", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, { "$type": "ParserRule", "name": "InvocationExpr", @@ -881,7 +999,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@26" }, "arguments": [] }, @@ -943,7 +1061,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@20" }, "arguments": [] }, @@ -1026,7 +1144,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@21" }, "arguments": [] }, @@ -1058,7 +1176,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@21" }, "arguments": [] } @@ -1088,7 +1206,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@22" }, "arguments": [] }, @@ -1137,7 +1255,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@22" }, "arguments": [] } @@ -1167,7 +1285,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@23" }, "arguments": [] }, @@ -1208,7 +1326,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1238,7 +1356,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@24" }, "arguments": [] }, @@ -1279,7 +1397,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1350,7 +1468,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@18" }, "arguments": [] }, @@ -1371,14 +1489,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@19" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@16" }, "arguments": [] } @@ -1391,117 +1509,6 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, - { - "$type": "ParserRule", - "name": "ObjectExpr", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "fields", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@26" - }, - "arguments": [] - } - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "," - }, - { - "$type": "Assignment", - "feature": "fields", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@26" - }, - "arguments": [] - } - } - ], - "cardinality": "*" - }, - { - "$type": "Keyword", - "value": ",", - "cardinality": "?" - } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "}" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "FieldInitializer", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@56" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "value", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, { "$type": "ParserRule", "name": "ArgumentList", @@ -1567,7 +1574,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1613,7 +1620,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -1630,7 +1637,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1661,7 +1668,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "arguments": [] } @@ -1695,7 +1702,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -1708,7 +1715,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1732,7 +1739,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -1763,7 +1770,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -1780,7 +1787,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] }, @@ -1840,7 +1847,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -1857,7 +1864,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1888,7 +1895,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "arguments": [] } @@ -1922,7 +1929,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [] }, @@ -1935,7 +1942,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1947,7 +1954,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] }, @@ -1971,7 +1978,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -1987,7 +1994,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2095,7 +2102,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -2107,7 +2114,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2163,7 +2170,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -2177,6 +2184,13 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "type": { "$ref": "#/types@1" }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" + }, + "arguments": [] + }, "deprecatedSyntax": false } } @@ -2220,7 +2234,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -2237,14 +2251,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2262,6 +2276,33 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, + { + "$type": "ParserRule", + "name": "RegularID", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@57" + }, + "arguments": [] + }, + { + "$type": "Keyword", + "value": "in" + } + ] + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, { "$type": "ParserRule", "name": "AttributeAttributeName", @@ -2353,21 +2394,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] } @@ -2389,7 +2430,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -2405,7 +2446,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2424,7 +2465,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2443,7 +2484,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2465,7 +2506,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [] }, @@ -2489,7 +2530,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -2511,7 +2552,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } @@ -2527,7 +2568,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" }, "arguments": [] } @@ -2560,7 +2601,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] }, @@ -2591,7 +2632,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] }, @@ -2651,12 +2692,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] }, @@ -2673,7 +2714,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [], "cardinality": "?" @@ -2703,7 +2744,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "*" @@ -2715,12 +2756,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, @@ -2737,7 +2778,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [], "cardinality": "?" @@ -2771,12 +2812,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] }, @@ -2793,7 +2834,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [], "cardinality": "?" @@ -2828,7 +2869,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2847,7 +2888,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2879,7 +2920,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@38" }, "arguments": [] } diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 40566e697..d26ec8ab2 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -10,24 +10,24 @@ AbstractDeclaration: // datasource DataSource: - TRIPLE_SLASH_COMMENT* 'datasource' name=ID '{' (fields+=DataSourceField)* '}'; + TRIPLE_SLASH_COMMENT* 'datasource' name=RegularID '{' (fields+=DataSourceField)* '}'; DataSourceField: - TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | InvocationExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=RegularID '=' value=(LiteralExpr | InvocationExpr | ArrayExpr); // generator GeneratorDecl: - TRIPLE_SLASH_COMMENT* 'generator' name=ID '{' (fields+=GeneratorField)* '}'; + TRIPLE_SLASH_COMMENT* 'generator' name=RegularID '{' (fields+=GeneratorField)* '}'; GeneratorField: - TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=RegularID '=' value=(LiteralExpr | ArrayExpr); // plugin Plugin: - TRIPLE_SLASH_COMMENT* 'plugin' name=ID '{' (fields+=PluginField)* '}'; + TRIPLE_SLASH_COMMENT* 'plugin' name=RegularID '{' (fields+=PluginField)* '}'; PluginField: - TRIPLE_SLASH_COMMENT* name=ID '=' value=(LiteralExpr | ArrayExpr); + TRIPLE_SLASH_COMMENT* name=RegularID '=' value=(LiteralExpr | ArrayExpr | ObjectExpr); // expression Expression: @@ -48,7 +48,7 @@ NullExpr: value=NULL; ReferenceExpr: - target=[ReferenceTarget:ID] ('(' ReferenceArgList ')')?; + target=[ReferenceTarget:RegularID] ('(' ReferenceArgList ')')?; fragment ReferenceArgList: args+=ReferenceArg (',' args+=ReferenceArg)*; @@ -56,6 +56,15 @@ fragment ReferenceArgList: ReferenceArg: name=('sort') ':' value=('Asc' | 'Desc'); + +ObjectExpr: + '{' + (fields+=FieldInitializer (',' fields+=FieldInitializer)* ','?)? + '}'; + +FieldInitializer: + name=RegularID ':' value=(Expression); + InvocationExpr: function=[FunctionDecl] '(' ArgumentList? ')'; @@ -133,24 +142,16 @@ PrimaryExpr infers Expression: UnaryExpr | ObjectExpr; -ObjectExpr: - '{' - (fields+=FieldInitializer (',' fields+=FieldInitializer)* ','?)? - '}'; - -FieldInitializer: - name=ID ':' value=(Expression); - fragment ArgumentList: args+=Argument (',' args+=Argument)*; Argument: - (name=ID ':')? value=Expression; + (name=RegularID ':')? value=Expression; // model DataModel: (comments+=TRIPLE_SLASH_COMMENT)* - 'model' name=ID '{' ( + 'model' name=RegularID '{' ( fields+=DataModelField | attributes+=DataModelAttribute )+ @@ -158,15 +159,15 @@ DataModel: DataModelField: (comments+=TRIPLE_SLASH_COMMENT)* - name=ID type=DataModelFieldType (attributes+=DataModelFieldAttribute)*; + name=RegularID type=DataModelFieldType (attributes+=DataModelFieldAttribute)*; DataModelFieldType: - (type=BuiltinType | reference=[TypeDeclaration:ID]) (array?='[' ']')? (optional?='?')?; + (type=BuiltinType | reference=[TypeDeclaration:RegularID]) (array?='[' ']')? (optional?='?')?; // enum Enum: (comments+=TRIPLE_SLASH_COMMENT)* - 'enum' name=ID '{' ( + 'enum' name=RegularID '{' ( fields+=EnumField | attributes+=DataModelAttribute )+ @@ -174,22 +175,27 @@ Enum: EnumField: (comments+=TRIPLE_SLASH_COMMENT)* - name=ID (attributes+=DataModelFieldAttribute)*; + name=RegularID (attributes+=DataModelFieldAttribute)*; // function FunctionDecl: - TRIPLE_SLASH_COMMENT* 'function' name=ID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; + TRIPLE_SLASH_COMMENT* 'function' name=RegularID '(' (params+=FunctionParam (',' params+=FunctionParam)*)? ')' ':' returnType=FunctionParamType '{' (expression=Expression)? '}'; FunctionParam: - TRIPLE_SLASH_COMMENT* name=ID ':' type=FunctionParamType (optional?='?')?; + TRIPLE_SLASH_COMMENT* name=RegularID ':' type=FunctionParamType (optional?='?')?; FunctionParamType: - (type=ExpressionType | reference=[TypeDeclaration]) (array?='[' ']')?; + (type=ExpressionType | reference=[TypeDeclaration:RegularID]) (array?='[' ']')?; QualifiedName returns string: // TODO: is this the right way to deal with token precedence? ID ('.' (ID|BuiltinType))*; +// https://github.com/langium/langium/discussions/1012 +RegularID returns string: + // include keywords that we'd like to work as ID in most places + ID | 'in'; + // attribute-level attribute AttributeAttributeName returns string: '@@@' QualifiedName; @@ -210,12 +216,12 @@ Attribute: TRIPLE_SLASH_COMMENT* 'attribute' name=AttributeName '(' (params+=AttributeParam (',' params+=AttributeParam)*)? ')' (attributes+=AttributeAttribute)*; AttributeParam: - TRIPLE_SLASH_COMMENT* (default?='_')? name=ID ':' type=AttributeParamType; + TRIPLE_SLASH_COMMENT* (default?='_')? name=RegularID ':' type=AttributeParamType; // 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:RegularID]) (array?='[' ']')? (optional?='?')?; type TypeDeclaration = DataModel | Enum; @@ -232,7 +238,7 @@ fragment AttributeArgList: args+=AttributeArg (',' args+=AttributeArg)*; AttributeArg: - (name=ID ':')? value=Expression; + (name=RegularID ':')? value=Expression; ExpressionType returns string: 'String' | 'Int' | 'Float' | 'Boolean' | 'DateTime' | 'Null' | 'Object' | 'Any'; diff --git a/packages/next/package.json b/packages/next/package.json index 8ee8fa601..9671ebe2e 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/next", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "displayName": "ZenStack Next.js integration", "description": "ZenStack Next.js integration", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 08e037ed5..b4436899a 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { @@ -30,7 +30,9 @@ "change-case": "^4.1.2", "openapi-types": "^12.1.0", "tiny-invariant": "^1.3.1", - "yaml": "^2.2.1" + "yaml": "^2.2.1", + "zod": "^3.19.1", + "zod-validation-error": "^0.2.1" }, "devDependencies": { "@prisma/internals": "^4.7.1", diff --git a/packages/plugins/openapi/src/generator.ts b/packages/plugins/openapi/src/generator.ts index ebb03cf21..f72e28cfc 100644 --- a/packages/plugins/openapi/src/generator.ts +++ b/packages/plugins/openapi/src/generator.ts @@ -17,7 +17,9 @@ import type { OpenAPIV3_1 as OAPI } from 'openapi-types'; import * as path from 'path'; import invariant from 'tiny-invariant'; import YAML from 'yaml'; +import { fromZodError } from 'zod-validation-error'; import { getModelResourceMeta } from './meta'; +import { SecuritySchemesSchema } from './schema'; /** * Generates OpenAPI specification. @@ -54,6 +56,13 @@ export class OpenAPIGenerator { const components = this.generateComponents(); const paths = this.generatePaths(components); + // generate security schemes, and root-level security + this.generateSecuritySchemes(components); + let security: OAPI.Document['security'] | undefined = undefined; + if (components.securitySchemes && Object.keys(components.securitySchemes).length > 0) { + security = Object.keys(components.securitySchemes).map((scheme) => ({ [scheme]: [] })); + } + // prune unused component schemas this.pruneComponents(components); @@ -62,15 +71,19 @@ export class OpenAPIGenerator { info: { title: this.getOption('title', 'ZenStack Generated API'), version: this.getOption('version', '1.0.0'), - description: this.getOption('description', undefined), - summary: this.getOption('summary', undefined), + description: this.getOption('description'), + summary: this.getOption('summary'), }, - tags: this.includedModels.map((model) => ({ - name: camelCase(model.name), - description: `${model.name} operations`, - })), + tags: this.includedModels.map((model) => { + const meta = getModelResourceMeta(model); + return { + name: camelCase(model.name), + description: meta?.tagDescription ?? `${model.name} operations`, + }; + }), components, paths, + security, }; const ext = path.extname(output); @@ -83,6 +96,17 @@ export class OpenAPIGenerator { return this.warnings; } + private generateSecuritySchemes(components: OAPI.ComponentsObject) { + const securitySchemes = this.getOption[]>('securitySchemes'); + if (securitySchemes) { + const parsed = SecuritySchemesSchema.safeParse(securitySchemes); + if (!parsed.success) { + throw new PluginError(`"securitySchemes" option is invalid: ${fromZodError(parsed.error)}`); + } + components.securitySchemes = parsed.data; + } + } + private pruneComponents(components: OAPI.ComponentsObject) { const schemas = components.schemas; if (schemas) { @@ -482,11 +506,13 @@ export class OpenAPIGenerator { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - const def: any = { + const def: OAPI.OperationObject = { operationId: `${operation}${model.name}`, description: meta?.description ?? description, tags: meta?.tags || [camelCase(model.name)], summary: meta?.summary, + security: meta?.security, + deprecated: meta?.deprecated, responses: { [successCode !== undefined ? successCode : '200']: { description: 'Successful operation', @@ -566,14 +592,13 @@ export class OpenAPIGenerator { return this.ref(name); } - private getOption( - name: string, - defaultValue: T - ): T extends string ? string : string | undefined { + private getOption(name: string): T | undefined; + private getOption(name: string, defaultValue: D): T; + private getOption(name: string, defaultValue?: T): T | undefined { const value = this.options[name]; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error - return typeof value === 'string' ? value : defaultValue; + return value === undefined ? defaultValue : value; } private generateComponents() { diff --git a/packages/plugins/openapi/src/meta.ts b/packages/plugins/openapi/src/meta.ts index 2b7d1e450..ce2fff972 100644 --- a/packages/plugins/openapi/src/meta.ts +++ b/packages/plugins/openapi/src/meta.ts @@ -1,22 +1,31 @@ import { getObjectLiteral } from '@zenstackhq/sdk'; import { DataModel } from '@zenstackhq/sdk/ast'; +/** + * Metadata for a resource, expressed by @@openapi.meta attribute. + */ +export type ModelMeta = { + tagDescription?: string; +}; + /** * Metadata for a resource operation, expressed by @@openapi.meta attribute. */ export type OperationMeta = { - ignore: boolean; - method: string; - path: string; + ignore?: boolean; + method?: string; + path?: string; summary?: string; description?: string; tags?: string[]; + deprecated?: boolean; + security?: Array>; }; /** * Metadata for a resource, expressed by @@openapi.meta attribute. */ -export type ResourceMeta = Record; +export type ResourceMeta = ModelMeta & Record; export function getModelResourceMeta(model: DataModel) { return getObjectLiteral( diff --git a/packages/plugins/openapi/src/schema.ts b/packages/plugins/openapi/src/schema.ts new file mode 100644 index 000000000..ed44f144f --- /dev/null +++ b/packages/plugins/openapi/src/schema.ts @@ -0,0 +1,43 @@ +import z from 'zod'; + +/** + * Zod schema for OpenAPI security schemes: https://swagger.io/docs/specification/authentication/ + */ +export const SecuritySchemesSchema = z.record( + z.union([ + z.object({ type: z.literal('http'), scheme: z.literal('basic') }), + z.object({ type: z.literal('http'), scheme: z.literal('bearer'), bearerFormat: z.string().optional() }), + z.object({ + type: z.literal('apiKey'), + in: z.union([z.literal('header'), z.literal('query'), z.literal('cookie')]), + name: z.string(), + }), + z.object({ + type: z.literal('oauth2'), + description: z.string(), + flows: z.object({ + authorizationCode: z.object({ + authorizationUrl: z.string(), + tokenUrl: z.string(), + refreshUrl: z.string(), + scopes: z.record(z.string()), + }), + implicit: z.object({ + authorizationUrl: z.string(), + refreshUrl: z.string(), + scopes: z.record(z.string()), + }), + password: z.object({ + tokenUrl: z.string(), + refreshUrl: z.string(), + scopes: z.record(z.string()), + }), + clientCredentials: z.object({ + tokenUrl: z.string(), + refreshUrl: z.string(), + scopes: z.record(z.string()), + }), + }), + }), + ]) +); diff --git a/packages/plugins/openapi/tests/openapi.test.ts b/packages/plugins/openapi/tests/openapi.test.ts index 0289697ab..ffef100d8 100644 --- a/packages/plugins/openapi/tests/openapi.test.ts +++ b/packages/plugins/openapi/tests/openapi.test.ts @@ -2,13 +2,13 @@ /// import OpenAPIParser from '@readme/openapi-parser'; +import { getLiteral, getObjectLiteral } from '@zenstackhq/sdk'; +import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; import { loadZModelAndDmmf } from '@zenstackhq/testtools'; -import * as tmp from 'tmp'; import * as fs from 'fs'; -import generate from '../src'; +import * as tmp from 'tmp'; import YAML from 'yaml'; -import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast'; -import { getLiteral } from '@zenstackhq/sdk'; +import generate from '../src'; describe('Open API Plugin Tests', () => { it('run plugin', async () => { @@ -39,7 +39,8 @@ model User { path: 'dodelete', description: 'Delete a unique user', summary: 'Delete a user yeah yeah', - tags: ['delete', 'user'] + tags: ['delete', 'user'], + deprecated: true }, }) } @@ -55,6 +56,7 @@ model Post { viewCount Int @default(0) @@openapi.meta({ + tagDescription: 'Post-related operations', findMany: { ignore: true } @@ -75,7 +77,7 @@ model Bar { const { name: output } = tmp.fileSync({ postfix: '.yaml' }); const options = buildOptions(model, modelFile, output); - generate(model, options, dmmf); + await generate(model, options, dmmf); console.log('OpenAPI specification generated:', output); @@ -83,13 +85,21 @@ model Bar { expect(parsed.openapi).toBe('3.1.0'); const api = await OpenAPIParser.validate(output); + + expect(api.tags).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: 'user', description: 'User operations' }), + expect.objectContaining({ name: 'post', description: 'Post-related operations' }), + ]) + ); + expect(api.paths?.['/user/findMany']?.['get']?.description).toBe('Find users matching the given conditions'); const del = api.paths?.['/user/dodelete']?.['put']; expect(del?.description).toBe('Delete a unique user'); expect(del?.summary).toBe('Delete a user yeah yeah'); expect(del?.tags).toEqual(expect.arrayContaining(['delete', 'user'])); + expect(del?.deprecated).toBe(true); expect(api.paths?.['/post/findMany']).toBeUndefined(); - expect(api.paths?.['/foo/findMany']).toBeUndefined(); expect(api.paths?.['/bar/findMany']).toBeUndefined(); }); @@ -112,7 +122,7 @@ model User { const { name: output } = tmp.fileSync({ postfix: '.yaml' }); const options = buildOptions(model, modelFile, output); - generate(model, options, dmmf); + await generate(model, options, dmmf); console.log('OpenAPI specification generated:', output); @@ -131,6 +141,88 @@ model User { expect(api.paths?.['/myapi/user/findMany']).toBeTruthy(); }); + it('security schemes valid', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' + securitySchemes = { + myBasic: { type: 'http', scheme: 'basic' }, + myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' } + } +} + +model User { + id String @id +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + await OpenAPIParser.validate(output); + + const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); + expect(parsed.components.securitySchemes).toEqual( + expect.objectContaining({ + myBasic: { type: 'http', scheme: 'basic' }, + myBearer: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + myApiKey: { type: 'apiKey', in: 'header', name: 'X-API-KEY' }, + }) + ); + expect(parsed.security).toEqual(expect.arrayContaining([{ myBasic: [] }, { myBearer: [] }])); + }); + + it('security schemes invalid', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' + securitySchemes = { + myBasic: { type: 'invalid', scheme: 'basic' } + } +} + +model User { + id String @id +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await expect(generate(model, options, dmmf)).rejects.toEqual( + expect.objectContaining({ message: expect.stringContaining('"securitySchemes" option is invalid') }) + ); + }); + + it('security override', async () => { + const { model, dmmf, modelFile } = await loadZModelAndDmmf(` +plugin openapi { + provider = '${process.cwd()}/dist' +} + +model User { + id String @id + + @@openapi.meta({ + findMany: { + security: [] + } + }) +} + `); + + const { name: output } = tmp.fileSync({ postfix: '.yaml' }); + const options = buildOptions(model, modelFile, output); + await generate(model, options, dmmf); + + console.log('OpenAPI specification generated:', output); + + const api = await OpenAPIParser.validate(output); + expect(api.paths?.['/user/findMany']?.['get']?.security).toHaveLength(0); + }); + it('v3.1.0 fields', async () => { const { model, dmmf, modelFile } = await loadZModelAndDmmf(` plugin openapi { @@ -145,21 +237,20 @@ model User { const { name: output } = tmp.fileSync({ postfix: '.yaml' }); const options = buildOptions(model, modelFile, output); - generate(model, options, dmmf); + await generate(model, options, dmmf); console.log('OpenAPI specification generated:', output); + await OpenAPIParser.validate(output); const parsed = YAML.parse(fs.readFileSync(output, 'utf-8')); expect(parsed.openapi).toBe('3.1.0'); - - const api = await OpenAPIParser.validate(output); - expect((api.info as any).summary).toEqual('awesome api'); + expect(parsed.info.summary).toEqual('awesome api'); }); }); function buildOptions(model: Model, modelFile: string, output: string) { const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || []; const options: any = { schemaPath: modelFile, output }; - optionFields.forEach((f) => (options[f.name] = getLiteral(f.value))); + optionFields.forEach((f) => (options[f.name] = getLiteral(f.value) ?? getObjectLiteral(f.value))); return options; } diff --git a/packages/plugins/react/package.json b/packages/plugins/react/package.json index a1b78c930..450a2f417 100644 --- a/packages/plugins/react/package.json +++ b/packages/plugins/react/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/react", "displayName": "ZenStack plugin and runtime for ReactJS", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "ZenStack plugin and runtime for ReactJS", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index d9750c2b3..04a17d02a 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/plugins/trpc/src/generator.ts b/packages/plugins/trpc/src/generator.ts index 1ef8873e1..26f535a60 100644 --- a/packages/plugins/trpc/src/generator.ts +++ b/packages/plugins/trpc/src/generator.ts @@ -1,14 +1,20 @@ import { DMMF } from '@prisma/generator-helper'; -import { PluginError, PluginOptions } from '@zenstackhq/sdk'; +import { CrudFailureReason, PluginError, PluginOptions, RUNTIME_PACKAGE } from '@zenstackhq/sdk'; import { Model } from '@zenstackhq/sdk/ast'; +import { camelCase } from 'change-case'; import { promises as fs } from 'fs'; import path from 'path'; -import { generate as PrismaZodGenerator } from './zod/generator'; -import { generateProcedure, generateRouterSchemaImports, getInputTypeByOpName, resolveModelsComments } from './helpers'; +import { Project } from 'ts-morph'; +import { + generateHelperImport, + generateProcedure, + generateRouterSchemaImports, + getInputTypeByOpName, + resolveModelsComments, +} from './helpers'; import { project } from './project'; import removeDir from './utils/removeDir'; -import { camelCase } from 'change-case'; -import { Project } from 'ts-morph'; +import { generate as PrismaZodGenerator } from './zod/generator'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { let outDir = options.output as string; @@ -33,6 +39,13 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. const hiddenModels: string[] = []; resolveModelsComments(models, hiddenModels); + createAppRouter(outDir, modelOperations, hiddenModels); + createHelper(outDir); + + await project.save(); +} + +function createAppRouter(outDir: string, modelOperations: DMMF.ModelMapping[], hiddenModels: string[]) { const appRouter = project.createSourceFile(path.resolve(outDir, 'routers', `index.ts`), undefined, { overwrite: true, }); @@ -110,7 +123,6 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF. }); appRouter.formatText(); - await project.save(); } function generateModelCreateRouter( @@ -133,6 +145,7 @@ function generateModelCreateRouter( ]); generateRouterSchemaImports(modelRouter, model); + generateHelperImport(modelRouter); modelRouter .addFunction({ @@ -162,3 +175,108 @@ function generateModelCreateRouter( modelRouter.formatText(); } + +function createHelper(outDir: string) { + const sf = project.createSourceFile(path.resolve(outDir, 'helper.ts'), undefined, { + overwrite: true, + }); + + sf.addStatements(`import { TRPCError } from '@trpc/server';`); + sf.addStatements(`import { isPrismaClientKnownRequestError } from '${RUNTIME_PACKAGE}';`); + + const checkMutate = sf.addFunction({ + name: 'checkMutate', + typeParameters: [{ name: 'T' }], + parameters: [ + { + name: 'promise', + type: 'Promise', + }, + ], + isAsync: true, + isExported: true, + returnType: 'Promise', + }); + + checkMutate.setBodyText( + `try { + return await promise; + } catch (err: any) { + if (isPrismaClientKnownRequestError(err)) { + if (err.code === 'P2004') { + if (err.meta?.reason === '${CrudFailureReason.RESULT_NOT_READABLE}') { + // unable to readback data + return undefined; + } else { + // rejected by policy + throw new TRPCError({ + code: 'FORBIDDEN', + message: err.message, + cause: err, + }); + } + } else { + // request error + throw new TRPCError({ + code: 'BAD_REQUEST', + message: err.message, + cause: err, + }); + } + } else { + throw err; + } + } + ` + ); + checkMutate.formatText(); + + const checkRead = sf.addFunction({ + name: 'checkRead', + typeParameters: [{ name: 'T' }], + parameters: [ + { + name: 'promise', + type: 'Promise', + }, + ], + isAsync: true, + isExported: true, + returnType: 'Promise', + }); + + checkRead.setBodyText( + `try { + return await promise; + } catch (err: any) { + if (isPrismaClientKnownRequestError(err)) { + if (err.code === 'P2004') { + // rejected by policy + throw new TRPCError({ + code: 'FORBIDDEN', + message: err.message, + cause: err, + }); + } else if (err.code === 'P2025') { + // not found + throw new TRPCError({ + code: 'NOT_FOUND', + message: err.message, + cause: err, + }); + } else { + // request error + throw new TRPCError({ + code: 'BAD_REQUEST', + message: err.message, + cause: err, + }) + } + } else { + throw err; + } + } + ` + ); + checkRead.formatText(); +} diff --git a/packages/plugins/trpc/src/helpers.ts b/packages/plugins/trpc/src/helpers.ts index b7be4dc29..3972c79e0 100644 --- a/packages/plugins/trpc/src/helpers.ts +++ b/packages/plugins/trpc/src/helpers.ts @@ -1,22 +1,7 @@ import { DMMF } from '@prisma/generator-helper'; -import { CrudFailureReason } from '@zenstackhq/sdk'; import { CodeBlockWriter, SourceFile } from 'ts-morph'; import { uncapitalizeFirstLetter } from './utils/uncapitalizeFirstLetter'; -export const generatetRPCImport = (sourceFile: SourceFile) => { - sourceFile.addImportDeclaration({ - moduleSpecifier: '@trpc/server', - namespaceImport: 'trpc', - }); -}; - -export const generateRouterImport = (sourceFile: SourceFile, modelNamePlural: string, modelNameCamelCase: string) => { - sourceFile.addImportDeclaration({ - moduleSpecifier: `./${modelNameCamelCase}.router`, - namedImports: [`${modelNamePlural}Router`], - }); -}; - export function generateProcedure( writer: CodeBlockWriter, opType: string, @@ -29,24 +14,15 @@ export function generateProcedure( if (procType === 'query') { writer.write(` - ${opType}: procedure.input(${typeName}).query(({ctx, input}) => db(ctx).${uncapitalizeFirstLetter( + ${opType}: procedure.input(${typeName}).query(({ctx, input}) => checkRead(db(ctx).${uncapitalizeFirstLetter( modelName - )}.${prismaMethod}(input)), + )}.${prismaMethod}(input))), `); } else if (procType === 'mutation') { writer.write(` - ${opType}: procedure.input(${typeName}).mutation(async ({ctx, input}) => { - try { - return await db(ctx).${uncapitalizeFirstLetter(modelName)}.${prismaMethod}(input); - } catch (err: any) { - if (err.code === 'P2004' && err.meta?.reason === '${CrudFailureReason.RESULT_NOT_READABLE}') { - // unable to readback data - return undefined; - } else { - throw err; - } - } - }), + ${opType}: procedure.input(${typeName}).mutation(async ({ctx, input}) => checkMutate(db(ctx).${uncapitalizeFirstLetter( + modelName + )}.${prismaMethod}(input))), `); } } @@ -55,6 +31,10 @@ export function generateRouterSchemaImports(sourceFile: SourceFile, name: string sourceFile.addStatements(`import { ${name}Schema } from '../schemas/${name}.schema';`); } +export function generateHelperImport(sourceFile: SourceFile) { + sourceFile.addStatements(`import { checkRead, checkMutate } from '../helper';`); +} + export const getInputTypeByOpName = (opName: string, modelName: string) => { let inputType; switch (opName) { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index d655b67cf..4b7886b2a 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index e9cb9982f..50d80736f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "author": { "name": "ZenStack Team" }, @@ -66,7 +66,7 @@ "main": "./bundle/extension.js", "scripts": { "vscode:publish": "vsce publish --no-dependencies", - "vscode:prepublish": "pnpm lint && pnpm bundle", + "vscode:prepublish": "pnpm bundle", "vscode:package": "vsce package --no-dependencies", "clean": "rimraf bundle dist", "build": "pnpm clean && pnpm lint && tsc && copyfiles -F \"bin/*\" dist && copyfiles ./README-global.md ./LICENSE ./package.json dist && renamer --replace \"README.md\" dist/README-global.md && copyfiles -u 1 \"src/res/*\" dist && node build/post-build.js", @@ -92,6 +92,7 @@ "colors": "1.4.0", "commander": "^8.3.0", "cuid": "^2.1.8", + "get-latest-version": "^5.0.1", "langium": "1.1.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", @@ -108,7 +109,7 @@ "vscode-languageserver-textdocument": "^1.0.7", "vscode-uri": "^3.0.6", "zod": "^3.19.1", - "get-latest-version": "^5.0.1" + "zod-validation-error": "^0.2.1" }, "devDependencies": { "@types/async-exit-hook": "^2.0.0", @@ -130,7 +131,6 @@ "eslint": "^8.27.0", "eslint-plugin-jest": "^27.1.7", "jest": "^29.2.1", - "langium-cli": "^1.0.0", "prisma": "^4.0.0", "renamer": "^4.0.0", "rimraf": "^3.0.2", diff --git a/packages/schema/src/cli/config.ts b/packages/schema/src/cli/config.ts new file mode 100644 index 000000000..a9ac8a2b3 --- /dev/null +++ b/packages/schema/src/cli/config.ts @@ -0,0 +1,40 @@ +import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk'; +import fs from 'fs'; +import z from 'zod'; +import { fromZodError } from 'zod-validation-error'; +import { CliError } from './cli-error'; + +const schema = z + .object({ + guardFieldName: z.string().default(GUARD_FIELD_NAME), + transactionFieldName: z.string().default(TRANSACTION_FIELD_NAME), + }) + .strict(); + +export type ConfigType = z.infer; + +export let config: ConfigType = schema.parse({}); + +/** + * Loads and validates CLI configuration file. + * @returns + */ +export function loadConfig(filename: string) { + if (!fs.existsSync(filename)) { + return; + } + + let content: unknown; + try { + content = JSON.parse(fs.readFileSync(filename, 'utf-8')); + } catch { + throw new CliError(`Config is not a valid JSON file: ${filename}`); + } + + const parsed = schema.safeParse(content); + if (!parsed.success) { + throw new CliError(`Config file ${filename} is not valid: ${fromZodError(parsed.error)}`); + } + + config = parsed.data; +} diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index db5ee0939..f9f402d16 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -2,16 +2,20 @@ import { ZModelLanguageMetaData } from '@zenstackhq/language/module'; import colors from 'colors'; import { Command, Option } from 'commander'; +import fs from 'fs'; import * as semver from 'semver'; import telemetry from '../telemetry'; import { PackageManagers } from '../utils/pkg-utils'; import { getVersion } from '../utils/version-utils'; import { CliError } from './cli-error'; import { dumpInfo, initProject, runPlugins } from './cli-util'; +import { loadConfig } from './config'; // required minimal version of Prisma export const requiredPrismaVersion = '4.0.0'; +const DEFAULT_CONFIG_FILE = 'zenstack.config.json'; + export const initAction = async ( projectPath: string, options: { @@ -97,6 +101,8 @@ export function createProgram() { './schema.zmodel' ); + const configOption = new Option('-c, --config [file]', 'config file'); + const pmOption = new Option('-p, --package-manager ', 'package manager to use').choices([ 'npm', 'yarn', @@ -114,6 +120,7 @@ export function createProgram() { program .command('init') .description('Initialize an existing project for ZenStack.') + .addOption(configOption) .addOption(pmOption) .addOption(new Option('--prisma ', 'location of Prisma schema file to bootstrap from')) .addOption(new Option('--tag [tag]', 'the NPM package tag to use when installing dependencies')) @@ -124,9 +131,27 @@ export function createProgram() { .command('generate') .description('Run code generation.') .addOption(schemaOption) + .addOption(configOption) .addOption(pmOption) .addOption(noDependencyCheck) .action(generateAction); + + // make sure config is loaded before actions run + program.hook('preAction', async (_, actionCommand) => { + let configFile: string | undefined = actionCommand.opts().config; + if (!configFile && fs.existsSync(DEFAULT_CONFIG_FILE)) { + configFile = DEFAULT_CONFIG_FILE; + } + + if (configFile) { + if (fs.existsSync(configFile)) { + loadConfig(configFile); + } else { + throw new CliError(`Config file ${configFile} not found`); + } + } + }); + return program; } diff --git a/packages/schema/src/cli/plugin-runner.ts b/packages/schema/src/cli/plugin-runner.ts index 2800855dd..3cc375216 100644 --- a/packages/schema/src/cli/plugin-runner.ts +++ b/packages/schema/src/cli/plugin-runner.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import { DMMF } from '@prisma/generator-helper'; +import type { DMMF } from '@prisma/generator-helper'; import { getDMMF } from '@prisma/internals'; import { isPlugin, Plugin } from '@zenstackhq/language/ast'; import { getLiteral, getLiteralArray, PluginError, PluginFunction, PluginOptions } from '@zenstackhq/sdk'; @@ -8,7 +8,8 @@ import fs from 'fs'; import ora from 'ora'; import path from 'path'; import telemetry from '../telemetry'; -import { Context } from '../types'; +import type { Context } from '../types'; +import { config } from './config'; /** * ZenStack code generator @@ -133,7 +134,7 @@ export class PluginRunner { plugin: name, }, async () => { - let result = run(context.schema, options, dmmf); + let result = run(context.schema, options, dmmf, config); if (result instanceof Promise) { result = await result; } diff --git a/packages/schema/src/language-server/zmodel-linker.ts b/packages/schema/src/language-server/zmodel-linker.ts index c98c0890c..16748b119 100644 --- a/packages/schema/src/language-server/zmodel-linker.ts +++ b/packages/schema/src/language-server/zmodel-linker.ts @@ -66,10 +66,6 @@ export class ZModelLinker extends DefaultLinker { //#region Reference linking async link(document: LangiumDocument, cancelToken = CancellationToken.None): Promise { - if (document.parseResult.lexerErrors?.length > 0 || document.parseResult.parserErrors?.length > 0) { - return; - } - for (const node of streamContents(document.parseResult.value)) { await interruptAndCheck(cancelToken); this.resolve(node, document); diff --git a/packages/schema/src/language-server/zmodel-workspace-manager.ts b/packages/schema/src/language-server/zmodel-workspace-manager.ts index b4146cfd0..79b5bfb5e 100644 --- a/packages/schema/src/language-server/zmodel-workspace-manager.ts +++ b/packages/schema/src/language-server/zmodel-workspace-manager.ts @@ -1,13 +1,17 @@ -import { DefaultWorkspaceManager, LangiumDocument } from 'langium'; +import { isPlugin, Model } from '@zenstackhq/language/ast'; +import { getLiteral } from '@zenstackhq/sdk'; +import { DefaultWorkspaceManager, interruptAndCheck, LangiumDocument } from 'langium'; import path from 'path'; -import { WorkspaceFolder } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; -import { STD_LIB_MODULE_NAME } from './constants'; +import { CancellationToken, WorkspaceFolder } from 'vscode-languageserver'; +import { URI, Utils } from 'vscode-uri'; +import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from './constants'; /** * Custom Langium WorkspaceManager implementation which automatically loads stdlib.zmodel */ export default class ZModelWorkspaceManager extends DefaultWorkspaceManager { + public pluginModels = new Set(); + protected async loadAdditionalDocuments( _folders: WorkspaceFolder[], _collector: (document: LangiumDocument) => void @@ -18,4 +22,115 @@ export default class ZModelWorkspaceManager extends DefaultWorkspaceManager { const stdlib = this.langiumDocuments.getOrCreateDocument(stdLibUri); _collector(stdlib); } + + override async initializeWorkspace( + folders: WorkspaceFolder[], + cancelToken = CancellationToken.None + ): Promise { + const fileExtensions = this.serviceRegistry.all.flatMap((e) => e.LanguageMetaData.fileExtensions); + const documents: LangiumDocument[] = []; + const collector = (document: LangiumDocument) => { + documents.push(document); + if (!this.langiumDocuments.hasDocument(document.uri)) { + this.langiumDocuments.addDocument(document); + } + }; + + await this.loadAdditionalDocuments(folders, collector); + await Promise.all( + folders + .map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI]) + .map(async (entry) => this.traverseFolder(...entry, fileExtensions, collector)) + ); + + // find plugin models + documents.forEach((doc) => { + const parsed = doc.parseResult.value as Model; + parsed.declarations.forEach((decl) => { + if (isPlugin(decl)) { + const providerField = decl.fields.find((f) => f.name === 'provider'); + if (providerField) { + const provider = getLiteral(providerField.value); + if (provider) { + this.pluginModels.add(provider); + } + } + } + }); + }); + + if (this.pluginModels.size > 0) { + console.log(`Used plugin documents: ${Array.from(this.pluginModels)}`); + + // the loaded plugin models would be removed from the set + const unLoadedPluginModels = new Set(this.pluginModels); + + await Promise.all( + folders + .map((wf) => [wf, this.getRootFolder(wf)] as [WorkspaceFolder, URI]) + .map(async (entry) => this.loadPluginModels(...entry, unLoadedPluginModels, collector)) + ); + + if (unLoadedPluginModels.size > 0) { + console.warn(`The following plugin documents could not be loaded: ${Array.from(unLoadedPluginModels)}`); + } + } + + // Only after creating all documents do we check whether we need to cancel the initialization + // The document builder will later pick up on all unprocessed documents + await interruptAndCheck(cancelToken); + await this.documentBuilder.build(documents, undefined, cancelToken); + } + + protected async loadPluginModels( + workspaceFolder: WorkspaceFolder, + folderPath: URI, + pluginModels: Set, + collector: (document: LangiumDocument) => void + ): Promise { + const content = await ( + await this.fileSystemProvider.readDirectory(folderPath) + ).sort((a, b) => { + // make sure the node_moudules folder is always the first one to be checked + // so it could be early exited if the plugin is found + if (a.isDirectory && b.isDirectory) { + const aName = Utils.basename(a.uri); + if (aName === 'node_modules') { + return -1; + } else { + return 1; + } + } else { + return 0; + } + }); + + for (const entry of content) { + if (entry.isDirectory) { + const name = Utils.basename(entry.uri); + if (name === 'node_modules') { + for (const plugin of Array.from(pluginModels)) { + const path = Utils.joinPath(entry.uri, plugin, PLUGIN_MODULE_NAME); + try { + this.fileSystemProvider.readFileSync(path); + const document = this.langiumDocuments.getOrCreateDocument(path); + collector(document); + console.log(`Adding plugin document from ${path.path}`); + + pluginModels.delete(plugin); + // early exit if all plugins are loaded + if (pluginModels.size === 0) { + return; + } + } catch { + // no-op. The module might be found in another node_modules folder + // will show the warning message eventually if not found + } + } + } else { + await this.loadPluginModels(workspaceFolder, entry.uri, pluginModels, collector); + } + } + } + } } diff --git a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts index a42c6560c..c1979d590 100644 --- a/packages/schema/src/plugins/access-policy/policy-guard-generator.ts +++ b/packages/schema/src/plugins/access-policy/policy-guard-generator.ts @@ -14,7 +14,15 @@ import { Model, } from '@zenstackhq/language/ast'; import type { PolicyKind, PolicyOperationKind } from '@zenstackhq/runtime'; -import { getDataModels, getLiteral, GUARD_FIELD_NAME, PluginError, PluginOptions, resolved } from '@zenstackhq/sdk'; +import { + getDataModels, + getLiteral, + GUARD_FIELD_NAME, + PluginError, + PluginOptions, + resolved, + RUNTIME_PACKAGE, +} from '@zenstackhq/sdk'; import { camelCase } from 'change-case'; import { streamAllContents } from 'langium'; import path from 'path'; @@ -22,7 +30,7 @@ import { FunctionDeclaration, Project, SourceFile, VariableDeclarationKind } fro import { name } from '.'; import { isFromStdlib } from '../../language-server/utils'; import { analyzePolicies, getIdFields } from '../../utils/ast-utils'; -import { ALL_OPERATION_KINDS, getDefaultOutputFolder, RUNTIME_PACKAGE } from '../plugin-utils'; +import { ALL_OPERATION_KINDS, getDefaultOutputFolder } from '../plugin-utils'; import { ExpressionWriter } from './expression-writer'; import { isFutureExpr } from './utils'; import { ZodSchemaGenerator } from './zod-schema-generator'; diff --git a/packages/schema/src/plugins/plugin-utils.ts b/packages/schema/src/plugins/plugin-utils.ts index 7d3cc1320..d10be7c94 100644 --- a/packages/schema/src/plugins/plugin-utils.ts +++ b/packages/schema/src/plugins/plugin-utils.ts @@ -2,7 +2,6 @@ import type { PolicyOperationKind } from '@zenstackhq/runtime'; import fs from 'fs'; import path from 'path'; -export const RUNTIME_PACKAGE = '@zenstackhq/runtime'; export const ALL_OPERATION_KINDS: PolicyOperationKind[] = ['create', 'update', 'postUpdate', 'read', 'delete']; /** diff --git a/packages/schema/src/plugins/prisma/index.ts b/packages/schema/src/plugins/prisma/index.ts index c9bb7ac08..6ffc0a6a8 100644 --- a/packages/schema/src/plugins/prisma/index.ts +++ b/packages/schema/src/plugins/prisma/index.ts @@ -1,9 +1,15 @@ +import type { DMMF } from '@prisma/generator-helper'; import { Model } from '@zenstackhq/language/ast'; import { PluginOptions } from '@zenstackhq/sdk'; import PrismaSchemaGenerator from './schema-generator'; export const name = 'Prisma'; -export default async function run(model: Model, options: PluginOptions) { - return new PrismaSchemaGenerator().generate(model, options); +export default async function run( + model: Model, + options: PluginOptions, + _dmmf?: DMMF.Document, + config?: Record +) { + return new PrismaSchemaGenerator().generate(model, options, config); } diff --git a/packages/schema/src/plugins/prisma/schema-generator.ts b/packages/schema/src/plugins/prisma/schema-generator.ts index 4066270e3..6d2c32e9b 100644 --- a/packages/schema/src/plugins/prisma/schema-generator.ts +++ b/packages/schema/src/plugins/prisma/schema-generator.ts @@ -69,7 +69,7 @@ export default class PrismaSchemaGenerator { `; - async generate(model: Model, options: PluginOptions) { + async generate(model: Model, options: PluginOptions, config?: Record) { const prisma = new PrismaModel(); for (const decl of model.declarations) { @@ -83,7 +83,7 @@ export default class PrismaSchemaGenerator { break; case DataModel: - this.generateModel(prisma, decl as DataModel); + this.generateModel(prisma, decl as DataModel, config); break; case GeneratorDecl: @@ -191,26 +191,33 @@ export default class PrismaSchemaGenerator { ); } - private generateModel(prisma: PrismaModel, decl: DataModel) { + private generateModel(prisma: PrismaModel, decl: DataModel, config?: Record) { const model = prisma.addModel(decl.name); for (const field of decl.fields) { this.generateModelField(model, field); } // add an "zenstack_guard" field for dealing with boolean conditions - model.addField(GUARD_FIELD_NAME, 'Boolean', [ + const guardField = model.addField(GUARD_FIELD_NAME, 'Boolean', [ new PrismaFieldAttribute('@default', [ new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('Boolean', true)), ]), ]); + if (config?.guardFieldName && config?.guardFieldName !== GUARD_FIELD_NAME) { + // generate a @map to rename field in the database + guardField.addAttribute('@map', [ + new PrismaAttributeArg(undefined, new PrismaAttributeArgValue('String', config.guardFieldName)), + ]); + } + const { allowAll, denyAll, hasFieldValidation } = analyzePolicies(decl); if ((!allowAll && !denyAll) || hasFieldValidation) { // generate auxiliary fields for policy check // add an "zenstack_transaction" field for tracking records created/updated with nested writes - model.addField(TRANSACTION_FIELD_NAME, 'String?'); + const transactionField = model.addField(TRANSACTION_FIELD_NAME, 'String?'); // create an index for "zenstack_transaction" field model.addAttribute('@@index', [ @@ -221,6 +228,16 @@ export default class PrismaSchemaGenerator { ]) ), ]); + + if (config?.transactionFieldName && config?.transactionFieldName !== TRANSACTION_FIELD_NAME) { + // generate a @map to rename field in the database + transactionField.addAttribute('@map', [ + new PrismaAttributeArg( + undefined, + new PrismaAttributeArgValue('String', config.transactionFieldName) + ), + ]); + } } for (const attr of decl.attributes.filter((attr) => this.isPrismaAttribute(attr))) { diff --git a/packages/schema/tests/cli/cli.test.ts b/packages/schema/tests/cli/command.test.ts similarity index 88% rename from packages/schema/tests/cli/cli.test.ts rename to packages/schema/tests/cli/command.test.ts index f165c0803..73f735976 100644 --- a/packages/schema/tests/cli/cli.test.ts +++ b/packages/schema/tests/cli/command.test.ts @@ -8,7 +8,7 @@ import * as tmp from 'tmp'; import { createProgram } from '../../src/cli'; import { execSync } from '../../src/utils/exec-utils'; -describe('CLI Tests', () => { +describe('CLI Command Tests', () => { let projDir: string; let origDir: string; @@ -37,7 +37,7 @@ describe('CLI Tests', () => { createNpmrc(); const program = createProgram(); - program.parse(['init', '--tag', 'latest'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); @@ -53,7 +53,7 @@ describe('CLI Tests', () => { createNpmrc(); const program = createProgram(); - program.parse(['init', '--tag', 'latest'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); @@ -69,7 +69,7 @@ describe('CLI Tests', () => { createNpmrc(); const program = createProgram(); - program.parse(['init', '--tag', 'latest'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/schema.prisma', 'utf-8')); @@ -86,7 +86,7 @@ describe('CLI Tests', () => { fs.renameSync('prisma/schema.prisma', 'prisma/my.prisma'); const program = createProgram(); - program.parse(['init', '--tag', 'latest', '--prisma', 'prisma/my.prisma'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest', '--prisma', 'prisma/my.prisma'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(fs.readFileSync('prisma/my.prisma', 'utf-8')); }); @@ -96,7 +96,7 @@ describe('CLI Tests', () => { fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); createNpmrc(); const program = createProgram(); - program.parse(['init', '--tag', 'latest'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toBeTruthy(); }); @@ -111,7 +111,7 @@ describe('CLI Tests', () => { fs.writeFileSync('schema.zmodel', origZModelContent); createNpmrc(); const program = createProgram(); - program.parse(['init', '--tag', 'latest'], { from: 'user' }); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(origZModelContent); }); }); diff --git a/packages/schema/tests/cli/config.test.ts b/packages/schema/tests/cli/config.test.ts new file mode 100644 index 000000000..9259d1c42 --- /dev/null +++ b/packages/schema/tests/cli/config.test.ts @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/// + +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import { createProgram } from '../../src/cli'; +import { CliError } from '../../src/cli/cli-error'; +import { config } from '../../src/cli/config'; +import { GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '@zenstackhq/sdk'; + +describe('CLI Config Tests', () => { + let projDir: string; + let origDir: string; + + beforeEach(() => { + origDir = process.cwd(); + const r = tmp.dirSync(); + projDir = r.name; + console.log(`Project dir: ${projDir}`); + process.chdir(projDir); + }); + + afterEach(() => { + fs.rmSync(projDir, { recursive: true, force: true }); + process.chdir(origDir); + }); + + it('invalid default config', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + fs.writeFileSync('zenstack.config.json', JSON.stringify({ abc: 'def' })); + + const program = createProgram(); + await expect(program.parseAsync(['init', '--tag', 'latest'], { from: 'user' })).rejects.toBeInstanceOf( + CliError + ); + }); + + it('valid default config empty', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + fs.writeFileSync('zenstack.config.json', JSON.stringify({})); + + const program = createProgram(); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); + + // custom config + expect(config.guardFieldName).toBe(GUARD_FIELD_NAME); + + // default value + expect(config.transactionFieldName).toBe(TRANSACTION_FIELD_NAME); + }); + + it('valid default config non-empty', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + fs.writeFileSync( + 'zenstack.config.json', + JSON.stringify({ guardFieldName: 'myGuardField', transactionFieldName: 'myTransactionField' }) + ); + + const program = createProgram(); + await program.parseAsync(['init', '--tag', 'latest'], { from: 'user' }); + + // custom config + expect(config.guardFieldName).toBe('myGuardField'); + + // default value + expect(config.transactionFieldName).toBe('myTransactionField'); + }); + + it('config not found', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + const program = createProgram(); + await expect( + program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }) + ).rejects.toBeInstanceOf(CliError); + }); + + it('valid custom config file', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + fs.writeFileSync('my.config.json', JSON.stringify({ guardFieldName: 'myGuardField' })); + const program = createProgram(); + await program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }); + + // custom config + expect(config.guardFieldName).toBe('myGuardField'); + + // default value + expect(config.transactionFieldName).toBe(TRANSACTION_FIELD_NAME); + }); + + it('invalid custom config file', async () => { + fs.writeFileSync('package.json', JSON.stringify({ name: 'my app', version: '1.0.0' })); + fs.writeFileSync('my.config.json', JSON.stringify({ abc: 'def' })); + const program = createProgram(); + await expect( + program.parseAsync(['init', '--tag', 'latest', '--config', 'my.config.json'], { from: 'user' }) + ).rejects.toBeInstanceOf(CliError); + }); +}); diff --git a/packages/schema/tests/generator/prisma-generator.test.ts b/packages/schema/tests/generator/prisma-generator.test.ts index 263c335e6..d763376b1 100644 --- a/packages/schema/tests/generator/prisma-generator.test.ts +++ b/packages/schema/tests/generator/prisma-generator.test.ts @@ -191,4 +191,35 @@ describe('Prisma generator test', () => { expect(content).toContain('@@schema("base")'); expect(content).toContain('schemas = ["base","transactional"]'); }); + + it('custom aux field names', async () => { + const model = await loadModel(` + datasource db { + provider = 'postgresql' + url = env('URL') + } + + model Foo { + id String @id + value Int + @@allow('create', value > 0) + } + `); + + const { name } = tmp.fileSync({ postfix: '.prisma' }); + await new PrismaSchemaGenerator().generate( + model, + { + provider: '@core/prisma', + schemaPath: 'schema.zmodel', + output: name, + }, + { guardFieldName: 'myGuardField', transactionFieldName: 'myTransactionField' } + ); + + const content = fs.readFileSync(name, 'utf-8'); + await getDMMF({ datamodel: content }); + expect(content).toContain('@map("myGuardField")'); + expect(content).toContain('@map("myTransactionField")'); + }); }); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index cafac42b1..400fc7a83 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/sdk/src/constants.ts b/packages/sdk/src/constants.ts index 038a7cebe..da7d620e4 100644 --- a/packages/sdk/src/constants.ts +++ b/packages/sdk/src/constants.ts @@ -22,3 +22,8 @@ export enum CrudFailureReason { */ RESULT_NOT_READABLE = 'RESULT_NOT_READABLE', } + +/** + * @zenstackhq/runtime package name + */ +export const RUNTIME_PACKAGE = '@zenstackhq/runtime'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index bf9d2c3c0..bb967d234 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -17,7 +17,8 @@ export type PluginOptions = { provider?: string; schemaPath: string } & Record ) => Promise | string[] | Promise | void; /** diff --git a/packages/server/package.json b/packages/server/package.json index 0667d4708..7779ffec1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/server/src/express/middleware.ts b/packages/server/src/express/middleware.ts index 54d6e86be..214b30b64 100644 --- a/packages/server/src/express/middleware.ts +++ b/packages/server/src/express/middleware.ts @@ -19,7 +19,8 @@ export interface MiddlewareOptions { logger?: LoggerConfig; /** - * Zod schemas for validating request input. Pass `true` to load from standard location (need to enable `@core/zod` plugin in schema.zmodel). + * Zod schemas for validating request input. Pass `true` to load from standard location + * (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation. */ zodSchemas?: ModelZodSchema | boolean; } diff --git a/packages/server/src/fastify/plugin.ts b/packages/server/src/fastify/plugin.ts index 4999b5d8a..c22d64a29 100644 --- a/packages/server/src/fastify/plugin.ts +++ b/packages/server/src/fastify/plugin.ts @@ -25,7 +25,8 @@ export interface PluginOptions { logger?: LoggerConfig; /** - * Zod schemas for validating request input. Pass `true` to load from standard location (need to enable `@core/zod` plugin in schema.zmodel). + * Zod schemas for validating request input. Pass `true` to load from standard location + * (need to enable `@core/zod` plugin in schema.zmodel) or omit to disable input validation. */ zodSchemas?: ModelZodSchema | boolean; } diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 0a86153df..0a68046f5 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 9aa684d95..0f5f3c213 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -34,13 +34,17 @@ export function run(cmd: string, env?: Record, cwd?: string) { }); } +function normalizePath(p: string) { + return p ? p.split(path.sep).join(path.posix.sep) : p; +} + export function getWorkspaceRoot(start: string) { - let curr = start; + let curr = normalizePath(start); while (curr && curr !== '/') { if (fs.existsSync(path.join(curr, 'pnpm-workspace.yaml'))) { return curr; } else { - curr = path.dirname(curr); + curr = normalizePath(path.dirname(curr)); } } return undefined; @@ -93,9 +97,11 @@ export async function loadSchema( const { name: projectRoot } = tmp.dirSync(); const root = getWorkspaceRoot(__dirname); + if (!root) { throw new Error('Could not find workspace root'); } + console.log('Workspace root:', root); const pkgContent = fs.readFileSync(path.join(__dirname, 'package.template.json'), { encoding: 'utf-8' }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1becdcd2a..6d8bf8fd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,12 @@ importers: yaml: specifier: ^2.2.1 version: 2.2.1 + zod: + specifier: ^3.19.1 + version: 3.19.1 + zod-validation-error: + specifier: ^0.2.1 + version: 0.2.1(zod@3.19.1) devDependencies: '@prisma/internals': specifier: ^4.7.1 @@ -415,6 +421,9 @@ importers: zod: specifier: ^3.19.1 version: 3.19.1 + zod-validation-error: + specifier: ^0.2.1 + version: 0.2.1(zod@3.19.1) devDependencies: '@types/async-exit-hook': specifier: ^2.0.0 @@ -473,9 +482,6 @@ importers: jest: specifier: ^29.2.1 version: 29.2.1(@types/node@14.18.32)(ts-node@10.9.1) - langium-cli: - specifier: ^1.0.0 - version: 1.0.0 prisma: specifier: ^4.0.0 version: 4.7.0 @@ -4278,11 +4284,6 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - dev: true - /atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -5023,6 +5024,7 @@ packages: /commander@8.3.0: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + dev: false /commander@9.4.1: resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==} @@ -6584,16 +6586,6 @@ packages: universalify: 0.1.2 dev: true - /fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.10 - jsonfile: 6.1.0 - universalify: 2.0.0 - dev: true - /fs-jetpack@5.1.0: resolution: {integrity: sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==} dependencies: @@ -9281,19 +9273,6 @@ packages: engines: {node: '>=6'} dev: true - /langium-cli@1.0.0: - resolution: {integrity: sha512-F5RZu3/PrlRfebMM6irtHDk2cj5PdUzM4y3M5kNu8k5IC3gIhc4QN+aDD2XHYz8hQSA+bCRUIDwCdhy/7VN4cg==} - engines: {node: '>=12.0.0'} - hasBin: true - dependencies: - chalk: 4.1.2 - commander: 8.3.0 - fs-extra: 9.1.0 - jsonschema: 1.4.1 - langium: 1.0.1 - lodash: 4.17.21 - dev: true - /langium-cli@1.1.0: resolution: {integrity: sha512-vnv037FHqXqMeNiNF90v47VrJGiJPzH721UIbbHcu6Nfx0C1UC6SmQhGHtZIDRovT5qJsiXRIPDTZYrIkm4KJQ==} engines: {node: '>=14.0.0'} @@ -9307,17 +9286,6 @@ packages: lodash: 4.17.21 dev: true - /langium@1.0.1: - resolution: {integrity: sha512-9w5NRsspYOOXV56q1EhXeFJWkboAVKZDzIEqPLraMQPQy6fvq104wlVwgvF6w9H4IcCpDHCsHJLsfzQMWBsjAA==} - engines: {node: '>=14.0.0'} - dependencies: - chevrotain: 10.4.2 - chevrotain-allstar: 0.1.4 - vscode-languageserver: 8.0.2 - vscode-languageserver-textdocument: 1.0.7 - vscode-uri: 3.0.7 - dev: true - /langium@1.1.0: resolution: {integrity: sha512-TsWY/DIOR73se9/YaMQZpvfFWWrhWP0FQS9MrpxWEnMJR0FoKVpMF1thPWXZexLSfyEm1pn2oYzCdW4KUBqXxA==} engines: {node: '>=14.0.0'} @@ -12058,6 +12026,7 @@ packages: /vscode-languageserver-textdocument@1.0.7: resolution: {integrity: sha512-bFJH7UQxlXT8kKeyiyu41r22jCZXG8kuuVVA33OEJn1diWOZK5n8zBSPZFHVBOu8kXZ6h0LIRhf5UnCo61J4Hg==} + dev: false /vscode-languageserver-textdocument@1.0.8: resolution: {integrity: sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==} diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index dbc68fd38..30f26aea4 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -126,7 +126,7 @@ }, "../../../packages/runtime/dist": { "name": "@zenstackhq/runtime", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "license": "MIT", "dependencies": { "@types/bcryptjs": "^2.4.2", @@ -158,14 +158,13 @@ }, "../../../packages/schema/dist": { "name": "zenstack", - "version": "1.0.0-alpha.98", + "version": "1.0.0-alpha.99", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@prisma/generator-helper": "^4.7.1", - "@prisma/internals": "^4.7.1", + "@prisma/generator-helper": "^4.0.0", + "@prisma/internals": "^4.0.0", "@zenstackhq/language": "workspace:*", - "@zenstackhq/runtime": "workspace:*", "@zenstackhq/sdk": "workspace:*", "async-exit-hook": "^2.0.1", "change-case": "^4.1.2", @@ -173,12 +172,12 @@ "colors": "1.4.0", "commander": "^8.3.0", "cuid": "^2.1.8", + "get-latest-version": "^5.0.1", "langium": "1.1.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", "ora": "^5.4.1", "pluralize": "^8.0.0", - "prisma": "~4.7.0", "promisify": "^0.0.3", "semver": "^7.3.8", "sleep-promise": "^9.1.0", @@ -189,7 +188,8 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", "vscode-uri": "^3.0.6", - "zod": "^3.19.1" + "zod": "^3.19.1", + "zod-validation-error": "^0.2.1" }, "bin": { "zenstack": "bin/cli" @@ -205,6 +205,7 @@ "@types/vscode": "^1.56.0", "@typescript-eslint/eslint-plugin": "^5.42.0", "@typescript-eslint/parser": "^5.42.0", + "@zenstackhq/runtime": "workspace:*", "@zenstackhq/testtools": "workspace:*", "concurrently": "^7.4.0", "copyfiles": "^2.4.1", @@ -213,7 +214,7 @@ "eslint": "^8.27.0", "eslint-plugin-jest": "^27.1.7", "jest": "^29.2.1", - "langium-cli": "^1.0.0", + "prisma": "^4.0.0", "renamer": "^4.0.0", "rimraf": "^3.0.2", "tmp": "^0.2.1", @@ -226,6 +227,9 @@ }, "engines": { "vscode": "^1.56.0" + }, + "peerDependencies": { + "prisma": "^4.0.0" } }, "node_modules/@prisma/client": { @@ -397,8 +401,8 @@ "zenstack": { "version": "file:../../../packages/schema/dist", "requires": { - "@prisma/generator-helper": "^4.7.1", - "@prisma/internals": "^4.7.1", + "@prisma/generator-helper": "^4.0.0", + "@prisma/internals": "^4.0.0", "@types/async-exit-hook": "^2.0.0", "@types/jest": "^29.2.0", "@types/node": "^14.18.32", @@ -425,14 +429,14 @@ "esbuild": "^0.15.12", "eslint": "^8.27.0", "eslint-plugin-jest": "^27.1.7", + "get-latest-version": "^5.0.1", "jest": "^29.2.1", "langium": "1.1.0", - "langium-cli": "^1.0.0", "mixpanel": "^0.17.0", "node-machine-id": "^1.1.12", "ora": "^5.4.1", "pluralize": "^8.0.0", - "prisma": "~4.7.0", + "prisma": "^4.0.0", "promisify": "^0.0.3", "renamer": "^4.0.0", "rimraf": "^3.0.2", @@ -452,7 +456,8 @@ "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.7", "vscode-uri": "^3.0.6", - "zod": "^3.19.1" + "zod": "^3.19.1", + "zod-validation-error": "^0.2.1" } }, "zod": {