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

Adding multiple items to the security array of the JSON Schema breaks all the types #40

Open
2 tasks done
DemianD opened this issue Sep 14, 2022 · 13 comments
Open
2 tasks done

Comments

@DemianD
Copy link

DemianD commented Sep 14, 2022

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

Fastify version

4.5.3

Plugin version

2.3.0

Node.js version

18.7.0

Operating system

macOS

Operating system version (i.e. 20.04, 11.3, 10)

11.5.2

Description

We have a problem that our types are not automatically inferred when we add multiple items to the security property of the JSON schema that you can define on the route.

You can see this in the example below:

Steps to Reproduce

import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import Fastify from 'fastify';

export const fastify = Fastify().withTypeProvider<TypeBoxTypeProvider>();

fastify.get('/', {
  schema: {
    body: Type.Object({
      x: Type.String(),
      y: Type.Number(),
      z: Type.Boolean(),
    }),
    // Adding multiple items here breaks the automatically type inferring. 
    security: [{ OAuth2: ['x:readOnly'] }, { OpenIDConnect: [] }],
  },
}, (req) => {
  // The `x`, `y`, `z` types are not inferred anymore because there are multiple items in the array
  const { x, y, z } = req.body;
});

Expected Behavior

We'd like that our types are inferred when adding multiple items to the security array.

@mcollina
Copy link
Member

cc @fastify/typescript

@Amphaal
Copy link

Amphaal commented Feb 20, 2023

Had the same issue, but when providing an additional onRequest handler instead of security.

Using the alternative syntax using route() for route declaration instead of get() / post() / whateverVerb() seems to make the issue disappear.

@mcollina
Copy link
Member

@sinclairzx81 could you take a look?

@sinclairzx81
Copy link
Contributor

Hi @mcollina ! Just had a quick look at the original issue code, this seems to be working ok...

TypeScript Link Here

image

General Recommendation:

  • Ensure latest version of Fastify (noting this issue dates back to Sep last year so there may have been some type updates since)
  • Ensure latest version of the TS compiler (currently typescript@4.9.5)
  • Ensure compilerOptions: { strict: true } is configured in tsconfig.json

Happy to look a bit deeper if there's a way to repro the inference issue, but this appears to be working alright :)

Hope this helps!
S

@mcollina
Copy link
Member

Thanks!

@Amphaal
Copy link

Amphaal commented Feb 21, 2023

Hi @mcollina ! Just had a quick look at the original issue code, this seems to be working ok...

TypeScript Link Here

image

General Recommendation:

  • Ensure latest version of Fastify (noting this issue dates back to Sep last year so there may have been some type updates since)
  • Ensure latest version of the TS compiler (currently typescript@4.9.5)
  • Ensure compilerOptions: { strict: true } is configured in tsconfig.json

Happy to look a bit deeper if there's a way to repro the inference issue, but this appears to be working alright :)

Hope this helps! S

Well, seems like my first assumption was not correct, my issue popped up in all (fastify.post | fastify.get) && fastify.route scenarii after all.

The issue I had, which I do not think is related to OP's issue, was that the function which I wanted to override onRequest with had explicitly defined parameters as generic FastifyRequest / FastifyReply, instead of my fastify's instance super-seeded generics (FastifyRequest<..., TypeBoxTypeProvider> / FastifyReply<..., TypeBoxTypeProvider>)

Capture d’écran 2023-02-21 à 13 09 03

...by enforcing these basic types, functionality breaks and my request components all resolves as unknown, which is kinda understandable.

Thank you guys @mcollina @sinclairzx81 for your time :)

@andreaspalsson
Copy link

I ran into the same issues with multiple items in the security array making body unknown however it's using FastifyPluginAsyncTypebox could that be a another source for this issue. I have tested with the latest version of everything but still get the same issues. I created a small repo where the issue is reproduced.

https://github.com/andreaspalsson/typebox-schema-security

Also ran it through GitHub Actions to make sure it wasn't something local and the types fails there as well

https://github.com/andreaspalsson/typebox-schema-security/actions/runs/4232737696/jobs/7352833222

Versions:

"@fastify/swagger": "^8.3.1",
"@fastify/type-provider-typebox": "^2.4.0",
"@sinclair/typebox": "^0.25.23",
"fastify": "^4.13.0",
"typescript": "^4.9.5"

@sinclairzx81
Copy link
Contributor

