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