From 99f3d79e45c5532d62ac9a3a555ce2949e454a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 12 Sep 2019 10:48:12 +0200 Subject: [PATCH 1/2] feat(repository-json-schema): introduce new option "title" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- .../integration/build-schema.integration.ts | 44 +++++++++++++++++++ .../src/__tests__/unit/build-schema.unit.ts | 5 +++ .../src/build-schema.ts | 23 +++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts index dc0183c6c438..c584129b6423 100644 --- a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts +++ b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts @@ -630,6 +630,34 @@ describe('build-schema', () => { expect(schema).to.deepEqual(expectedSchema); }); }); + + it('uses title from model metadata instead of model name', () => { + @model({title: 'MyCustomer'}) + class Customer {} + + const schema = modelToJsonSchema(Customer, { + // trigger build of a custom title + partial: true, + }); + + expect(schema.title).to.equal('MyCustomerPartial'); + }); + + it('uses title from options instead of model name and computed suffix', () => { + @model({title: 'ShouldBeIgnored'}) + class TestModel { + @property() + id: string; + } + + const schema = modelToJsonSchema(TestModel, { + title: 'NewTestModel', + partial: true, + exclude: ['id'], + }); + + expect(schema.title).to.equal('NewTestModel'); + }); }); describe('getJsonSchema', () => { @@ -1082,5 +1110,21 @@ describe('build-schema', () => { expect(optionalNameSchema.title).to.equal('ProductPartial'); }); }); + + it('creates new cache entry for each custom title', () => { + @model() + class TestModel {} + + // populate the cache + getJsonSchema(TestModel, {title: 'First'}); + getJsonSchema(TestModel, {title: 'Second'}); + + // obtain cached instances & verify the title + const schema1 = getJsonSchema(TestModel, {title: 'First'}); + expect(schema1.title).to.equal('First'); + + const schema2 = getJsonSchema(TestModel, {title: 'Second'}); + expect(schema2.title).to.equal('Second'); + }); }); }); diff --git a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts index 844f6ffd75e9..061db295bc46 100644 --- a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts @@ -325,5 +325,10 @@ describe('build-schema', () => { 'modelOptional[name]Excluding[id,_rev]WithRelations', ); }); + + it('includes custom title', () => { + const key = buildModelCacheKey({title: 'NewProduct', partial: true}); + expect(key).to.equal('modelNewProductPartial'); + }); }); }); diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index 5e6e6f499fa7..d98358b99363 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -18,6 +18,15 @@ import {JSON_SCHEMA_KEY, MODEL_TYPE_KEYS} from './keys'; const debug = debugFactory('loopback:repository-json-schema:build-schema'); export interface JsonSchemaOptions { + /** + * The title to use in the generated schema. + * + * When using options like `exclude`, the auto-generated title can be + * difficult to read for humans. Use this option to change the title to + * a more meaningful value. + */ + title?: string; + /** * Set this flag if you want the schema to define navigational properties * for model relations. @@ -62,7 +71,7 @@ export function buildModelCacheKey( // New key schema: use the same suffix as we use for schema title // For example: "modelPartialWithRelations" // Note this new key schema preserves the old key "modelWithRelations" - return 'model' + getTitleSuffix(options); + return 'model' + (options.title || '') + getTitleSuffix(options); } /** @@ -271,6 +280,16 @@ export function getNavigationalPropertyForRelation( } } +function buildSchemaTitle( + ctor: Function & {prototype: T}, + meta: ModelDefinition, + options: JsonSchemaOptions, +) { + if (options.title) return options.title; + const title = meta.title || ctor.name; + return title + getTitleSuffix(options); +} + function getTitleSuffix(options: JsonSchemaOptions = {}) { let suffix = ''; if (options.optional && options.optional.length) { @@ -319,7 +338,7 @@ export function modelToJsonSchema( return {}; } - const title = (meta.title || ctor.name) + getTitleSuffix(options); + const title = buildSchemaTitle(ctor, meta, options); if (options.visited[title]) return options.visited[title]; From 31ddb95f702816e51fd762a4a4b9b18c8e589ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 12 Sep 2019 11:01:00 +0200 Subject: [PATCH 2/2] feat: use descriptive title to describe schema of POST (create) request bodies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- docs/site/Controller-generator.md | 2 +- .../tutorials/todo-list/todo-list-tutorial-controller.md | 4 ++-- .../express-composition/src/controllers/note.controller.ts | 2 +- .../todo-list/src/controllers/todo-list-todo.controller.ts | 1 + examples/todo-list/src/controllers/todo-list.controller.ts | 5 ++++- examples/todo-list/src/controllers/todo.controller.ts | 2 +- examples/todo/src/controllers/todo.controller.ts | 2 +- .../src/controllers/controller-rest-template.ts.ejs | 5 ++++- .../templates/controller-relation-template-has-many.ts.ejs | 1 + .../test/integration/generators/controller.integration.js | 2 +- packages/rest-crud/src/crud-rest.controller.ts | 5 ++++- 11 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/site/Controller-generator.md b/docs/site/Controller-generator.md index 649044335ba0..9732f6a42d69 100644 --- a/docs/site/Controller-generator.md +++ b/docs/site/Controller-generator.md @@ -134,7 +134,7 @@ export class TodoController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Todo, {exclude: ['id']}), + schema: getModelSchemaRef(Todo, {title: 'NewTodo', exclude: ['id']}), }, }, }) diff --git a/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md b/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md index 6ccce5f2f89c..ef98e5b2570b 100644 --- a/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md +++ b/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md @@ -181,7 +181,7 @@ export class TodoListTodoController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Todo, {exclude: ['id']}), + schema: getModelSchemaRef(Todo, {title: 'NewTodo', exclude: ['id']}), }, }, }) @@ -237,7 +237,7 @@ export class TodoListTodoController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Todo, {exclude: ['id']}), + schema: getModelSchemaRef(Todo, {title: 'NewTodo', exclude: ['id']}), }, }, }) diff --git a/examples/express-composition/src/controllers/note.controller.ts b/examples/express-composition/src/controllers/note.controller.ts index de219cf0dbf6..ff9c604cc03a 100644 --- a/examples/express-composition/src/controllers/note.controller.ts +++ b/examples/express-composition/src/controllers/note.controller.ts @@ -43,7 +43,7 @@ export class NoteController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Note, {exclude: ['id']}), + schema: getModelSchemaRef(Note, {title: 'NewNote', exclude: ['id']}), }, }, }) diff --git a/examples/todo-list/src/controllers/todo-list-todo.controller.ts b/examples/todo-list/src/controllers/todo-list-todo.controller.ts index 43a23504231a..3b322db2ea63 100644 --- a/examples/todo-list/src/controllers/todo-list-todo.controller.ts +++ b/examples/todo-list/src/controllers/todo-list-todo.controller.ts @@ -42,6 +42,7 @@ export class TodoListTodoController { content: { 'application/json': { schema: getModelSchemaRef(Todo, { + title: 'NewTodoInTodoList', exclude: ['id'], optional: ['todoListId'], }), diff --git a/examples/todo-list/src/controllers/todo-list.controller.ts b/examples/todo-list/src/controllers/todo-list.controller.ts index 753393e2b3fd..4108360ae6f7 100644 --- a/examples/todo-list/src/controllers/todo-list.controller.ts +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -42,7 +42,10 @@ export class TodoListController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(TodoList, {exclude: ['id']}), + schema: getModelSchemaRef(TodoList, { + title: 'NewTodoList', + exclude: ['id'], + }), }, }, }) diff --git a/examples/todo-list/src/controllers/todo.controller.ts b/examples/todo-list/src/controllers/todo.controller.ts index ac657688612c..8a81c7600498 100644 --- a/examples/todo-list/src/controllers/todo.controller.ts +++ b/examples/todo-list/src/controllers/todo.controller.ts @@ -33,7 +33,7 @@ export class TodoController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Todo, {exclude: ['id']}), + schema: getModelSchemaRef(Todo, {title: 'NewTodo', exclude: ['id']}), }, }, }) diff --git a/examples/todo/src/controllers/todo.controller.ts b/examples/todo/src/controllers/todo.controller.ts index cae69a7d1619..c636010d4a80 100644 --- a/examples/todo/src/controllers/todo.controller.ts +++ b/examples/todo/src/controllers/todo.controller.ts @@ -38,7 +38,7 @@ export class TodoController { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(Todo, {exclude: ['id']}), + schema: getModelSchemaRef(Todo, {title: 'NewTodo', exclude: ['id']}), }, }, }) diff --git a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs index 6a45c6686903..5ab592f5fc9b 100644 --- a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs +++ b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs @@ -38,7 +38,10 @@ export class <%= className %>Controller { @requestBody({ content: { 'application/json': { - schema: getModelSchemaRef(<%= modelName %>, {exclude: ['<%= id %>']}), + schema: getModelSchemaRef(<%= modelName %>, { + title: 'New<%= modelName %>', + exclude: ['<%= id %>'], + }), }, }, }) diff --git a/packages/cli/generators/relation/templates/controller-relation-template-has-many.ts.ejs b/packages/cli/generators/relation/templates/controller-relation-template-has-many.ts.ejs index e2c83f2f60a5..4ca12b7e748b 100644 --- a/packages/cli/generators/relation/templates/controller-relation-template-has-many.ts.ejs +++ b/packages/cli/generators/relation/templates/controller-relation-template-has-many.ts.ejs @@ -59,6 +59,7 @@ export class <%= controllerClassName %> { content: { 'application/json': { schema: getModelSchemaRef(<%= targetModelClassName %>, { + title: 'New <%= targetModelClassName %>In<%= sourceModelClassName %>', exclude: ['<%= targetModelPrimaryKey %>'], optional: ['<%= foreignKeyName %>'] }), diff --git a/packages/cli/test/integration/generators/controller.integration.js b/packages/cli/test/integration/generators/controller.integration.js index 4ac91b35c7b8..58ae3c5ea711 100644 --- a/packages/cli/test/integration/generators/controller.integration.js +++ b/packages/cli/test/integration/generators/controller.integration.js @@ -249,7 +249,7 @@ function checkRestCrudContents() { /'200': {/, /description: 'ProductReview model instance'/, /content: {'application\/json': {schema: getModelSchemaRef\(ProductReview\)}},\s{1,}},\s{1,}},\s{1,}}\)/, - /async create\(\s+\@requestBody\({\s+content: {\s+'application\/json': {\s+schema: getModelSchemaRef\(ProductReview, {exclude: \['productId'\]}\),\s+},\s+},\s+}\)\s+productReview: Omit,\s+\)/, + /async create\(\s+\@requestBody\({\s+content: {\s+'application\/json': {\s+schema: getModelSchemaRef\(ProductReview, {\s+title: 'NewProductReview',\s+exclude: \['productId'\],\s+}\),\s+},\s+},\s+}\)\s+productReview: Omit,\s+\)/, ]; postCreateRegEx.forEach(regex => { assert.fileContent(expectedFile, regex); diff --git a/packages/rest-crud/src/crud-rest.controller.ts b/packages/rest-crud/src/crud-rest.controller.ts index 4ad0dd34105c..ac5c47183ff8 100644 --- a/packages/rest-crud/src/crud-rest.controller.ts +++ b/packages/rest-crud/src/crud-rest.controller.ts @@ -152,7 +152,10 @@ export function defineCrudRestController< ...response.model(200, `${modelName} instance created`, modelCtor), }) async create( - @body(modelCtor, {exclude: modelCtor.getIdProperties() as (keyof T)[]}) + @body(modelCtor, { + title: `New${modelName}`, + exclude: modelCtor.getIdProperties() as (keyof T)[], + }) data: Omit, ): Promise { return this.repository.create(