@andreaspalsson Hey, thanks for the repro! Had another look at this and can reproduce the issue on the TSP. The issue seems to be related to the definitions inside @fastify/swagger. See TypeScript link below and uncomment the following line to reproduce.

TypeScript Link Here

// import '@fastify/swagger' // uncomment to introduce error

Issue

The swagger package performs a declaration merge on the FastifySchema to include a set of additional properties specific to swagger. One of which being the security property. This is defined as follows (code can be found here)

  interface FastifySchema {
    hide?: boolean;
    deprecated?: boolean;
    tags?: string[];
    description?: string;
    summary?: string;
    consumes?: string[];
    produces?: string[];
    externalDocs?: OpenAPIV2.ExternalDocumentationObject | OpenAPIV3.ExternalDocumentationObject;
    security?: Array<{ [securityLabel: string]: string[] }>; // <-- issue here !
    /**
     * OpenAPI operation unique identifier
     */
    operationId?: string;
  }

TypeScript can have some difficulties dealing with arbitrary length array / property key definitions when mapping types, but its curious why it's causing issues here as this property is not mapped. My best guess is due to the declaration merge, this is causing the security property (and type) to be propagated to the route (via req.routeSchema.security) and as the property type is non-mappable, it's causing TS to inference to break down (even though there's no mapping on it, which is strange)

In situations like this though, it's common to turn to readonly and as const assertions, this to provide TS enough assurances as to the shape of the security array.

Possible Fix

The following updates the security property to be readonly. This will need to be updated in the @fastify/swagger declarations here).

// ------------------------------------------------------------------------------
// Fix: @fastify/swagger
//
// Here we mandate that the security array is readonly, and the caller must
// pass with a 'as const' assertion. This enables TS to propagate the type as 
// fixed readonly data structure. Similar techniques can be seen in libraries
// like json-schema-to-ts which mandate 'as const' to infer object property
// keys encoded in JSON Schema.
// ------------------------------------------------------------------------------

declare module "fastify" {
  // added: readonly is required for propagation of dictionary types.
  export type SecurityArray = readonly {
    [securityLabel: string]: readonly string[]
  }[]
  interface FastifySchema {
    hide?: boolean;
    deprecated?: boolean;
    tags?: string[];
    description?: string;
    summary?: string;
    consumes?: string[];
    produces?: string[];
    externalDocs?: OpenAPIV2.ExternalDocumentationObject | OpenAPIV3.ExternalDocumentationObject;
    security?: SecurityArray; // Fix
    /**
     * OpenAPI operation unique identifier
     */
    operationId?: string;
  }
}

Example

Here's an example of things working in the TSP with the as const applied to the security array per route.

TypeScript Link Here

  server.post(
    "/double-security",
    {
      schema: {
        body: Type.Object({
          foo: Type.Number(),
          bar: Type.String(),
        }),
        security: [{ apiKey: ['foo'] }, { authHeader: ['bar'] }] as const // mandate const readonly assertion
      },
    },
    (request, reply) => {
      const { foo, bar } = request.body; // ok
      return { foo, bar };
    }
  );
};

Future

While in TS 4.0 the as const assertion is required, In TS 5.0, it should be possible to automatically apply the as const with the upcoming TS 5.0 const generics language feature. Setting up readonly now should get things up for a later TS 5.0 enhancement once it lands.

Hope this helps
S

CC @mcollina @Amphaal

@sinclairzx81
Copy link
Contributor

@andreaspalsson @Amphaal Oh, one other thing. In the interim, you should be able to get around the inference issues with an as any assertion on the security array (which isn't ideal, but works)

security: [{ apiKey: [] }, { authHeader: [] }] as any // replace with "as const" on fix

Cheers

@andreaspalsson
Copy link

@sinclairzx81 Thanks for a great explanation of the problem and a workaround. We only have a few endpoints that have multiple security items so this interim solution will work fine for us. Thanks for the quick help.

@mcollina mcollina reopened this Feb 21, 2023
@mcollina
Copy link
Member

Is this a problem i @fastify/swagger then? Could we fix it there?

@sinclairzx81
Copy link
Contributor

@mcollina Yeah, it might be worth opening an issue on @fastify/swagger and linking back to this one. The suggested fix would require an update in the swagger declarations (so would need a review from the Fastify TS team), also there's actually a few ways to approach a solution, so would be good to discuss those within the context of the swagger project.

@mcollina
Copy link
Member

Should we move this issue?

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

No branches or pull requests

5 participants