@msn-control/liftoff 0.2.0 → 0.3.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.
@@ -0,0 +1,797 @@
1
+ const stackBuilders = {
2
+ 'python-fastapi': addPythonArtifacts,
3
+ 'node-fastify': addNodeArtifacts,
4
+ 'go-huma': addGoArtifacts
5
+ };
6
+ const sourceString = (value) => JSON.stringify(value);
7
+ const escapeHtml = (value) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
8
+ const localPostgresUrl = (host, database) => `postgresql:${'//'}postgres:postgres@${host}:5432/${database}`;
9
+ export function addStandardStackArtifacts(add, plan) {
10
+ stackBuilders[plan.apiStack.id](add, plan);
11
+ }
12
+ export function renderStandardDockerfile(plan) {
13
+ switch (plan.apiStack.id) {
14
+ case 'python-fastapi':
15
+ return `FROM python:3.12-slim
16
+
17
+ WORKDIR /app
18
+
19
+ ENV PYTHONDONTWRITEBYTECODE=1
20
+ ENV PYTHONUNBUFFERED=1
21
+ ENV PYTHONPATH=/app
22
+
23
+ COPY backend/pyproject.toml /app/backend/pyproject.toml
24
+ RUN pip install --no-cache-dir /app/backend
25
+
26
+ COPY backend /app/backend
27
+ COPY database /app/database
28
+
29
+ EXPOSE 8000
30
+ CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
31
+ `;
32
+ case 'node-fastify':
33
+ return `FROM node:20-alpine AS build
34
+
35
+ WORKDIR /app/backend
36
+ COPY backend/package*.json ./
37
+ RUN npm install
38
+ COPY backend ./
39
+ RUN npm run build
40
+
41
+ FROM node:20-alpine AS runtime
42
+ WORKDIR /app/backend
43
+ ENV NODE_ENV=production
44
+ COPY backend/package*.json ./
45
+ RUN npm install --omit=dev
46
+ COPY --from=build /app/backend/dist ./dist
47
+ COPY database /app/database
48
+
49
+ EXPOSE 8000
50
+ CMD ["node", "dist/server.js"]
51
+ `;
52
+ case 'go-huma':
53
+ return `FROM golang:1.23-alpine AS build
54
+
55
+ WORKDIR /src/backend
56
+ COPY backend/go.mod ./
57
+ RUN go mod download
58
+ COPY backend ./
59
+ RUN CGO_ENABLED=0 GOOS=linux go build -o /out/api ./cmd/api
60
+
61
+ FROM alpine:3.20
62
+ RUN adduser -D -u 10001 liftoff
63
+ USER liftoff
64
+ WORKDIR /app
65
+ COPY --from=build /out/api /app/api
66
+ COPY database /app/database
67
+
68
+ EXPOSE 8000
69
+ CMD ["/app/api"]
70
+ `;
71
+ }
72
+ }
73
+ export function renderStandardEnv(plan, environment = 'dev') {
74
+ const local = environment === 'dev';
75
+ const databaseUrl = localPostgresUrl('postgres', plan.safeProjectName.replace(/-/g, '_'));
76
+ return `APP_ENV=${environment}
77
+ APP_NAME=${plan.safeProjectName}
78
+ API_STACK=${plan.apiStack.id}
79
+ CLOUD_PROVIDER=${plan.provider.id}
80
+ AZURE_REGION=${plan.region.slug}
81
+ DATABASE_URL=${databaseUrl}
82
+ REDIS_URL=redis://redis:6379/0
83
+ MESSAGING_TRANSPORT=${local ? 'redis-streams' : 'azure-service-bus'}
84
+ BLOB_ENDPOINT=${local ? 'http://azurite:10000/devstoreaccount1' : ''}
85
+ `;
86
+ }
87
+ function addPythonArtifacts(add, plan) {
88
+ add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderPythonPyproject(plan));
89
+ add('backend-package', 'backend', ['backend', '__init__.py'], '');
90
+ add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
91
+ add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderPythonMain(plan));
92
+ add('backend-health-routes', 'backend', ['backend', 'apis', 'routes', 'health.py'], renderPythonHealthRoutes());
93
+ add('backend-routes-package', 'backend', ['backend', 'apis', 'routes', '__init__.py'], '');
94
+ add('backend-auth-dependency', 'backend', ['backend', 'apis', 'dependencies', 'auth.py'], renderPythonAuthDependency());
95
+ add('backend-config-package', 'backend', ['backend', 'config', '__init__.py'], '');
96
+ add('backend-settings', 'backend', ['backend', 'config', 'settings.py'], renderPythonSettings(plan));
97
+ add('backend-observability', 'backend', ['backend', 'observability', 'logging.py'], renderPythonLogging());
98
+ add('backend-observability-package', 'backend', ['backend', 'observability', '__init__.py'], '');
99
+ add('backend-test-health', 'backend-test', ['backend', 'tests', 'test_health.py'], renderPythonHealthTest());
100
+ add('database-alembic-ini', 'database', ['database', 'alembic.ini'], renderAlembicIni());
101
+ add('database-alembic-env', 'database', ['database', 'migrations', 'env.py'], renderAlembicEnv());
102
+ add('database-initial-migration', 'database', ['database', 'migrations', 'versions', '0001_initial.py'], renderPythonMigration());
103
+ add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderStandardSchema(plan));
104
+ }
105
+ function renderPythonPyproject(plan) {
106
+ return `[project]
107
+ name = "${plan.safeProjectName}-backend"
108
+ version = "0.1.0"
109
+ requires-python = ">=3.12"
110
+ dependencies = [
111
+ "fastapi>=0.111",
112
+ "uvicorn[standard]>=0.30",
113
+ "pydantic>=2.7",
114
+ "pydantic-settings>=2.3",
115
+ "scalar-fastapi>=1.0",
116
+ "sqlalchemy[asyncio]>=2.0",
117
+ "asyncpg>=0.29",
118
+ "psycopg[binary]>=3.2",
119
+ "alembic>=1.13",
120
+ "redis>=5.0",
121
+ "azure-servicebus>=7.12",
122
+ "azure-storage-blob>=12.20",
123
+ "azure-communication-email>=1.0"
124
+ ]
125
+
126
+ [project.optional-dependencies]
127
+ test = ["pytest>=8.2", "httpx>=0.27"]
128
+
129
+ [build-system]
130
+ requires = ["setuptools>=70"]
131
+ build-backend = "setuptools.build_meta"
132
+
133
+ [tool.setuptools]
134
+ packages = []
135
+
136
+ [tool.pytest.ini_options]
137
+ pythonpath = [".."]
138
+ testpaths = ["tests"]
139
+ `;
140
+ }
141
+ function renderPythonMain(plan) {
142
+ return `from fastapi import FastAPI
143
+
144
+ try:
145
+ from scalar_fastapi import get_scalar_api_reference
146
+ except ImportError: # pragma: no cover - dependency is present in generated runtime
147
+ get_scalar_api_reference = None
148
+
149
+ from backend.apis.routes import health
150
+ from backend.config.settings import get_settings
151
+
152
+
153
+ settings = get_settings()
154
+ app = FastAPI(title=settings.app_name, version="0.1.0")
155
+ app.include_router(health.router)
156
+
157
+
158
+ @app.get("/scalar", include_in_schema=False)
159
+ def scalar_reference():
160
+ if get_scalar_api_reference is None:
161
+ return {"message": "Install scalar-fastapi to enable the Scalar developer portal."}
162
+ return get_scalar_api_reference(openapi_url=app.openapi_url, title=f"{app.title} API")
163
+
164
+
165
+ @app.get("/api")
166
+ def api_root():
167
+ return {"name": ${sourceString(plan.projectName)}, "stack": "python-fastapi"}
168
+ `;
169
+ }
170
+ function renderPythonHealthRoutes() {
171
+ return `from fastapi import APIRouter
172
+
173
+ router = APIRouter(tags=["operations"])
174
+
175
+
176
+ @router.get("/health")
177
+ def health():
178
+ return {"status": "ok"}
179
+
180
+
181
+ @router.get("/ready")
182
+ def ready():
183
+ return {"status": "ready"}
184
+ `;
185
+ }
186
+ function renderPythonAuthDependency() {
187
+ return `from dataclasses import dataclass
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class CurrentUser:
192
+ subject: str = "local-developer"
193
+
194
+
195
+ async def get_current_user() -> CurrentUser:
196
+ return CurrentUser()
197
+ `;
198
+ }
199
+ function renderPythonSettings(plan) {
200
+ return `from functools import lru_cache
201
+ from pydantic_settings import BaseSettings, SettingsConfigDict
202
+
203
+
204
+ class Settings(BaseSettings):
205
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
206
+
207
+ app_name: str = ${sourceString(plan.projectName)}
208
+ app_env: str = "dev"
209
+ api_stack: str = "python-fastapi"
210
+ cloud_provider: str = "${plan.provider.id}"
211
+ azure_region: str = "${plan.region.slug}"
212
+ database_url: str
213
+ redis_url: str
214
+ messaging_transport: str = "redis-streams"
215
+ blob_endpoint: str | None = None
216
+
217
+
218
+ @lru_cache
219
+ def get_settings() -> Settings:
220
+ return Settings()
221
+ `;
222
+ }
223
+ function renderPythonLogging() {
224
+ return `import logging
225
+
226
+
227
+ def configure_logging() -> None:
228
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
229
+ `;
230
+ }
231
+ function renderPythonHealthTest() {
232
+ return `from fastapi.testclient import TestClient
233
+
234
+ from backend.apis.main import app
235
+
236
+
237
+ def test_health():
238
+ response = TestClient(app).get("/health")
239
+ assert response.status_code == 200
240
+ assert response.json()["status"] == "ok"
241
+
242
+
243
+ def test_ready():
244
+ response = TestClient(app).get("/ready")
245
+ assert response.status_code == 200
246
+ assert response.json()["status"] == "ready"
247
+ `;
248
+ }
249
+ function renderAlembicIni() {
250
+ return `[alembic]
251
+ script_location = %(here)s/migrations
252
+ sqlalchemy.url =
253
+ `;
254
+ }
255
+ function renderAlembicEnv() {
256
+ return `import os
257
+
258
+ from alembic import context
259
+ from sqlalchemy import create_engine
260
+
261
+ config = context.config
262
+ target_metadata = None
263
+
264
+
265
+ def run_migrations_online():
266
+ database_url = os.environ.get("DATABASE_URL")
267
+ if not database_url:
268
+ raise RuntimeError("DATABASE_URL is required to run migrations")
269
+ database_url = database_url.replace("postgresql+asyncpg://", "postgresql+psycopg://", 1)
270
+ database_url = database_url.replace("postgresql://", "postgresql+psycopg://", 1)
271
+ connectable = create_engine(database_url)
272
+ with connectable.connect() as connection:
273
+ context.configure(connection=connection, target_metadata=target_metadata)
274
+ with context.begin_transaction():
275
+ context.run_migrations()
276
+
277
+
278
+ run_migrations_online()
279
+ `;
280
+ }
281
+ function renderPythonMigration() {
282
+ return `from alembic import op
283
+ import sqlalchemy as sa
284
+
285
+ revision = "0001_initial"
286
+ down_revision = None
287
+ branch_labels = None
288
+ depends_on = None
289
+
290
+
291
+ def upgrade():
292
+ op.create_table(
293
+ "app_records",
294
+ sa.Column("id", sa.Integer(), primary_key=True),
295
+ sa.Column("name", sa.String(length=255), nullable=False),
296
+ sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
297
+ )
298
+
299
+
300
+ def downgrade():
301
+ op.drop_table("app_records")
302
+ `;
303
+ }
304
+ function addNodeArtifacts(add, plan) {
305
+ add('node-backend-package', 'backend', ['backend', 'package.json'], renderNodePackage(plan));
306
+ add('node-backend-tsconfig', 'backend', ['backend', 'tsconfig.json'], renderNodeTsconfig());
307
+ add('node-backend-drizzle-config', 'backend', ['backend', 'drizzle.config.ts'], renderNodeDrizzleConfig());
308
+ add('node-backend-config', 'backend', ['backend', 'src', 'config.ts'], renderNodeConfig(plan));
309
+ add('node-backend-app', 'backend', ['backend', 'src', 'app.ts'], renderNodeApp(plan));
310
+ add('node-backend-server', 'backend', ['backend', 'src', 'server.ts'], renderNodeServer());
311
+ add('node-backend-database', 'backend', ['backend', 'src', 'database.ts'], renderNodeDatabase());
312
+ add('node-backend-schema', 'backend', ['backend', 'src', 'db', 'schema.ts'], renderNodeSchema());
313
+ add('node-backend-test-health', 'backend-test', ['backend', 'test', 'health.test.ts'], renderNodeHealthTest());
314
+ add('database-node-migration', 'database', ['database', 'migrations', '0000_initial.sql'], renderNodeMigration());
315
+ add('database-node-migration-journal', 'database', ['database', 'migrations', 'meta', '_journal.json'], renderNodeMigrationJournal());
316
+ add('database-node-migration-snapshot', 'database', ['database', 'migrations', 'meta', '0000_snapshot.json'], renderNodeMigrationSnapshot());
317
+ add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderStandardSchema(plan));
318
+ }
319
+ function renderNodePackage(plan) {
320
+ return JSON.stringify({
321
+ name: `${plan.safeProjectName}-backend`,
322
+ version: '0.1.0',
323
+ private: true,
324
+ type: 'module',
325
+ engines: { node: '>=20' },
326
+ scripts: {
327
+ dev: 'tsx watch src/server.ts',
328
+ build: 'tsc -p tsconfig.json',
329
+ start: 'node dist/server.js',
330
+ test: 'vitest run',
331
+ 'db:generate': 'drizzle-kit generate',
332
+ 'db:migrate': 'drizzle-kit migrate'
333
+ },
334
+ dependencies: {
335
+ '@fastify/swagger': '^9.4.0',
336
+ 'drizzle-orm': '^0.44.0',
337
+ fastify: '^5.4.0',
338
+ pg: '^8.16.0'
339
+ },
340
+ devDependencies: {
341
+ '@types/node': '^20.14.10',
342
+ '@types/pg': '^8.15.0',
343
+ 'drizzle-kit': '^0.31.0',
344
+ tsx: '^4.20.0',
345
+ typescript: '^5.5.4',
346
+ vitest: '^4.1.9'
347
+ }
348
+ }, null, 2);
349
+ }
350
+ function renderNodeTsconfig() {
351
+ return JSON.stringify({
352
+ compilerOptions: {
353
+ target: 'ES2022',
354
+ module: 'NodeNext',
355
+ moduleResolution: 'NodeNext',
356
+ rootDir: 'src',
357
+ outDir: 'dist',
358
+ strict: true,
359
+ esModuleInterop: true,
360
+ skipLibCheck: true
361
+ },
362
+ include: ['src/**/*.ts']
363
+ }, null, 2);
364
+ }
365
+ function renderNodeDrizzleConfig() {
366
+ const fallbackUrl = localPostgresUrl('localhost', 'postgres');
367
+ return `import { defineConfig } from 'drizzle-kit';
368
+
369
+ export default defineConfig({
370
+ dialect: 'postgresql',
371
+ schema: './src/db/schema.ts',
372
+ out: '../database/migrations',
373
+ dbCredentials: {
374
+ url: process.env.DATABASE_URL ?? ${sourceString(fallbackUrl)}
375
+ }
376
+ });
377
+ `;
378
+ }
379
+ function renderNodeConfig(plan) {
380
+ return `export interface AppConfig {
381
+ appName: string;
382
+ appEnv: string;
383
+ port: number;
384
+ cloudProvider: string;
385
+ azureRegion: string;
386
+ databaseUrl: string;
387
+ redisUrl: string;
388
+ messagingTransport: string;
389
+ blobEndpoint?: string;
390
+ }
391
+
392
+ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
393
+ const databaseUrl = env.DATABASE_URL;
394
+ const redisUrl = env.REDIS_URL;
395
+ if (!databaseUrl || !redisUrl) {
396
+ throw new Error('DATABASE_URL and REDIS_URL are required.');
397
+ }
398
+ return {
399
+ appName: env.APP_NAME ?? ${sourceString(plan.projectName)},
400
+ appEnv: env.APP_ENV ?? 'dev',
401
+ port: Number.parseInt(env.PORT ?? '8000', 10),
402
+ cloudProvider: env.CLOUD_PROVIDER ?? '${plan.provider.id}',
403
+ azureRegion: env.AZURE_REGION ?? '${plan.region.slug}',
404
+ databaseUrl,
405
+ redisUrl,
406
+ messagingTransport: env.MESSAGING_TRANSPORT ?? 'redis-streams',
407
+ blobEndpoint: env.BLOB_ENDPOINT || undefined
408
+ };
409
+ }
410
+ `;
411
+ }
412
+ function renderNodeApp(plan) {
413
+ const scalarPage = `<!doctype html><html><head><title>${escapeHtml(plan.projectName)} API</title></head><body><script id="api-reference" data-url="/openapi.json"></script><script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script></body></html>`;
414
+ return `import swagger from '@fastify/swagger';
415
+ import Fastify from 'fastify';
416
+
417
+ const scalarPage = ${sourceString(scalarPage)};
418
+
419
+ export async function buildApp() {
420
+ const app = Fastify({ logger: true });
421
+ await app.register(swagger, {
422
+ openapi: {
423
+ info: { title: ${sourceString(`${plan.projectName} API`)}, version: '0.1.0' }
424
+ }
425
+ });
426
+
427
+ const statusSchema = {
428
+ response: {
429
+ 200: {
430
+ type: 'object',
431
+ required: ['status'],
432
+ properties: { status: { type: 'string' } }
433
+ }
434
+ }
435
+ } as const;
436
+
437
+ app.get('/health', { schema: statusSchema }, async () => ({ status: 'ok' }));
438
+ app.get('/ready', { schema: statusSchema }, async () => ({ status: 'ready' }));
439
+ app.get('/api', async () => ({ name: ${sourceString(plan.projectName)}, stack: 'node-fastify' }));
440
+ app.get('/openapi.json', async () => app.swagger());
441
+ app.get('/scalar', async (_request, reply) => reply.type('text/html').send(scalarPage));
442
+ return app;
443
+ }
444
+ `;
445
+ }
446
+ function renderNodeServer() {
447
+ return `import { buildApp } from './app.js';
448
+ import { loadConfig } from './config.js';
449
+
450
+ const config = loadConfig();
451
+ const app = await buildApp();
452
+
453
+ try {
454
+ await app.listen({ host: '0.0.0.0', port: config.port });
455
+ } catch (error) {
456
+ app.log.error(error);
457
+ process.exitCode = 1;
458
+ }
459
+ `;
460
+ }
461
+ function renderNodeDatabase() {
462
+ return `import { drizzle } from 'drizzle-orm/node-postgres';
463
+ import { Pool } from 'pg';
464
+ import { loadConfig } from './config.js';
465
+
466
+ const pool = new Pool({ connectionString: loadConfig().databaseUrl });
467
+ export const database = drizzle(pool);
468
+ `;
469
+ }
470
+ function renderNodeSchema() {
471
+ return `import { integer, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core';
472
+
473
+ export const appRecords = pgTable('app_records', {
474
+ id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
475
+ name: varchar('name', { length: 255 }).notNull(),
476
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
477
+ });
478
+ `;
479
+ }
480
+ function renderNodeHealthTest() {
481
+ return `import { describe, expect, it } from 'vitest';
482
+ import { buildApp } from '../src/app.js';
483
+
484
+ describe('health endpoints', () => {
485
+ it('reports healthy and ready', async () => {
486
+ const app = await buildApp();
487
+ const health = await app.inject({ method: 'GET', url: '/health' });
488
+ const ready = await app.inject({ method: 'GET', url: '/ready' });
489
+ expect(health.json()).toEqual({ status: 'ok' });
490
+ expect(ready.json()).toEqual({ status: 'ready' });
491
+ await app.close();
492
+ });
493
+ });
494
+ `;
495
+ }
496
+ function renderNodeMigration() {
497
+ return `CREATE TABLE app_records (
498
+ id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
499
+ name varchar(255) NOT NULL,
500
+ created_at timestamptz NOT NULL DEFAULT now()
501
+ );
502
+ `;
503
+ }
504
+ function renderNodeMigrationJournal() {
505
+ return JSON.stringify({
506
+ version: '7',
507
+ dialect: 'postgresql',
508
+ entries: [
509
+ {
510
+ idx: 0,
511
+ version: '7',
512
+ when: 0,
513
+ tag: '0000_initial',
514
+ breakpoints: true
515
+ }
516
+ ]
517
+ }, null, 2);
518
+ }
519
+ function renderNodeMigrationSnapshot() {
520
+ return JSON.stringify({
521
+ id: '00000000-0000-4000-8000-000000000001',
522
+ prevId: '00000000-0000-0000-0000-000000000000',
523
+ version: '7',
524
+ dialect: 'postgresql',
525
+ tables: {
526
+ 'public.app_records': {
527
+ name: 'app_records',
528
+ schema: '',
529
+ columns: {
530
+ id: {
531
+ name: 'id',
532
+ type: 'integer',
533
+ primaryKey: true,
534
+ notNull: true,
535
+ identity: {
536
+ type: 'always',
537
+ name: 'app_records_id_seq',
538
+ schema: 'public',
539
+ increment: '1',
540
+ startWith: '1',
541
+ minValue: '1',
542
+ maxValue: '2147483647',
543
+ cache: '1',
544
+ cycle: false
545
+ }
546
+ },
547
+ name: {
548
+ name: 'name',
549
+ type: 'varchar(255)',
550
+ primaryKey: false,
551
+ notNull: true
552
+ },
553
+ created_at: {
554
+ name: 'created_at',
555
+ type: 'timestamp with time zone',
556
+ primaryKey: false,
557
+ notNull: true,
558
+ default: 'now()'
559
+ }
560
+ },
561
+ indexes: {},
562
+ foreignKeys: {},
563
+ compositePrimaryKeys: {},
564
+ uniqueConstraints: {},
565
+ policies: {},
566
+ checkConstraints: {},
567
+ isRLSEnabled: false
568
+ }
569
+ },
570
+ enums: {},
571
+ schemas: {},
572
+ sequences: {},
573
+ roles: {},
574
+ policies: {},
575
+ views: {},
576
+ _meta: {
577
+ columns: {},
578
+ schemas: {},
579
+ tables: {}
580
+ }
581
+ }, null, 2);
582
+ }
583
+ function addGoArtifacts(add, plan) {
584
+ add('go-backend-module', 'backend', ['backend', 'go.mod'], renderGoModule(plan));
585
+ add('go-backend-makefile', 'backend', ['backend', 'Makefile'], renderGoMakefile());
586
+ add('go-backend-main', 'backend', ['backend', 'cmd', 'api', 'main.go'], renderGoMain(plan));
587
+ add('go-backend-api', 'backend', ['backend', 'internal', 'api', 'api.go'], renderGoApi(plan));
588
+ add('go-backend-config', 'backend', ['backend', 'internal', 'config', 'config.go'], renderGoConfig(plan));
589
+ add('go-backend-database', 'backend', ['backend', 'internal', 'database', 'database.go'], renderGoDatabase());
590
+ add('go-backend-test-health', 'backend-test', ['backend', 'internal', 'api', 'api_test.go'], renderGoHealthTest(plan));
591
+ add('database-go-migration', 'database', ['database', 'migrations', '0001_initial.sql'], renderGoMigration());
592
+ add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderStandardSchema(plan));
593
+ }
594
+ function goModule(plan) {
595
+ return `example.com/${plan.packageName}/backend`;
596
+ }
597
+ function renderGoModule(plan) {
598
+ return `module ${goModule(plan)}
599
+
600
+ go 1.23
601
+
602
+ require (
603
+ github.com/danielgtaylor/huma/v2 v2.27.0
604
+ github.com/go-chi/chi/v5 v5.2.1
605
+ github.com/jackc/pgx/v5 v5.7.2
606
+ )
607
+ `;
608
+ }
609
+ function renderGoMakefile() {
610
+ return `GOOSE_VERSION := v3.24.1
611
+
612
+ .PHONY: test migrate
613
+
614
+ test:
615
+ go test ./...
616
+
617
+ migrate:
618
+ go run github.com/pressly/goose/v3/cmd/goose@$(GOOSE_VERSION) -dir ../database/migrations postgres "$(DATABASE_URL)" up
619
+ `;
620
+ }
621
+ function renderGoMain(plan) {
622
+ return `package main
623
+
624
+ import (
625
+ "log"
626
+ "net/http"
627
+
628
+ "${goModule(plan)}/internal/api"
629
+ "${goModule(plan)}/internal/config"
630
+ )
631
+
632
+ func main() {
633
+ cfg, err := config.Load()
634
+ if err != nil {
635
+ log.Fatal(err)
636
+ }
637
+ log.Printf("%s listening on :%s", cfg.AppName, cfg.Port)
638
+ log.Fatal(http.ListenAndServe(":"+cfg.Port, api.New(cfg.AppName)))
639
+ }
640
+ `;
641
+ }
642
+ function renderGoApi(plan) {
643
+ return `package api
644
+
645
+ import (
646
+ "context"
647
+ "fmt"
648
+ "net/http"
649
+
650
+ "github.com/danielgtaylor/huma/v2"
651
+ "github.com/danielgtaylor/huma/v2/adapters/humachi"
652
+ "github.com/go-chi/chi/v5"
653
+ )
654
+
655
+ type statusOutput struct {
656
+ Body struct {
657
+ Status string \`json:"status"\`
658
+ }
659
+ }
660
+
661
+ func New(name string) http.Handler {
662
+ router := chi.NewRouter()
663
+ config := huma.DefaultConfig(name+" API", "0.1.0")
664
+ config.OpenAPIPath = "/openapi"
665
+ config.DocsPath = ""
666
+ api := humachi.New(router, config)
667
+
668
+ huma.Get(api, "/health", func(context.Context, *struct{}) (*statusOutput, error) {
669
+ output := &statusOutput{}
670
+ output.Body.Status = "ok"
671
+ return output, nil
672
+ })
673
+ huma.Get(api, "/ready", func(context.Context, *struct{}) (*statusOutput, error) {
674
+ output := &statusOutput{}
675
+ output.Body.Status = "ready"
676
+ return output, nil
677
+ })
678
+
679
+ router.Get("/api", func(response http.ResponseWriter, _ *http.Request) {
680
+ response.Header().Set("Content-Type", "application/json")
681
+ fmt.Fprint(response, ${sourceString(JSON.stringify({ name: plan.projectName, stack: 'go-huma' }))})
682
+ })
683
+ router.Get("/scalar", scalarReference)
684
+ return router
685
+ }
686
+
687
+ func scalarReference(response http.ResponseWriter, _ *http.Request) {
688
+ response.Header().Set("Content-Type", "text/html; charset=utf-8")
689
+ fmt.Fprint(response, \`<!doctype html><html><head><title>API Reference</title></head><body><script id="api-reference" data-url="/openapi.json"></script><script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script></body></html>\`)
690
+ }
691
+ `;
692
+ }
693
+ function renderGoConfig(plan) {
694
+ return `package config
695
+
696
+ import (
697
+ "fmt"
698
+ "os"
699
+ )
700
+
701
+ type Config struct {
702
+ AppName string
703
+ AppEnv string
704
+ Port string
705
+ CloudProvider string
706
+ AzureRegion string
707
+ DatabaseURL string
708
+ RedisURL string
709
+ MessagingTransport string
710
+ BlobEndpoint string
711
+ }
712
+
713
+ func Load() (Config, error) {
714
+ databaseURL := os.Getenv("DATABASE_URL")
715
+ redisURL := os.Getenv("REDIS_URL")
716
+ if databaseURL == "" || redisURL == "" {
717
+ return Config{}, fmt.Errorf("DATABASE_URL and REDIS_URL are required")
718
+ }
719
+ return Config{
720
+ AppName: value("APP_NAME", ${sourceString(plan.projectName)}),
721
+ AppEnv: value("APP_ENV", "dev"),
722
+ Port: value("PORT", "8000"),
723
+ CloudProvider: value("CLOUD_PROVIDER", "${plan.provider.id}"),
724
+ AzureRegion: value("AZURE_REGION", "${plan.region.slug}"),
725
+ DatabaseURL: databaseURL,
726
+ RedisURL: redisURL,
727
+ MessagingTransport: value("MESSAGING_TRANSPORT", "redis-streams"),
728
+ BlobEndpoint: os.Getenv("BLOB_ENDPOINT"),
729
+ }, nil
730
+ }
731
+
732
+ func value(name, fallback string) string {
733
+ if current := os.Getenv(name); current != "" {
734
+ return current
735
+ }
736
+ return fallback
737
+ }
738
+ `;
739
+ }
740
+ function renderGoDatabase() {
741
+ return `package database
742
+
743
+ import (
744
+ "context"
745
+
746
+ "github.com/jackc/pgx/v5/pgxpool"
747
+ )
748
+
749
+ func Open(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
750
+ return pgxpool.New(ctx, databaseURL)
751
+ }
752
+ `;
753
+ }
754
+ function renderGoHealthTest(plan) {
755
+ return `package api
756
+
757
+ import (
758
+ "net/http"
759
+ "net/http/httptest"
760
+ "testing"
761
+ )
762
+
763
+ func TestHealthAndReady(t *testing.T) {
764
+ handler := New(${sourceString(plan.projectName)})
765
+ for _, path := range []string{"/health", "/ready"} {
766
+ request := httptest.NewRequest(http.MethodGet, path, nil)
767
+ response := httptest.NewRecorder()
768
+ handler.ServeHTTP(response, request)
769
+ if response.Code != http.StatusOK {
770
+ t.Fatalf("%s returned %d", path, response.Code)
771
+ }
772
+ }
773
+ }
774
+ `;
775
+ }
776
+ function renderGoMigration() {
777
+ return `-- +goose Up
778
+ CREATE TABLE app_records (
779
+ id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
780
+ name varchar(255) NOT NULL,
781
+ created_at timestamptz NOT NULL DEFAULT now()
782
+ );
783
+
784
+ -- +goose Down
785
+ DROP TABLE app_records;
786
+ `;
787
+ }
788
+ function renderStandardSchema(plan) {
789
+ return `-- ${plan.safeProjectName} standard application schema
790
+ CREATE TABLE IF NOT EXISTS app_records (
791
+ id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
792
+ name varchar(255) NOT NULL,
793
+ created_at timestamptz NOT NULL DEFAULT now()
794
+ );
795
+ `;
796
+ }
797
+ //# sourceMappingURL=standard-templates.js.map