Skip to content

Commit dd62f8d

Browse files
authored
fix: openapi plugin bugs - relation handling and spec version (#317)
1 parent 30fa093 commit dd62f8d

File tree

4 files changed

+113
-26
lines changed

4 files changed

+113
-26
lines changed

packages/plugins/openapi/src/generator.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class OpenAPIGenerator {
5858
this.pruneComponents(components);
5959

6060
const openapi: OAPI.Document = {
61-
openapi: '3.1.0',
61+
openapi: this.getOption('specVersion', '3.1.0'),
6262
info: {
6363
title: this.getOption('title', 'ZenStack Generated API'),
6464
version: this.getOption('version', '1.0.0'),
@@ -180,6 +180,7 @@ export class OpenAPIGenerator {
180180
};
181181

182182
const definitions: OperationDefinition[] = [];
183+
const hasRelation = zmodel.fields.some((f) => isDataModel(f.$resolvedType?.decl));
183184

184185
if (ops['createOne']) {
185186
definitions.push({
@@ -191,7 +192,7 @@ export class OpenAPIGenerator {
191192
type: 'object',
192193
properties: {
193194
select: this.ref(`${model.name}Select`),
194-
include: this.ref(`${model.name}Include`),
195+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
195196
data: this.ref(`${model.name}CreateInput`),
196197
},
197198
},
@@ -233,7 +234,7 @@ export class OpenAPIGenerator {
233234
type: 'object',
234235
properties: {
235236
select: this.ref(`${model.name}Select`),
236-
include: this.ref(`${model.name}Include`),
237+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
237238
where: this.ref(`${model.name}WhereUniqueInput`),
238239
},
239240
},
@@ -254,7 +255,7 @@ export class OpenAPIGenerator {
254255
type: 'object',
255256
properties: {
256257
select: this.ref(`${model.name}Select`),
257-
include: this.ref(`${model.name}Include`),
258+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
258259
where: this.ref(`${model.name}WhereInput`),
259260
},
260261
},
@@ -275,7 +276,7 @@ export class OpenAPIGenerator {
275276
type: 'object',
276277
properties: {
277278
select: this.ref(`${model.name}Select`),
278-
include: this.ref(`${model.name}Include`),
279+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
279280
where: this.ref(`${model.name}WhereInput`),
280281
},
281282
},
@@ -296,7 +297,7 @@ export class OpenAPIGenerator {
296297
type: 'object',
297298
properties: {
298299
select: this.ref(`${model.name}Select`),
299-
include: this.ref(`${model.name}Include`),
300+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
300301
where: this.ref(`${model.name}WhereUniqueInput`),
301302
data: this.ref(`${model.name}UpdateInput`),
302303
},
@@ -338,7 +339,7 @@ export class OpenAPIGenerator {
338339
type: 'object',
339340
properties: {
340341
select: this.ref(`${model.name}Select`),
341-
include: this.ref(`${model.name}Include`),
342+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
342343
where: this.ref(`${model.name}WhereUniqueInput`),
343344
create: this.ref(`${model.name}CreateInput`),
344345
update: this.ref(`${model.name}UpdateInput`),
@@ -361,7 +362,7 @@ export class OpenAPIGenerator {
361362
type: 'object',
362363
properties: {
363364
select: this.ref(`${model.name}Select`),
364-
include: this.ref(`${model.name}Include`),
365+
include: hasRelation ? this.ref(`${model.name}Include`) : undefined,
365366
where: this.ref(`${model.name}WhereUniqueInput`),
366367
},
367368
},

packages/plugins/openapi/tests/openapi.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
/// <reference types="@types/jest" />
23

34
import OpenAPIParser from '@readme/openapi-parser';
45
import { loadZModelAndDmmf } from '@zenstackhq/testtools';
56
import * as tmp from 'tmp';
7+
import * as fs from 'fs';
68
import generate from '../src';
9+
import YAML from 'yaml';
10+
import { isPlugin, Model, Plugin } from '@zenstackhq/sdk/ast';
11+
import { getLiteral } from '@zenstackhq/sdk';
712

813
describe('Open API Plugin Tests', () => {
914
it('run plugin', async () => {
@@ -68,10 +73,15 @@ model Bar {
6873
`);
6974

7075
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
71-
generate(model, { schemaPath: modelFile, output }, dmmf);
76+
77+
const options = buildOptions(model, modelFile, output);
78+
generate(model, options, dmmf);
7279

7380
console.log('OpenAPI specification generated:', output);
7481

82+
const parsed = YAML.parse(fs.readFileSync(output, 'utf-8'));
83+
expect(parsed.openapi).toBe('3.1.0');
84+
7585
const api = await OpenAPIParser.validate(output);
7686
expect(api.paths?.['/user/findMany']?.['get']?.description).toBe('Find users matching the given conditions');
7787
const del = api.paths?.['/user/dodelete']?.['put'];
@@ -83,4 +93,73 @@ model Bar {
8393
expect(api.paths?.['/foo/findMany']).toBeUndefined();
8494
expect(api.paths?.['/bar/findMany']).toBeUndefined();
8595
});
96+
97+
it('options', async () => {
98+
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
99+
plugin openapi {
100+
provider = '${process.cwd()}/dist'
101+
specVersion = '3.0.0'
102+
title = 'My Awesome API'
103+
version = '1.0.0'
104+
description = 'awesome api'
105+
prefix = '/myapi'
106+
}
107+
108+
model User {
109+
id String @id
110+
}
111+
`);
112+
113+
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
114+
const options = buildOptions(model, modelFile, output);
115+
generate(model, options, dmmf);
116+
117+
console.log('OpenAPI specification generated:', output);
118+
119+
const parsed = YAML.parse(fs.readFileSync(output, 'utf-8'));
120+
expect(parsed.openapi).toBe('3.0.0');
121+
122+
const api = await OpenAPIParser.validate(output);
123+
expect(api.info).toEqual(
124+
expect.objectContaining({
125+
title: 'My Awesome API',
126+
version: '1.0.0',
127+
description: 'awesome api',
128+
})
129+
);
130+
131+
expect(api.paths?.['/myapi/user/findMany']).toBeTruthy();
132+
});
133+
134+
it('v3.1.0 fields', async () => {
135+
const { model, dmmf, modelFile } = await loadZModelAndDmmf(`
136+
plugin openapi {
137+
provider = '${process.cwd()}/dist'
138+
summary = 'awesome api'
139+
}
140+
141+
model User {
142+
id String @id
143+
}
144+
`);
145+
146+
const { name: output } = tmp.fileSync({ postfix: '.yaml' });
147+
const options = buildOptions(model, modelFile, output);
148+
generate(model, options, dmmf);
149+
150+
console.log('OpenAPI specification generated:', output);
151+
152+
const parsed = YAML.parse(fs.readFileSync(output, 'utf-8'));
153+
expect(parsed.openapi).toBe('3.1.0');
154+
155+
const api = await OpenAPIParser.validate(output);
156+
expect((api.info as any).summary).toEqual('awesome api');
157+
});
86158
});
159+
160+
function buildOptions(model: Model, modelFile: string, output: string) {
161+
const optionFields = model.declarations.find((d): d is Plugin => isPlugin(d))?.fields || [];
162+
const options: any = { schemaPath: modelFile, output };
163+
optionFields.forEach((f) => (options[f.name] = getLiteral(f.value)));
164+
return options;
165+
}

packages/schema/src/cli/plugin-runner.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
import { DMMF } from '@prisma/generator-helper';
33
import { getDMMF } from '@prisma/internals';
4-
import { Plugin, isPlugin } from '@zenstackhq/language/ast';
5-
import { PluginFunction, PluginOptions, getLiteral, getLiteralArray } from '@zenstackhq/sdk';
4+
import { isPlugin, Plugin } from '@zenstackhq/language/ast';
5+
import { getLiteral, getLiteralArray, PluginError, PluginFunction, PluginOptions } from '@zenstackhq/sdk';
66
import colors from 'colors';
77
import fs from 'fs';
88
import ora from 'ora';
99
import path from 'path';
1010
import telemetry from '../telemetry';
1111
import { Context } from '../types';
12-
import { CliError } from './cli-error';
1312

1413
/**
1514
* ZenStack code generator
@@ -44,9 +43,9 @@ export class PluginRunner {
4443
const options: PluginOptions = { schemaPath: context.schemaPath };
4544

4645
plugin.fields.forEach((f) => {
47-
const value = getLiteral(f.value) || getLiteralArray(f.value);
48-
if (!value) {
49-
throw new CliError(`Invalid plugin value for ${f.name}`);
46+
const value = getLiteral(f.value) ?? getLiteralArray(f.value);
47+
if (value === undefined) {
48+
throw new PluginError(`Invalid plugin value for ${f.name}`);
5049
}
5150
options[f.name] = value;
5251
});
@@ -58,12 +57,12 @@ export class PluginRunner {
5857
pluginModule = require(pluginModulePath);
5958
} catch (err) {
6059
console.error(`Unable to load plugin module ${pluginProvider}: ${pluginModulePath}, ${err}`);
61-
throw new CliError(`Unable to load plugin module ${pluginProvider}`);
60+
throw new PluginError(`Unable to load plugin module ${pluginProvider}`);
6261
}
6362

6463
if (!pluginModule.default || typeof pluginModule.default !== 'function') {
6564
console.error(`Plugin provider ${pluginProvider} is missing a default function export`);
66-
throw new CliError(`Plugin provider ${pluginProvider} is missing a default function export`);
65+
throw new PluginError(`Plugin provider ${pluginProvider} is missing a default function export`);
6766
}
6867
plugins.push({
6968
name: this.getPluginName(pluginModule, pluginProvider),

packages/schema/src/telemetry.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type TelemetryEvents =
1616
| 'cli:start'
1717
| 'cli:complete'
1818
| 'cli:error'
19+
| 'cli:crash'
1920
| 'cli:command:start'
2021
| 'cli:command:complete'
2122
| 'cli:command:error'
@@ -50,19 +51,26 @@ export class Telemetry {
5051
});
5152

5253
const errorHandler = async (err: Error) => {
53-
this.track('cli:error', {
54-
message: err.message,
55-
stack: err.stack,
56-
});
57-
if (this.mixpanel) {
58-
// a small delay to ensure telemetry is sent
59-
await sleep(this.exitWait);
60-
}
61-
6254
if (err instanceof CliError || err instanceof CommanderError) {
55+
this.track('cli:error', {
56+
message: err.message,
57+
stack: err.stack,
58+
});
59+
if (this.mixpanel) {
60+
// a small delay to ensure telemetry is sent
61+
await sleep(this.exitWait);
62+
}
6363
// error already logged
6464
} else {
6565
console.error('\nAn unexpected error occurred:\n', err);
66+
this.track('cli:crash', {
67+
message: err.message,
68+
stack: err.stack,
69+
});
70+
if (this.mixpanel) {
71+
// a small delay to ensure telemetry is sent
72+
await sleep(this.exitWait);
73+
}
6674
}
6775

6876
process.exit(1);

0 commit comments

Comments
 (0)