Skip to content

Commit bcdfbaa

Browse files
committed
Last improvements
1 parent 4abbe6b commit bcdfbaa

File tree

2 files changed

+51
-13
lines changed

2 files changed

+51
-13
lines changed

fastapi_users_db_dynamodb/__init__.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ class Meta:
9494

9595
oauth_name = UnicodeAttribute(hash_key=True)
9696

97+
class UserIdIndex(GlobalSecondaryIndex):
98+
class Meta:
99+
index_name = "user_id-index"
100+
projection = AllProjection()
101+
102+
user_id = GUID(hash_key=True)
103+
97104
if TYPE_CHECKING: # pragma: no cover
98105
id: ID
99106
oauth_name: str
@@ -113,6 +120,7 @@ class Meta:
113120
# Global Secondary Index
114121
account_id_index = AccountIdIndex()
115122
oauth_name_index = OAuthNameIndex()
123+
user_id_index = UserIdIndex()
116124

117125

118126
class DynamoDBBaseOAuthAccountTableUUID(DynamoDBBaseOAuthAccountTable[UUID_ID]):
@@ -140,12 +148,37 @@ def __init__(
140148
self.user_table = user_table
141149
self.oauth_account_table = oauth_account_table
142150

151+
async def _hydrate_oauth_accounts(
152+
self,
153+
user: UP,
154+
instant_update: bool = False,
155+
) -> UP:
156+
"""
157+
Populate the `oauth_accounts` list of a user by querying the OAuth table.
158+
This mimics SQLAlchemy's lazy relationship loading.
159+
"""
160+
if self.oauth_account_table is None:
161+
return user
162+
await ensure_tables_exist(self.oauth_account_table)
163+
164+
user.oauth_accounts = [] # type: ignore
165+
166+
async for oauth_acc in self.oauth_account_table.user_id_index.query( # type: ignore
167+
user.id,
168+
consistent_read=instant_update,
169+
):
170+
user.oauth_accounts.append(oauth_acc) # type: ignore
171+
172+
return user
173+
143174
async def get(self, id: ID, instant_update: bool = False) -> UP | None:
144175
"""Get a user by id and hydrate oauth_accounts if available."""
145176
await ensure_tables_exist(self.user_table) # type: ignore
146177

147178
try:
148-
return await self.user_table.get(id, consistent_read=instant_update) # type: ignore
179+
user = await self.user_table.get(id, consistent_read=instant_update) # type: ignore
180+
user = await self._hydrate_oauth_accounts(user, instant_update)
181+
return user
149182
except self.user_table.DoesNotExist: # type: ignore
150183
return None
151184

@@ -157,7 +190,9 @@ async def get_by_email(self, email: str, instant_update: bool = False) -> UP | N
157190
async for user in self.user_table.email_index.query( # type: ignore
158191
email_lower,
159192
consistent_read=instant_update,
193+
limit=1,
160194
):
195+
user = await self._hydrate_oauth_accounts(user, instant_update)
161196
return user
162197
return None
163198

@@ -172,18 +207,21 @@ async def get_by_oauth_account(
172207
raise NotImplementedError()
173208
await ensure_tables_exist(self.user_table, self.oauth_account_table) # type: ignore
174209

175-
async for oauth_acc in self.oauth_account_table.oauth_name_index.query(
176-
oauth,
210+
async for oauth_acc in self.oauth_account_table.account_id_index.query(
211+
account_id,
177212
consistent_read=instant_update,
213+
filter_condition=self.oauth_account_table.oauth_name == oauth, # type: ignore
214+
limit=1,
178215
):
179-
if oauth_acc.account_id == account_id:
180-
try:
181-
return await self.user_table.get( # type: ignore
182-
oauth_acc.user_id,
183-
consistent_read=instant_update,
184-
)
185-
except self.user_table.DoesNotExist: # type: ignore # pragma: no cover
186-
return None
216+
try:
217+
user = await self.user_table.get( # type: ignore
218+
oauth_acc.user_id,
219+
consistent_read=instant_update,
220+
)
221+
user = await self._hydrate_oauth_accounts(user, instant_update)
222+
return user
223+
except self.user_table.DoesNotExist: # type: ignore # pragma: no cover
224+
return None
187225
return None
188226

189227
async def create(self, create_dict: dict[str, Any] | UP) -> UP:

tests/test_users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,13 @@ def _get_account(_user: UserOAuth):
206206
)
207207
assert _get_account(user).access_token == "NEW_TOKEN" # type: ignore
208208

209-
#! IMPORTANT: Since DynamoDB uses eventual consistency, we need a small delay (e.g. `time.sleep(0.01)`) \
209+
#! NOTE: Since DynamoDB uses eventual consistency, we need a small delay (e.g. `time.sleep(0.01)`) \
210210
#! to ensure the user was fully updated. In production, this should be negligible. \
211211
#! Alternatively, most methods of the `DynamoDBDatabase` class (e.g. `get`, `update`, ...) allow users \
212212
#! to enable consistent reads via the `instant_update` argument.
213213

214214
# Get by id
215-
id_user = await dynamodb_user_db_oauth.get(user.id, instant_update=True)
215+
id_user = await dynamodb_user_db_oauth.get(user.id)
216216
assert id_user is not None
217217
assert id_user.id == user.id
218218
assert _get_account(id_user).access_token == "NEW_TOKEN" # type: ignore

0 commit comments

Comments
 (0)