feat: add lighthouse migration file

This commit is contained in:
Chandrapal Badshah
2025-09-30 14:32:48 +05:30
parent a02cc6364b
commit cacef6f0cd
@@ -0,0 +1,273 @@
# Generated by Django 5.1.12 on 2025-09-29 15:52
import json
import uuid
import django.db.models.deletion
from django.db import migrations, models
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0047_remove_integration_unique_configuration_per_tenant"),
]
operations = [
migrations.CreateModel(
name="LighthouseProviderConfiguration",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"provider_type",
models.CharField(
choices=[("openai", "OpenAI")],
help_text="LLM provider name",
max_length=50,
),
),
("base_url", models.URLField(blank=True, null=True)),
(
"credentials",
models.BinaryField(
help_text="Encrypted JSON credentials for the provider"
),
),
("is_active", models.BooleanField(default=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "lighthouse_provider_configurations",
"abstract": False,
},
),
migrations.CreateModel(
name="LighthouseProviderModels",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("model_id", models.CharField(max_length=100)),
("default_parameters", models.JSONField(blank=True, default=dict)),
(
"provider_configuration",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="available_models",
to="api.lighthouseproviderconfiguration",
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "lighthouse_provider_models",
"abstract": False,
},
),
migrations.CreateModel(
name="LighthouseTenantConfiguration",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("business_context", models.TextField(blank=True, default="")),
("default_provider", models.CharField(blank=True, max_length=50)),
("default_models", models.JSONField(default=dict)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "lighthouse_tenant_config",
"abstract": False,
},
),
migrations.AddIndex(
model_name="lighthouseproviderconfiguration",
index=models.Index(
fields=["tenant_id", "provider_type"], name="lh_pc_tenant_type_idx"
),
),
migrations.AddConstraint(
model_name="lighthouseproviderconfiguration",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id", name="rls_on_lighthouseproviderconfiguration"
),
),
migrations.AddConstraint(
model_name="lighthouseproviderconfiguration",
constraint=models.UniqueConstraint(
fields=("tenant_id", "provider_type"),
name="unique_provider_config_per_tenant",
),
),
migrations.AddIndex(
model_name="lighthouseprovidermodels",
index=models.Index(
fields=["tenant_id", "provider_configuration"],
name="lh_prov_models_cfg_idx",
),
),
migrations.AddConstraint(
model_name="lighthouseprovidermodels",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id", name="rls_on_lighthouseprovidermodels"
),
),
migrations.AddConstraint(
model_name="lighthouseprovidermodels",
constraint=models.UniqueConstraint(
fields=("tenant_id", "provider_configuration", "model_id"),
name="unique_provider_model_per_configuration",
),
),
migrations.AddConstraint(
model_name="lighthousetenantconfiguration",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id", name="rls_on_lighthousetenantconfiguration"
),
),
migrations.AddConstraint(
model_name="lighthousetenantconfiguration",
constraint=models.UniqueConstraint(
fields=("tenant_id",), name="unique_tenant_lighthouse_config"
),
),
# Data backfill from the previous single-table lighthouse configuration to the new schema
migrations.RunPython(
code=lambda apps, schema_editor: _copy_lighthouse_config_to_new_models(
apps, schema_editor
),
reverse_code=migrations.RunPython.noop,
),
]
def _copy_lighthouse_config_to_new_models(apps, schema_editor):
"""Idempotent backfill from old LighthouseConfiguration to new models.
- No-op if old table doesn't exist or has no rows
- Per-tenant RLS context is set before inserts
- Only OpenAI provider is handled for now (future providers can extend)
"""
# Ensure the old table exists before attempting to read
introspection = schema_editor.connection.introspection
if "lighthouse_configurations" not in introspection.table_names():
return
# Historical models
OldConfig = apps.get_model("api", "LighthouseConfiguration")
ProviderCfg = apps.get_model("api", "LighthouseProviderConfiguration")
TenantCfg = apps.get_model("api", "LighthouseTenantConfiguration")
from django.conf import settings
try:
from cryptography.fernet import Fernet, InvalidToken
except Exception:
# If cryptography isn't available, skip credentials backfill (schema remains valid)
Fernet = None # type: ignore
InvalidToken = Exception # type: ignore
fernet = None
if getattr(settings, "SECRETS_ENCRYPTION_KEY", None):
try:
fernet = (
Fernet(settings.SECRETS_ENCRYPTION_KEY.encode()) if Fernet else None
)
except Exception:
fernet = None
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
# Iterate old configs (expected one per tenant)
with schema_editor.connection.cursor() as cursor:
for old in OldConfig.objects.all().only(
"tenant_id",
"business_context",
"model",
"api_key",
"is_active",
"temperature",
"max_tokens",
):
# Set RLS context for tenant
cursor.execute(SET_CONFIG_QUERY, ["api.tenant_id", str(old.tenant_id)])
# Create or get tenant-level settings
if not TenantCfg.objects.filter(tenant_id=old.tenant_id).exists():
TenantCfg.objects.create(
tenant_id=old.tenant_id,
business_context=old.business_context or "",
default_provider="openai",
default_models={"openai": old.model},
)
# Create or get provider configuration for OpenAI
provider_cfg = ProviderCfg.objects.filter(
tenant_id=old.tenant_id, provider_type="openai"
).first()
if provider_cfg is None:
enc_credentials = None
# Best-effort decrypt old api_key and re-encrypt as JSON credentials
if fernet and getattr(old, "api_key", None):
try:
plaintext = fernet.decrypt(bytes(old.api_key)).decode()
creds_json = json.dumps({"api_key": plaintext}).encode()
enc_credentials = fernet.encrypt(creds_json)
except InvalidToken:
enc_credentials = None
except Exception:
enc_credentials = None
# Only create provider configuration if we have credentials
if enc_credentials is not None:
provider_cfg = ProviderCfg.objects.create(
tenant_id=old.tenant_id,
provider_type="openai",
base_url=None,
credentials=enc_credentials,
is_active=bool(getattr(old, "is_active", True)),
)