Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/model with id required #3667

Merged
merged 1 commit into from
Sep 16, 2019
Merged

Fix/model with id required #3667

merged 1 commit into from
Sep 16, 2019

Conversation

jannyHou
Copy link
Contributor

@jannyHou jannyHou commented Sep 4, 2019

Fixes #3645

When id is required in a model, the expected create method in REST controller should be

  @post('/todos', {
    responses: {
      '200': {
        description: 'Todo model instance',
        content: {'application/json': {schema: getModelSchemaRef(Todo)}},
      },
    },
  })
  async createTodo(
    @requestBody({
      content: {
        'application/json': {
          // DIFFERENCE
          // instead of `getModelSchemaRef(Todo, {exclude: ['id']})
          schema: getModelSchemaRef(Todo),
        },
      },
    })
   // DIFFERENCE
   // instead of `Omit<Todo, 'id'>`
    todo: Todo,
  ): Promise<Todo> {
    return this.todoRepo.create(todo);
  }

This PR generates the right signature for create in the controller generator's template.

At first I didn't know if create method is the only one affected, so I added another model with id required in the example todo to figure out the fix's scope. It turns out other methods are fine.

I temporary keep the new TodoWithId model and its related tests in the example package so reviewers know what's the expected controller file content, and in case you have questions regarding other methods.

Will remove the changes in examples/todo when land the PR.
-->

Checklist

👉 Read and sign the CLA (Contributor License Agreement) 👈

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

👉 Check out how to submit a PR 👈

@jannyHou jannyHou force-pushed the fix/model-with-id-required branch from fca2503 to e14031f Compare September 4, 2019 19:07
type: 'confirm',
name: 'idRequired',
message: 'Is the id required when creating an instance?',
default: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the generated flag for the id property? See #3643

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondfeng The fix in this PR is in the controller generator, so it doesn't have the generated flag (#3643 is modifying model generator)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the controller code generator have access the model definition to check?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if lb4 controller could automatically infer whether the id property is required or forbidden when creating new models. At the same time, I think the current proposal is a meaningful improvement, therefore it should be landed. We can improve the generator to parse id property definition later.

IIRC, there are multiple ways how to end up with a model that requires id values on creation. The model-level setting forceId plays an important role.

@model({settings: {forceId: false}})
export class TodoWithId extends Entity {
  @property({
    type: 'number',
    id: true,
  })
  id?: number;

  // ...
}

When forceId is disabled, clients have can either supply their own id, or let the database generate a new unique id for them. (In the schema, the id property should be shown as optional.) That's my understanding of how things should work, I did not run a test to verify the actual behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raymondfeng

Can the controller code generator have access the model definition to check?

It should have access to the model def if the file exist. Then use ts-morph to read the element.

And I agree with @bajtos that we probably can land this PR first, since in some situations, if idRequired cannot be inferred from the model class, a prompt is still needed.

Like the id name prompt, when id cannot be inferred from the artifactInfo, the prompt will ask for it.

I will submit another PR to read the idRequired from model file and add a when to the prompt. I believe there would be more discussion about how we infer the flag :)

type: 'confirm',
name: 'idRequired',
message: 'Is the id required when creating an instance?',
default: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if lb4 controller could automatically infer whether the id property is required or forbidden when creating new models. At the same time, I think the current proposal is a meaningful improvement, therefore it should be landed. We can improve the generator to parse id property definition later.

IIRC, there are multiple ways how to end up with a model that requires id values on creation. The model-level setting forceId plays an important role.

@model({settings: {forceId: false}})
export class TodoWithId extends Entity {
  @property({
    type: 'number',
    id: true,
  })
  id?: number;

  // ...
}

When forceId is disabled, clients have can either supply their own id, or let the database generate a new unique id for them. (In the schema, the id property should be shown as optional.) That's my understanding of how things should work, I did not run a test to verify the actual behavior.

Object.assign({}, restCLIInputComplete, {idRequired: true}),
);

