Skip to content

Commit 887ae84

Browse files
committed
feat: add include and partial flags to JsonSchemaOptions
1 parent 23dec5d commit 887ae84

File tree

5 files changed

+247
-29
lines changed

5 files changed

+247
-29
lines changed

_SPIKE_.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,172 @@ issue for the discussions we already have had in the past.
77
The code in this spike is building on top of the spike for Inclusion of related
88
models ([PR#2592)(https://github.com/strongloop/loopback-next/pull/2592)),
99
please ignore the first commit.
10+
11+
## Overview
12+
13+
I am proposing to leverage the interface `JsonSchemaOptions` and
14+
`getModelSchemaRef` helper to build schema with additional constraints. Later
15+
on, we can explore different ways how to enable includeRelations flag via
16+
OpenAPI spec extensions.
17+
18+
### Exclude properties from CREATE requests
19+
20+
An example showing a controller method excluding the property `id`:
21+
22+
```ts
23+
class TodoListController {
24+
// ...
25+
26+
@post('/todo-lists', {
27+
responses: {
28+
// left out for brevity
29+
},
30+
})
31+
async create(
32+
@requestBody({
33+
content: {
34+
'application/json': {
35+
schema: getModelSchemaRef(TodoList, {exclude: ['id']}),
36+
/***** ^^^ THIS IS IMPORTANT - OPENAPI SCHEMA ^^^ ****/
37+
},
38+
},
39+
})
40+
obj: Pick<TodoList, Exclude<keyof TodoList, 'id'>>,
41+
/***** ^^^ THIS IS IMPORTANT - TYPESCRIPT TYPE ^^^ ****/
42+
): Promise<TodoList> {
43+
return await this.todoListRepository.create(obj);
44+
}
45+
}
46+
```
47+
48+
An example schema produced by the helper in the request body spec:
49+
50+
```json
51+
{
52+
"requestBody": {
53+
"content": {
54+
"application/json": {
55+
"schema": {
56+
"$ref": "#/components/schemas/TodoListWithout(id)"
57+
}
58+
}
59+
}
60+
}
61+
}
62+
```
63+
64+
The full schema in component schemas:
65+
66+
```json
67+
{
68+
"TodoListWithout(id)": {
69+
"title": "TodoListWithout(id)",
70+
"properties": {
71+
"title": {
72+
"type": "string"
73+
},
74+
"color": {
75+
"type": "string"
76+
}
77+
},
78+
"required": ["title"]
79+
}
80+
}
81+
```
82+
83+
### Mark all properties as optional
84+
85+
An example showing a controller method accepting a partial model instance:
86+
87+
```ts
88+
class TodoListController {
89+
// ...
90+
91+
@patch('/todo-lists/{id}', {
92+
responses: {
93+
// left out for brevity
94+
},
95+
})
96+
async updateById(
97+
@param.path.number('id') id: number,
98+
@requestBody({
99+
content: {
100+
'application/json': {
101+
schema: getModelSchemaRef(TodoList, {partial: true}),
102+
/***** ^^^ THIS IS IMPORTANT - OPENAPI SCHEMA ^^^ ****/
103+
},
104+
},
105+
})
106+
obj: Partial<TodoList>,
107+
/***** ^^^ THIS IS IMPORTANT - TYPESCRIPT TYPE ^^^ ****/
108+
): Promise<void> {
109+
await this.todoListRepository.updateById(id, obj);
110+
}
111+
}
112+
```
113+
114+
An example schema in components:
115+
116+
```json
117+
{
118+
"TodoListPartial": {
119+
"title": "TodoListPartial",
120+
"properties": {
121+
"id": {
122+
"type": "number"
123+
},
124+
"title": {
125+
"type": "string"
126+
},
127+
"color": {
128+
"type": "string"
129+
}
130+
}
131+
}
132+
}
133+
```
134+
135+
### Using spec extension instead of a code-first helper
136+
137+
Later on, we can explore different ways how to enable `partial` and `exclude`
138+
flags via OpenAPI spec extensions. For example:
139+
140+
```
141+
schema: {'x-ts-type': Category, 'x-partial': true},
142+
schema: {'x-ts-type': Category, 'x-exclude': ['id']},
143+
```
144+
145+
In
146+
https://github.com/strongloop/loopback-next/issues/1722#issuecomment-422439405,
147+
a different format was proposed:
148+
149+
```
150+
schema: {
151+
'x-ts-type': Order,
152+
'x-ts-type-options': {
153+
partial: true,
154+
exclude: []
155+
}
156+
}
157+
```
158+
159+
Either way, I am proposing to leave this part out of the initial implementation.
160+
161+
## Follow-up stories
162+
163+
1. Wait until the following stories from "Inclusion of related models" are
164+
implemented:
165+
166+
1. Support `schema: {$ref, definitions}` in resolveControllerSpec
167+
[#2629](https://github.com/strongloop/loopback-next/issues/2629)
168+
169+
2. Implement `getJsonSchemaRef` and `getModelSchemaRef` helpers
170+
[#2631](https://github.com/strongloop/loopback-next/issues/2631)
171+
172+
2. Enhance `getJsonSchema` with a new flag: `partial?: boolean`, also update CLI
173+
templates and example apps accordingly.
174+
175+
3. Enhance `getJsonSchema` with a new flag: `exclude?: string[]`, also update
176+
CLI templates and example apps accordingly.
177+
178+
4. Spike: configure `JsonSchemaOptions` via an OpenAPI spec extension

examples/todo-list/src/controllers/todo-list.controller.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
del,
1515
get,
1616
getFilterSchemaFor,
17-
getWhereSchemaFor,
1817
getModelSchemaRef,
18+
getWhereSchemaFor,
1919
param,
2020
patch,
2121
post,
@@ -38,7 +38,16 @@ export class TodoListController {
3838
},
3939
},
4040
})
41-
async create(@requestBody() obj: TodoList): Promise<TodoList> {
41+
async create(
42+
@requestBody({
43+
content: {
44+
'application/json': {
45+
schema: getModelSchemaRef(TodoList, {exclude: ['id']}),
46+
},
47+
},
48+
})
49+
obj: Pick<TodoList, Exclude<keyof TodoList, 'id'>>,
50+
): Promise<TodoList> {
4251
return await this.todoListRepository.create(obj);
4352
}
4453

@@ -86,7 +95,14 @@ export class TodoListController {
8695
},
8796
})
8897
async updateAll(
89-
@requestBody() obj: Partial<TodoList>,
98+
@requestBody({
99+
content: {
100+
'application/json': {
101+
schema: getModelSchemaRef(TodoList, {partial: true}),
102+
},
103+
},
104+
})
105+
obj: Partial<TodoList>,
90106
@param.query.object('where', getWhereSchemaFor(TodoList)) where?: Where,
91107
): Promise<Count> {
92108
return await this.todoListRepository.updateAll(obj, where);
@@ -113,7 +129,14 @@ export class TodoListController {
113129
})
114130
async updateById(
115131
@param.path.number('id') id: number,
116-
@requestBody() obj: TodoList,
132+
@requestBody({
133+
content: {
134+
'application/json': {
135+
schema: getModelSchemaRef(TodoList, {partial: true}),
136+
},
137+
},
138+
})
139+
obj: Partial<TodoList>,
117140
): Promise<void> {
118141
await this.todoListRepository.updateById(id, obj);
119142
}

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

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

