Skip to content

Commit d57f272

Browse files
nabdelgadirbajtos
andcommitted
feat(openapi-v3): allow controller to reference models via openapispec
Controller methods can now reference model schema through OpenAPI spec without the need to use the `x-ts-type` extension. Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
1 parent 1800acb commit d57f272

File tree

2 files changed

+373
-30
lines changed

2 files changed

+373
-30
lines changed

packages/openapi-v3/src/__tests__/integration/controller-spec.integration.ts

Lines changed: 314 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6-
import {ParameterObject, SchemaObject} from '@loopback/openapi-v3-types';
7-
import {model, property} from '@loopback/repository';
6+
import {
7+
OperationObject,
8+
ParameterObject,
9+
SchemaObject,
10+
} from '@loopback/openapi-v3-types';
11+
import {Entity, model, property} from '@loopback/repository';
812
import {expect} from '@loopback/testlab';
913
import {
14+
api,
1015
ControllerSpec,
1116
get,
1217
getControllerSpec,
@@ -203,6 +208,313 @@ describe('controller spec', () => {
203208
});
204209
});
205210

211+
context('reference models via spec', () => {
212+
it('allows operations to provide definitions of referenced models through #/components/schema', () => {
213+
class MyController {
214+
@get('/todos', {
215+
responses: {
216+
'200': {
217+
description: 'Array of Category model instances',
218+
content: {
219+
'application/json': {
220+
schema: {
221+
$ref: '#/components/schemas/Todo',
222+
definitions: {
223+
Todo: {
224+
title: 'Todo',
225+
properties: {
226+
title: {type: 'string'},
227+
},
228+
},
229+
},
230+
},
231+
},
232+
},
233+
},
234+
},
235+
})
236+
async find(): Promise<object[]> {
237+
return []; // dummy implementation, it's never called
238+
}
239+
}
240+
241+
const spec = getControllerSpec(MyController);
242+
const opSpec: OperationObject = spec.paths['/todos'].get;
243+
const responseSpec = opSpec.responses['200'].content['application/json'];
244+
expect(responseSpec.schema).to.deepEqual({
245+
$ref: '#/components/schemas/Todo',
246+
});
247+
248+
const globalSchemas = (spec.components || {}).schemas;
249+
expect(globalSchemas).to.deepEqual({
250+
Todo: {
251+
title: 'Todo',
252+
properties: {
253+
title: {
254+
type: 'string',
255+
},
256+
},
257+
},
258+
});
259+
});
260+
261+
it('allows operations to provide definitions of referenced models through #/definitions', () => {
262+
class MyController {
263+
@get('/todos', {
264+
responses: {
265+
'200': {
266+
description: 'Array of Category model instances',
267+
content: {
268+
'application/json': {
269+
schema: {
270+
$ref: '#/definitions/Todo',
271+
definitions: {
272+
Todo: {
273+
title: 'Todo',
274+
properties: {
275+
title: {type: 'string'},
276+
},
277+
},
278+
},
279+
},
280+
},
281+
},
282+
},
283+
},
284+
})
285+
async find(): Promise<object[]> {
286+
return []; // dummy implementation, it's never called
287+
}
288+
}
289+
290+
const spec = getControllerSpec(MyController);
291+
const opSpec: OperationObject = spec.paths['/todos'].get;
292+
const responseSpec = opSpec.responses['200'].content['application/json'];
293+
expect(responseSpec.schema).to.deepEqual({
294+
$ref: '#/definitions/Todo',
295+
});
296+
297+
const globalSchemas = (spec.components || {}).schemas;
298+
expect(globalSchemas).to.deepEqual({
299+
Todo: {
300+
title: 'Todo',
301+
properties: {
302+
title: {
303+
type: 'string',
304+
},
305+
},
306+
},
307+
});
308+
});
309+
310+
it('allows operations to get definitions of models when defined through a different method', async () => {
311+
@model()
312+
class Todo extends Entity {
313+
@property({
314+
type: 'string',
315+
required: true,
316+
})
317+
title: string;
318+
}
319+
320+
class MyController {
321+
@get('/todos', {
322+
responses: {
323+
'200': {
324+
description: 'Array of Category model instances',
325+
content: {
326+
'application/json': {
327+
schema: {
328+
$ref: '#/definitions/Todo',
329+
definitions: {
330+
Todo: {
331+
title: 'Todo',
332+
properties: {
333+
title: {type: 'string'},
334+
},
335+
},
336+
},
337+
},
338+
},
339+
},
340+
},
341+
},
342+
})
343+
async find(): Promise<object[]> {
344+
return []; // dummy implementation, it's never called
345+
}
346+
347+
@get('/todos/{id}', {
348+
responses: {
349+
'200': {
350+
content: {
351+
'application/json': {
352+
schema: {$ref: '#/components/schemas/Todo'},
353+
},
354+
},
355+
},
356+
},
357+
})
358+
async findById(): Promise<Todo> {
359+
return new Todo();
360+
}
361+
}
362+
363+
const spec = getControllerSpec(MyController);
364+
const opSpec: OperationObject = spec.paths['/todos/{id}'].get;
365+
const responseSpec = opSpec.responses['200'].content['application/json'];
366+
expect(responseSpec.schema).to.deepEqual({
367+
$ref: '#/components/schemas/Todo',
368+
});
369+
370+
const controller = new MyController();
371+
const todo = await controller.findById();
372+
expect(todo instanceof Todo).to.be.true();
373+
});
374+
375+
it('returns undefined when it cannot find definition of referenced model', () => {
376+
class MyController {
377+
@get('/todos', {
378+
responses: {
379+
'200': {
380+
description: 'Array of Category model instances',
381+
content: {
382+
'application/json': {
383+
schema: {
384+
$ref: '#/definitions/Todo',
385+
},
386+
},
387+
},
388+
},
389+
},
390+
})
391+
async find(): Promise<object[]> {
392+
return []; // dummy implementation, it's never called
393+
}
394+
}
395+
396+
const spec = getControllerSpec(MyController);
397+
const globalSchemas = (spec.components || {}).schemas;
398+
expect(globalSchemas).to.be.undefined();
399+
});
400+
401+
it('gets definition from outside the method decorator when it is not provided', () => {
402+
@api({
403+
paths: {},
404+
components: {
405+
schemas: {
406+
Todo: {
407+
title: 'Todo',
408+
properties: {
409+
title: {
410+
type: 'string',
411+
},
412+
},
413+
},
414+
},
415+
},
416+
})
417+
class MyController {
418+
@get('/todos', {
419+
responses: {
420+
'200': {
421+
description: 'Array of Category model instances',
422+
content: {
423+
'application/json': {
424+
schema: {
425+
$ref: '#/definitions/Todo',
426+
},
427+
},
428+
},
429+
},
430+
},
431+
})
432+
async find(): Promise<object[]> {
433+
return []; // dummy implementation, it's never called
434+
}
435+
}
436+
437+
const spec = getControllerSpec(MyController);
438+
const opSpec: OperationObject = spec.paths['/todos'].get;
439+
const responseSpec = opSpec.responses['200'].content['application/json'];
440+
expect(responseSpec.schema).to.deepEqual({
441+
$ref: '#/definitions/Todo',
442+
});
443+
444+
const globalSchemas = (spec.components || {}).schemas;
445+
expect(globalSchemas).to.deepEqual({
446+
Todo: {
447+
title: 'Todo',
448+
properties: {
449+
title: {
450+
type: 'string',
451+
},
452+
},
453+
},
454+
});
455+
});
456+
457+
it('allows a class to reference schemas at @api level', () => {
458+
@api({
459+
paths: {
460+
'/todos': {
461+
get: {
462+
'x-operation-name': 'find',
463+
'x-controller-name': 'MyController',
464+
responses: {
465+
'200': {
466+
content: {
467+
'application/json': {
468+
schema: {
469+
$ref: '#/components/schemas/Todo',
470+
},
471+
},
472+
},
473+
},
474+
},
475+
},
476+
},
477+
},
478+
components: {
479+
schemas: {
480+
Todo: {
481+
title: 'Todo',
482+
properties: {
483+
title: {
484+
type: 'string',
485+
},
486+
},
487+
},
488+
},
489+
},
490+
})
491+
class MyController {
492+
async find(): Promise<object[]> {
493+
return []; // dummy implementation, it's never called
494+
}
495+
}
496+
497+
const spec = getControllerSpec(MyController);
498+
const opSpec: OperationObject = spec.paths['/todos'].get;
499+
const responseSpec = opSpec.responses['200'].content['application/json'];
500+
expect(responseSpec.schema).to.deepEqual({
501+
$ref: '#/components/schemas/Todo',
502+
});
503+
504+
const globalSchemas = (spec.components || {}).schemas;
505+
expect(globalSchemas).to.deepEqual({
506+
Todo: {
507+
title: 'Todo',
508+
properties: {
509+
title: {
510+
type: 'string',
511+
},
512+
},
513+
},
514+
});
515+
});
516+
});
517+
206518
describe('x-ts-type', () => {
207519
@model()
208520
class MyModel {

0 commit comments

Comments
 (0)