@loopback/repository-json-schema 4.0.0-alpha.8 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +15 -10
- package/dist/build-schema.d.ts +137 -0
- package/dist/build-schema.js +445 -0
- package/dist/build-schema.js.map +1 -0
- package/dist/filter-json-schema.d.ts +54 -0
- package/dist/filter-json-schema.js +198 -0
- package/dist/filter-json-schema.js.map +1 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +8 -0
- package/dist/keys.js +13 -0
- package/dist/keys.js.map +1 -0
- package/package.json +42 -33
- package/src/build-schema.ts +491 -79
- package/src/filter-json-schema.ts +237 -0
- package/src/index.ts +42 -1
- package/src/keys.ts +15 -0
- package/CHANGELOG.md +0 -83
- package/api-docs/apple-touch-icon-114x114-precomposed.png +0 -0
- package/api-docs/apple-touch-icon-144x144-precomposed.png +0 -0
- package/api-docs/apple-touch-icon-57x57-precomposed.png +0 -0
- package/api-docs/apple-touch-icon-72x72-precomposed.png +0 -0
- package/api-docs/apple-touch-icon-precomposed.png +0 -0
- package/api-docs/apple-touch-icon.png +0 -0
- package/api-docs/css/bootstrap.min.css +0 -9
- package/api-docs/css/code-themes/arta.css +0 -158
- package/api-docs/css/code-themes/ascetic.css +0 -50
- package/api-docs/css/code-themes/brown_paper.css +0 -104
- package/api-docs/css/code-themes/brown_papersq.png +0 -0
- package/api-docs/css/code-themes/dark.css +0 -103
- package/api-docs/css/code-themes/default.css +0 -135
- package/api-docs/css/code-themes/far.css +0 -111
- package/api-docs/css/code-themes/github.css +0 -127
- package/api-docs/css/code-themes/googlecode.css +0 -144
- package/api-docs/css/code-themes/idea.css +0 -121
- package/api-docs/css/code-themes/ir_black.css +0 -104
- package/api-docs/css/code-themes/magula.css +0 -121
- package/api-docs/css/code-themes/monokai.css +0 -114
- package/api-docs/css/code-themes/pojoaque.css +0 -104
- package/api-docs/css/code-themes/pojoaque.jpg +0 -0
- package/api-docs/css/code-themes/rainbow.css +0 -114
- package/api-docs/css/code-themes/school_book.css +0 -111
- package/api-docs/css/code-themes/school_book.png +0 -0
- package/api-docs/css/code-themes/sl-theme.css +0 -45
- package/api-docs/css/code-themes/solarized_dark.css +0 -88
- package/api-docs/css/code-themes/solarized_light.css +0 -88
- package/api-docs/css/code-themes/sunburst.css +0 -158
- package/api-docs/css/code-themes/tomorrow-night-blue.css +0 -52
- package/api-docs/css/code-themes/tomorrow-night-bright.css +0 -51
- package/api-docs/css/code-themes/tomorrow-night-eighties.css +0 -51
- package/api-docs/css/code-themes/tomorrow-night.css +0 -52
- package/api-docs/css/code-themes/tomorrow.css +0 -49
- package/api-docs/css/code-themes/vs.css +0 -86
- package/api-docs/css/code-themes/xcode.css +0 -154
- package/api-docs/css/code-themes/zenburn.css +0 -115
- package/api-docs/css/main.css +0 -139
- package/api-docs/favicon.ico +0 -0
- package/api-docs/fonts/0ihfXUL2emPh0ROJezvraLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
- package/api-docs/fonts/OsJ2DjdpjqFRVUSto6IffLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
- package/api-docs/fonts/_aijTyevf54tkVDLy-dlnLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
- package/api-docs/index.html +0 -717
- package/api-docs/js/main.js +0 -19
- package/api-docs/js/vendor/bootstrap.min.js +0 -6
- package/api-docs/js/vendor/jquery-1.10.1.min.js +0 -6
- package/api-docs/js/vendor/jquery.scrollTo-1.4.3.1.js +0 -218
- package/api-docs/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js +0 -11
- package/dist/src/build-schema.d.ts +0 -50
- package/dist/src/build-schema.js +0 -143
- package/dist/src/build-schema.js.map +0 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -11
- package/dist/src/index.js.map +0 -1
- package/index.d.ts +0 -6
- package/index.js +0 -6
package/src/build-schema.ts
CHANGED
|
@@ -1,58 +1,203 @@
|
|
|
1
|
-
// Copyright IBM Corp. 2018. All Rights Reserved.
|
|
1
|
+
// Copyright IBM Corp. 2018,2020. All Rights Reserved.
|
|
2
2
|
// Node module: @loopback/repository-json-schema
|
|
3
3
|
// This file is licensed under the MIT License.
|
|
4
4
|
// License text available at https://opensource.org/licenses/MIT
|
|
5
5
|
|
|
6
|
+
import {MetadataInspector} from '@loopback/core';
|
|
6
7
|
import {
|
|
8
|
+
isBuiltinType,
|
|
9
|
+
ModelDefinition,
|
|
7
10
|
ModelMetadataHelper,
|
|
11
|
+
Null,
|
|
8
12
|
PropertyDefinition,
|
|
9
|
-
|
|
13
|
+
PropertyType,
|
|
14
|
+
RelationMetadata,
|
|
15
|
+
resolveType,
|
|
10
16
|
} from '@loopback/repository';
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
17
|
+
import debugFactory from 'debug';
|
|
18
|
+
import {inspect} from 'util';
|
|
19
|
+
import {JsonSchema} from './index';
|
|
20
|
+
import {JSON_SCHEMA_KEY} from './keys';
|
|
21
|
+
const debug = debugFactory('loopback:repository-json-schema:build-schema');
|
|
22
|
+
|
|
23
|
+
export interface JsonSchemaOptions<T extends object> {
|
|
24
|
+
/**
|
|
25
|
+
* The title to use in the generated schema.
|
|
26
|
+
*
|
|
27
|
+
* When using options like `exclude`, the auto-generated title can be
|
|
28
|
+
* difficult to read for humans. Use this option to change the title to
|
|
29
|
+
* a more meaningful value.
|
|
30
|
+
*/
|
|
31
|
+
title?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set this flag if you want the schema to define navigational properties
|
|
35
|
+
* for model relations.
|
|
36
|
+
*/
|
|
37
|
+
includeRelations?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set this flag to mark all model properties as optional. This is typically
|
|
41
|
+
* used to describe request body of PATCH endpoints. This option will be
|
|
42
|
+
* overridden by the "optional" option if it is set and non-empty.
|
|
43
|
+
*
|
|
44
|
+
* The flag also applies to nested model instances if its value is set to
|
|
45
|
+
* 'deep', such as:
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* @model()
|
|
50
|
+
* class Address {
|
|
51
|
+
* @property()
|
|
52
|
+
* street: string;
|
|
53
|
+
* @property()
|
|
54
|
+
* city: string;
|
|
55
|
+
* @property()
|
|
56
|
+
* state: string;
|
|
57
|
+
* @property()
|
|
58
|
+
* zipCode: string;
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* @model()
|
|
62
|
+
* class Customer {
|
|
63
|
+
* @property()
|
|
64
|
+
* address: Address;
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* // The following schema allows properties of `customer` optional, but not
|
|
68
|
+
* // `customer.address`
|
|
69
|
+
* const schemaRef1 = getModelSchemaRef(Customer, {partial: true});
|
|
70
|
+
*
|
|
71
|
+
* // The following schema allows properties of `customer` and
|
|
72
|
+
* // `customer.address` optional
|
|
73
|
+
* const schemaRef2 = getModelSchemaRef(Customer, {partial: 'deep'});
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
partial?: boolean | 'deep';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* List of model properties to exclude from the schema.
|
|
80
|
+
*/
|
|
81
|
+
exclude?: (keyof T)[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List of model properties to mark as optional. Overrides the "partial"
|
|
85
|
+
* option if it is not empty.
|
|
86
|
+
*/
|
|
87
|
+
optional?: (keyof T)[];
|
|
14
88
|
|
|
15
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
visited?: {[key: string]: JsonSchema};
|
|
93
|
+
}
|
|
16
94
|
|
|
17
95
|
/**
|
|
18
|
-
*
|
|
96
|
+
* @internal
|
|
19
97
|
*/
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
98
|
+
export function buildModelCacheKey<T extends object>(
|
|
99
|
+
options: JsonSchemaOptions<T> = {},
|
|
100
|
+
): string {
|
|
101
|
+
// Backwards compatibility: preserve cache key "modelOnly"
|
|
102
|
+
if (Object.keys(options).length === 0) {
|
|
103
|
+
return 'modelOnly';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// New key schema: use the same suffix as we use for schema title
|
|
107
|
+
// For example: "modelPartialWithRelations"
|
|
108
|
+
// Note this new key schema preserves the old key "modelWithRelations"
|
|
109
|
+
return 'model' + (options.title ?? '') + getTitleSuffix(options);
|
|
32
110
|
}
|
|
33
111
|
|
|
34
112
|
/**
|
|
35
113
|
* Gets the JSON Schema of a TypeScript model/class by seeing if one exists
|
|
36
114
|
* in a cache. If not, one is generated and then cached.
|
|
37
|
-
* @param ctor
|
|
115
|
+
* @param ctor - Constructor of class to get JSON Schema from
|
|
38
116
|
*/
|
|
39
|
-
export function getJsonSchema
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
export function getJsonSchema<T extends object>(
|
|
118
|
+
ctor: Function & {prototype: T},
|
|
119
|
+
options?: JsonSchemaOptions<T>,
|
|
120
|
+
): JsonSchema {
|
|
121
|
+
// In the near future the metadata will be an object with
|
|
122
|
+
// different titles as keys
|
|
123
|
+
const cached = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor, {
|
|
124
|
+
ownMetadataOnly: true,
|
|
125
|
+
});
|
|
126
|
+
const key = buildModelCacheKey(options);
|
|
127
|
+
let schema = cached?.[key];
|
|
128
|
+
|
|
129
|
+
if (!schema) {
|
|
130
|
+
// Create new json schema from model
|
|
131
|
+
// if not found in cache for specific key
|
|
132
|
+
schema = modelToJsonSchema(ctor, options);
|
|
133
|
+
if (cached) {
|
|
134
|
+
// Add a new key to the cached schema of the model
|
|
135
|
+
cached[key] = schema;
|
|
136
|
+
} else {
|
|
137
|
+
// Define new metadata and set in cache
|
|
138
|
+
MetadataInspector.defineMetadata(
|
|
139
|
+
JSON_SCHEMA_KEY.key,
|
|
140
|
+
{[key]: schema},
|
|
141
|
+
ctor,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
48
144
|
}
|
|
145
|
+
|
|
146
|
+
return schema;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Describe the provided Model as a reference to a definition shared by multiple
|
|
151
|
+
* endpoints. The definition is included in the returned schema.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* const schema = {
|
|
157
|
+
* $ref: '/definitions/Product',
|
|
158
|
+
* definitions: {
|
|
159
|
+
* Product: {
|
|
160
|
+
* title: 'Product',
|
|
161
|
+
* properties: {
|
|
162
|
+
* // etc.
|
|
163
|
+
* }
|
|
164
|
+
* }
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @param modelCtor - The model constructor (e.g. `Product`)
|
|
170
|
+
* @param options - Additional options
|
|
171
|
+
*/
|
|
172
|
+
export function getJsonSchemaRef<T extends object>(
|
|
173
|
+
modelCtor: Function & {prototype: T},
|
|
174
|
+
options?: JsonSchemaOptions<T>,
|
|
175
|
+
): JsonSchema {
|
|
176
|
+
const schemaWithDefinitions = getJsonSchema(modelCtor, options);
|
|
177
|
+
const key = schemaWithDefinitions.title;
|
|
178
|
+
|
|
179
|
+
// ctor is not a model
|
|
180
|
+
if (!key) return schemaWithDefinitions;
|
|
181
|
+
|
|
182
|
+
const definitions = Object.assign({}, schemaWithDefinitions.definitions);
|
|
183
|
+
const schema = Object.assign({}, schemaWithDefinitions);
|
|
184
|
+
delete schema.definitions;
|
|
185
|
+
definitions[key] = schema;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
$ref: `#/definitions/${key}`,
|
|
189
|
+
definitions,
|
|
190
|
+
};
|
|
49
191
|
}
|
|
50
192
|
|
|
51
193
|
/**
|
|
52
194
|
* Gets the wrapper function of primitives string, number, and boolean
|
|
53
|
-
* @param type Name of type
|
|
195
|
+
* @param type - Name of type
|
|
54
196
|
*/
|
|
55
|
-
export function stringTypeToWrapper(type: string): Function {
|
|
197
|
+
export function stringTypeToWrapper(type: string | Function): Function {
|
|
198
|
+
if (typeof type === 'function') {
|
|
199
|
+
return type;
|
|
200
|
+
}
|
|
56
201
|
type = type.toLowerCase();
|
|
57
202
|
let wrapper;
|
|
58
203
|
switch (type) {
|
|
@@ -68,50 +213,203 @@ export function stringTypeToWrapper(type: string): Function {
|
|
|
68
213
|
wrapper = Boolean;
|
|
69
214
|
break;
|
|
70
215
|
}
|
|
216
|
+
case 'array': {
|
|
217
|
+
wrapper = Array;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case 'object':
|
|
221
|
+
case 'any': {
|
|
222
|
+
wrapper = Object;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'date': {
|
|
226
|
+
wrapper = Date;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'buffer': {
|
|
230
|
+
wrapper = Buffer;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'null': {
|
|
234
|
+
wrapper = Null;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
71
237
|
default: {
|
|
72
|
-
throw new Error('Unsupported type');
|
|
238
|
+
throw new Error('Unsupported type: ' + type);
|
|
73
239
|
}
|
|
74
240
|
}
|
|
75
241
|
return wrapper;
|
|
76
242
|
}
|
|
77
243
|
|
|
78
244
|
/**
|
|
79
|
-
* Determines whether
|
|
80
|
-
* @param
|
|
245
|
+
* Determines whether a given string or constructor is array type or not
|
|
246
|
+
* @param type - Type as string or wrapper
|
|
81
247
|
*/
|
|
82
|
-
export function
|
|
83
|
-
return
|
|
248
|
+
export function isArrayType(type: string | Function | PropertyType) {
|
|
249
|
+
return type === Array || type === 'array';
|
|
84
250
|
}
|
|
85
251
|
|
|
86
252
|
/**
|
|
87
253
|
* Converts property metadata into a JSON property definition
|
|
88
254
|
* @param meta
|
|
89
255
|
*/
|
|
90
|
-
export function metaToJsonProperty(meta: PropertyDefinition):
|
|
91
|
-
|
|
92
|
-
let
|
|
256
|
+
export function metaToJsonProperty(meta: PropertyDefinition): JsonSchema {
|
|
257
|
+
const propDef: JsonSchema = {};
|
|
258
|
+
let result: JsonSchema;
|
|
259
|
+
let propertyType = meta.type as string | Function;
|
|
260
|
+
|
|
261
|
+
if (isArrayType(propertyType) && meta.itemType) {
|
|
262
|
+
if (isArrayType(meta.itemType) && !meta.jsonSchema) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'You must provide the "jsonSchema" field when define ' +
|
|
265
|
+
'a nested array property',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
result = {type: 'array', items: propDef};
|
|
269
|
+
propertyType = meta.itemType as string | Function;
|
|
270
|
+
} else {
|
|
271
|
+
result = propDef;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const wrappedType = stringTypeToWrapper(propertyType);
|
|
275
|
+
const resolvedType = resolveType(wrappedType);
|
|
276
|
+
|
|
277
|
+
if (resolvedType === Date) {
|
|
278
|
+
Object.assign(propDef, {
|
|
279
|
+
type: 'string',
|
|
280
|
+
format: 'date-time',
|
|
281
|
+
});
|
|
282
|
+
} else if (propertyType === 'any') {
|
|
283
|
+
// no-op, the json schema for any type is {}
|
|
284
|
+
} else if (isBuiltinType(resolvedType)) {
|
|
285
|
+
Object.assign(propDef, {
|
|
286
|
+
type: resolvedType.name.toLowerCase(),
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
Object.assign(propDef, {$ref: `#/definitions/${resolvedType.name}`});
|
|
290
|
+
}
|
|
93
291
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
292
|
+
if (meta.description) {
|
|
293
|
+
Object.assign(propDef, {
|
|
294
|
+
description: meta.description,
|
|
295
|
+
});
|
|
97
296
|
}
|
|
98
297
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
298
|
+
if (meta.jsonSchema) {
|
|
299
|
+
Object.assign(propDef, meta.jsonSchema);
|
|
101
300
|
}
|
|
102
301
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
: {type: ctor.name.toLowerCase()};
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
106
304
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Checks and return navigational property definition for the relation
|
|
307
|
+
* @param relMeta Relation metadata object
|
|
308
|
+
* @param targetRef Schema definition for the target model
|
|
309
|
+
*/
|
|
310
|
+
export function getNavigationalPropertyForRelation(
|
|
311
|
+
relMeta: RelationMetadata,
|
|
312
|
+
targetRef: JsonSchema,
|
|
313
|
+
): JsonSchema {
|
|
314
|
+
if (relMeta.targetsMany === true) {
|
|
315
|
+
// Targets an array of object, like, hasMany
|
|
316
|
+
return {
|
|
317
|
+
type: 'array',
|
|
318
|
+
items: targetRef,
|
|
319
|
+
};
|
|
320
|
+
} else if (relMeta.targetsMany === false) {
|
|
321
|
+
// Targets single object, like, hasOne, belongsTo
|
|
322
|
+
return targetRef;
|
|
110
323
|
} else {
|
|
111
|
-
|
|
324
|
+
// targetsMany is undefined or null
|
|
325
|
+
// not allowed if includeRelations is true
|
|
326
|
+
throw new Error(`targetsMany attribute missing for ${relMeta.name}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function buildSchemaTitle<T extends object>(
|
|
331
|
+
ctor: Function & {prototype: T},
|
|
332
|
+
meta: ModelDefinition,
|
|
333
|
+
options: JsonSchemaOptions<T>,
|
|
334
|
+
) {
|
|
335
|
+
if (options.title) return options.title;
|
|
336
|
+
const title = meta.title || ctor.name;
|
|
337
|
+
return title + getTitleSuffix(options);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Checks the options and generates a descriptive suffix using compatible chars
|
|
342
|
+
* @param options json schema options
|
|
343
|
+
*/
|
|
344
|
+
function getTitleSuffix<T extends object>(options: JsonSchemaOptions<T> = {}) {
|
|
345
|
+
let suffix = '';
|
|
346
|
+
|
|
347
|
+
if (options.optional?.length) {
|
|
348
|
+
suffix += `Optional_${options.optional.join('-')}_`;
|
|
349
|
+
} else if (options.partial) {
|
|
350
|
+
suffix += 'Partial';
|
|
351
|
+
}
|
|
352
|
+
if (options.exclude?.length) {
|
|
353
|
+
suffix += `Excluding_${options.exclude.join('-')}_`;
|
|
354
|
+
}
|
|
355
|
+
if (options.includeRelations) {
|
|
356
|
+
suffix += 'WithRelations';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return suffix;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function stringifyOptions(modelSettings: object = {}) {
|
|
363
|
+
return inspect(modelSettings, {
|
|
364
|
+
depth: Infinity,
|
|
365
|
+
maxArrayLength: Infinity,
|
|
366
|
+
breakLength: Infinity,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function isEmptyJson(obj: object) {
|
|
371
|
+
return !(obj && Object.keys(obj).length);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Checks the options and generates a descriptive suffix that contains the
|
|
376
|
+
* TypeScript type and options
|
|
377
|
+
* @param typeName - TypeScript's type name
|
|
378
|
+
* @param options - json schema options
|
|
379
|
+
*/
|
|
380
|
+
function getDescriptionSuffix<T extends object>(
|
|
381
|
+
typeName: string,
|
|
382
|
+
rawOptions: JsonSchemaOptions<T> = {},
|
|
383
|
+
) {
|
|
384
|
+
const options = {...rawOptions};
|
|
385
|
+
|
|
386
|
+
delete options.visited;
|
|
387
|
+
if (options.optional && !options.optional.length) {
|
|
388
|
+
delete options.optional;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const type = typeName;
|
|
392
|
+
let tsType = type;
|
|
393
|
+
if (options.includeRelations) {
|
|
394
|
+
tsType = `${type}WithRelations`;
|
|
395
|
+
}
|
|
396
|
+
if (options.partial) {
|
|
397
|
+
tsType = `Partial<${tsType}>`;
|
|
398
|
+
}
|
|
399
|
+
if (options.exclude) {
|
|
400
|
+
const excludedProps = options.exclude.map(p => `'${p}'`);
|
|
401
|
+
tsType = `Omit<${tsType}, ${excludedProps.join(' | ')}>`;
|
|
402
|
+
}
|
|
403
|
+
if (options.optional) {
|
|
404
|
+
const optionalProps = options.optional.map(p => `'${p}'`);
|
|
405
|
+
tsType = `@loopback/repository-json-schema#Optional<${tsType}, ${optionalProps.join(
|
|
406
|
+
' | ',
|
|
407
|
+
)}>`;
|
|
112
408
|
}
|
|
113
409
|
|
|
114
|
-
return
|
|
410
|
+
return !isEmptyJson(options)
|
|
411
|
+
? `(tsType: ${tsType}, schemaOptions: ${stringifyOptions(options)})`
|
|
412
|
+
: '';
|
|
115
413
|
}
|
|
116
414
|
|
|
117
415
|
// NOTE(shimks) no metadata for: union, optional, nested array, any, enum,
|
|
@@ -120,61 +418,175 @@ export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition {
|
|
|
120
418
|
/**
|
|
121
419
|
* Converts a TypeScript class into a JSON Schema using TypeScript's
|
|
122
420
|
* reflection API
|
|
123
|
-
* @param ctor Constructor of class to convert from
|
|
421
|
+
* @param ctor - Constructor of class to convert from
|
|
124
422
|
*/
|
|
125
|
-
export function modelToJsonSchema
|
|
126
|
-
|
|
127
|
-
|
|
423
|
+
export function modelToJsonSchema<T extends object>(
|
|
424
|
+
ctor: Function & {prototype: T},
|
|
425
|
+
jsonSchemaOptions: JsonSchemaOptions<T> = {},
|
|
426
|
+
): JsonSchema {
|
|
427
|
+
const options = {...jsonSchemaOptions};
|
|
428
|
+
options.visited = options.visited ?? {};
|
|
429
|
+
options.optional = options.optional ?? [];
|
|
430
|
+
const partial = options.partial && !options.optional.length;
|
|
431
|
+
|
|
432
|
+
if (options.partial && !partial) {
|
|
433
|
+
debug('Overriding "partial" option with "optional" option');
|
|
434
|
+
delete options.partial;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
debug('Creating schema for model %s', ctor.name);
|
|
438
|
+
debug('JSON schema options: %o', options);
|
|
439
|
+
|
|
440
|
+
const modelDef = ModelMetadataHelper.getModelMetadata(ctor);
|
|
128
441
|
|
|
129
442
|
// returns an empty object if metadata is an empty object
|
|
130
|
-
if (
|
|
443
|
+
if (modelDef == null || Object.keys(modelDef).length === 0) {
|
|
131
444
|
return {};
|
|
132
445
|
}
|
|
133
446
|
|
|
134
|
-
|
|
447
|
+
const meta = modelDef as ModelDefinition;
|
|
448
|
+
|
|
449
|
+
debug('Model settings', meta.settings);
|
|
450
|
+
|
|
451
|
+
const title = buildSchemaTitle(ctor, meta, options);
|
|
452
|
+
if (options.visited[title]) return options.visited[title];
|
|
453
|
+
|
|
454
|
+
const result: JsonSchema = {title};
|
|
455
|
+
options.visited[title] = result;
|
|
456
|
+
|
|
457
|
+
result.type = 'object';
|
|
458
|
+
|
|
459
|
+
const descriptionSuffix = getDescriptionSuffix(ctor.name, options);
|
|
135
460
|
|
|
136
461
|
if (meta.description) {
|
|
137
|
-
|
|
462
|
+
const formatSuffix = descriptionSuffix ? ` ${descriptionSuffix}` : '';
|
|
463
|
+
|
|
464
|
+
result.description = meta.description + formatSuffix;
|
|
465
|
+
} else if (descriptionSuffix) {
|
|
466
|
+
result.description = descriptionSuffix;
|
|
138
467
|
}
|
|
139
468
|
|
|
140
469
|
for (const p in meta.properties) {
|
|
141
|
-
if (
|
|
470
|
+
if (options.exclude?.includes(p as keyof T)) {
|
|
471
|
+
debug('Property % is excluded by %s', p, options.exclude);
|
|
142
472
|
continue;
|
|
143
473
|
}
|
|
144
474
|
|
|
145
|
-
|
|
475
|
+
if (meta.properties[p].type == null) {
|
|
476
|
+
// Circular import of model classes can lead to this situation
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Property ${ctor.name}.${p} does not have "type" in its definition`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
result.properties = result.properties ?? {};
|
|
146
483
|
result.properties[p] = result.properties[p] || {};
|
|
147
484
|
|
|
148
|
-
const metaProperty = meta.properties[p];
|
|
149
|
-
const metaType = metaProperty.type;
|
|
485
|
+
const metaProperty = Object.assign({}, meta.properties[p]);
|
|
150
486
|
|
|
151
487
|
// populating "properties" key
|
|
152
488
|
result.properties[p] = metaToJsonProperty(metaProperty);
|
|
153
489
|
|
|
490
|
+
// handling 'required' metadata
|
|
491
|
+
const optional = options.optional.includes(p as keyof T);
|
|
492
|
+
|
|
493
|
+
if (metaProperty.required && !(partial || optional)) {
|
|
494
|
+
result.required = result.required ?? [];
|
|
495
|
+
result.required.push(p);
|
|
496
|
+
}
|
|
497
|
+
|
|
154
498
|
// populating JSON Schema 'definitions'
|
|
155
|
-
|
|
156
|
-
|
|
499
|
+
// shimks: ugly type casting; this should be replaced by logic to throw
|
|
500
|
+
// error if itemType/type is not a string or a function
|
|
501
|
+
const resolvedType = resolveType(metaProperty.type) as string | Function;
|
|
502
|
+
const referenceType = isArrayType(resolvedType)
|
|
503
|
+
? // shimks: ugly type casting; this should be replaced by logic to throw
|
|
504
|
+
// error if itemType/type is not a string or a function
|
|
505
|
+
resolveType(metaProperty.itemType as string | Function)
|
|
506
|
+
: resolvedType;
|
|
507
|
+
|
|
508
|
+
if (typeof referenceType !== 'function' || isBuiltinType(referenceType)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
157
511
|
|
|
158
|
-
|
|
159
|
-
|
|
512
|
+
const propOptions = {...options};
|
|
513
|
+
if (propOptions.partial !== 'deep') {
|
|
514
|
+
// Do not cascade `partial` to nested properties
|
|
515
|
+
delete propOptions.partial;
|
|
516
|
+
}
|
|
517
|
+
if (propOptions.includeRelations === true) {
|
|
518
|
+
// Do not cascade `includeRelations` to nested properties
|
|
519
|
+
delete propOptions.includeRelations;
|
|
520
|
+
}
|
|
521
|
+
// `title` is the unique identity of a schema,
|
|
522
|
+
// it should be removed from the `options`
|
|
523
|
+
// when generating the relation or property schemas
|
|
524
|
+
delete propOptions.title;
|
|
160
525
|
|
|
161
|
-
|
|
162
|
-
if (propSchema.definitions) {
|
|
163
|
-
for (const key in propSchema.definitions) {
|
|
164
|
-
result.definitions[key] = propSchema.definitions[key];
|
|
165
|
-
}
|
|
166
|
-
delete propSchema.definitions;
|
|
167
|
-
}
|
|
526
|
+
const propSchema = getJsonSchema(referenceType, propOptions);
|
|
168
527
|
|
|
169
|
-
|
|
528
|
+
// JSONSchema6Definition allows both boolean and JSONSchema6 types
|
|
529
|
+
if (typeof result.properties[p] !== 'boolean') {
|
|
530
|
+
const prop = result.properties[p] as JsonSchema;
|
|
531
|
+
const propTitle = propSchema.title ?? referenceType.name;
|
|
532
|
+
const targetRef = {$ref: `#/definitions/${propTitle}`};
|
|
533
|
+
|
|
534
|
+
if (prop.type === 'array' && prop.items) {
|
|
535
|
+
// Update $ref for array type
|
|
536
|
+
prop.items = targetRef;
|
|
537
|
+
} else {
|
|
538
|
+
result.properties[p] = targetRef;
|
|
170
539
|
}
|
|
540
|
+
includeReferencedSchema(propTitle, propSchema);
|
|
171
541
|
}
|
|
542
|
+
}
|
|
172
543
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
544
|
+
result.additionalProperties = meta.settings.strict === false;
|
|
545
|
+
debug(' additionalProperties?', result.additionalProperties);
|
|
546
|
+
|
|
547
|
+
if (options.includeRelations) {
|
|
548
|
+
for (const r in meta.relations) {
|
|
549
|
+
result.properties = result.properties ?? {};
|
|
550
|
+
const relMeta = meta.relations[r];
|
|
551
|
+
const targetType = resolveType(relMeta.target);
|
|
552
|
+
|
|
553
|
+
// `title` is the unique identity of a schema,
|
|
554
|
+
// it should be removed from the `options`
|
|
555
|
+
// when generating the relation or property schemas
|
|
556
|
+
const targetOptions = {...options};
|
|
557
|
+
delete targetOptions.title;
|
|
558
|
+
|
|
559
|
+
const targetSchema = getJsonSchema(targetType, targetOptions);
|
|
560
|
+
const targetRef = {$ref: `#/definitions/${targetSchema.title}`};
|
|
561
|
+
const propDef = getNavigationalPropertyForRelation(relMeta, targetRef);
|
|
562
|
+
|
|
563
|
+
result.properties[relMeta.name] =
|
|
564
|
+
result.properties[relMeta.name] || propDef;
|
|
565
|
+
includeReferencedSchema(targetSchema.title!, targetSchema);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function includeReferencedSchema(name: string, schema: JsonSchema) {
|
|
570
|
+
if (!schema || !Object.keys(schema).length) return;
|
|
571
|
+
|
|
572
|
+
// promote nested definition to the top level
|
|
573
|
+
if (result !== schema?.definitions) {
|
|
574
|
+
for (const key in schema.definitions) {
|
|
575
|
+
if (key === title) continue;
|
|
576
|
+
result.definitions = result.definitions ?? {};
|
|
577
|
+
result.definitions[key] = schema.definitions[key];
|
|
578
|
+
}
|
|
579
|
+
delete schema.definitions;
|
|
177
580
|
}
|
|
581
|
+
|
|
582
|
+
if (result !== schema) {
|
|
583
|
+
result.definitions = result.definitions ?? {};
|
|
584
|
+
result.definitions[name] = schema;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (meta.jsonSchema) {
|
|
589
|
+
Object.assign(result, meta.jsonSchema);
|
|
178
590
|
}
|
|
179
591
|
return result;
|
|
180
592
|
}
|