remdb 0.3.146__py3-none-any.whl → 0.3.163__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (40) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +310 -0
  3. rem/agentic/context.py +81 -3
  4. rem/agentic/context_builder.py +18 -3
  5. rem/api/deps.py +3 -5
  6. rem/api/main.py +22 -3
  7. rem/api/mcp_router/server.py +2 -0
  8. rem/api/mcp_router/tools.py +90 -0
  9. rem/api/middleware/tracking.py +5 -5
  10. rem/api/routers/auth.py +346 -5
  11. rem/api/routers/chat/completions.py +4 -2
  12. rem/api/routers/chat/streaming.py +77 -22
  13. rem/api/routers/messages.py +24 -15
  14. rem/auth/__init__.py +13 -3
  15. rem/auth/jwt.py +352 -0
  16. rem/auth/middleware.py +42 -5
  17. rem/auth/providers/__init__.py +4 -1
  18. rem/auth/providers/email.py +215 -0
  19. rem/models/entities/__init__.py +4 -0
  20. rem/models/entities/subscriber.py +175 -0
  21. rem/models/entities/user.py +1 -0
  22. rem/schemas/agents/core/agent-builder.yaml +134 -0
  23. rem/services/__init__.py +3 -1
  24. rem/services/content/service.py +4 -3
  25. rem/services/email/__init__.py +10 -0
  26. rem/services/email/service.py +511 -0
  27. rem/services/email/templates.py +360 -0
  28. rem/services/postgres/README.md +38 -0
  29. rem/services/postgres/diff_service.py +19 -3
  30. rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
  31. rem/services/postgres/repository.py +5 -4
  32. rem/services/session/compression.py +113 -50
  33. rem/services/session/reload.py +14 -7
  34. rem/services/user_service.py +29 -0
  35. rem/settings.py +175 -0
  36. rem/sql/migrations/005_schema_update.sql +145 -0
  37. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/METADATA +1 -1
  38. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/RECORD +40 -31
  39. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/WHEEL +0 -0
  40. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,360 @@
