Skip to content

Commit bf07ff9

Browse files
committed
feat: helpers for building JSON/OpenAPI schema referencing shared definitions
Introduce the following helpers: - `getModelSchemaRef` returning OpenAPI spec - `getJsonSchemaRef` returning JSON Schema Signed-off-by: Miroslav Bajtoš <mbajtoss@gmail.com>
1 parent c5eb4d5 commit bf07ff9

File tree

8 files changed

+218
-23
lines changed

8 files changed

+218
-23
lines changed

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
param,
1919
post,
2020
requestBody,
21+
getModelSchemaRef,
2122
} from '../..';
2223

2324
describe('controller spec', () => {
@@ -733,4 +734,51 @@ describe('controller spec', () => {
733734
return MyController;
734735
}
735736
});
737+
738+
describe('getModelSchemaRef', () => {
739+
it('creates spec referencing shared model schema', () => {
740+
@model()
741+
class MyModel {
742+
@property()
743+
name: string;
744+
}
745+
746+
class MyController {
747+
@get('/my', {
748+
responses: {
749+
'200': {
750+
description: 'Array of MyModel model instances',
751+
content: {
752+
'application/json': {
753+
schema: getModelSchemaRef(MyModel),
754+
},
755+
},
756+
},
757+
},
758+
})
759+
async find(): Promise<MyModel[]> {
760+
return [];
761+
}
762+
}
763+
764+
const spec = getControllerSpec(MyController);
765+
const opSpec: OperationObject = spec.paths['/my'].get;
766+
const responseSpec = opSpec.responses['200'].content['application/json'];
767+
expect(responseSpec.schema).to.deepEqual({
768+
$ref: '#/components/schemas/MyModel',
769+
});
770+
771+
const globalSchemas = (spec.components || {}).schemas;
772+
expect(globalSchemas).to.deepEqual({
773+
MyModel: {
774+
title: 'MyModel',
775+
properties: {
776+
name: {
777+
type: 'string',
778+
},
779+
},
780+
},
781+
});
782+
});
783+
});
736784
});

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

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ import {
1515
RequestBodyObject,
1616
ResponseObject,
1717
SchemaObject,
18+
SchemasObject,
1819
} from '@loopback/openapi-v3-types';
19-
import {getJsonSchema} from '@loopback/repository-json-schema';
20+
import {
21+
getJsonSchema,
22+
getJsonSchemaRef,
23+
JsonSchemaOptions,
24+
} from '@loopback/repository-json-schema';
2025
import * as _ from 'lodash';
2126
import {resolveSchema} from './generate-schema';
22-
import {jsonToSchemaObject} from './json-to-schema';
27+
import {jsonToSchemaObject, SchemaRef} from './json-to-schema';
2328
import {OAI3Keys} from './keys';
2429

