Skip to content

Commit 3405e48

Browse files
committed
feat: Use User and UserWithMetadata to strongly type the dashboard recipe
1 parent 2e8e989 commit 3405e48

File tree

6 files changed

+77
-106
lines changed

6 files changed

+77
-106
lines changed

supertokens_python/recipe/dashboard/api/userdetails/user_get.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
from supertokens_python.exceptions import raise_bad_input_exception
1+
from typing import Union
22

3+
from supertokens_python.exceptions import raise_bad_input_exception
4+
from supertokens_python.recipe.dashboard.utils import get_user_for_recipe_id
35
from supertokens_python.recipe.usermetadata import UserMetadataRecipe
46
from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata
57

6-
from supertokens_python.recipe.dashboard.utils import get_user_for_recipe_id
7-
88
from ...interfaces import (
99
APIInterface,
1010
APIOptions,
1111
UserGetAPINoUserFoundError,
1212
UserGetAPIOkResponse,
1313
)
1414
from ...utils import is_valid_recipe_id
15-
from typing import Union
1615

1716

1817
async def handle_user_get(
@@ -36,23 +35,19 @@ async def handle_user_get(
3635

3736
user = user_response.user
3837

39-
# FIXME: Shouldn't be required, no?
40-
recipe_id_: str = recipe_id # type: ignore
41-
user_id_: str = user_id # type: ignore
42-
4338
try:
4439
UserMetadataRecipe.get_instance()
4540
except Exception:
4641
user.first_name = "FEATURE_NOT_ENABLED"
4742
user.last_name = "FEATURE_NOT_ENABLED"
4843

49-
return UserGetAPIOkResponse(recipe_id_, user)
44+
return UserGetAPIOkResponse(recipe_id, user)
5045

51-
user_metadata = await get_user_metadata(user_id_)
46+
user_metadata = await get_user_metadata(user_id)
5247
first_name = user_metadata.metadata.get("first_name", "")
5348
last_name = user_metadata.metadata.get("last_name", "")
5449

5550
user.first_name = first_name
5651
user.last_name = last_name
5752

58-
return UserGetAPIOkResponse(recipe_id_, user)
53+
return UserGetAPIOkResponse(recipe_id, user)

supertokens_python/recipe/dashboard/api/userdetails/user_put.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def update_email_for_recipe_id(
9393
if recipe_id == "thirdpartyemailpassword":
9494
form_fields = (
9595
ThirdPartyEmailPasswordRecipe.get_instance().email_password_recipe.config.sign_up_feature.form_fields
96-
) # TODO: Using config of EP recipe object here. Verify if it is correct.
96+
)
9797
email_form_fields = [
9898
form_field
9999
for form_field in form_fields

supertokens_python/recipe/dashboard/api/users_get.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,13 @@
1414
from __future__ import annotations
1515

1616
import asyncio
17-
from typing import TYPE_CHECKING, Any, Awaitable, Dict, List
17+
from typing import TYPE_CHECKING, Any, Awaitable, List
1818

19-
from supertokens_python.recipe.dashboard.utils import DashboardUser
2019
from supertokens_python.supertokens import Supertokens
2120

2221
from ...usermetadata import UserMetadataRecipe
2322
from ...usermetadata.asyncio import get_user_metadata
24-
from ..interfaces import (
25-
DashboardUsersGetResponse,
26-
DashboardUsersGetResponseWithMetadata,
27-
)
23+
from ..interfaces import DashboardUsersGetResponse, UserWithMetadata
2824

2925
if TYPE_CHECKING:
3026
from supertokens_python.recipe.dashboard.interfaces import (
@@ -69,9 +65,9 @@ async def handle_users_get_api(
6965
users_response.users, users_response.next_pagination_token
7066
)
7167

72-
updated_users_arr: List[Dict[str, Any]] = DashboardUsersGetResponse(
73-
users_response.users, users_response.next_pagination_token
74-
).users
68+
users_with_metadata: List[UserWithMetadata] = [
69+
UserWithMetadata(user) for user in users_response.users
70+
]
7571
metadata_fetch_awaitables: List[Awaitable[Any]] = []
7672

7773
async def get_user_metadata_and_update_user(user_idx: int) -> None:
@@ -80,12 +76,9 @@ async def get_user_metadata_and_update_user(user_idx: int) -> None:
8076
first_name = user_metadata.metadata.get("first_name")
8177
last_name = user_metadata.metadata.get("last_name")
8278

83-
updated_users_arr[user_idx]["user"].update(
84-
{
85-
"firstName": first_name,
86-
"lastName": last_name, # None becomes null which is acceptable for the dashboard.
87-
}
88-
)
79+
# None becomes null which is acceptable for the dashboard.
80+
users_with_metadata[user_idx].first_name = first_name
81+
users_with_metadata[user_idx].last_name = last_name
8982

9083
# Batch calls to get user metadata:
9184
for i, _ in enumerate(users_response.users):
@@ -110,7 +103,7 @@ async def get_user_metadata_and_update_user(user_idx: int) -> None:
110103

111104
promise_arr_start_position += batch_size
112105

113-
return DashboardUsersGetResponseWithMetadata(
114-
updated_users_arr,
106+
return DashboardUsersGetResponse(
107+
users_with_metadata,
115108
users_response.next_pagination_token,
116109
)

supertokens_python/recipe/dashboard/interfaces.py

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
from __future__ import annotations
1515

1616
from abc import ABC, abstractmethod
17-
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional
17+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
1818

1919
from ...supertokens import AppInfo
2020
from ...types import APIResponse, User
21-
from .utils import DashboardConfig, DashboardUser
21+
from .utils import DashboardConfig, GetUserForRecipeUser
2222

2323
if TYPE_CHECKING:
2424
from supertokens_python.framework import BaseRequest, BaseResponse
@@ -69,49 +69,43 @@ def __init__(self):
6969
] = None
7070

7171

72-
class DashboardUsersGetResponse(APIResponse):
73-
status: str = "OK"
74-
75-
def __init__(self, users: List[User], next_pagination_token: Optional[str]):
76-
self.users = users
77-
self.next_pagination_token = next_pagination_token
72+
class UserWithMetadata:
73+
def __init__(
74+
self,
75+
user: User,
76+
first_name: Optional[str] = None,
77+
last_name: Optional[str] = None,
78+
):
79+
self.user = user
80+
self.first_name = first_name
81+
self.last_name = last_name
7882

7983
def to_json(self) -> Dict[str, Any]:
80-
users_json = [
84+
res = self.user.to_json()
85+
res["user"].update(
8186
{
82-
"recipeId": u.recipe_id,
83-
"user": {
84-
"id": u.user_id,
85-
"email": u.email,
86-
"timeJoined": u.time_joined,
87-
"thirdParty": None
88-
if u.third_party_info is None
89-
else u.third_party_info.__dict__,
90-
"phoneNumber": u.phone_number,
91-
},
87+
"firstName": self.first_name,
88+
"lastName": self.last_name,
9289
}
93-
for u in self.users
94-
]
95-
return {
96-
"status": self.status,
97-
"users": users_json,
98-
"nextPaginationToken": self.next_pagination_token,
99-
}
90+
)
91+
return res
10092

10193

102-
class DashboardUsersGetResponseWithMetadata(APIResponse):
94+
class DashboardUsersGetResponse(APIResponse):
10395
status: str = "OK"
10496

10597
def __init__(
106-
self, users: List[Dict[str, Any]], next_pagination_token: Optional[str]
98+
self,
99+
users: Union[List[User], List[UserWithMetadata]],
100+
next_pagination_token: Optional[str],
107101
):
108102
self.users = users
109103
self.next_pagination_token = next_pagination_token
110104

111105
def to_json(self) -> Dict[str, Any]:
112106
return {
113107
"status": self.status,
114-
"users": self.users,
108+
"users": [u.to_json() for u in self.users],
115109
"nextPaginationToken": self.next_pagination_token,
116110
}
117111

@@ -129,7 +123,7 @@ def to_json(self) -> Dict[str, Any]:
129123
class UserGetAPIOkResponse(APIResponse):
130124
status: str = "OK"
131125

132-
def __init__(self, recipe_id: str, user: DashboardUser):
126+
def __init__(self, recipe_id: str, user: GetUserForRecipeUser):
133127
self.recipe_id = recipe_id
134128
self.user = user
135129

supertokens_python/recipe/dashboard/utils.py

Lines changed: 20 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
if TYPE_CHECKING:
5454
from .interfaces import APIInterface, RecipeInterface
5555

56+
from supertokens_python.recipe.dashboard.interfaces import UserWithMetadata
57+
from supertokens_python.types import User
58+
5659

5760
class InputOverrideConfig:
5861
def __init__(
@@ -152,99 +155,70 @@ def is_valid_recipe_id(recipe_id: str) -> bool:
152155
return recipe_id in ("emailpassword", "thirdparty", "passwordless")
153156

154157

155-
class DashboardUser:
156-
def __init__(self, user: Dict[str, Any]):
157-
self.user_id: str = user["user_id"]
158-
self.email: Optional[str] = user.get("email")
159-
self.phone: Optional[str] = user.get("phone")
160-
self.time_joined: int = user["time_joined"]
161-
self.first_name: Optional[str] = user.get("first_name")
162-
self.last_name: Optional[str] = user.get("last_name")
163-
164-
def to_json(self) -> Dict[str, Any]:
165-
return {
166-
"id": self.user_id,
167-
"email": self.email,
168-
"phoneNumber": self.phone,
169-
"timeJoined": self.time_joined,
170-
"firstName": self.first_name,
171-
"lastName": self.last_name,
172-
}
173-
174-
175158
class GetUserForRecipeIdResult:
176-
def __init__(self, user: DashboardUser, recipe: str):
159+
def __init__(self, user: UserWithMetadata, recipe: str):
177160
self.user = user
178161
self.recipe = recipe
179162

180163

181164
async def get_user_for_recipe_id(
182165
user_id: str, recipe_id: str
183166
) -> Optional[GetUserForRecipeIdResult]:
184-
user: Optional[Dict[str, Any]] = None
167+
user: Optional[UserWithMetadata] = None
185168
recipe: Optional[str] = None
186169

187-
async def update_user(
188-
get_user_func1: Callable[[str], Awaitable[Any]],
189-
get_user_func2: Callable[[str], Awaitable[Any]],
170+
async def update_user_dict(
171+
get_user_func1: Callable[[str], Awaitable[Optional[User]]],
172+
get_user_func2: Callable[[str], Awaitable[Optional[User]]],
190173
recipe1: str,
191174
recipe2: str,
192175
):
193176
nonlocal user, user_id, recipe
194177

195178
try:
196-
user_response = await get_user_func1(user_id) # type: ignore
197-
198-
if user_response is not None:
199-
user = {
200-
**user_response.__dict__,
201-
"firstName": "",
202-
"lastName": "",
203-
}
179+
matching_user = await get_user_func1(user_id) # type: ignore
180+
181+
if matching_user is not None:
182+
user = UserWithMetadata(matching_user, first_name="", last_name="")
204183
recipe = recipe1
205184
except Exception:
206185
pass
207186

208187
if user is None:
209188
try:
210-
user_response = await get_user_func2(user_id)
211-
212-
if user_response is not None:
213-
user = {
214-
**user_response.__dict__,
215-
"firstName": "",
216-
"lastName": "",
217-
}
189+
matching_user = await get_user_func2(user_id)
190+
191+
if matching_user is not None:
192+
user = UserWithMetadata(matching_user, first_name="", last_name="")
218193
recipe = recipe2
219194
except Exception:
220195
pass
221196

222197
if recipe_id == EmailPasswordRecipe.recipe_id:
223-
await update_user(
198+
await update_user_dict(
224199
ep_get_user_by_id,
225200
tpep_get_user_by_id,
226201
"emailpassword",
227202
"thirdpartyemailpassword",
228203
)
229204

230205
elif recipe_id == ThirdPartyRecipe.recipe_id:
231-
await update_user(
206+
await update_user_dict(
232207
tp_get_user_by_idx,
233208
tpep_get_user_by_id,
234209
"thirdparty",
235210
"thirdpartyemailpassword",
236211
)
237212

238213
elif recipe_id == PasswordlessRecipe.recipe_id:
239-
await update_user(
214+
await update_user_dict(
240215
pless_get_user_by_id,
241216
tppless_get_user_by_id,
242217
"passwordless",
243218
"thirdpartypasswordless",
244219
)
245220

246221
if user is not None and recipe is not None:
247-
dashboard_user = DashboardUser(user)
248-
return GetUserForRecipeIdResult(dashboard_user, recipe)
222+
return GetUserForRecipeIdResult(user, recipe)
249223

250224
return None

supertokens_python/types.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414
from abc import ABC, abstractmethod
15-
from typing import Any, Dict, List, Union, TypeVar, Awaitable
15+
from typing import Any, Awaitable, Dict, List, TypeVar, Union
1616

1717
_T = TypeVar("_T")
1818

@@ -40,6 +40,21 @@ def __init__(
4040
self.third_party_info = third_party_info
4141
self.phone_number = phone_number
4242

43+
def to_json(self) -> Dict[str, Any]:
44+
45+
return {
46+
"recipeId": self.recipe_id,
47+
"user": {
48+
"id": self.user_id,
49+
"email": self.email,
50+
"timeJoined": self.time_joined,
51+
"thirdParty": None
52+
if self.third_party_info is None
53+
else self.third_party_info.__dict__,
54+
"phoneNumber": self.phone_number,
55+
},
56+
}
57+
4358

4459
class UsersResponse:
4560
def __init__(self, users: List[User], next_pagination_token: Union[str, None]):

0 commit comments

Comments
 (0)