@loopback/repository-json-schema 4.0.0-alpha.7 → 4.0.2

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.
Files changed (76) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -10
  3. package/dist/build-schema.d.ts +137 -0
  4. package/dist/build-schema.js +445 -0
  5. package/dist/build-schema.js.map +1 -0
  6. package/dist/filter-json-schema.d.ts +54 -0
  7. package/dist/filter-json-schema.js +198 -0
  8. package/dist/filter-json-schema.js.map +1 -0
  9. package/dist/index.d.ts +36 -1
  10. package/dist/index.js +18 -7
  11. package/dist/index.js.map +1 -0
  12. package/dist/keys.d.ts +8 -0
  13. package/dist/keys.js +13 -0
  14. package/dist/keys.js.map +1 -0
  15. package/package.json +42 -33
  16. package/src/build-schema.ts +491 -79
  17. package/src/filter-json-schema.ts +237 -0
  18. package/src/index.ts +42 -1
  19. package/src/keys.ts +15 -0
  20. package/CHANGELOG.md +0 -75
  21. package/api-docs/apple-touch-icon-114x114-precomposed.png +0 -0
  22. package/api-docs/apple-touch-icon-144x144-precomposed.png +0 -0
  23. package/api-docs/apple-touch-icon-57x57-precomposed.png +0 -0
  24. package/api-docs/apple-touch-icon-72x72-precomposed.png +0 -0
  25. package/api-docs/apple-touch-icon-precomposed.png +0 -0
  26. package/api-docs/apple-touch-icon.png +0 -0
  27. package/api-docs/css/bootstrap.min.css +0 -9
  28. package/api-docs/css/code-themes/arta.css +0 -158
  29. package/api-docs/css/code-themes/ascetic.css +0 -50
  30. package/api-docs/css/code-themes/brown_paper.css +0 -104
  31. package/api-docs/css/code-themes/brown_papersq.png +0 -0
  32. package/api-docs/css/code-themes/dark.css +0 -103
  33. package/api-docs/css/code-themes/default.css +0 -135
  34. package/api-docs/css/code-themes/far.css +0 -111
  35. package/api-docs/css/code-themes/github.css +0 -127
  36. package/api-docs/css/code-themes/googlecode.css +0 -144
  37. package/api-docs/css/code-themes/idea.css +0 -121
  38. package/api-docs/css/code-themes/ir_black.css +0 -104
  39. package/api-docs/css/code-themes/magula.css +0 -121
  40. package/api-docs/css/code-themes/monokai.css +0 -114
  41. package/api-docs/css/code-themes/pojoaque.css +0 -104
  42. package/api-docs/css/code-themes/pojoaque.jpg +0 -0
  43. package/api-docs/css/code-themes/rainbow.css +0 -114
  44. package/api-docs/css/code-themes/school_book.css +0 -111
  45. package/api-docs/css/code-themes/school_book.png +0 -0
  46. package/api-docs/css/code-themes/sl-theme.css +0 -45
  47. package/api-docs/css/code-themes/solarized_dark.css +0 -88
  48. package/api-docs/css/code-themes/solarized_light.css +0 -88
  49. package/api-docs/css/code-themes/sunburst.css +0 -158
  50. package/api-docs/css/code-themes/tomorrow-night-blue.css +0 -52
  51. package/api-docs/css/code-themes/tomorrow-night-bright.css +0 -51
  52. package/api-docs/css/code-themes/tomorrow-night-eighties.css +0 -51
  53. package/api-docs/css/code-themes/tomorrow-night.css +0 -52
  54. package/api-docs/css/code-themes/tomorrow.css +0 -49
  55. package/api-docs/css/code-themes/vs.css +0 -86
  56. package/api-docs/css/code-themes/xcode.css +0 -154
  57. package/api-docs/css/code-themes/zenburn.css +0 -115
  58. package/api-docs/css/main.css +0 -139
  59. package/api-docs/favicon.ico +0 -0
  60. package/api-docs/fonts/0ihfXUL2emPh0ROJezvraLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
  61. package/api-docs/fonts/OsJ2DjdpjqFRVUSto6IffLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
  62. package/api-docs/fonts/_aijTyevf54tkVDLy-dlnLO3LdcAZYWl9Si6vvxL-qU.woff +0 -0
  63. package/api-docs/index.html +0 -53
  64. package/api-docs/js/main.js +0 -19
  65. package/api-docs/js/vendor/bootstrap.min.js +0 -6
  66. package/api-docs/js/vendor/jquery-1.10.1.min.js +0 -6
  67. package/api-docs/js/vendor/jquery.scrollTo-1.4.3.1.js +0 -218
  68. package/api-docs/js/vendor/modernizr-2.6.2-respond-1.1.0.min.js +0 -11
  69. package/dist/src/build-schema.d.ts +0 -50
  70. package/dist/src/build-schema.js +0 -143
  71. package/dist/src/build-schema.js.map +0 -1
  72. package/dist/src/index.d.ts +0 -1
  73. package/dist/src/index.js +0 -11
  74. package/dist/src/index.js.map +0 -1
  75. package/index.d.ts +0 -6
  76. package/index.js +0 -6
