Skip to content

Commit 946de02

Browse files
committed
feat(repository-json-schema): add an option to make properties optional
Add a new option `optional: []` so that `getJsonSchema` and related helpers can request a model schema that marks specified properties as optional
1 parent 7f7feaa commit 946de02

File tree

5 files changed

+154
-6
lines changed

5 files changed

+154
-6
lines changed

packages/repository-json-schema/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/repository-json-schema/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@
2626
"@loopback/context": "^1.20.2",
2727
"@loopback/metadata": "^1.2.5",
2828
"@loopback/repository": "^1.8.2",
29-
"@types/json-schema": "^7.0.3"
29+
"@types/json-schema": "^7.0.3",
30+
"debug": "^4.1.1"
3031
},
3132
"devDependencies": {
3233
"@loopback/build": "^2.0.3",
3334
"@loopback/eslint-config": "^2.0.0",
3435
"@loopback/testlab": "^1.6.3",
36+
"@types/debug": "^4.1.4",
3537
"@types/node": "^10.14.12",
3638
"ajv": "^6.10.2"
3739
},

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

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,7 +983,7 @@ describe('build-schema', () => {
983983
);
984984
});
985985

986-
it('doesn\'t exclude properties when the option "exclude" is set to exclude no properties', () => {
986+
it(`doesn't exclude properties when the option "exclude" is set to exclude no properties`, () => {
987987
const originalSchema = getJsonSchema(Product);
988988
expect(originalSchema.properties).to.deepEqual({
989989
id: {type: 'number'},
@@ -1001,5 +1001,79 @@ describe('build-schema', () => {
10011001
expect(excludeNothingSchema.title).to.equal('Product');
10021002
});
10031003
});
1004+
1005+
context('optional properties when option "optional" is set', () => {
1006+
@model()
1007+
class Product extends Entity {
1008+
@property({id: true, required: true})
1009+
id: number;
1010+
1011+
@property({required: true})
1012+
name: string;
1013+
1014+
@property()
1015+
description: string;
1016+
}
1017+
1018+
it('makes one property optional when the option "optional" includes one property', () => {
1019+
const originalSchema = getJsonSchema(Product);
1020+
expect(originalSchema.required).to.deepEqual(['id', 'name']);
1021+
expect(originalSchema.title).to.equal('Product');
1022+
1023+
const optionalIdSchema = getJsonSchema(Product, {optional: ['id']});
1024+
expect(optionalIdSchema.required).to.deepEqual(['name']);
1025+
expect(optionalIdSchema.title).to.equal('ProductOptional[id]');
1026+
});
1027+
1028+
it('makes multiple properties optional when the option "optional" includes multiple properties', () => {
1029+
const originalSchema = getJsonSchema(Product);
1030+
expect(originalSchema.required).to.deepEqual(['id', 'name']);
1031+
expect(originalSchema.title).to.equal('Product');
1032+
1033+
const optionalIdAndNameSchema = getJsonSchema(Product, {
1034+
optional: ['id', 'name'],
1035+
});
1036+
expect(optionalIdAndNameSchema.required).to.equal(undefined);
1037+
expect(optionalIdAndNameSchema.title).to.equal(
1038+
'ProductOptional[id,name]',
1039+
);
1040+
});
1041+
1042+
it(`doesn't make properties optional when the option "optional" includes no properties`, () => {
1043+
const originalSchema = getJsonSchema(Product);
1044+
expect(originalSchema.required).to.deepEqual(['id', 'name']);
1045+
expect(originalSchema.title).to.equal('Product');
1046+
1047+
const optionalNothingSchema = getJsonSchema(Product, {optional: []});
1048+
expect(optionalNothingSchema.required).to.deepEqual(['id', 'name']);
1049+
expect(optionalNothingSchema.title).to.equal('Product');
1050+
});
1051+
1052+
it('overrides "partial" option when "optional" options is set', () => {
1053+
const originalSchema = getJsonSchema(Product);
1054+
expect(originalSchema.required).to.deepEqual(['id', 'name']);
1055+
expect(originalSchema.title).to.equal('Product');
1056+
1057+
const optionalNameSchema = getJsonSchema(Product, {
1058+
partial: true,
1059+
optional: ['name'],
1060+
});
1061+
expect(optionalNameSchema.required).to.deepEqual(['id']);
1062+
expect(optionalNameSchema.title).to.equal('ProductOptional[name]');
1063+
});
1064+
1065+
it('uses "partial" option, if provided, when "optional" options is set but empty', () => {
1066+
const originalSchema = getJsonSchema(Product);
1067+
expect(originalSchema.required).to.deepEqual(['id', 'name']);
1068+
expect(originalSchema.title).to.equal('Product');
1069+
1070+
const optionalNameSchema = getJsonSchema(Product, {
1071+
partial: true,
1072+
optional: [],
1073+
});
1074+
expect(optionalNameSchema.required).to.equal(undefined);
1075+
expect(optionalNameSchema.title).to.equal('ProductPartial');
1076+
});
1077+
});
10041078
});
10051079
});

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,47 @@ describe('build-schema', () => {
246246
expect(key).to.equal('modelPartialWithRelations');
247247
});
248248

