Skip to content

Commit e76ee35

Browse files
authored
fix: bug fixes for openapi plugin (#432)
1 parent 764ff2a commit e76ee35

File tree

6 files changed

+311
-100
lines changed

6 files changed

+311
-100
lines changed

packages/plugins/openapi/src/generator-base.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,59 @@ export abstract class OpenAPIGeneratorBase {
6060
}
6161
return undefined;
6262
}
63+
64+
protected pruneComponents(paths: OAPI.PathsObject, components: OAPI.ComponentsObject) {
65+
const schemas = components.schemas;
66+
if (schemas) {
67+
const roots = new Set<string>();
68+
for (const path of Object.values(paths)) {
69+
this.collectUsedComponents(path, roots);
70+
}
71+
72+
// build a transitive closure for all reachable schemas from roots
73+
const allUsed = new Set<string>(roots);
74+
75+
let todo = [...allUsed];
76+
while (todo.length > 0) {
77+
const curr = new Set<string>(allUsed);
78+
Object.entries(schemas)
79+
.filter(([key]) => todo.includes(key))
80+
.forEach(([, value]) => {
81+
this.collectUsedComponents(value, allUsed);
82+
});
83+
todo = [...allUsed].filter((e) => !curr.has(e));
84+
}
85+
86+
// prune unused schemas
87+
Object.keys(schemas).forEach((key) => {
88+
if (!allUsed.has(key)) {
89+
delete schemas[key];
90+
}
91+
});
92+
}
93+
}
94+
95+
private collectUsedComponents(value: unknown, allUsed: Set<string>) {
96+
if (!value) {
97+
return;
98+
}
99+
100+
if (Array.isArray(value)) {
101+
value.forEach((item) => {
102+
this.collectUsedComponents(item, allUsed);
103+
});
104+
} else if (typeof value === 'object') {
105+
Object.entries(value).forEach(([subKey, subValue]) => {
106+
if (subKey === '$ref') {
107+
const ref = subValue as string;
108+
const name = ref.split('/').pop();
109+
if (name && !allUsed.has(name)) {
110+
allUsed.add(name);
111+
}
112+
} else {
113+
this.collectUsedComponents(subValue, allUsed);
114+
}
115+
});
116+
}
117+
}
63118
}

packages/plugins/openapi/src/index.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { DMMF } from '@prisma/generator-helper';
2-
import { PluginOptions } from '@zenstackhq/sdk';
2+
import { PluginError, PluginOptions } from '@zenstackhq/sdk';
33
import { Model } from '@zenstackhq/sdk/ast';
44
import { RESTfulOpenAPIGenerator } from './rest-generator';
55
import { RPCOpenAPIGenerator } from './rpc-generator';
66

77
export const name = 'OpenAPI';
88

99
export default async function run(model: Model, options: PluginOptions, dmmf: DMMF.Document) {
10-
const flavor = options.flavor ? (options.flavor as string) : 'restful';
11-
if (flavor === 'restful') {
12-
return new RESTfulOpenAPIGenerator(model, options, dmmf).generate();
13-
} else {
14-
return new RPCOpenAPIGenerator(model, options, dmmf).generate();
10+
const flavor = options.flavor ? (options.flavor as string) : 'rest';
11+
12+
switch (flavor) {
13+
case 'rest':
14+
return new RESTfulOpenAPIGenerator(model, options, dmmf).generate();
15+
case 'rpc':
16+
return new RPCOpenAPIGenerator(model, options, dmmf).generate();
17+
default:
18+
throw new PluginError(name, `Unknown flavor: ${flavor}`);
1519
}
1620
}

packages/plugins/openapi/src/rest-generator.ts

Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
AUXILIARY_FIELDS,
66
analyzePolicies,
77
getDataModels,
8+
hasAttribute,
89
isForeignKeyField,
910
isIdField,
1011
isRelationshipField,
@@ -37,6 +38,9 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
3738
const components = this.generateComponents();
3839
const paths = this.generatePaths();
3940

41+
// prune unused component schemas
42+
this.pruneComponents(paths, components);
43+
4044
// generate security schemes, and root-level security
4145
components.securitySchemes = this.generateSecuritySchemes();
4246
let security: OAPI.Document['security'] | undefined = undefined;
@@ -108,20 +112,24 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
108112
prefix = prefix.substring(0, prefix.length - 1);
109113
}
110114

115+
const resourceMeta = getModelResourceMeta(zmodel);
116+
111117
// GET /resource
112118
// POST /resource
113119
result[`${prefix}/${lowerCaseFirst(model.name)}`] = {
114-
get: this.makeResourceList(zmodel, policies),
115-
post: this.makeResourceCreate(zmodel, policies),
120+
get: this.makeResourceList(zmodel, policies, resourceMeta),
121+
post: this.makeResourceCreate(zmodel, policies, resourceMeta),
116122
};
117123