2530
const debug = require('debug')('loopback:openapi3:metadata:controller-spec');
@@ -56,8 +61,6 @@ export interface RestEndpoint {
5661

5762
export const TS_TYPE_KEY = 'x-ts-type';
5863

59-
type ComponentSchemaMap = {[key: string]: SchemaObject};
60-
6164
/**
6265
* Build the api spec from class and method level decorations
6366
* @param constructor - Controller class
@@ -139,7 +142,7 @@ function resolveControllerSpec(constructor: Function): ControllerSpec {
139142
params = DecoratorFactory.cloneDeep<ParameterObject[]>(params);
140143
/**
141144
* If a controller method uses dependency injection, the parameters
142-
* might be sparsed. For example,
145+
* might be sparse. For example,
143146
* ```ts
144147
* class MyController {
145148
* greet(
@@ -304,7 +307,7 @@ function generateOpenAPISchema(spec: ControllerSpec, tsType: Function) {
304307
*/
305308
function assignRelatedSchemas(
306309
spec: ControllerSpec,
307-
definitions?: ComponentSchemaMap,
310+
definitions?: SchemasObject,
308311
) {
309312
if (!definitions) return;
310313
debug(
@@ -348,3 +351,34 @@ export function getControllerSpec(constructor: Function): ControllerSpec {
348351
}
349352
return spec;
350353
}
354+
355+
/**
356+
* Describe the provided Model as a reference to a definition shared by multiple
357+
* endpoints. The definition is included in the returned schema.
358+
*
359+
* @example
360+
*
361+
* ```ts
362+
* const schema = {
363+
* $ref: '#/components/schemas/Product',
364+
* definitions: {
365+
* Product: {
366+
* title: 'Product',
367+
* properties: {
368+
* // etc.
369+
* }
370+
* }
371+
* }
372+
* }
373+
* ```
374+
*
375+
* @param modelCtor - The model constructor (e.g. `Product`)
376+
* @param options - Additional options
377+
*/
378+
export function getModelSchemaRef(
379+
modelCtor: Function,
380+
options?: JsonSchemaOptions,
381+
): SchemaRef {
382+
const jsonSchema = getJsonSchemaRef(modelCtor, options);
383+
return jsonToSchemaObject(jsonSchema) as SchemaRef;
384+
}

packages/openapi-v3/src/json-to-schema.ts

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

6+
import {
7+
ReferenceObject,
8+
SchemaObject,
9+
SchemasObject,
10+
} from '@loopback/openapi-v3-types';
611
import {JsonSchema} from '@loopback/repository-json-schema';
7-
import {SchemaObject} from '@loopback/openapi-v3-types';
812
import * as _ from 'lodash';
913

14+
/**
15+
* Custom LoopBack extension: a reference to Schema object that's bundled
16+
* inside `definitions` property.
17+
*
18+
* @example
19+
*
20+
* ```ts
21+
* const spec: SchemaRef = {
22+
* $ref: '/components/schemas/Product',
23+
* definitions: {
24+
* Product: {
25+
* title: 'Product',
26+
* properties: {
27+
* // etc.
28+
* }
29+
* }
30+
* }
31+
* }
32+
* ```
33+
*/
34+
export type SchemaRef = ReferenceObject & {definitions: SchemasObject};
35+
1036
/**
1137
* Converts JSON Schemas into a SchemaObject
1238
* @param json - JSON Schema to convert from
1339
*/
14-
export function jsonToSchemaObject(json: JsonSchema): SchemaObject {
40+
export function jsonToSchemaObject(json: JsonSchema): SchemaObject | SchemaRef {
1541
const result: SchemaObject = {};
1642
const propsToIgnore = [
1743
'anyOf',
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright IBM Corp. 2019. All Rights Reserved.
2+
// Node module: @loopback/repository-json-schema
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {expect} from '@loopback/testlab';
7+
import * as Ajv from 'ajv';
8+
import {JsonSchema} from '../..';
9+
10+
export function expectValidJsonSchema(schema: JsonSchema) {
11+
const ajv = new Ajv();
12+
const validate = ajv.compile(
13+
require('ajv/lib/refs/json-schema-draft-06.json'),
14+
);
15+
const isValid = validate(schema);
16+
const result = isValid
17+
? 'JSON Schema is valid'
18+
: ajv.errorsText(validate.errors!);
19+
expect(result).to.equal('JSON Schema is valid');
20+
}

packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import {
1212
property,
1313
} from '@loopback/repository';
1414
import {expect} from '@loopback/testlab';
15-
import * as Ajv from 'ajv';
1615
import {
1716
getJsonSchema,
1817
JsonSchema,
1918
JSON_SCHEMA_KEY,
2019
modelToJsonSchema,
2120
} from '../..';
21+
import {expectValidJsonSchema} from '../helpers/expect-valid-json-schema';
2222

2323
describe('build-schema', () => {
2424
describe('modelToJsonSchema', () => {
@@ -629,21 +629,9 @@ describe('build-schema', () => {
629629
expect(schema).to.deepEqual(expectedSchema);
630630
});
631631
});
632-
633-
function expectValidJsonSchema(schema: JsonSchema) {
634-
const ajv = new Ajv();
635-
const validate = ajv.compile(
636-
require('ajv/lib/refs/json-schema-draft-06.json'),
637-
);
638-
const isValid = validate(schema);
639-
const result = isValid
640-
? 'JSON Schema is valid'
641-
: ajv.errorsText(validate.errors!);
642-
expect(result).to.equal('JSON Schema is valid');
643-
}
644632
});
645633

646-
describe('getjsonSchema', () => {
634+
describe('getJsonSchema', () => {
647635
it('gets cached JSON schema if one exists', () => {
648636
@model()
649637
class TestModel {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/repository-json-schema
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {model, property} from '@loopback/repository';
7+
import {expect} from '@loopback/testlab';
8+
import {getJsonSchemaRef} from '../..';
9+
10+
describe('getJsonSchemaRef', () => {
11+
it('creates spec referencing shared model schema', () => {
12+
@model()
13+
class MyModel {
14+
@property()
15+
name: string;
16+
}
17+
18+
const spec = getJsonSchemaRef(MyModel);
19+
20+
expect(spec).to.deepEqual({
21+
$ref: '#/definitions/MyModel',
22+
definitions: {
23+
MyModel: {
24+
title: 'MyModel',
25+
properties: {
26+
name: {
27+
type: 'string',
28+
},
29+
},
30+
},
31+
},
32+
});
33+
});
34+
});

packages/repository-json-schema/src/build-schema.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,50 @@ export function getJsonSchema(
4040
}
4141
}
4242

43+
/**
44+
* Describe the provided Model as a reference to a definition shared by multiple
45+
* endpoints. The definition is included in the returned schema.
46+
*
47+
* @example
48+
*
49+
* ```ts
50+
* const schema = {
51+
* $ref: '/definitions/Product',
52+
* definitions: {
53+
* Product: {
54+
* title: 'Product',
55+
* properties: {
56+
* // etc.
57+
* }
58+
* }
59+
* }
60+
* }
61+
* ```
62+
*
63+
* @param modelCtor - The model constructor (e.g. `Product`)
64+
* @param options - Additional options
65+
*/
66+
export function getJsonSchemaRef(
67+
modelCtor: Function,
68+
options?: JsonSchemaOptions,
69+
): JSONSchema {
70+
const schemaWithDefinitions = getJsonSchema(modelCtor, options);
71+
const key = schemaWithDefinitions.title;
72+
73+
// ctor is not a model
74+
if (!key) return schemaWithDefinitions;
75+
76+
const definitions = Object.assign({}, schemaWithDefinitions.definitions);
77+
const schema = Object.assign({}, schemaWithDefinitions);
78+
delete schema.definitions;
79+
definitions[key] = schema;
80+
81+
return {
82+
$ref: `#/definitions/${key}`,
83+
definitions,
84+
};
85+
}
86+
4387
/**
4488
* Gets the wrapper function of primitives string, number, and boolean
4589
* @param type - Name of type

packages/rest/src/__tests__/acceptance/validation/validation.acceptance.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
post,
1818
requestBody,
1919
RestApplication,
20+
SchemaObject,
2021
} from '../../..';
2122
import {aBodySpec} from '../../helpers';
2223

@@ -45,7 +46,7 @@ describe('Validation at REST level', () => {
4546
// Add a schema that requires `description`
4647
const PRODUCT_SPEC_WITH_DESCRIPTION = jsonToSchemaObject(
4748
getJsonSchema(Product),
48-
);
49+
) as SchemaObject;
4950
PRODUCT_SPEC_WITH_DESCRIPTION.required!.push('description');
5051

5152
// This is the standard use case that most LB4 applications should use.

0 commit comments

Comments
 (0)