1
+ """
2
+ Email Templates.
3
+
4
+ HTML email templates for transactional emails.
5
+ Uses inline CSS for maximum email client compatibility.
6
+
7
+ Downstream apps can customize by:
8
+ 1. Overriding COLORS dict
9
+ 2. Setting custom LOGO_URL and TAGLINE
10
+ 3. Creating custom templates using base_template()
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+
17
+ # Default colors - override in downstream apps
18
+ COLORS = {
19
+ "background": "#F5F5F5",
20
+ "foreground": "#333333",
21
+ "primary": "#4A90D9",
22
+ "accent": "#5CB85C",
23
+ "card": "#FFFFFF",
24
+ "muted": "#6b7280",
25
+ "border": "#E0E0E0",
26
+ }
27
+
28
+ # Branding - override in downstream apps
29
+ LOGO_URL: str | None = None
30
+ APP_NAME = "REM"
31
+ TAGLINE = "Your AI-powered platform"
32
+ WEBSITE_URL = "https://rem.ai"
33
+ PRIVACY_URL = "https://rem.ai/privacy"
34
+ TERMS_URL = "https://rem.ai/terms"
35
+
36
+
37
+ @dataclass
38
+ class EmailTemplate:
39
+ """Email template with subject and HTML body."""
40
+
41
+ subject: str
42
+ html_body: str
43
+
44
+
45
+ def base_template(
46
+ content: str,
47
+ preheader: Optional[str] = None,
48
+ colors: dict | None = None,
49
+ logo_url: str | None = None,
50
+ app_name: str | None = None,
51
+ tagline: str | None = None,
52
+ website_url: str | None = None,
53
+ privacy_url: str | None = None,
54
+ terms_url: str | None = None,
55
+ ) -> str:
56
+ """
57
+ Base email template.
58
+
59
+ Args:
60
+ content: The main content HTML
61
+ preheader: Optional preview text shown in email clients
62
+ colors: Color overrides (merges with COLORS)
63
+ logo_url: Logo image URL
64
+ app_name: Application name
65
+ tagline: Footer tagline
66
+ website_url: Main website URL
67
+ privacy_url: Privacy policy URL
68
+ terms_url: Terms of service URL
69
+
70
+ Returns:
71
+ Complete HTML email
72
+ """
73
+ # Merge colors
74
+ c = {**COLORS, **(colors or {})}
75
+
76
+ # Use provided values or module defaults
77
+ logo = logo_url or LOGO_URL
78
+ name = app_name or APP_NAME
79
+ tag = tagline or TAGLINE
80
+ web = website_url or WEBSITE_URL
81
+ privacy = privacy_url or PRIVACY_URL
82
+ terms = terms_url or TERMS_URL
83
+
84
+ preheader_html = ""
85
+ if preheader:
86
+ preheader_html = f'''
87
+ <div style="display: none; max-height: 0; overflow: hidden;">
88
+ {preheader}
89
+ </div>
90
+ '''
91
+
92
+ logo_html = ""
93
+ if logo:
94
+ logo_html = f'''
95
+ <img src="{logo}" alt="{name}" width="40" height="40" style="display: block; margin: 0 auto 16px auto; border-radius: 8px;">
96
+ '''
97
+
98
+ return f'''<!DOCTYPE html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
104
+ <title>{name}</title>
105
+ <!--[if mso]>
106
+ <style type="text/css">
107
+ body, table, td {{font-family: Arial, sans-serif !important;}}
108
+ </style>
109
+ <![endif]-->
110
+ </head>
111
+ <body style="margin: 0; padding: 0; background-color: {c['background']}; font-family: 'Georgia', 'Times New Roman', serif;">
112
+ {preheader_html}
113
+
114
+ <!-- Email Container -->
115
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: {c['background']};">
116
+ <tr>
117
+ <td style="padding: 40px 20px;">
118
+ <!-- Inner Container -->
119
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 560px; margin: 0 auto;">
120
+
121
+ <!-- Main Content Card -->
122
+ <tr>
123
+ <td style="background-color: {c['card']}; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);">
124
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
125
+ <tr>
126
+ <td style="padding: 40px;">
127
+ {content}
128
+ </td>
129
+ </tr>
130
+ </table>
131
+ </td>
132
+ </tr>
133
+
134
+ <!-- Footer -->
135
+ <tr>
136
+ <td style="padding: 32px 20px; text-align: center;">
137
+ {logo_html}
138
+
139
+ <!-- Tagline -->
140
+ <p style="margin: 0 0 8px 0; font-family: 'Georgia', serif; font-size: 14px; color: {c['muted']}; font-style: italic;">
141
+ {tag}
142
+ </p>
143
+
144
+ <!-- Footer Links -->
145
+ <p style="margin: 0; font-family: Arial, sans-serif; font-size: 12px; color: {c['muted']};">
146
+ <a href="{web}" style="color: {c['primary']}; text-decoration: none;">{name}</a>
147
+ &nbsp;&bull;&nbsp;
148
+ <a href="{privacy}" style="color: {c['primary']}; text-decoration: none;">Privacy</a>
149
+ &nbsp;&bull;&nbsp;
150
+ <a href="{terms}" style="color: {c['primary']}; text-decoration: none;">Terms</a>
151
+ </p>
152
+
153
+ <!-- Copyright -->
154
+ <p style="margin: 16px 0 0 0; font-family: Arial, sans-serif; font-size: 11px; color: {c['muted']};">
155
+ &copy; 2025 {name}. All rights reserved.
156
+ </p>
157
+ </td>
158
+ </tr>
159
+
160
+ </table>
161
+ </td>
162
+ </tr>
163
+ </table>
164
+ </body>
165
+ </html>'''
166
+
167
+
168
+ def login_code_template(
169
+ code: str,
170
+ email: str,
171
+ colors: dict | None = None,
172
+ app_name: str | None = None,
173
+ **kwargs,
174
+ ) -> EmailTemplate:
175
+ """
176
+ Generate a login code email template.
177
+
178
+ Args:
179
+ code: The 6-digit login code
180
+ email: The recipient's email address
181
+ colors: Color overrides
182
+ app_name: Application name
183
+ **kwargs: Additional arguments passed to base_template
184
+
185
+ Returns:
186
+ EmailTemplate with subject and HTML body
187
+ """
188
+ c = {**COLORS, **(colors or {})}
189
+ name = app_name or APP_NAME
190
+
191
+ # Format code with spaces for readability (e.g., "123 456")
192
+ formatted_code = f"{code[:3]} {code[3:]}" if len(code) == 6 else code
193
+
194
+ content = f'''
195
+ <!-- Greeting -->
196
+ <h1 style="margin: 0 0 8px 0; font-family: 'Arial', sans-serif; font-size: 24px; font-weight: 600; color: {c['foreground']};">
197
+ Hi there!
198
+ </h1>
199
+
200
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 16px; line-height: 1.6; color: {c['foreground']};">
201
+ Here's your login code for {name}. Enter this code to securely access your account.
202
+ </p>
203
+
204
+ <!-- Code Box -->
205
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
206
+ <tr>
207
+ <td align="center">
208
+ <div style="
209
+ display: inline-block;
210
+ padding: 20px 40px;
211
+ background-color: {c['background']};
212
+ border: 2px solid {c['border']};
213
+ border-radius: 12px;
214
+ ">
215
+ <span style="
216
+ font-family: 'Courier New', monospace;
217
+ font-size: 32px;
218
+ font-weight: 700;
219
+ letter-spacing: 6px;
220
+ color: {c['foreground']};
221
+ ">{formatted_code}</span>
222
+ </div>
223
+ </td>
224
+ </tr>
225
+ </table>
226
+
227
+ <!-- Instructions -->
228
+ <p style="margin: 0 0 8px 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']};">
229
+ This code expires in <strong>10 minutes</strong>.
230
+ </p>
231
+
232
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']};">
233
+ If you didn't request this code, you can safely ignore this email.
234
+ </p>
235
+
236
+ <!-- Divider -->
237
+ <hr style="border: none; border-top: 1px solid {c['border']}; margin: 24px 0;">
238
+
239
+ <!-- Security Note -->
240
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
241
+ <tr>
242
+ <td style="padding: 16px; background-color: {c['background']}; border-radius: 8px;">
243
+ <p style="margin: 0; font-family: Arial, sans-serif; font-size: 12px; color: {c['muted']};">
244
+ <strong style="color: {c['primary']};">Security tip:</strong>
245
+ Never share your login code with anyone. {name} will never ask for your code via phone or text.
246
+ </p>
247
+ </td>
248
+ </tr>
249
+ </table>
250
+ '''
251
+
252
+ return EmailTemplate(
253
+ subject=f"Your {name} Login Code",
254
+ html_body=base_template(
255
+ content=content,
256
+ preheader=f"Your login code is {formatted_code}",
257
+ colors=colors,
258
+ app_name=app_name,
259
+ **kwargs,
260
+ ),
261
+ )
262
+
263
+
264
+ def welcome_template(
265
+ name: Optional[str] = None,
266
+ colors: dict | None = None,
267
+ app_name: str | None = None,
268
+ features: list[str] | None = None,
269
+ cta_url: str | None = None,
270
+ cta_text: str = "Get Started",
271
+ **kwargs,
272
+ ) -> EmailTemplate:
273
+ """
274
+ Generate a welcome email template for new users.
275
+
276
+ Args:
277
+ name: Optional user's name
278
+ colors: Color overrides
279
+ app_name: Application name
280
+ features: List of feature descriptions
281
+ cta_url: Call-to-action button URL
282
+ cta_text: Call-to-action button text
283
+ **kwargs: Additional arguments passed to base_template
284
+
285
+ Returns:
286
+ EmailTemplate with subject and HTML body
287
+ """
288
+ c = {**COLORS, **(colors or {})}
289
+ app = app_name or APP_NAME
290
+ greeting = f"Hi {name}!" if name else "Welcome!"
291
+ url = cta_url or WEBSITE_URL
292
+
293
+ # Default features if not provided
294
+ default_features = [
295
+ "Access powerful AI capabilities",
296
+ "Store and organize your data",
297
+ "Collaborate with your team",
298
+ ]
299
+ feature_list = features or default_features
300
+
301
+ features_html = "".join(f"<li>{f}</li>" for f in feature_list)
302
+
303
+ content = f'''
304
+ <!-- Greeting -->
305
+ <h1 style="margin: 0 0 8px 0; font-family: 'Arial', sans-serif; font-size: 24px; font-weight: 600; color: {c['foreground']};">
306
+ {greeting}
307
+ </h1>
308
+
309
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 16px; line-height: 1.6; color: {c['foreground']};">
310
+ Welcome to {app}! We're excited to have you on board.
311
+ </p>
312
+
313
+ <!-- Feature List -->
314
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
315
+ <tr>
316
+ <td style="padding: 16px; background-color: {c['background']}; border-radius: 8px;">
317
+ <p style="margin: 0 0 12px 0; font-family: Arial, sans-serif; font-size: 14px; font-weight: 600; color: {c['primary']};">
318
+ What you can do with {app}:
319
+ </p>
320
+ <ul style="margin: 0; padding-left: 20px; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.8; color: {c['foreground']};">
321
+ {features_html}
322
+ </ul>
323
+ </td>
324
+ </tr>
325
+ </table>
326
+
327
+ <!-- CTA Button -->
328
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
329
+ <tr>
330
+ <td align="center">
331
+ <a href="{url}" style="
332
+ display: inline-block;
333
+ padding: 14px 32px;
334
+ background-color: {c['primary']};
335
+ color: #FFFFFF;
336
+ font-family: Arial, sans-serif;
337
+ font-size: 16px;
338
+ font-weight: 600;
339
+ text-decoration: none;
340
+ border-radius: 8px;
341
+ ">{cta_text}</a>
342
+ </td>
343
+ </tr>
344
+ </table>
345
+
346
+ <p style="margin: 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']}; text-align: center;">
347
+ We're glad you're here!
348
+ </p>
349
+ '''
350
+
351
+ return EmailTemplate(
352
+ subject=f"Welcome to {app}",
353
+ html_body=base_template(
354
+ content=content,
355
+ preheader=f"Welcome to {app}! Get started today.",
356
+ colors=colors,
357
+ app_name=app_name,
358
+ **kwargs,
359
+ ),
360
+ )
@@ -688,6 +688,44 @@ from rem.api.main import app # Use REM's FastAPI app
688
688
  # Or build your own app using rem.services
689
689
  ```
690
690
 
691
+ ## Adding Models & Migrations
692
+
693
+ Quick workflow for adding new database models:
694
+
695
+ 1. **Create a model** in `models/__init__.py` (or a submodule):
696
+ ```python
697
+ import rem
698
+ from rem.models.core import CoreModel
699
+
700
+ @rem.register_model
701
+ class MyEntity(CoreModel):
702
+ name: str
703
+ description: str # Auto-embedded (common field name)
704
+ ```
705
+
706
+ 2. **Check for schema drift** - REM auto-detects `./models` directory:
707
+ ```bash
708
+ rem db diff # Show pending changes (additive only)
709
+ rem db diff --strategy full # Include destructive changes
710
+ ```
711
+
712
+ 3. **Generate migration** (optional - for version-controlled SQL):
713
+ ```bash
714
+ rem db diff --generate # Creates numbered .sql file
715
+ ```
716
+
717
+ 4. **Apply changes**:
718
+ ```bash
719
+ rem db migrate # Apply all pending migrations
720
+ ```
721
+
722
+ **Key points:**
723
+ - Models in `./models/` are auto-discovered (must have `__init__.py`)
724
+ - Or set `MODELS__IMPORT_MODULES=myapp.models` for custom paths
725
+ - `CoreModel` provides: `id`, `tenant_id`, `user_id`, `created_at`, `updated_at`, `deleted_at`, `graph_edges`, `metadata`, `tags`
726
+ - Fields named `content`, `description`, `summary`, `text`, `body`, `message`, `notes` get embeddings by default
727
+ - Use `Field(json_schema_extra={"embed": True})` to embed other fields
728
+
691
729
  ## Configuration
692
730
 
693
731
  Environment variables:
@@ -22,6 +22,7 @@ from alembic.runtime.migration import MigrationContext
22
22
  from alembic.script import ScriptDirectory
23
23
  from loguru import logger
24
24
  from sqlalchemy import create_engine, text
25
+ from sqlalchemy.dialects import postgresql
25
26
 
26
27
  from ...settings import settings
27
28
  from .pydantic_to_sqlalchemy import get_target_metadata
@@ -472,6 +473,18 @@ class DiffService:
472
473
 
473
474
  return "\n".join(lines) + "\n"
474
475
 
476
+ def _compile_type(self, col_type) -> str:
477
+ """Compile SQLAlchemy type to PostgreSQL DDL string.
478
+
479
+ SQLAlchemy types like ARRAY(Text) need dialect-specific compilation
480
+ to render correctly (e.g., "TEXT[]" instead of just "ARRAY").
481
+ """
482
+ try:
483
+ return col_type.compile(dialect=postgresql.dialect())
484
+ except Exception:
485
+ # Fallback to string representation if compilation fails
486
+ return str(col_type)
487
+
475
488
  def _op_to_sql(self, op: ops.MigrateOperation) -> list[str]:
476
489
  """Convert operation to SQL statements."""
477
490
  lines = []
@@ -481,7 +494,8 @@ class DiffService:
481
494
  for col in op.columns:
482
495
  if hasattr(col, 'name') and hasattr(col, 'type'):
483
496
  nullable = "" if getattr(col, 'nullable', True) else " NOT NULL"
484
- cols.append(f" {col.name} {col.type}{nullable}")
497
+ type_str = self._compile_type(col.type)
498
+ cols.append(f" {col.name} {type_str}{nullable}")
485
499
  col_str = ",\n".join(cols)
486
500
  lines.append(f"CREATE TABLE IF NOT EXISTS {op.table_name} (\n{col_str}\n);")
487
501
 
@@ -491,14 +505,16 @@ class DiffService:
491
505
  elif isinstance(op, ops.AddColumnOp):
492
506
  col = op.column
493
507
  nullable = "" if getattr(col, 'nullable', True) else " NOT NULL"
494
- lines.append(f"ALTER TABLE {op.table_name} ADD COLUMN IF NOT EXISTS {col.name} {col.type}{nullable};")
508
+ type_str = self._compile_type(col.type)
509
+ lines.append(f"ALTER TABLE {op.table_name} ADD COLUMN IF NOT EXISTS {col.name} {type_str}{nullable};")
495
510
 
496
511
  elif isinstance(op, ops.DropColumnOp):
497
512
  lines.append(f"ALTER TABLE {op.table_name} DROP COLUMN IF EXISTS {op.column_name};")
498
513
 
499
514
  elif isinstance(op, ops.AlterColumnOp):
500
515
  if op.modify_type is not None:
501
- lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} TYPE {op.modify_type};")
516
+ type_str = self._compile_type(op.modify_type)
517
+ lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} TYPE {type_str};")
502
518
  if op.modify_nullable is not None:
503
519
  if op.modify_nullable:
504
520
  lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} DROP NOT NULL;")
@@ -494,12 +494,13 @@ def _build_embeddings_table(base_table_name: str, metadata: MetaData) -> Table:
494
494
  ]
495
495
 
496
496
  # Create table with unique constraint
497
- # Note: constraint name matches PostgreSQL's auto-generated naming convention
497
+ # Truncate constraint name to fit PostgreSQL's 63-char identifier limit
498
+ constraint_name = f"uq_{base_table_name[:30]}_emb_entity_field_prov"
498
499
  table = Table(
499
500
  embeddings_table_name,
500
501
  metadata,
501
502
  *columns,
502
- UniqueConstraint("entity_id", "field_name", "provider", name=f"{embeddings_table_name}_entity_id_field_name_provider_key"),
503
+ UniqueConstraint("entity_id", "field_name", "provider", name=constraint_name),
503
504
  )
504
505
 
505
506
  # Add indexes (matching register_type output)
@@ -509,6 +510,30 @@ def _build_embeddings_table(base_table_name: str, metadata: MetaData) -> Table:
509
510
  return table
510
511
 
511
512
 
513
+ def _import_model_modules() -> list[str]:
514
+ """
515
+ Import modules specified in MODELS__IMPORT_MODULES setting.
516
+
517
+ This ensures downstream models decorated with @rem.register_model
518
+ are registered before schema generation.
519
+
520
+ Returns:
521
+ List of successfully imported module names
522
+ """
523
+ import importlib
524
+ from ...settings import settings
525
+
526
+ imported = []
527
+ for module_name in settings.models.module_list:
528
+ try:
529
+ importlib.import_module(module_name)
530
+ imported.append(module_name)
531
+ logger.debug(f"Imported model module: {module_name}")
532
+ except ImportError as e:
533
+ logger.warning(f"Failed to import model module '{module_name}': {e}")
534
+ return imported
535
+
536
+
512
537
  def get_target_metadata() -> MetaData:
513
538
  """