checkRestCrudContents(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Boolean arguments are difficult to understand. What does 1 mean in this context? I have to look up the definition of checkRestCrudContents to understand that.

Let's use named flags (an options arg) please.

Suggested change
checkRestCrudContents(1);
checkRestCrudContents({idRequired: true});

id: true,
required: true,
})
id?: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since id is required: true, I think we should tell TypeScript to make it require too?

Suggested change
id?: number;
id: number;

@bajtos
Copy link
Member

bajtos commented Sep 6, 2019

@jannyHou one more thing: please update the rest-crud package to stay in sync with the template. See packages/rest-crud/src/crud-rest.controller.ts.

I am not sure if it's possible to apply both changes (JSON schema, TypeScript type). To me, it's important to emit the correct JSON schema, so that the REST API validation does not reject valid requests. As for the TypeScript type, we can start with something like Todo & {id?: ID} - I think that should cover all cases (id required, id forbidden).

@jannyHou jannyHou force-pushed the fix/model-with-id-required branch 2 times, most recently from 03e8048 to 17f9bca Compare September 10, 2019 20:50
// * Implementation of the endpoint `POST /`.
// * @param data Model data
// */
// create(data: Omit<T, IdName>): Promise<T>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @bajtos Since the type of data is not determined (w/ or w/o id), I removed the create function in this interface. Thought?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am quite unhappy with the changes made in this file, I feel they are introducing too much complexity. We have started with one option - id is required or generated - and have two controller classes. Imagine what is going happen when the number of options grows. Are we going to have 8 different classes when the number of options grows to 3?

I prefer to sacrifice compile-time safety and keep the implementation simple. I expect there will be very few people consuming these runtime-generated controllers directly from TypeScript code.

I tried my suggestions to replace Omit<T, IdName> with Todo & {id?: ID} and I see it does not work. Can we use the following type instead?

   async create(
      data: T | Omit<T, IdName>,
    ): Promise<T>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imagine what is going happen when the number of options grows. Are we going to have 8 different classes when the number of options grows to 3?

I share your concern and I am also not satisfied with the current approach, while honestly I couldn't think of any better solution in a reasonable time 😞

The type of data is not the biggest problem, the generated request body spec when id is excluded need to differ from the one that includes id.

See https://github.com/strongloop/loopback-next/blob/master/packages/rest-crud/src/crud-rest.controller.ts#L155

HttpCachingProxy,
} from '../helpers';