118124
// GET /resource/{id}
125+
// PUT /resource/{id}
119126
// PATCH /resource/{id}
120127
// DELETE /resource/{id}
121128
result[`${prefix}/${lowerCaseFirst(model.name)}/{id}`] = {
122-
get: this.makeResourceFetch(zmodel, policies),
123-
patch: this.makeResourceUpdate(zmodel, policies),
124-
delete: this.makeResourceDelete(zmodel, policies),
129+
get: this.makeResourceFetch(zmodel, policies, resourceMeta),
130+
put: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-put`, resourceMeta),
131+
patch: this.makeResourceUpdate(zmodel, policies, `update-${model.name}-patch`, resourceMeta),
132+
delete: this.makeResourceDelete(zmodel, policies, resourceMeta),
125133
};
126134

127135
// paths for related resources and relationships
@@ -131,33 +139,49 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
131139
continue;
132140
}
133141

134-
// GET /resource/{id}/field
142+
// GET /resource/{id}/{relationship}
135143
const relatedDataPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/${field.name}`;
136144
let container = result[relatedDataPath];
137145
if (!container) {
138146
container = result[relatedDataPath] = {};
139147
}
140-
container.get = this.makeRelatedFetch(zmodel, field, relationDecl);
148+
container.get = this.makeRelatedFetch(zmodel, field, relationDecl, resourceMeta);
141149

142150
const relationshipPath = `${prefix}/${lowerCaseFirst(model.name)}/{id}/relationships/${field.name}`;
143151
container = result[relationshipPath];
144152
if (!container) {
145153
container = result[relationshipPath] = {};
146154
}
147-
// GET /resource/{id}/relationships/field
148-
container.get = this.makeRelationshipFetch(zmodel, field, policies);
149-
// PATCH /resource/{id}/relationships/field
150-
container.patch = this.makeRelationshipUpdate(zmodel, field, policies);
155+
// GET /resource/{id}/relationships/{relationship}
156+
container.get = this.makeRelationshipFetch(zmodel, field, policies, resourceMeta);
157+
158+
// PUT /resource/{id}/relationships/{relationship}
159+
container.put = this.makeRelationshipUpdate(
160+
zmodel,
161+
field,
162+
policies,
163+
`update-${model.name}-relationship-${field.name}-put`,
164+
resourceMeta
165+
);
166+
// PATCH /resource/{id}/relationships/{relationship}
167+
container.patch = this.makeRelationshipUpdate(
168+
zmodel,
169+
field,
170+
policies,
171+
`update-${model.name}-relationship-${field.name}-patch`,
172+
resourceMeta
173+
);
174+
151175
if (field.type.array) {
152-
// POST /resource/{id}/relationships/field
153-
container.post = this.makeRelationshipCreate(zmodel, field, policies);
176+
// POST /resource/{id}/relationships/{relationship}
177+
container.post = this.makeRelationshipCreate(zmodel, field, policies, resourceMeta);
154178
}
155179
}
156180

157181
return result;
158182
}
159183

160-
private makeResourceList(model: DataModel, policies: Policies) {
184+
private makeResourceList(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) {
161185
return {
162186
operationId: `list-${model.name}`,
163187
description: `List "${model.name}" resources`,
@@ -173,11 +197,11 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
173197
'200': this.success(`${model.name}ListResponse`),
174198
'403': this.forbidden(),
175199
},
176-
security: policies.read === true ? [] : undefined,
200+
security: resourceMeta?.security ?? policies.read === true ? [] : undefined,
177201
};
178202
}
179203

180-
private makeResourceCreate(model: DataModel, policies: Policies) {
204+
private makeResourceCreate(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) {
181205
return {
182206
operationId: `create-${model.name}`,
183207
description: `Create a "${model.name}" resource`,
@@ -193,11 +217,11 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
193217
'201': this.success(`${model.name}Response`),
194218
'403': this.forbidden(),
195219
},
196-
security: policies.create === true ? [] : undefined,
220+
security: resourceMeta?.security ?? policies.create === true ? [] : undefined,
197221
};
198222
}
199223

200-
private makeResourceFetch(model: DataModel, policies: Policies) {
224+
private makeResourceFetch(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) {
201225
return {
202226
operationId: `fetch-${model.name}`,
203227
description: `Fetch a "${model.name}" resource`,
@@ -208,11 +232,16 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
208232
'403': this.forbidden(),
209233
'404': this.notFound(),
210234
},
211-
security: policies.read === true ? [] : undefined,
235+
security: resourceMeta?.security ?? policies.read === true ? [] : undefined,
212236
};
213237
}
214238