514
539
  Get SQLAlchemy metadata for Alembic autogenerate.
@@ -519,9 +544,19 @@ def get_target_metadata() -> MetaData:
519
544
  - Core REM models (Resource, Message, User, etc.)
520
545
  - User-registered models via @rem.register_model decorator
521
546
 
547
+ Before building metadata, imports model modules from settings to ensure
548
+ downstream models are registered. This supports:
549
+ - Auto-detection of ./models directory (convention)
550
+ - MODELS__IMPORT_MODULES env var (explicit configuration)
551
+
522
552
  Returns:
523
553
  SQLAlchemy MetaData object representing all registered Pydantic models
524
554
  """
555
+ # Import model modules first (auto-detects ./models or uses MODELS__IMPORT_MODULES)
556
+ imported = _import_model_modules()
557
+ if imported:
558
+ logger.info(f"Imported model modules: {imported}")
559
+
525
560
  # build_sqlalchemy_metadata_from_pydantic uses the registry internally,
526
561
  # so no directory path is needed (the parameter is kept for backwards compat)
527
562
  return build_sqlalchemy_metadata_from_pydantic()
@@ -141,13 +141,13 @@ class Repository(Generic[T]):
141
141
  # Return single item or list to match input type
142
142
  return records_list[0] if is_single else records_list
143
143
 
144
- async def get_by_id(self, record_id: str, tenant_id: str) -> T | None:
144
+ async def get_by_id(self, record_id: str, tenant_id: str | None = None) -> T | None:
145
145
  """
146
146
  Get a single record by ID.
147
147
 
148
148
  Args:
149
149
  record_id: Record identifier
150
- tenant_id: Tenant identifier for multi-tenancy isolation
150
+ tenant_id: Optional tenant identifier (deprecated, not used for filtering)
151
151
 
152
152
  Returns:
153
153
  Model instance or None if not found
@@ -164,13 +164,14 @@ class Repository(Generic[T]):
164
164
  if not self.db.pool:
165
165
  raise RuntimeError("Failed to establish database connection")
166
166
 
167
+ # Note: tenant_id filtering removed - use user_id for access control instead
167
168
  query = f"""
168
169
  SELECT * FROM {self.table_name}
169
- WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
170
+ WHERE id = $1 AND deleted_at IS NULL
170
171
  """
171
172
 
172
173
  async with self.db.pool.acquire() as conn:
173
- row = await conn.fetchrow(query, record_id, tenant_id)
174
+ row = await conn.fetchrow(query, record_id)
174
175
 
175
176
  if not row:
176
177
  return None