popeye-cli 1.7.0 → 1.9.0

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.
Files changed (174) hide show
  1. package/README.md +148 -7
  2. package/cheatsheet.md +440 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +3 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +3 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/commands/review.d.ts +31 -0
  16. package/dist/cli/commands/review.d.ts.map +1 -0
  17. package/dist/cli/commands/review.js +156 -0
  18. package/dist/cli/commands/review.js.map +1 -0
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +4 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/interactive.d.ts.map +1 -1
  23. package/dist/cli/interactive.js +218 -61
  24. package/dist/cli/interactive.js.map +1 -1
  25. package/dist/generators/admin-wizard.d.ts +25 -0
  26. package/dist/generators/admin-wizard.d.ts.map +1 -0
  27. package/dist/generators/admin-wizard.js +123 -0
  28. package/dist/generators/admin-wizard.js.map +1 -0
  29. package/dist/generators/all.d.ts.map +1 -1
  30. package/dist/generators/all.js +10 -3
  31. package/dist/generators/all.js.map +1 -1
  32. package/dist/generators/database.d.ts +58 -0
  33. package/dist/generators/database.d.ts.map +1 -0
  34. package/dist/generators/database.js +229 -0
  35. package/dist/generators/database.js.map +1 -0
  36. package/dist/generators/fullstack.d.ts.map +1 -1
  37. package/dist/generators/fullstack.js +23 -7
  38. package/dist/generators/fullstack.js.map +1 -1
  39. package/dist/generators/index.d.ts +2 -0
  40. package/dist/generators/index.d.ts.map +1 -1
  41. package/dist/generators/index.js +2 -0
  42. package/dist/generators/index.js.map +1 -1
  43. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  44. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-python.js +425 -0
  46. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  47. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  48. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  49. package/dist/generators/templates/admin-wizard-react.js +554 -0
  50. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  51. package/dist/generators/templates/database-docker.d.ts +23 -0
  52. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  53. package/dist/generators/templates/database-docker.js +221 -0
  54. package/dist/generators/templates/database-docker.js.map +1 -0
  55. package/dist/generators/templates/database-python.d.ts +54 -0
  56. package/dist/generators/templates/database-python.d.ts.map +1 -0
  57. package/dist/generators/templates/database-python.js +723 -0
  58. package/dist/generators/templates/database-python.js.map +1 -0
  59. package/dist/generators/templates/database-typescript.d.ts +34 -0
  60. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  61. package/dist/generators/templates/database-typescript.js +232 -0
  62. package/dist/generators/templates/database-typescript.js.map +1 -0
  63. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  64. package/dist/generators/templates/fullstack.js +29 -0
  65. package/dist/generators/templates/fullstack.js.map +1 -1
  66. package/dist/generators/templates/index.d.ts +5 -0
  67. package/dist/generators/templates/index.d.ts.map +1 -1
  68. package/dist/generators/templates/index.js +5 -0
  69. package/dist/generators/templates/index.js.map +1 -1
  70. package/dist/state/index.d.ts +10 -0
  71. package/dist/state/index.d.ts.map +1 -1
  72. package/dist/state/index.js +21 -0
  73. package/dist/state/index.js.map +1 -1
  74. package/dist/types/audit.d.ts +623 -0
  75. package/dist/types/audit.d.ts.map +1 -0
  76. package/dist/types/audit.js +240 -0
  77. package/dist/types/audit.js.map +1 -0
  78. package/dist/types/database-runtime.d.ts +86 -0
  79. package/dist/types/database-runtime.d.ts.map +1 -0
  80. package/dist/types/database-runtime.js +61 -0
  81. package/dist/types/database-runtime.js.map +1 -0
  82. package/dist/types/database.d.ts +85 -0
  83. package/dist/types/database.d.ts.map +1 -0
  84. package/dist/types/database.js +71 -0
  85. package/dist/types/database.js.map +1 -0
  86. package/dist/types/index.d.ts +2 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +4 -0
  89. package/dist/types/index.js.map +1 -1
  90. package/dist/types/workflow.d.ts +36 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +7 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/audit-analyzer.d.ts +58 -0
  95. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  96. package/dist/workflow/audit-analyzer.js +420 -0
  97. package/dist/workflow/audit-analyzer.js.map +1 -0
  98. package/dist/workflow/audit-mode.d.ts +28 -0
  99. package/dist/workflow/audit-mode.d.ts.map +1 -0
  100. package/dist/workflow/audit-mode.js +169 -0
  101. package/dist/workflow/audit-mode.js.map +1 -0
  102. package/dist/workflow/audit-recovery.d.ts +61 -0
  103. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  104. package/dist/workflow/audit-recovery.js +242 -0
  105. package/dist/workflow/audit-recovery.js.map +1 -0
  106. package/dist/workflow/audit-reporter.d.ts +65 -0
  107. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  108. package/dist/workflow/audit-reporter.js +301 -0
  109. package/dist/workflow/audit-reporter.js.map +1 -0
  110. package/dist/workflow/audit-scanner.d.ts +87 -0
  111. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  112. package/dist/workflow/audit-scanner.js +768 -0
  113. package/dist/workflow/audit-scanner.js.map +1 -0
  114. package/dist/workflow/db-setup-runner.d.ts +63 -0
  115. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  116. package/dist/workflow/db-setup-runner.js +336 -0
  117. package/dist/workflow/db-setup-runner.js.map +1 -0
  118. package/dist/workflow/db-state-machine.d.ts +30 -0
  119. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  120. package/dist/workflow/db-state-machine.js +51 -0
  121. package/dist/workflow/db-state-machine.js.map +1 -0
  122. package/dist/workflow/index.d.ts +7 -0
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +7 -0
  125. package/dist/workflow/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/commands/db.ts +281 -0
  128. package/src/cli/commands/doctor.ts +273 -0
  129. package/src/cli/commands/index.ts +3 -0
  130. package/src/cli/commands/review.ts +187 -0
  131. package/src/cli/index.ts +6 -0
  132. package/src/cli/interactive.ts +174 -4
  133. package/src/generators/admin-wizard.ts +146 -0
  134. package/src/generators/all.ts +10 -3
  135. package/src/generators/database.ts +286 -0
  136. package/src/generators/fullstack.ts +26 -9
  137. package/src/generators/index.ts +12 -0
  138. package/src/generators/templates/admin-wizard-python.ts +431 -0
  139. package/src/generators/templates/admin-wizard-react.ts +560 -0
  140. package/src/generators/templates/database-docker.ts +227 -0
  141. package/src/generators/templates/database-python.ts +734 -0
  142. package/src/generators/templates/database-typescript.ts +238 -0
  143. package/src/generators/templates/fullstack.ts +29 -0
  144. package/src/generators/templates/index.ts +5 -0
  145. package/src/state/index.ts +28 -0
  146. package/src/types/audit.ts +294 -0
  147. package/src/types/database-runtime.ts +69 -0
  148. package/src/types/database.ts +84 -0
  149. package/src/types/index.ts +29 -0
  150. package/src/types/workflow.ts +20 -0
  151. package/src/workflow/audit-analyzer.ts +491 -0
  152. package/src/workflow/audit-mode.ts +240 -0
  153. package/src/workflow/audit-recovery.ts +284 -0
  154. package/src/workflow/audit-reporter.ts +370 -0
  155. package/src/workflow/audit-scanner.ts +873 -0
  156. package/src/workflow/db-setup-runner.ts +391 -0
  157. package/src/workflow/db-state-machine.ts +58 -0
  158. package/src/workflow/index.ts +7 -0
  159. package/tests/cli/commands/review.test.ts +52 -0
  160. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  161. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  162. package/tests/generators/cross-phase-integration.test.ts +383 -0
  163. package/tests/generators/database.test.ts +456 -0
  164. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  165. package/tests/types/audit.test.ts +250 -0
  166. package/tests/types/database-runtime.test.ts +158 -0
  167. package/tests/types/database.test.ts +187 -0
  168. package/tests/workflow/audit-analyzer.test.ts +281 -0
  169. package/tests/workflow/audit-mode.test.ts +114 -0
  170. package/tests/workflow/audit-recovery.test.ts +237 -0
  171. package/tests/workflow/audit-reporter.test.ts +254 -0
  172. package/tests/workflow/audit-scanner.test.ts +270 -0
  173. package/tests/workflow/db-setup-runner.test.ts +211 -0
  174. package/tests/workflow/db-state-machine.test.ts +117 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * Python backend database template functions