215-
private makeRelatedFetch(model: DataModel, field: DataModelField, relationDecl: DataModel) {
239+
private makeRelatedFetch(
240+
model: DataModel,
241+
field: DataModelField,
242+
relationDecl: DataModel,
243+
resourceMeta: { security?: object } | undefined
244+
) {
216245
const policies = analyzePolicies(relationDecl);
217246
const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id'), this.parameter('include')];
218247
if (field.type.array) {
@@ -235,14 +264,19 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
235264
'403': this.forbidden(),
236265
'404': this.notFound(),
237266
},
238-
security: policies.read === true ? [] : undefined,
267+
security: resourceMeta?.security ?? policies.read === true ? [] : undefined,
239268
};
240269
return result;
241270
}
242271

243-
private makeResourceUpdate(model: DataModel, policies: Policies) {
272+
private makeResourceUpdate(
273+
model: DataModel,
274+
policies: Policies,
275+
operationId: string,
276+
resourceMeta: { security?: object } | undefined
277+
) {
244278
return {
245-
operationId: `update-${model.name}`,
279+
operationId,
246280
description: `Update a "${model.name}" resource`,
247281
tags: [lowerCaseFirst(model.name)],
248282
parameters: [this.parameter('id')],
@@ -258,11 +292,11 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
258292
'403': this.forbidden(),
259293
'404': this.notFound(),
260294
},
261-
security: policies.update === true ? [] : undefined,
295+
security: resourceMeta?.security ?? policies.update === true ? [] : undefined,
262296
};
263297
}
264298

265-
private makeResourceDelete(model: DataModel, policies: Policies) {
299+
private makeResourceDelete(model: DataModel, policies: Policies, resourceMeta: { security?: object } | undefined) {
266300
return {
267301
operationId: `delete-${model.name}`,
268302
description: `Delete a "${model.name}" resource`,
@@ -273,11 +307,16 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
273307
'403': this.forbidden(),
274308
'404': this.notFound(),
275309
},
276-
security: policies.delete === true ? [] : undefined,
310+
security: resourceMeta?.security ?? policies.delete === true ? [] : undefined,
277311
};
278312
}
279313

280-
private makeRelationshipFetch(model: DataModel, field: DataModelField, policies: Policies) {
314+
private makeRelationshipFetch(
315+
model: DataModel,
316+
field: DataModelField,
317+
policies: Policies,
318+
resourceMeta: { security?: object } | undefined
319+
) {
281320
const parameters: OAPI.OperationObject['parameters'] = [this.parameter('id')];
282321
if (field.type.array) {
283322
parameters.push(
@@ -299,11 +338,16 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
299338
'403': this.forbidden(),
300339
'404': this.notFound(),
301340
},
302-
security: policies.read === true ? [] : undefined,
341+
security: resourceMeta?.security ?? policies.read === true ? [] : undefined,
303342
};
304343
}
305344

306-
private makeRelationshipCreate(model: DataModel, field: DataModelField, policies: Policies) {
345+
private makeRelationshipCreate(
346+
model: DataModel,
347+
field: DataModelField,
348+
policies: Policies,
349+
resourceMeta: { security?: object } | undefined
350+
) {
307351
return {
308352
operationId: `create-${model.name}-relationship-${field.name}`,
309353
description: `Create new "${field.name}" relationships for a "${model.name}"`,
@@ -321,13 +365,19 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
321365
'403': this.forbidden(),
322366
'404': this.notFound(),
323367
},
324-
security: policies.update === true ? [] : undefined,
368+
security: resourceMeta?.security ?? policies.update === true ? [] : undefined,
325369
};
326370
}
327371

328-
private makeRelationshipUpdate(model: DataModel, field: DataModelField, policies: Policies) {
372+
private makeRelationshipUpdate(
373+
model: DataModel,
374+
field: DataModelField,
375+
policies: Policies,
376+
operationId: string,
377+
resourceMeta: { security?: object } | undefined
378+
) {
329379
return {
330-
operationId: `update-${model.name}-relationship-${field.name}`,
380+
operationId,
331381
description: `Update "${field.name}" ${pluralize('relationship', field.type.array ? 2 : 1)} for a "${
332382
model.name
333383
}"`,
@@ -349,7 +399,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
349399
'403': this.forbidden(),
350400
'404': this.notFound(),
351401
},
352-
security: policies.update === true ? [] : undefined,
402+
security: resourceMeta?.security ?? policies.update === true ? [] : undefined,
353403
};
354404
}
355405

@@ -784,6 +834,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
784834
if (
785835
mode === 'create' &&
786836
!field.type.optional &&
837+
!hasAttribute(field, '@default') &&
787838
// collection relation fields are implicitly optional
788839
!(isDataModel(field.$resolvedType?.decl) && field.type.array)
789840
) {

0 commit comments

Comments
 (0)