first commit

This commit is contained in:
2026-08-08 21:14:10 -05:00
commit e1600da8b1
55 changed files with 2687 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from '../sdk'
export const actions = sdk.Actions.of()
+23
View File
@@ -0,0 +1,23 @@
import { storeJson } from './fileModels/store.json'
import { sdk } from './sdk'
import { databaseName, databaseUser } from './utils'
export const { createBackup, restoreInit } = sdk.setupBackups(async () =>
sdk.Backups.withPgDump({
imageId: 'postgres',
dbVolume: 'postgres',
mountpoint: '/var/lib/postgresql/data',
pgdataPath: '',
database: databaseName,
user: databaseUser,
password: async () => {
const password = await storeJson
.read(value => value.databasePassword)
.once()
if (!password) throw new Error('Database password is not initialized')
return password
},
})
.addVolume('config')
.addVolume('storage'),
)
+3
View File
@@ -0,0 +1,3 @@
import { sdk } from './sdk'
export const setDependencies = sdk.setupDependencies(async () => ({}))
+12
View File
@@ -0,0 +1,12 @@
import { FileHelper, z } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
const shape = z.object({
databasePassword: z.string().min(32).catch(''),
secretKeyBase: z.string().min(64).catch(''),
})
export const storeJson = FileHelper.json(
{ base: sdk.volumes.config, subpath: 'store.json' },
shape,
)
+14
View File
@@ -0,0 +1,14 @@
export const DEFAULT_LANG = 'en_US'
const dict = {
Database: 0,
'The database is not ready': 1,
'Todo API': 2,
'The Todo API is ready': 3,
'The Todo API is not ready': 4,
'REST API for todo items': 5,
} as const
export type I18nKey = keyof typeof dict
export type LangDict = Record<(typeof dict)[I18nKey], string>
export default dict
@@ -0,0 +1,8 @@
import { LangDict } from './default'
export default {
es_ES: { 0: 'Base de datos', 1: 'La base de datos no está lista', 2: 'API de tareas', 3: 'La API de tareas está lista', 4: 'La API de tareas no está lista', 5: 'API REST para tareas' },
de_DE: { 0: 'Datenbank', 1: 'Die Datenbank ist nicht bereit', 2: 'Aufgaben-API', 3: 'Die Aufgaben-API ist bereit', 4: 'Die Aufgaben-API ist nicht bereit', 5: 'REST-API für Aufgaben' },
pl_PL: { 0: 'Baza danych', 1: 'Baza danych nie jest gotowa', 2: 'API zadań', 3: 'API zadań jest gotowe', 4: 'API zadań nie jest gotowe', 5: 'REST API dla zadań' },
fr_FR: { 0: 'Base de données', 1: "La base de données n'est pas prête", 2: 'API de tâches', 3: "L'API de tâches est prête", 4: "L'API de tâches n'est pas prête", 5: 'API REST pour les tâches' },
} satisfies Record<string, LangDict>
+5
View File
@@ -0,0 +1,5 @@
import { setupI18n } from '@start9labs/start-sdk'
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
import translations from './dictionaries/translations'
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
+8
View File
@@ -0,0 +1,8 @@
export { createBackup } from './backups'
export { main } from './main'
export { init, uninit } from './init'
export { actions } from './actions'
import { buildManifest } from '@start9labs/start-sdk'
import { manifest as sdkManifest } from './manifest'
import { versionGraph } from './versions'
export const manifest = buildManifest(versionGraph, sdkManifest)
+18
View File
@@ -0,0 +1,18 @@
import { actions } from '../actions'
import { restoreInit } from '../backups'
import { setDependencies } from '../dependencies'
import { setInterfaces } from '../interfaces'
import { sdk } from '../sdk'
import { versionGraph } from '../versions'
import { seedSecrets } from './seedSecrets'
export const init = sdk.setupInit(
restoreInit,
versionGraph,
setInterfaces,
setDependencies,
actions,
seedSecrets,
)
export const uninit = sdk.setupUninit(versionGraph)
+18
View File
@@ -0,0 +1,18 @@
import { utils } from '@start9labs/start-sdk'
import { storeJson } from '../fileModels/store.json'
import { sdk } from '../sdk'
export const seedSecrets = sdk.setupOnInit(async (effects, kind) => {
if (kind !== 'install') return
await storeJson.merge(effects, {
databasePassword: utils.getDefaultString({
charset: 'a-z,A-Z,0-9',
len: 48,
}),
secretKeyBase: utils.getDefaultString({
charset: 'a-z,A-Z,0-9',
len: 128,
}),
})
})
+31
View File
@@ -0,0 +1,31 @@
import { i18n } from './i18n'
import { sdk } from './sdk'
import { appPort } from './utils'
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const host = sdk.MultiHost.of(effects, 'api')
const origin = await host.bindPort(appPort, {
protocol: 'http',
preferredExternalPort: 80,
addSsl: {
alpn: { specified: ['http/1.1'] },
preferredExternalPort: 443,
addXForwardedHeaders: true,
auth: null,
},
})
const api = sdk.createInterface(effects, {
name: i18n('Todo API'),
id: 'api',
description: i18n('REST API for todo items'),
type: 'api',
masked: false,
schemeOverride: null,
username: null,
path: '/',
query: {},
})
return [await origin.export([api])]
})
+97
View File
@@ -0,0 +1,97 @@
import { storeJson } from './fileModels/store.json'
import { i18n } from './i18n'
import { sdk } from './sdk'
import { appPort, databaseName, databaseUser } from './utils'
export const main = sdk.setupMain(async ({ effects }) => {
const secrets = await storeJson.read().once()
if (!secrets?.databasePassword || !secrets.secretKeyBase) {
throw new Error('Todo Back secrets have not been initialized')
}
const databaseUrl = `postgresql://${databaseUser}:${secrets.databasePassword}@127.0.0.1:5432/${databaseName}`
const appEnv = {
DATABASE_URL: databaseUrl,
SECRET_KEY_BASE: secrets.secretKeyBase,
RAILS_ENV: 'production',
RAILS_LOG_TO_STDOUT: 'true',
PORT: String(appPort),
}
const postgresSub = sdk.SubContainer.of(
effects,
{ imageId: 'postgres' },
sdk.Mounts.of().mountVolume({
volumeId: 'postgres',
subpath: null,
mountpoint: '/var/lib/postgresql/data',
readonly: false,
}),
'postgres-subcontainer',
)
const appSub = sdk.SubContainer.of(
effects,
{ imageId: 'app' },
sdk.Mounts.of().mountVolume({
volumeId: 'storage',
subpath: null,
mountpoint: '/app/storage',
readonly: false,
}),
'app-subcontainer',
)
return sdk.Daemons.of(effects)
.addDaemon('postgres', {
subcontainer: postgresSub,
exec: {
command: sdk.useEntrypoint(['-c', 'listen_addresses=127.0.0.1']),
env: {
POSTGRES_DB: databaseName,
POSTGRES_USER: databaseUser,
POSTGRES_PASSWORD: secrets.databasePassword,
PGDATA: '/var/lib/postgresql/data',
},
},
ready: {
display: i18n('Database'),
fn: () =>
sdk.healthCheck.runHealthScript(
[
'pg_isready',
'-h',
'127.0.0.1',
'-U',
databaseUser,
'-d',
databaseName,
],
postgresSub,
{ errorMessage: i18n('The database is not ready') },
),
},
requires: [],
})
.addOneshot('database-prepare', {
subcontainer: appSub,
exec: { command: ['bin/rails', 'db:prepare'], env: appEnv },
requires: ['postgres'],
})
.addDaemon('app', {
subcontainer: appSub,
exec: {
command: ['bundle', 'exec', 'puma', '-C', 'config/puma.rb'],
env: appEnv,
},
ready: {
display: i18n('Todo API'),
fn: () =>
sdk.healthCheck.checkWebUrl(effects, `http://127.0.0.1:${appPort}/up`, {
successMessage: i18n('The Todo API is ready'),
errorMessage: i18n('The Todo API is not ready'),
}),
},
requires: ['database-prepare'],
})
})
+15
View File
@@ -0,0 +1,15 @@
export const short = {
en_US: 'A minimal, self-hosted JSON API for todo items.',
es_ES: 'Una API JSON mínima y autohospedada para tareas.',
de_DE: 'Eine minimale, selbst gehostete JSON-API für Aufgaben.',
pl_PL: 'Minimalne, samodzielnie hostowane API JSON dla zadań.',
fr_FR: 'Une API JSON minimale et auto-hébergée pour les tâches.',
}
export const long = {
en_US: 'Create, read, update, and delete todo items through a small Rails API backed by PostgreSQL.',
es_ES: 'Crea, consulta, actualiza y elimina tareas mediante una pequeña API Rails respaldada por PostgreSQL.',
de_DE: 'Erstellen, lesen, aktualisieren und löschen Sie Aufgaben über eine kleine Rails-API mit PostgreSQL.',
pl_PL: 'Twórz, odczytuj, aktualizuj i usuwaj zadania przez małe API Rails oparte na PostgreSQL.',
fr_FR: 'Créez, consultez, modifiez et supprimez des tâches via une petite API Rails utilisant PostgreSQL.',
}
+25
View File
@@ -0,0 +1,25 @@
import { setupManifest } from '@start9labs/start-sdk'
import { long, short } from './i18n'
export const manifest = setupManifest({
id: 'todo-back',
title: 'Todo Back',
license: 'MIT',
packageRepo: 'https://github.com/alextab93/todo-back',
upstreamRepo: 'https://github.com/alextab93/todo-back',
marketingUrl: 'https://github.com/alextab93/todo-back',
donationUrl: null,
description: { short, long },
volumes: ['config', 'postgres', 'storage'],
images: {
app: {
source: { dockerBuild: {} },
arch: ['x86_64'],
},
postgres: {
source: { dockerTag: 'postgres:17.6-bookworm' },
arch: ['x86_64'],
},
},
dependencies: {},
})
+4
View File
@@ -0,0 +1,4 @@
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
export const sdk = StartSdk.of().withManifest(manifest).build(true)
+3
View File
@@ -0,0 +1,3 @@
export const appPort = 3000
export const databaseName = 'todo_back_production'
export const databaseUser = 'todo_back'
+16
View File
@@ -0,0 +1,16 @@
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
export const current = VersionInfo.of({
version: '0.1.1:0',
releaseNotes: {
en_US: 'Fix PostgreSQL readiness checks and HTTP/1.1 proxying; health now verifies the database.',
es_ES: 'Corrige las comprobaciones de PostgreSQL y el proxy HTTP/1.1; la salud ahora verifica la base de datos.',
de_DE: 'Korrigiert PostgreSQL-Prüfungen und HTTP/1.1-Proxying; der Status prüft nun die Datenbank.',
pl_PL: 'Poprawia test PostgreSQL i proxy HTTP/1.1; kontrola stanu sprawdza teraz bazę danych.',
fr_FR: 'Corrige les contrôles PostgreSQL et le proxy HTTP/1.1 ; la santé vérifie désormais la base de données.',
},
migrations: {
up: async () => {},
down: IMPOSSIBLE,
},
})
+4
View File
@@ -0,0 +1,4 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { current } from './current'
export const versionGraph = VersionGraph.of({ current, other: [] })