6-
import {MetadataInspector, DecoratorFactory} from '@loopback/context';
7-
6+
import {DecoratorFactory, MetadataInspector} from '@loopback/context';
87
import {
8+
ComponentsObject,
9+
ISpecificationExtension,
10+
isReferenceObject,
911
OperationObject,
1012
ParameterObject,
1113
PathObject,
12-
ComponentsObject,
14+
ReferenceObject,
1315
RequestBodyObject,
1416
ResponseObject,
15-
ReferenceObject,
1617
SchemaObject,
17-
isReferenceObject,
18-
ISpecificationExtension,
1918
} from '@loopback/openapi-v3-types';
2019
import {
2120
getJsonSchema,
22-
JsonSchemaOptions,
2321
getJsonSchemaRef,
22+
JsonSchemaOptions,
2423
} from '@loopback/repository-json-schema';
25-
import {OAI3Keys} from './keys';
26-
import {jsonToSchemaObject} from './json-to-schema';
2724
import * as _ from 'lodash';
2825
import {resolveSchema} from './generate-schema';
26+
import {jsonToSchemaObject} from './json-to-schema';
27+
import {OAI3Keys} from './keys';
2928

3029
const debug = require('debug')('loopback:openapi3:metadata:controller-spec');
3130

@@ -349,9 +348,9 @@ export function getControllerSpec(constructor: Function): ControllerSpec {
349348
return spec;
350349
}
351350

352-
export function getModelSchemaRef(
353-
modelCtor: Function,
354-
options: JsonSchemaOptions,
351+
export function getModelSchemaRef<T extends object = any>(
352+
modelCtor: Function & {prototype: T},
353+
options: JsonSchemaOptions<T> = {},
355354
) {
356355
const jsonSchema = getJsonSchemaRef(modelCtor, options);
357356
return jsonToSchemaObject(jsonSchema);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ describe('build-schema', () => {
582582
};
583583
MetadataInspector.defineMetadata(
584584
JSON_SCHEMA_KEY,
585-
{modelOnly: cachedSchema},
585+
{'config:{}': cachedSchema},
586586
TestModel,
587587
);
588588
const jsonSchema = getJsonSchema(TestModel);

0 commit comments

Comments
 (0)