3
+ * Generates SQLAlchemy + Alembic + asyncpg + pgvector files
4
+ * Each function returns a string of the generated file content
5
+ */
6
+ /**
7
+ * Generate database connection module with AsyncEngine and session management
8
+ */
9
+ export function generateDbConnection(_packageName) {
10
+ return `"""
11
+ Database connection management.
12
+
13
+ Provides async engine, session factory, and FastAPI dependency.
14
+ """
15
+
16
+ import logging
17
+ import os
18
+ from contextlib import asynccontextmanager
19
+ from typing import AsyncGenerator
20
+
21
+ from sqlalchemy.ext.asyncio import (
22
+ AsyncSession,
23
+ async_sessionmaker,
24
+ create_async_engine,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ DATABASE_URL = os.getenv("DATABASE_URL", "")
30
+
31
+ # Create async engine (lazy - only connects when DATABASE_URL is set)
32
+ engine = (
33
+ create_async_engine(
34
+ DATABASE_URL,
35
+ echo=False,
36
+ pool_size=5,
37
+ max_overflow=10,
38
+ pool_pre_ping=True,
39
+ )
40
+ if DATABASE_URL
41
+ else None
42
+ )
43
+
44
+ # Session factory
45
+ async_session_factory = (
46
+ async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
47
+ if engine
48
+ else None
49
+ )
50
+
51
+
52
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
53
+ """
54
+ FastAPI dependency that provides an async database session.
55
+
56
+ Yields:
57
+ AsyncSession: Database session (auto-committed on success, rolled back on error).
58
+
59
+ Raises:
60
+ RuntimeError: If DATABASE_URL is not configured.
61
+ """
62
+ if async_session_factory is None:
63
+ raise RuntimeError(
64
+ "Database not configured. Set DATABASE_URL environment variable."
65
+ )
66
+ async with async_session_factory() as session:
67
+ try:
68
+ yield session
69
+ await session.commit()
70
+ except Exception:
71
+ await session.rollback()
72
+ raise
73
+
74
+
75
+ async def check_db_connection() -> dict:
76
+ """
77
+ Health check helper - tests database connectivity.
78
+
79
+ Returns:
80
+ dict: Connection status with details.
81
+ """
82
+ if engine is None:
83
+ return {"connected": False, "error": "DATABASE_URL not configured"}
84
+ try:
85
+ async with engine.connect() as conn:
86
+ result = await conn.execute(
87
+ __import__("sqlalchemy").text("SELECT 1")
88
+ )
89
+ result.scalar()
90
+ return {"connected": True}
91
+ except Exception as e:
92
+ logger.error(f"Database connection check failed: {e}")
93
+ return {"connected": False, "error": str(e)}
94
+ `;
95
+ }
96
+ /**
97
+ * Generate database models with Base, TimestampMixin, and AppSettings
98
+ */
99
+ export function generateDbModels(_packageName) {
100
+ return `"""
101
+ Database models.
102
+
103
+ Provides SQLAlchemy declarative base, mixins, and core models.
104
+ """
105
+
106
+ import datetime
107
+ from sqlalchemy import Column, DateTime, Integer, String, Text, func
108
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
109
+
110
+
111
+ class Base(DeclarativeBase):
112
+ """Declarative base for all models."""
113
+ pass
114
+
115
+
116
+ class TimestampMixin:
117
+ """
118
+ Mixin that adds created_at and updated_at columns.
119
+
120
+ Usage:
121
+ class MyModel(Base, TimestampMixin):
122
+ __tablename__ = "my_table"
123
+ ...
124
+ """
125
+ created_at: Mapped[datetime.datetime] = mapped_column(
126
+ DateTime(timezone=True),
127
+ server_default=func.now(),
128
+ nullable=False,
129
+ )
130
+ updated_at: Mapped[datetime.datetime] = mapped_column(
131
+ DateTime(timezone=True),
132
+ server_default=func.now(),
133
+ onupdate=func.now(),
134
+ nullable=False,
135
+ )
136
+
137
+
138
+ class AppSettings(Base, TimestampMixin):
139
+ """
140
+ Key-value configuration storage.
141
+
142
+ Used for runtime settings that persist across restarts.
143
+ """
144
+ __tablename__ = "app_settings"
145
+
146
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
147
+ key: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
148
+ value: Mapped[str] = mapped_column(Text, nullable=False)
149
+
150
+ def __repr__(self) -> str:
151
+ return f"<AppSettings(key={self.key!r})>"
152
+ `;
153
+ }
154
+ /**
155
+ * Generate database package __init__.py with re-exports
156
+ */
157
+ export function generateDbInit(_packageName) {
158
+ return `"""
159
+ Database package.
160
+
161
+ Re-exports core database components for convenient access.
162
+ """
163
+
164
+ from .connection import engine, get_session, check_db_connection
165
+ from .models import Base, TimestampMixin, AppSettings
166
+
167
+ __all__ = [
168
+ "engine",
169
+ "get_session",
170
+ "check_db_connection",
171
+ "Base",
172
+ "TimestampMixin",
173
+ "AppSettings",
174
+ ]
175
+ `;
176
+ }
177
+ /**
178
+ * Generate database settings module using pydantic-settings
179
+ */
180
+ export function generateDbSettings(_packageName) {
181
+ return `"""
182
+ Database settings.
183
+
184
+ Reads DATABASE_URL from environment with mode detection.
185
+ """
186
+
187
+ import os
188
+ from pydantic_settings import BaseSettings
189
+
190
+
191
+ class DatabaseSettings(BaseSettings):
192
+ """
193
+ Database configuration loaded from environment.
194
+
195
+ Attributes:
196
+ database_url: PostgreSQL connection string.
197
+ db_vector_required: Whether pgvector extension is needed.
198
+ """
199
+ database_url: str = ""
200
+ db_vector_required: bool = True
201
+
202
+ @property
203
+ def is_configured(self) -> bool:
204
+ """Check if a database URL has been provided."""
205
+ return bool(self.database_url)
206
+
207
+ @property
208
+ def is_local_docker(self) -> bool:
209
+ """Detect if using local Docker PostgreSQL."""
210
+ return "localhost" in self.database_url or "postgres:" in self.database_url
211
+
212
+ class Config:
213
+ env_file = ".env"
214
+ extra = "ignore"
215
+
216
+
217
+ db_settings = DatabaseSettings()
218
+ `;
219
+ }
220
+ /**
221
+ * Generate alembic.ini configuration
222
+ */
223
+ export function generateAlembicIni(_packageName) {
224
+ return `[alembic]
225
+ script_location = migrations
226
+ prepend_sys_path = .
227
+
228
+ [loggers]
229
+ keys = root,sqlalchemy,alembic
230
+
231
+ [handlers]
232
+ keys = console
233
+
234
+ [formatters]
235
+ keys = generic
236
+
237
+ [logger_root]
238
+ level = WARN
239
+ handlers = console
240
+ qualname =
241
+
242
+ [logger_sqlalchemy]
243
+ level = WARN
244
+ handlers =
245
+ qualname = sqlalchemy.engine
246
+
247
+ [logger_alembic]
248
+ level = INFO
249
+ handlers =
250
+ qualname = alembic
251
+
252
+ [handler_console]
253
+ class = StreamHandler
254
+ args = (sys.stderr,)
255
+ level = NOTSET
256
+ formatter = generic
257
+
258
+ [formatter_generic]
259
+ format = %(levelname)-5.5s [%(name)s] %(message)s
260
+ datefmt = %H:%M:%S
261
+ `;
262
+ }
263
+ /**
264
+ * Generate Alembic async env.py
265
+ */
266
+ export function generateAlembicEnvPy(packageName) {
267
+ return `"""
268
+ Alembic environment configuration.
269
+
270
+ Supports async migrations with SQLAlchemy.
271
+ """
272
+
273
+ import asyncio
274
+ import os
275
+ from logging.config import fileConfig
276
+
277
+ from alembic import context
278
+ from sqlalchemy import pool
279
+ from sqlalchemy.ext.asyncio import async_engine_from_config
280
+
281
+ # Import models to register metadata
282
+ from src.${packageName}.database.models import Base
283
+
284
+ # Alembic Config object
285
+ config = context.config
286
+
287
+ # Set sqlalchemy.url from environment
288
+ database_url = os.getenv("DATABASE_URL", "")
289
+ if database_url:
290
+ config.set_main_option("sqlalchemy.url", database_url)
291
+
292
+ # Setup logging
293
+ if config.config_file_name is not None:
294
+ fileConfig(config.config_file_name)
295
+
296
+ # Target metadata for autogenerate
297
+ target_metadata = Base.metadata
298
+
299
+
300
+ def run_migrations_offline() -> None:
301
+ """
302
+ Run migrations in 'offline' mode.
303
+
304
+ Generates SQL script without connecting to the database.
305
+ """
306
+ url = config.get_main_option("sqlalchemy.url")
307
+ context.configure(
308
+ url=url,
309
+ target_metadata=target_metadata,
310
+ literal_binds=True,
311
+ dialect_opts={"paramstyle": "named"},
312
+ )
313
+ with context.begin_transaction():
314
+ context.run_migrations()
315
+
316
+
317
+ def do_run_migrations(connection):
318
+ """Run migrations with the given connection."""
319
+ context.configure(connection=connection, target_metadata=target_metadata)
320
+ with context.begin_transaction():
321
+ context.run_migrations()
322
+
323
+
324
+ async def run_migrations_online() -> None:
325
+ """
326
+ Run migrations in 'online' mode.
327
+
328
+ Creates an async engine and runs migrations.
329
+ """
330
+ connectable = async_engine_from_config(
331
+ config.get_section(config.config_ini_section, {}),
332
+ prefix="sqlalchemy.",
333
+ poolclass=pool.NullPool,
334
+ )
335
+ async with connectable.connect() as connection:
336
+ await connection.run_sync(do_run_migrations)
337
+ await connectable.dispose()
338
+
339
+
340
+ if context.is_offline_mode():
341
+ run_migrations_offline()
342
+ else:
343
+ asyncio.run(run_migrations_online())
344
+ `;
345
+ }
346
+ /**
347
+ * Generate Alembic script.py.mako template
348
+ */
349
+ export function generateAlembicScriptMako() {
350
+ return `"""$\{message}
351
+
352
+ Revision ID: $\{up_revision}
353
+ Revises: $\{down_revision | comma,n}
354
+ Create Date: $\{create_date}
355
+ """
356
+ from typing import Sequence, Union
357
+
358
+ from alembic import op
359
+ import sqlalchemy as sa
360
+ $\{imports if imports else ""}
361
+
362
+ # revision identifiers
363
+ revision: str = $\{repr(up_revision)}
364
+ down_revision: Union[str, None] = $\{repr(down_revision)}
365
+ branch_labels: Union[str, Sequence[str], None] = $\{repr(branch_labels)}
366
+ depends_on: Union[str, Sequence[str], None] = $\{repr(depends_on)}
367
+
368
+
369
+ def upgrade() -> None:
370
+ $\{upgrades if upgrades else "pass"}
371
+
372
+
373
+ def downgrade() -> None:
374
+ $\{downgrades if downgrades else "pass"}
375
+ `;
376
+ }
377
+ /**
378
+ * Generate initial migration: pgvector extension + app_settings table
379
+ */
380
+ export function generateInitialMigration(_packageName) {
381
+ return `"""Initial migration: pgvector extension and app_settings table.
382
+
383
+ Revision ID: 001
384
+ Revises: None
385
+ Create Date: 2024-01-01 00:00:00.000000
386
+
387
+ # popeye:requires_extension=vector
388
+ """
389
+ from typing import Sequence, Union
390
+
391
+ from alembic import op
392
+ import sqlalchemy as sa
393
+
394
+ revision: str = "001"
395
+ down_revision: Union[str, None] = None
396
+ branch_labels: Union[str, Sequence[str], None] = None
397
+ depends_on: Union[str, Sequence[str], None] = None
398
+
399
+
400
+ def upgrade() -> None:
401
+ # Enable pgvector extension (requires superuser or rds_superuser on managed DBs)
402
+ op.execute("CREATE EXTENSION IF NOT EXISTS vector")
403
+
404
+ op.create_table(
405
+ "app_settings",
406
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
407
+ sa.Column("key", sa.String(length=255), nullable=False),
408
+ sa.Column("value", sa.Text(), nullable=False),
409
+ sa.Column(
410
+ "created_at",
411
+ sa.DateTime(timezone=True),
412
+ server_default=sa.func.now(),
413
+ nullable=False,
414
+ ),
415
+ sa.Column(
416
+ "updated_at",
417
+ sa.DateTime(timezone=True),
418
+ server_default=sa.func.now(),
419
+ nullable=False,
420
+ ),
421
+ sa.PrimaryKeyConstraint("id"),
422
+ sa.UniqueConstraint("key"),
423
+ )
424
+ op.create_index("ix_app_settings_key", "app_settings", ["key"])
425
+
426
+
427
+ def downgrade() -> None:
428
+ op.drop_index("ix_app_settings_key", table_name="app_settings")
429
+ op.drop_table("app_settings")
430
+ op.execute("DROP EXTENSION IF EXISTS vector")
431
+ `;
432
+ }
433
+ /**
434
+ * Generate pgvector helper utilities
435
+ */
436
+ export function generateDbVectorHelpers(_packageName) {
437
+ return `"""
438
+ pgvector helper utilities.
439
+
440
+ Provides vector column type, similarity search, and sanity checks.
441
+ """
442
+
443
+ import logging
444
+ from sqlalchemy import Column, text
445
+ from sqlalchemy.ext.asyncio import AsyncSession
446
+
447
+ logger = logging.getLogger(__name__)
448
+
449
+ # Reason: pgvector types are registered at import time by the pgvector package
450
+ try:
451
+ from pgvector.sqlalchemy import Vector
452
+ except ImportError:
453
+ Vector = None
454
+ logger.warning("pgvector package not installed - vector features unavailable")
455
+
456
+
457
+ def vector_column(dimensions: int = 1536, nullable: bool = True) -> Column:
458
+ """
459
+ Create a pgvector column with the specified dimensions.
460
+
461
+ Args:
462
+ dimensions: Vector dimensionality (default: 1536 for OpenAI embeddings).
463
+ nullable: Whether the column allows NULL values.
464
+
465
+ Returns:
466
+ Column: SQLAlchemy column with Vector type.
467
+
468
+ Raises:
469
+ ImportError: If pgvector package is not installed.
470
+ """
471
+ if Vector is None:
472
+ raise ImportError("pgvector package is required for vector columns")
473
+ return Column(Vector(dimensions), nullable=nullable)
474
+
475
+
476
+ async def cosine_similarity_search(
477
+ session: AsyncSession,
478
+ table_name: str,
479
+ column_name: str,
480
+ query_vector: list[float],
481
+ limit: int = 10,
482
+ ) -> list[dict]:
483
+ """
484
+ Perform cosine similarity search against a vector column.
485
+
486
+ Args:
487
+ session: Async database session.
488
+ table_name: Name of the table to search.
489
+ column_name: Name of the vector column.
490
+ query_vector: Query embedding vector.
491
+ limit: Maximum number of results.
492
+
493
+ Returns:
494
+ list[dict]: Results with id and similarity score.
495
+ """
496
+ vector_str = "[" + ",".join(str(v) for v in query_vector) + "]"
497
+ sql = text(
498
+ f"SELECT id, 1 - ({column_name} <=> :vec::vector) AS similarity "
499
+ f"FROM {table_name} "
500
+ f"ORDER BY {column_name} <=> :vec::vector "
501
+ f"LIMIT :lim"
502
+ )
503
+ result = await session.execute(sql, {"vec": vector_str, "lim": limit})
504
+ return [{"id": row[0], "similarity": row[1]} for row in result.fetchall()]
505
+
506
+
507
+ async def check_vector_extension(session: AsyncSession) -> bool:
508
+ """
509
+ Verify that the pgvector extension is installed and functional.
510
+
511
+ Args:
512
+ session: Async database session.
513
+
514
+ Returns:
515
+ bool: True if pgvector is available.
516
+ """
517
+ try:
518
+ result = await session.execute(
519
+ text("SELECT extname FROM pg_extension WHERE extname = 'vector'")
520
+ )
521
+ return result.scalar() is not None
522
+ except Exception as e:
523
+ logger.error(f"Vector extension check failed: {e}")
524
+ return False
525
+ `;
526
+ }
527
+ /**
528
+ * Generate graceful startup hook
529
+ */
530
+ export function generateDbStartupHook(_packageName) {
531
+ return `"""
532
+ Application startup hook.
533
+
534
+ Handles graceful startup when DATABASE_URL is not configured.
535
+ The app runs in limited mode without a database connection.
536
+ """
537
+
538
+ import logging
539
+ import os
540
+
541
+ logger = logging.getLogger(__name__)
542
+
543
+
544
+ async def on_startup() -> None:
545
+ """
546
+ Run startup checks for database connectivity.
547
+
548
+ If DATABASE_URL is not set, logs a warning and skips DB initialization.
549
+ The application continues to run in limited mode.
550
+ """
551
+ database_url = os.getenv("DATABASE_URL", "")
552
+
553
+ if not database_url:
554
+ logger.warning(
555
+ "DATABASE_URL is not set. "
556
+ "Application running in limited mode without database. "
557
+ "Set DATABASE_URL in .env or environment to enable full functionality."
558
+ )
559
+ return
560
+
561
+ logger.info("DATABASE_URL detected - initializing database connection")
562
+
563
+ try:
564
+ from .database.connection import check_db_connection
565
+
566
+ status = await check_db_connection()
567
+ if status.get("connected"):
568
+ logger.info("Database connection verified successfully")
569
+ else:
570
+ logger.error(
571
+ f"Database connection failed: {status.get('error', 'unknown')}"
572
+ )
573
+ except Exception as e:
574
+ logger.error(f"Database initialization error: {e}")
575
+ `;
576
+ }
577
+ /**
578
+ * Generate database health check route
579
+ */
580
+ export function generateDbHealthRoute(_packageName) {
581
+ return `"""
582
+ Database health check endpoint.
583
+
584
+ Returns database connectivity and migration status.
585
+ """
586
+
587
+ import logging
588
+ import os
589
+
590
+ from fastapi import APIRouter
591
+ from fastapi.responses import JSONResponse
592
+ from sqlalchemy import text
593
+
594
+ logger = logging.getLogger(__name__)
595
+
596
+ router = APIRouter(tags=["health"])
597
+
598
+
599
+ @router.get("/health/db")
600
+ async def health_db():
601
+ """
602
+ Database health check endpoint.
603
+
604
+ Returns:
605
+ JSONResponse: 200 with DB details if healthy, 503 if not ready.
606
+ """
607
+ database_url = os.getenv("DATABASE_URL", "")
608
+
609
+ if not database_url:
610
+ return JSONResponse(
611
+ status_code=503,
612
+ content={
613
+ "status": "DB_NOT_READY",
614
+ "message": "DATABASE_URL not configured",
615
+ "setup_hint": "Set DATABASE_URL in .env or run the setup wizard",
616
+ },
617
+ )
618
+
619
+ try:
620
+ from .database.connection import engine
621
+
622
+ if engine is None:
623
+ return JSONResponse(
624
+ status_code=503,
625
+ content={
626
+ "status": "DB_NOT_READY",
627
+ "message": "Database engine not initialized",
628
+ },
629
+ )
630
+
631
+ async with engine.connect() as conn:
632
+ # Check basic connectivity
633
+ await conn.execute(text("SELECT 1"))
634
+
635
+ # Check migration status via alembic_version table
636
+ migration_info = {"current_revision": None}
637
+ try:
638
+ result = await conn.execute(
639
+ text("SELECT version_num FROM alembic_version LIMIT 1")
640
+ )
641
+ row = result.first()
642
+ if row:
643
+ migration_info["current_revision"] = row[0]
644
+ except Exception:
645
+ migration_info["current_revision"] = "alembic_version table not found"
646
+
647
+ return JSONResponse(
648
+ status_code=200,
649
+ content={
650
+ "status": "healthy",
651
+ "database": "connected",
652
+ "migrations": migration_info,
653
+ },
654
+ )
655
+ except Exception as e:
656
+ logger.error(f"Database health check failed: {e}")
657
+ return JSONResponse(
658
+ status_code=503,
659
+ content={
660
+ "status": "DB_NOT_READY",
661
+ "message": f"Database connection failed: {str(e)}",
662
+ },
663
+ )
664
+ `;
665
+ }
666
+ /**
667
+ * Generate test DB fixtures (conftest_db.py)
668
+ */
669
+ export function generateDbConftest(packageName) {
670
+ return `"""
671
+ Database test fixtures.
672
+
673
+ Provides test database URL override and async session fixture.
674
+ """
675
+
676
+ import os
677
+ import pytest
678
+ import pytest_asyncio
679
+ from sqlalchemy.ext.asyncio import (
680
+ AsyncSession,
681
+ async_sessionmaker,
682
+ create_async_engine,
683
+ )
684
+
685
+ from src.${packageName}.database.models import Base
686
+
687
+
688
+ # Override DATABASE_URL for tests
689
+ TEST_DATABASE_URL = os.getenv(
690
+ "TEST_DATABASE_URL",
691
+ "postgresql+asyncpg://postgres:postgres@localhost:5432/test_db",
692
+ )
693
+
694
+
695
+ @pytest.fixture(scope="session")
696
+ def test_engine():
697
+ """Create a test database engine."""
698
+ return create_async_engine(TEST_DATABASE_URL, echo=True)
699
+
700
+
701
+ @pytest_asyncio.fixture
702
+ async def db_session(test_engine) -> AsyncSession:
703
+ """
704
+ Provide an async database session for tests.
705
+
706
+ Creates tables before tests and drops them after.
707
+ Each test gets a fresh transaction that is rolled back.
708
+ """
709
+ async with test_engine.begin() as conn:
710
+ await conn.run_sync(Base.metadata.create_all)
711
+
712
+ session_factory = async_sessionmaker(
713
+ test_engine, class_=AsyncSession, expire_on_commit=False
714
+ )
715
+
716
+ async with session_factory() as session:
717
+ yield session
718
+
719
+ async with test_engine.begin() as conn:
720
+ await conn.run_sync(Base.metadata.drop_all)
721
+ `;
722
+ }
723
+ //# sourceMappingURL=database-python.js.map