From 3b9e3567b81eec050f208ae5e97ae0c2e544ab0f Mon Sep 17 00:00:00 2001 From: Yiming Date: Sat, 8 Apr 2023 02:51:22 +0800 Subject: [PATCH] feat: improvements of openapi plugin (#335) --- packages/language/src/generated/ast.ts | 36 +- packages/language/src/generated/grammar.ts | 429 ++++++++++-------- packages/language/src/zmodel.langium | 60 +-- packages/plugins/openapi/package.json | 4 +- packages/plugins/openapi/src/generator.ts | 49 +- packages/plugins/openapi/src/meta.ts | 17 +- packages/plugins/openapi/src/schema.ts | 43 ++ .../plugins/openapi/tests/openapi.test.ts | 117 ++++- pnpm-lock.yaml | 6 + tests/integration/test-run/package-lock.json | 21 +- 10 files changed, 505 insertions(+), 277 deletions(-) create mode 100644 packages/plugins/openapi/src/schema.ts 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/plugins/openapi/package.json b/packages/plugins/openapi/package.json index 08e037ed5..02f998eb9 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -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/pnpm-lock.yaml b/pnpm-lock.yaml index 6a548c055..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 diff --git a/tests/integration/test-run/package-lock.json b/tests/integration/test-run/package-lock.json index dbc68fd38..12f5787fc 100644 --- a/tests/integration/test-run/package-lock.json +++ b/tests/integration/test-run/package-lock.json @@ -162,10 +162,9 @@ "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", @@ -205,6 +204,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 +213,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 +226,9 @@ }, "engines": { "vscode": "^1.56.0" + }, + "peerDependencies": { + "prisma": "^4.0.0" } }, "node_modules/@prisma/client": { @@ -397,8 +400,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 +428,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",