feat!: replace TypedUser with User and add AuthenticatedUser#17151
feat!: replace TypedUser with User and add AuthenticatedUser#17151AlessioGr wants to merge 19 commits into
TypedUser with User and add AuthenticatedUser#17151Conversation
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
User, AuthenticatedUser and ClientUser| if (!user) { | ||
| throw new Forbidden(args.req.t) | ||
| } | ||
|
|
There was a problem hiding this comment.
Bunch of unsafe code discovered by properly typing the user
| existingSession.expiresAt = new Date(now.getTime() + tokenExpInMs) | ||
|
|
||
| // Prevent updatedAt from being updated when only refreshing a session | ||
| user.updatedAt = null |
There was a problem hiding this comment.
This was wrong - updatedAt is a required field on the user. We should only do this for what we send to the updateOne operation => do it inline
|
|
||
| /** | ||
| * @deprecated Use `TypedUser` instead. This will be removed in 4.0. | ||
| * Note: AuthenticatedUser still carries the write-only `password` from `User` (always `undefined` at runtime). |
There was a problem hiding this comment.
Internal note, does not show in jsdocs for users hovering over the type since it's a separate comment block
| export type ClientUser = { | ||
| [key: string]: any | ||
| } & BaseUser | ||
| export type ClientUser = AuthenticatedUser |
There was a problem hiding this comment.
Since it's just an alias, I'm considering removing it in a separate PR
There was a problem hiding this comment.
I'm in favor of removal. Having multiple types that are identical is not helpful
|
|
||
| export type UserSession = { createdAt: Date | string; expiresAt: Date | string; id: string } | ||
| export type UserSession = { | ||
| createdAt?: Date | null | string |
There was a problem hiding this comment.
This makes it compatible with our generated User type
| const data = req.data | ||
| const payload = req.payload | ||
| const user = req.user | ||
| const user = req.user as null | UserWithCart |
There was a problem hiding this comment.
plugin-ecommerce adds new fields to the user collection. The best way to handle this internally is a new UserWithCart type, which extends our user with whatever plugin-ecommerce adds
| let currency: string = currenciesConfig.defaultCurrency | ||
| let cartID: DefaultDocumentIDType = data?.cartID | ||
| let cart = undefined | ||
| let cart: any = undefined |
There was a problem hiding this comment.
This was already untyped on main. Fixing it properly is out of scope for this PR
| } | ||
|
|
||
| if (!user && req.user) { | ||
| user = req?.user?.id ? req.user : req?.user?.user |
There was a problem hiding this comment.
This was incorrect and just there to hide an incorrectly written test.
Our tests were incorrectly passing the entire LoginResult to the plugin-import-export user:
export type LoginResult<TSlug extends AuthCollectionSlug> = {
exp?: number
token?: string
user?: AuthRuntimeFields & DataFromCollectionSlug<TSlug>
}This should have failed, but we typed the test as any and handled it in runtime by checking for user?.user, which should never exist or be handled.
| export type ClientUser = { | ||
| [key: string]: any | ||
| } & BaseUser | ||
| export type ClientUser = AuthenticatedUser |
There was a problem hiding this comment.
I'm in favor of removal. Having multiple types that are identical is not helpful
| }, | ||
| ), | ||
| [usersTenantsArrayFieldName]: ( | ||
| ((user as Record<string, unknown>)[usersTenantsArrayFieldName] as Record< |
There was a problem hiding this comment.
why is the type assertion needed as Record<string, unknown>? It can't just be User?
There was a problem hiding this comment.
Simplified example
const runtimePropertyNotStatic: string = 'myField'
const user: User = getUser()
const valueOfPropertyForUser = user[runtimePropertyNotStatic]The previous user type had a [property: string]: any index signature so we were able to do that. The new one does not have this index signature, so doing user[string] will error. In plugin-ecommerce this meant asserting user to a new, strongly typed UserAsCart type.
But in this example, we do not know the name of the property, since it's not static like cart. So we have to weaken the type to Record<string, unknown>. This is a rare scenario, and we do not lose any type-safety by doing that.
Why did we have to remove [property: string]: any?
This was needed in order to make the generated user type compatible with (assignable to) the fallback user type we use internally. We have a new type test for that: generated User is assignable to the untyped fallback user type.
What
This completes the user-type cleanup planned for Payload 4.0:
UntypedUserwas deprecated for removal in 4.0.TypedUserwas marked to be renamed toUserin 4.0.Previously, the public
Usertype was the loose, deprecatedUntypedUser, whileTypedUserwas the generated type for auth-enabled collections.ClientUserwas also loose, andreq.userdid not include the runtime auth fields_strategyand_sid.The types now have clear roles:
UserTypedUser. Without generated types, it falls back to a documented shape containing Payload's built-in auth fields. Contains both read and write fields.AuthenticatedUserUserplus the optional runtime fields_strategyand_sid. Used byPayloadRequest.user,payload.auth(), auth strategy results, and auth internals.ClientUseruseAuth().userandmeresponses. It is now an alias ofAuthenticatedUserI'm considering replacing
ClientUserin favor of justAuthenticatedUserin a separate PR.Breaking changes
TypedUserhas been removed. UseUser.UntypedUserhas been removed. UseUserfor a user document,AuthenticatedUserfor a signed-in request user, orClientUserin client code.UserandClientUserno longer have an[key: string]: anyindex signature. Custom auth-collection fields require generated types or an explicit augmented type.useroption is nowUser | nullinstead of the looseDocumenttype for:count,create,delete,duplicate,find,findByID,findDistinct, andupdatefindOneandupdateUserSession.createdAtis now optional and nullable:createdAt?: Date | null | string. This matches generated session types, but callers must handle a missing value.AuthenticatedUseris assignable toUser, so passingreq.userto these Local API operations continues to work.Other changes
payload.auth()and login results with the runtime auth fields, and usesAuthenticatedUserwhile login,me, refresh, and session code build signed-in users.createdAtvalues.updatedAtsolely to control database timestamps.plugin-mcpusesUserfor the authorized caller.plugin-import-exportremoves obsoletereq.user.userhandlingplugin-multi-tenantexplicitly casts accesses to plugin-defined user fields.plugin-ecommerceaddsUserWithCartfor the optional reversecartjoin that projects maydefine on their user collection.
Migration
Use
Userfor stored/read user documents andAuthenticatedUserwhen code specifically receives the signed-in user fromreq.user,payload.auth(), or an auth strategy.