first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const actions = sdk.Actions.of()
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
export const setDependencies = sdk.setupDependencies(async () => ({}))
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
})
|
||||
@@ -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])]
|
||||
})
|
||||
@@ -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'],
|
||||
})
|
||||
})
|
||||
@@ -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.',
|
||||
}
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import { StartSdk } from '@start9labs/start-sdk'
|
||||
import { manifest } from './manifest'
|
||||
|
||||
export const sdk = StartSdk.of().withManifest(manifest).build(true)
|
||||
@@ -0,0 +1,3 @@
|
||||
export const appPort = 3000
|
||||
export const databaseName = 'todo_back_production'
|
||||
export const databaseUser = 'todo_back'
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import { VersionGraph } from '@start9labs/start-sdk'
|
||||
import { current } from './current'
|
||||
|
||||
export const versionGraph = VersionGraph.of({ current, other: [] })
|
||||
Reference in New Issue
Block a user