@@ -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
- ModelDefinition,
13
+ PropertyType,
14
+ RelationMetadata,
15
+ resolveType,
10
16
  } from '@loopback/repository';
11
- import {includes} from 'lodash';
12
- import {Definition, PrimitiveType} from 'typescript-json-schema';
13
- import {MetadataInspector} from '@loopback/context';
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
- export const JSON_SCHEMA_KEY = 'loopback:json-schema';
89
+ /**
90
+ * @internal
91
+ */
92
+ visited?: {[key: string]: JsonSchema};
93
+ }
16
94
 
17
95
  /**
18
- * Type definition for JSON Schema
96
+ * @internal
19
97
  */
20
- export interface JsonDefinition extends Definition {
21
- allOf?: JsonDefinition[];
22
- oneOf?: JsonDefinition[];
23
- anyOf?: JsonDefinition[];
24
- items?: JsonDefinition | JsonDefinition[];
25
- additionalItems?: {
26
- anyOf: JsonDefinition[];
27
- };
28
- enum?: PrimitiveType[] | JsonDefinition[];
29
- additionalProperties?: JsonDefinition | boolean;
30
- definitions?: {[definition: string]: JsonDefinition};
31
- properties?: {[property: string]: JsonDefinition};
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 Contructor of class to get JSON Schema from
115
+ * @param ctor - Constructor of class to get JSON Schema from
38
116
  */
39
- export function getJsonSchema(ctor: Function): JsonDefinition {
40
- // NOTE(shimks) currently impossible to dynamically update
41
- const jsonSchema = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor);
42
- if (jsonSchema) {
43
- return jsonSchema;
44
- } else {
45
- const newSchema = modelToJsonSchema(ctor);
46
- MetadataInspector.defineMetadata(JSON_SCHEMA_KEY, newSchema, ctor);
47
- return newSchema;
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 the given constructor is a custom type or not
80
- * @param ctor Constructor
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 isComplexType(ctor: Function) {
83
- return !includes([String, Number, Boolean, Object, Function], ctor);
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): JsonDefinition {
91
- let ctor = meta.type as string | Function;
92
- let def: JsonDefinition = {};
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
- // errors out if @property.array() is not used on a property of array
95
- if (ctor === Array) {
96
- throw new Error('type is defined as an array');
292
+ if (meta.description) {
293
+ Object.assign(propDef, {
294
+ description: meta.description,
295
+ });
97
296
  }
98
297
 
99
- if (typeof ctor === 'string') {
100
- ctor = stringTypeToWrapper(ctor);
298
+ if (meta.jsonSchema) {
299
+ Object.assign(propDef, meta.jsonSchema);
101
300
  }
102
301
 
103
- const propDef = isComplexType(ctor)
104
- ? {$ref: `#/definitions/${ctor.name}`}
105
- : {type: ctor.name.toLowerCase()};
302
+ return result;
303
+ }
106
304
 
107
- if (meta.array) {
108
- def.type = 'array';
109
- def.items = propDef;
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
- Object.assign(def, propDef);
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 def;
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(ctor: Function): JsonDefinition {
126
- const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor);
127
- const result: JsonDefinition = {};
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 (!(meta instanceof ModelDefinition)) {
443
+ if (modelDef == null || Object.keys(modelDef).length === 0) {
131
444
  return {};
132
445
  }
133
446
 
134
- result.title = meta.title || ctor.name;
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
- result.description = meta.description;
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 (!meta.properties[p].type) {
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
- result.properties = result.properties || {};
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
- if (typeof metaType === 'function' && isComplexType(metaType)) {
156
- const propSchema = getJsonSchema(metaType);
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
- if (propSchema && Object.keys(propSchema).length > 0) {
159
- result.definitions = result.definitions || {};
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
- // delete nested definition
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
- result.definitions[metaType.name] = propSchema;
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
- // handling 'required' metadata
174
- if (metaProperty.required) {
175
- result.required = result.required || [];
176
- result.required.push(p);
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
  }