v1: Attempted fancy typing on Api doesn't allow fo...
# help
a
v1: Attempted fancy typing on Api doesn't allow for authorization to be configurable. It works if I hardcode it.
t
does your authorizer name have to be dynamic?
a
It's configurable whether there is an authorizer or not, depending on whether a user pool is given.
Having trouble too with Api#addRoutes, which looks to also depend on that authorizer name somehow.
t
can you write it like this
Copy code
authorizers: {
  myAuthorizer: authorizer
},
defaults: {
  authorizer: authorizer ? "myauthorizer": "none"
}
or
Copy code
authorizers: authorizer ? { myAuthorizer: authorizer } : undefined
a
The authorizer name is a variable simply so that I can't make a typo in the three places it needs to go.
Same error
Copy code
const userPool = this.props.userPool;
      let authorizers: Record<string, ApiAuthorizer> = {};

      if (userPool) {
        this.authorizerName = "CognitoPool";
        authorizers = {
          [this.authorizerName]: {
            type: "user_pool",
            userPool: {
              id: userPool.userPoolId,
              clientIds: [this.props.userPoolClient!.userPoolClientId],
            },
          },
        };

        environment.USER_POOL_ID = userPool.userPoolId;
        environment.USER_POOL_CLIENT_ID = this.props.userPoolClient!.userPoolClientId;
      }

      this.api = new Api(this, this.id, {
        authorizers,
        defaults: {
          authorizer: this.authorizerName ?? "none",
          function: { environment },
        },
        cors,
      });
t
well ideally you use TS to enforce that but you can also fix this by defining the name as a const variable eg
Copy code
authorizerName = "myname" as const
a
Then when I add routes, I need to give the authorizer name of "none" if none.
t
let me play with this one sec
a
On the routes, I'm trying to build them up from my own structure that also includes OpenAPI spec. This function just bleeds red.
Copy code
export type ApiFunctionRoutePropsMap = {
  [key: string]: ApiFunctionRouteProps<any>;
};

  private getApiFunctionRoutePropsFromRouteMap(routeMap: RouteMap[]): ApiFunctionRoutePropsMap {
    return routeMap.reduce<ApiFunctionRoutePropsMap>((routes, mapping) => {
      const { name, method, path, handler, authorization } = mapping;

      return {
        ...routes,
        [`${method} ${path}`]: {
          authorizer: authorization ? this.authorizerName : undefined,
          function: {
            handler,
            functionName: this.scope.logicalPrefixedName(`${this.id}-${name}`),
          },
        },
      };
    }, {} as ApiFunctionRoutePropsMap);
  }
The loss of many exported types and use of fancy generics is making it hard to figure out what the function signatures really are.
t
for the first part you can do this
Copy code
const condition = false

  const authorizers = !condition
    ? undefined
    : {
        authorizer: {
          type: "user_pool",
        } as const,
      }

  const api = new Api(ctx.stack, "id", {
    authorizers,
    defaults: {
      authorizer: "authorizer",
    },
  })
a
Your hardcoding a name in two places, setting up a bug, and setting a default authorizer that may not exist.
t
you can put a conditional there, I forgot to add it in
and the name isn't really hardcoded, you'll get autocomplete on it and it won't let you pass in the wrong value
a
I'm not sure how to read that. What is "user_pool"?
Copy code
let authorizers: Record<string, ApiAuthorizer> | undefined;

      if (userPool) {
        authorizers = {
          CognitoPool: {
            type: "user_pool",
            userPool: {
              id: userPool.userPoolId,
              clientIds: [this.props.userPoolClient!.userPoolClientId],
            },
          },
        };

        environment.USER_POOL_ID = userPool.userPoolId;
        environment.USER_POOL_CLIENT_ID = this.props.userPoolClient!.userPoolClientId;
      }

      this.api = new Api(this, this.id, {
        authorizers,
        defaults: {
          authorizer: authorizers ? "CognitoPool" : "none",
          function: { environment },
        },
        cors,
      });