249-
it('returns concatenated option names otherwise', () => {
249+
it('returns "optional[id,_rev]" when "optional" is set with two items', () => {
250+
const key = buildModelCacheKey({optional: ['id', '_rev']});
251+
expect(key).to.equal('modelOptional[id,_rev]');
252+
});
253+
254+
it('does not include "optional" in concatenated option names if it is empty', () => {
255+
const key = buildModelCacheKey({
256+
partial: true,
257+
optional: [],
258+
includeRelations: true,
259+
});
260+
expect(key).to.equal('modelPartialWithRelations');
261+
});
262+
263+
it('does not include "partial" in option names if "optional" is not empty', () => {
264+
const key = buildModelCacheKey({
265+
partial: true,
266+
optional: ['name'],
267+
});
268+
expect(key).to.equal('modelOptional[name]');
269+
});
270+
271+
it('includes "partial" in option names if "optional" is empty', () => {
272+
const key = buildModelCacheKey({
273+
partial: true,
274+
optional: [],
275+
});
276+
expect(key).to.equal('modelPartial');
277+
});
278+
279+
it('returns concatenated option names except "partial" otherwise', () => {
250280
const key = buildModelCacheKey({
251281
// important: object keys are defined in reverse order
252282
partial: true,
253283
exclude: ['id', '_rev'],
284+
optional: ['name'],
254285
includeRelations: true,
255286
});
256-
expect(key).to.equal('modelPartialExcluding[id,_rev]WithRelations');
287+
expect(key).to.equal(
288+
'modelOptional[name]Excluding[id,_rev]WithRelations',
289+
);
257290
});
258291
});
259292
});

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212
RelationMetadata,
1313
resolveType,
1414
} from '@loopback/repository';
15+
import * as debugFactory from 'debug';
1516
import {JSONSchema6 as JSONSchema} from 'json-schema';
1617
import {JSON_SCHEMA_KEY, MODEL_TYPE_KEYS} from './keys';
18+
const debug = debugFactory('loopback:repository-json-schema:build-schema');
1719

1820
export interface JsonSchemaOptions<T extends object> {
1921
/**
@@ -33,6 +35,11 @@ export interface JsonSchemaOptions<T extends object> {
3335
*/
3436
exclude?: (keyof T)[];
3537

38+
/**
39+
* List of model properties to mark as optional.
40+
*/
41+
optional?: (keyof T)[];
42+
3643
/**
3744
* @internal
3845
*/
@@ -264,7 +271,9 @@ export function getNavigationalPropertyForRelation(
264271

265272
function getTitleSuffix<T extends object>(options: JsonSchemaOptions<T> = {}) {
266273
let suffix = '';
267-
if (options.partial) {
274+
if (options.optional && options.optional.length) {
275+
suffix += 'Optional[' + options.optional + ']';
276+
} else if (options.partial) {
268277
suffix += 'Partial';
269278
}
270279
if (options.exclude && options.exclude.length) {
@@ -291,6 +300,15 @@ export function modelToJsonSchema<T extends object>(
291300
): JSONSchema {
292301
const options = {...jsonSchemaOptions};
293302
options.visited = options.visited || {};
303+
options.optional = options.optional || [];
304+
const partial = options.partial && !options.optional.length;
305+
306+
if (options.partial && !partial) {
307+
debug('Overriding "partial" option with "optional" option');
308+
delete options.partial;
309+
}
310+
311+
debug('JSON schema options: %o', options);
294312

295313
const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor);
296314

@@ -328,7 +346,9 @@ export function modelToJsonSchema<T extends object>(
328346
result.properties[p] = metaToJsonProperty(metaProperty);
329347

330348
// handling 'required' metadata
331-
if (metaProperty.required && !options.partial) {
349+
const optional = options.optional.includes(p as keyof T);
350+
351+
if (metaProperty.required && !(partial || optional)) {
332352
result.required = result.required || [];
333353
result.required.push(p);
334354
}

0 commit comments

Comments
 (0)