describe('TodoApplication - test TodoWithId', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does TodoWithId fit into the narrative of our Todo tutorial?

In my mind, the primary purpose of Todo example app is to support the tutorial and show a simple & meaningful LB4 application. It's not the best place for adding acceptance tests that are not related to the Todo tutorial.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos Oh the tests will be removed when land the PR, see my comment in #3667 (comment).

I should have added the comment in the code.

At first I didn't know if create method is the only one affected, so I added another model with id required in the example todo to figure out the fix's scope. It turns out other methods are fine.
I temporary keep the new TodoWithId model and its related tests in the example package so reviewers know what's the expected controller file content, and in case you have questions regarding other methods.
Will remove the changes in examples/todo when land the PR.

expect(created)
.to.have.property('id')
.of.type('number');
context('id not required', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context('id not required', () => {
context('id generated by the database', () => {

.post('/products')
.send({id: 1, name: 'a name'})
.expect(422);
context('id required', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context('id required', () => {
context('id required (supplied by the user)', () => {

// * Implementation of the endpoint `POST /`.
// * @param data Model data
// */
// create(data: Omit<T, IdName>): Promise<T>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am quite unhappy with the changes made in this file, I feel they are introducing too much complexity. We have started with one option - id is required or generated - and have two controller classes. Imagine what is going happen when the number of options grows. Are we going to have 8 different classes when the number of options grows to 3?

I prefer to sacrifice compile-time safety and keep the implementation simple. I expect there will be very few people consuming these runtime-generated controllers directly from TypeScript code.

I tried my suggestions to replace Omit<T, IdName> with Todo & {id?: ID} and I see it does not work. Can we use the following type instead?

   async create(
      data: T | Omit<T, IdName>,
    ): Promise<T>;

@@ -254,8 +241,43 @@ export function defineCrudRestController<
}
}

// See https://github.com/microsoft/TypeScript/issues/14607
return CrudRestControllerImpl;
if (options && options.idRequired) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At runtime, we already know whether the model requires the id to be provided by the user or whether it will be generated by the database. Let's use that information please and don't ask the user to supply it to us again.

Relevant code from LB3:

Please note that we need to support more update-only properties, for example the Cloudant connector uses property _rev that is always generated by the database.

I see this is topic is more complex than I initially thought :(

If you prefer, then I am ok to leave the changes in packages/rest-crud out of scope of this pull request, so that it can be landed sooner. Just open a follow-up issue and add it to the epic #2036 From model definition to REST API with no custom repository/controller classes please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see thank you for all the references. Yeah I would like to address it with @raymondfeng 's comment #3667 (comment) in another story.

The bug fix is not a committed story in September, so I would like to merge this quick fix first, then improve it :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Please open two stories - one to update rest-crud, another to improve the CLI tool.

@jannyHou
Copy link
Contributor Author

@bajtos Thanks for the detailed explanation, follow-up story created #3728 and also reverted the acceptance tests. PTAL

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but please consider my comment below.

{
type: 'confirm',
name: 'idRequired',
message: 'Is the id required when creating an instance?',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if this question makes it clear what are the implications of yes and no answers. Every model instance must eventually have some id value set in the database, thus one can argue that the id property is always required (in a way).

I think it may be better to instead ask the user if the id property is generated by the database. When the id is generated, then it must be excluded from create requests - I thinks this implication is easier to understand.

Suggested change
message: 'Is the id required when creating an instance?',
message: 'Is the id (primary key) value generated by the database?',

If you decide to accept my suggestion, then please rename the question/answer name from idRequired to something more sensible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos Hmm, I have a different opinion on this, I am thinking of being consistent with the LB 3 options.

See https://loopback.io/doc/en/lb3/Model-definition-JSON-file.html

required: Implies "Whether a value for the property is required. If true, then adding or updating a model instance requires a value for the property." and this is my purpose of adding the prompt.

generated: "The generated property indicates the ID will be automatically generated by the database. If true, the connector decides what type to use for the auto-generated key. For relational databases, such as Oracle or MySQL, it defaults to number. If your application generates unique IDs, set it to false." It describes a more complicated scenario than whether a property is required or not in the request payload...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point.

There is still one missing piece though: the id property is special, there are three possible variants:

  1. The id is required when creating an instance. (It's always generated by the client, useful e.g. for offline-first.) TS type to use: T (e.g. Product)
  2. The id is not required but can be provided when creating an instance. (The client can decide whether they want to supply their own id or let the DB create one.) TS type to use: T & {id?: <number|string>} (I am not entirely sure about the right type)
  3. The id must not be provided and will be always generated by the database. TS type to use: Omit<T, 'id'>.

By default, LB apps are configured for the option 3 - the id property is not allowed in "create" requests.

That's why I find your prompt confusing, because to me, it selects between the first and second variant, but then emits TypeScript type for the third variant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bajtos I see, maybe let me change the prompt to be "Is the id omitted when creating an instance?"

  • Yes: use Omit<T, 'id'>
  • No: use T

For the 2nd scenario, I think it could be same as 1 if id is defined as optional property in the model, like:

// scenario 1
@model()
class Product{
  @property()
   id: string;
}

// scenario 2
@model()
class Product{
  @property()
   id?: string;
}

@jannyHou
Copy link
Contributor Author

@bajtos see my comment and I applied the change accordingly: #3667 (comment), thank you

Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Please clean up the git history and wait for green CI before landing.

message: 'Is the id required when creating an instance?',
default: false,
name: 'idOmitted',
message: 'Is the id omitted when creating an instance?',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I would use "creating a new instance" to make it even more obvious that this prompt is related to new instances only

@jannyHou jannyHou force-pushed the fix/model-with-id-required branch 2 times, most recently from 11a65f3 to 4ffd1b3 Compare September 16, 2019 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

why Loopback + mongodb is not working?
3 participants