t
sorry ignore the faint text that's github copilot
a
This code ☝️ doesn't recognize "CognitoPool" as an option.
t
yeah the issue here is you're not relying on inference to let ts figure out the types. By defining the type on authorizers explicitly you're erasing the type information - the example I posted above doesn't do that
but I'm seeing if you can just skip all of this since the current patterns you have aren't TS friendly
a
I switched to the tertiary operator - still no good:
Copy code
let authorizers = !userPool
        ? undefined
        : {
            CognitoPool: {
              type: "user_pool",
              userPool: {
                id: userPool.userPoolId,
                clientIds: [this.props.userPoolClient!.userPoolClientId],
              },
            },
          } as const;

      if (userPool) {
        environment.USER_POOL_ID = userPool.userPoolId;
        environment.USER_POOL_CLIENT_ID = this.props.userPoolClient!.userPoolClientId;
      }

      this.api = new Api(this, this.id, {
        authorizers,
        defaults: {
          authorizer: userPool ? "CognitoPool" : "none",
          function: { environment },
        },
        cors,
      });
It doesn't even like the
authorizers,
line now.
I can probably get past this by doing separate
new Api
in an if/else. I don't know what types to use for routes then.
Nope. Same error:
Copy code
const userPool = this.props.userPool;
      if (userPool) {
        this.api = new Api(this, this.id, {
          authorizers: {
                CognitoPoolAuth: {
                  type: "user_pool",
                  userPool: {
                    id: userPool.userPoolId,
                    clientIds: [this.props.userPoolClient!.userPoolClientId],
                  },
                },
              },
          defaults: {
            authorizer: "CognitoPoolAuth",
            function: { environment },
          },
          cors,
        });
      }
      else {
        this.api = new Api(this, this.id, {
          defaults: {
            authorizer: "none",
            function: { environment },
          },
          cors,
        });
      }
t
ok I see the issue, if you move the tertiary inline and remove const I think it'll work
Copy code
const condition = false

  const api = new Api(ctx.stack, "id", {
    authorizers: !condition
      ? undefined
      : {
          CognitoPool: {
            type: "user_pool",
            userPool: {
              id: "asd",
              clientIds: [],
            },
          },
        },
    defaults: {
      authorizer: condition ? "none" : "CognitoPool",
    },
  })
but let me shift over to seeing if you can eject out of all of this
a
See above ☝️ no conditions, still not working. 🤯
t
probably has to do with the way
this.Api
is typed then one second we might need to patch the types here to be a bit looser for people who want to opt out
a
I like strongly typed, but I have multiple API stacks. Most use the Cognito user pool, one has no API-level auth, and some stacks that do auth turn it off for specific APIs. (Such as user creation - can't have auth for that!)
I don't want to copy & paste this stuff. And then as I said, I've enhanced this base stack class to also build OpenAPI spec.
t
yeah the issue here is to take advantage of this typing, you need to be using inference everywhere - so no defining specific types (or you define them but they're ugly). Unfortunately class stacks aren't super friendly to this (although I can put together how you'd do inference in class stacks)
I published a canary release with looser typing: 1.0.3-next.12 the following code wasn't working before but works in it now:
Copy code
let authorizers: Record<string, ApiAuthorizer> = undefined
  if (condition)
    authorizers = {
      foo: {
        type: "user_pool",
        userPool: {
          id: "asdasd",
          clientIds: [],
        },
      },
    }

  const napi = new Api(ctx.stack, "id", {
    authorizers: authorizers
    defaults: {
      authorizer: condition ? "none" : "CognitoPool",
    },
  })
You can use your variable for the authorizer name
a
The type checker is happy, even routing. 👍 Moving on with the migration...
Thank you for the incredible support!
t
np! This was on my mind and this was an excuse to get it done
@Derek Kershner FYI if you want to remove those generics you should be able to now
d
nah, I like generics, just was noting the adjustment when you create helper functions
I had the same issues as Adam above, but I just smashed my own
Authorizers
type into the generic everywhere, and all seems OK.
We also use a const for AuthorizerName as well, like Adam. In fact, most of our code and opinions seem near identical every time we interact, lol.
I think what you are probably running headfirst into regarding inference is a very common ESLint rule @thdxr: Link It is part of
@typescript-eslint/recommended
.
when combined with clean coding’s “small functions”, the amount inferred tends to be very small (which can be very important in large projects)
as a sidenote Adam, I think your original, original type issue was because
this.authorizerName
had type
string
instead of
"YourActualAuthorizerName"
I typically solve this like this:
Copy code
private authorizerName: "YourActualAuthorizerName"  = "YourActualAuthorizerName";
i skimmed though, feel free to ignore
a
Ah, I wanted it to be "none" also, but could have tried a string union. I'm fine with where we landed, but good idea.
@thdxr Is 1.0.3-next.12 now in v1.0.3, or coming in v1.0.4? (I didn't see anything in the 1.0.3 release list.)
t
it's in 1.0.4 - which is released. Had to remove 1.0.3 because it had a missing export
updated release notes