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

feat(core): Only consider non-readonly properties in request bodies #813

Merged
merged 1 commit into from
Apr 11, 2023
Merged

feat(core): Only consider non-readonly properties in request bodies #813

merged 1 commit into from
Apr 11, 2023

Conversation

maikdijkstra
Copy link
Contributor

@maikdijkstra maikdijkstra commented Mar 29, 2023

Status

READY

Description

Whenever a readonly property is defined in a request body then these may be sent as part of a response but should not be sent as part of a request according to the openapi 3.0 specification. To solve this using types, we can only allow the properties that are non-readonly since these are the actual request body.

Should fix: #616

Steps to Test or Reproduce

Outline the steps to test or reproduce the PR here.

  1. Use a readonly property in a requestBody
  2. Check that the readonly properties are discarded from the generated body type in the defined hooks.

@vercel
Copy link

vercel bot commented Mar 29, 2023

Someone is attempting to deploy a commit to a Personal Account owned by @anymaniax on Vercel.

@anymaniax first needs to authorize it.

@maikdijkstra maikdijkstra changed the title feat: Only consider non-readonly properties in request bodies feat(core): Only consider non-readonly properties in request bodies Mar 29, 2023
@maikdijkstra
Copy link
Contributor Author

@anymaniax I updated the PR, so now we keep track of whether the type contains any readonly properties, if it contains a readonly property anywhere in the object tree then the NonReadonly type is added to the imports at the request body. So, when readonly is not there than we also dont import it. We add the orval generated types in the index.ts of the schemas. Let me know whether this would be an appropriate solution.

@anymaniax
Copy link
Collaborator

@maikdijkstra thanks a lot. Seems great. I found it weird to inject it in the index.ts. What do you think about inject it in each body file that need it? I was thinking would this need to be deactivated for some case? I don’t think so but what is your thoughts about this? I thought also about another solution. Be able to do the same thing as with a mutator but without the need to define all the mutator.

@maikdijkstra
Copy link
Contributor Author

@anymaniax My reason to inject it into the index.ts is that it will not show up with any other schemas in a separate file but is importable from the schemas folder and therefore only generate this type once. Does the body file mean the file with the endpoints? If so, then yes that is also an option, you will get quite some duplication if you generate an endpoint per file and all of them need it.

I think this case should not be deactivatable, since then the specification is simply wrong according to the OpenApi specification.

I don't really follow your proposed solution using the mutator. Could you explain how that would work?

@anymaniax
Copy link
Collaborator

@maikdijkstra ok I think it’s good enough for the moment. Can you just add an example with it and a test in the tests folder? For the second solution it wouldn’t solve the main problem directly so it doesn’t make sense forget about it 😅. Again thanks for the help

Readonly properties only have effect on responses. So for request
bodies the properties that are readonly should be removed from
the type.
@maikdijkstra
Copy link
Contributor Author

@anymaniax Good to hear! I added a test case to the default config. I also updated the react-query/basic sample to have a readonly property in the request body

@anymaniax
Copy link
Collaborator

Super @maikdijkstra I plan to checkout and test the branch tomorrow and merge it after that. I will create a new release with it during the week.

@localrobot
Copy link

I was testing this and found that null | undefined needs to be included in the below snippet otherwise when the values can be null or undefined, a nested object is inferred.

export type NonReadonly<T> = {
  [P in keyof Writable<T>]: T[P] extends number | string | boolean | null | undefined
    ? T[P]
    : NonReadonly<NonNullable<T[P]>>;
};

Here is a small reproduction:

type Body = { id?: string, name: { first?: string, last: string | null }};

// Type 'string' is not assignable to type 'NonReadonly<string>'.ts(2322)
const obj: NonReadonly<Body> = { id: 'abd', name: { last: null} }

Above, the inline object is valid but can't be assigned to obj.

@anymaniax
Copy link
Collaborator

@maikdijkstra for me all is good when you have time can you check the comment of @localrobot

@maikdijkstra
Copy link
Contributor Author

I was testing this and found that null | undefined needs to be included in the below snippet otherwise when the values can be null or undefined, a nested object is inferred.

export type NonReadonly<T> = {
  [P in keyof Writable<T>]: T[P] extends number | string | boolean | null | undefined
    ? T[P]
    : NonReadonly<NonNullable<T[P]>>;
};

Here is a small reproduction:

type Body = { id?: string, name: { first?: string, last: string | null }};

// Type 'string' is not assignable to type 'NonReadonly<string>'.ts(2322)
const obj: NonReadonly<Body> = { id: 'abd', name: { last: null} }

Above, the inline object is valid but can't be assigned to obj.

@anymaniax, @localrobot - I think this was an issue in a previous version. I found this issue myself too so I changed the implementation to instead check whether it is a primitive reverse the statement to whether it extends object. This fixes this issue, so it should work in the latest version of this PR.

type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <
  T,
>() => T extends Y ? 1 : 2
  ? A
  : B;

type WritableKeys<T> = {
  [P in keyof T]-?: IfEquals<
    { [Q in P]: T[P] },
    { -readonly [Q in P]: T[P] },
    P
  >;
}[keyof T];

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I,
) => void
  ? I
  : never;
type DistributeReadOnlyOverUnions<T> = T extends any ? NonReadonly<T> : never;

type Writable<T> = Pick<T, WritableKeys<T>>;
export type NonReadonly<T> = [T] extends [UnionToIntersection<T>]
  ? {
      [P in keyof Writable<T>]: T[P] extends object
        ? NonReadonly<NonNullable<T[P]>>
        : T[P];
    }
  : DistributeReadOnlyOverUnions<T>;

type Body = { id?: string, name: { first?: string, last: string | null }};

// Type 'string' is not assignable to type 'NonReadonly<string>'.ts(2322)
const obj: NonReadonly<Body> = { id: 'abd', name: { last: null} }

This version is valid typescript

@anymaniax anymaniax merged commit f5b1ec9 into orval-labs:master Apr 11, 2023
@anymaniax
Copy link
Collaborator

Thanks a lot @maikdijkstra

@michaelslec
Copy link

Will we get the same functionality for writeonly fields?

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.

Incorrect code generated when readOnly: true and also included in required
4 participants