@podosoft/podokit 1.0.1 → 1.1.2
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.
- package/README.md +19 -5
- package/dist/add.d.ts +14 -1
- package/dist/add.js +42 -5
- package/dist/create.d.ts +4 -0
- package/dist/create.js +9 -0
- package/dist/deploy-compose-profile.d.ts +3 -0
- package/dist/deploy-compose-profile.js +74 -8
- package/dist/deploy-compose-render.d.ts +2 -1
- package/dist/deploy-compose-render.js +50 -5
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.js +18 -8
- package/dist/deploy-render.js +5 -5
- package/dist/deploy.js +157 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +41 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +8 -2
- package/dist/lockfile.d.ts +6 -1
- package/dist/lockfile.js +22 -4
- package/dist/provider.d.ts +24 -0
- package/dist/provider.js +142 -0
- package/dist/remove.js +6 -3
- package/dist/templates/fullstack/AGENTS.md +5 -4
- package/dist/templates/fullstack/README.md +10 -3
- package/dist/templates/fullstack/apps/api/package.json +3 -2
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +9 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +7 -2
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +30 -8
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +58 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +58 -2
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +24 -2
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.spec.ts +50 -0
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +134 -0
- package/dist/templates/fullstack/compose.dev.yaml +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-env.example +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +10 -2
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +2 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +13 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +17 -8
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +5 -4
- package/dist/templates/modules/audit-log/module.manifest.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.spec.ts +170 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.ts +260 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +4 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +4 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +55 -10
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +10 -14
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +22 -6
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +4 -3
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-settings.migration.spec.ts +15 -4
- package/dist/templates/modules/auth/files/apps/api/src/settings/auth-config.migration.spec.ts +30 -0
- package/dist/templates/modules/auth/module.manifest.json +1 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +10 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +107 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +12 -11
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +5 -0
- package/dist/templates/modules/bullmq/module.manifest.json +7 -0
- package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.spec.ts +16 -0
- package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.ts +12 -0
- package/dist/templates/modules/cache-memory/module.manifest.json +30 -0
- package/dist/templates/modules/events-memory/files/apps/api/src/events-provider-memory/events-memory.module.ts +14 -0
- package/dist/templates/modules/events-memory/module.manifest.json +30 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +16 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +75 -0
- package/dist/templates/modules/events-redis/module.manifest.json +32 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -5
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
- package/dist/templates/modules/file-upload/module.manifest.json +4 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +36 -14
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +9 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +16 -11
- package/dist/templates/modules/job-progress/module.manifest.json +7 -8
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +42 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +45 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +212 -0
- package/dist/templates/modules/jobs-local/module.manifest.json +34 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +7 -3
- package/dist/templates/modules/mailer/module.manifest.json +2 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +25 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +60 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +34 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +50 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +8 -0
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +4 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +20 -12
- package/dist/templates/modules/object-storage-s3/module.manifest.json +4 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +14 -1
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +18 -37
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +12 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +8 -6
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +82 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +6 -0
- package/dist/templates/modules/redis/module.manifest.json +2 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +5 -6
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +4 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +6 -5
- package/dist/templates/modules/sse/module.manifest.json +4 -6
- package/dist/templates/todo/AGENTS.md +1 -1
- package/dist/templates/todo/README.md +8 -3
- package/dist/toolchain.d.ts +1 -0
- package/dist/toolchain.js +4 -0
- package/package.json +2 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +0 -32
- package/dist/templates/modules/mailer/files/apps/api/src/mail/db.ts +0 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +0 -111
package/dist/remove.js
CHANGED
|
@@ -6,7 +6,6 @@ const node_path_1 = require("node:path");
|
|
|
6
6
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
7
7
|
const add_1 = require("./add");
|
|
8
8
|
const lockfile_1 = require("./lockfile");
|
|
9
|
-
const toolchain_1 = require("./toolchain");
|
|
10
9
|
function readModuleManifest(moduleDir, vars) {
|
|
11
10
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.join)(moduleDir, "module.manifest.json"), "utf8");
|
|
12
11
|
return JSON.parse(vars ? (0, podokit_template_engine_1.renderTokens)(source, vars) : source);
|
|
@@ -51,7 +50,7 @@ function removeModule(options) {
|
|
|
51
50
|
}
|
|
52
51
|
const vars = {
|
|
53
52
|
projectName: projectName(projectRoot),
|
|
54
|
-
...(0,
|
|
53
|
+
...(0, lockfile_1.manifestTemplateVars)(manifest),
|
|
55
54
|
};
|
|
56
55
|
const targetManifest = readModuleManifest(moduleDir, vars);
|
|
57
56
|
// Resolve the manifests of the OTHER installed modules once — used for the
|
|
@@ -64,7 +63,11 @@ function removeModule(options) {
|
|
|
64
63
|
})
|
|
65
64
|
.filter((m) => m !== null);
|
|
66
65
|
// Guard: refuse if another installed module requires this one.
|
|
67
|
-
const
|
|
66
|
+
const selectedCapabilities = Object.entries(targetManifest.provides ?? {})
|
|
67
|
+
.filter(([capability, provider]) => manifest.providers[capability] === provider)
|
|
68
|
+
.map(([capability]) => capability);
|
|
69
|
+
const dependents = others.filter((other) => (other.manifest.requires ?? []).includes(module)
|
|
70
|
+
|| (other.manifest.requiresCapabilities ?? []).some((capability) => selectedCapabilities.includes(capability))).map((other) => other.name);
|
|
68
71
|
if (dependents.length) {
|
|
69
72
|
throw new Error(`Cannot remove "${module}": required by ${dependents.join(", ")}. Remove ${dependents.length > 1 ? "those" : "that"} first.`);
|
|
70
73
|
}
|
|
@@ -8,9 +8,9 @@ Follow it so your changes match the project's conventions.
|
|
|
8
8
|
|
|
9
9
|
A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
|
|
10
10
|
|
|
11
|
-
- `apps/api` — **Elysia** backend (`{{projectName}}-api`), Bun.SQL +
|
|
11
|
+
- `apps/api` — **Elysia** backend (`{{projectName}}-api`), Bun.SQL + `{{databaseProvider}}`, merged OpenAPI at `/api-docs`.
|
|
12
12
|
- `apps/web` — **SvelteKit** frontend (`{{projectName}}-web`), Tailwind v4 + shadcn-svelte + typesafe-i18n.
|
|
13
|
-
- `infra/` — Docker Compose
|
|
13
|
+
- `infra/` — provider-aware Docker Compose and example k3s manifests.
|
|
14
14
|
- `.podokit/` — PodoKit generation metadata plus application-owned development and
|
|
15
15
|
deployment profiles (see "PodoKit tooling" below).
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
|
|
|
19
19
|
```bash
|
|
20
20
|
{{packageManager}} install
|
|
21
21
|
cp .env.example .env
|
|
22
|
-
docker compose -f infra/docker/docker-compose.yml up -d #
|
|
22
|
+
docker compose -f infra/docker/docker-compose.yml up -d # selected external providers only
|
|
23
23
|
{{rootRun}} dev # api on http://localhost:5002, web on http://localhost:5001
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -74,9 +74,10 @@ Useful commands:
|
|
|
74
74
|
exact API WebSocket allowlist; never use a prefix or wildcard for that allowlist.
|
|
75
75
|
- `podo add <module>` — add a feature (auth, admin-dashboard, redis, bullmq, …). It wires itself into the app; run `podo add` with no argument to list modules.
|
|
76
76
|
- `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
|
|
77
|
+
- `podo provider list` / `podo provider set <capability> <provider> [--apply]` — inspect or switch database, cache, object storage, events, and jobs. Set is dry-run by default and never migrates or deletes provider data. Local providers require one API process.
|
|
77
78
|
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
78
79
|
- `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
|
|
79
|
-
- `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
|
|
80
|
+
- `podo deploy` — plan, apply, verify, or roll back an immutable release on Kubernetes or one Docker host. Production API WebSocket paths belong in the deployment profile's exact `exposure.webSocketPaths` allowlist; Compose trusts forwarding headers only from `exposure.trustedProxyCidrs`. Tag `vX.Y.Z` to build and push the images (`.github/workflows/release.yml`; check its runner — GitHub-hosted minutes are free for public repositories only). Use `.agents/skills/podokit-deploy`.
|
|
80
81
|
- `podo deploy sync` — copy local build output into a running Compose deployment and restart it, for iterating without a release. Not a release: no image is published and the next `apply` discards it. Use `.agents/skills/podokit-deploy-fast`.
|
|
81
82
|
|
|
82
83
|
## Commit conventions
|
|
@@ -40,7 +40,7 @@ Run lifecycle and container commands from a second terminal:
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
`dev watch` reads `.podokit/manifest.json` and automatically activates `cache`,
|
|
43
|
-
`storage`, and `queue` when installed modules require Redis,
|
|
43
|
+
`storage`, and `queue` when installed modules require Redis, Silo, or a worker.
|
|
44
44
|
`dev up` uses the same shared gateway and module profiles without keeping Compose
|
|
45
45
|
Watch attached.
|
|
46
46
|
You can still activate an additional Compose profile explicitly:
|
|
@@ -118,5 +118,12 @@ docker build --build-arg PODOKIT_BUILD_ORIGIN=https://example.com -f apps/web/Do
|
|
|
118
118
|
|
|
119
119
|
For a Docker Compose deployment behind a trusted reverse proxy, set
|
|
120
120
|
`PROTOCOL_HEADER=x-forwarded-proto` and `HOST_HEADER=x-forwarded-host` in the
|
|
121
|
-
deployment profile runtime configuration
|
|
122
|
-
headers on a
|
|
121
|
+
deployment profile runtime configuration and list the proxy's direct CIDR blocks in
|
|
122
|
+
`exposure.trustedProxyCidrs`. Never trust client-controlled forwarded headers on a
|
|
123
|
+
directly exposed container.
|
|
124
|
+
|
|
125
|
+
If the production API exposes WebSocket endpoints, list their exact paths under
|
|
126
|
+
`exposure.webSocketPaths` in the deployment profile. The Compose driver then keeps a
|
|
127
|
+
single published port, sends only those paths to API, and sends all other traffic to
|
|
128
|
+
web. The external TLS proxy continues to use the same single upstream. Keep
|
|
129
|
+
`trustedProxyCidrs` empty unless that upstream is a known reverse proxy.
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"contract": "bun scripts/verify-api-contract.ts",
|
|
12
12
|
"migrate:all": "bun dist/database/migrate.js",
|
|
13
13
|
"typeorm": "bunx typeorm-ts-node-commonjs -d src/database/data-source.ts",
|
|
14
|
-
"migration:run": "bun
|
|
15
|
-
"migration:revert": "bun
|
|
14
|
+
"migration:run": "bun src/database/migrate.ts",
|
|
15
|
+
"migration:revert": "bun src/database/migrate.ts --revert",
|
|
16
16
|
"migration:generate": "bun run typeorm -- migration:generate"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@podosoft/podokit-contracts": "^0.4.0",
|
|
20
|
+
"@podosoft/podokit-runtime": "^0.2.0",
|
|
20
21
|
"@elysia/cors": "^1.4.2",
|
|
21
22
|
"@elysia/openapi": "^1.4.15",
|
|
22
23
|
"dotenv": "^16.4.5",
|
|
@@ -5,6 +5,7 @@ const schema = z.object({
|
|
|
5
5
|
NODE_ENV: z.string().default("development"),
|
|
6
6
|
PORT: z.coerce.number().default(5002),
|
|
7
7
|
CORS_ORIGIN: z.string().optional(),
|
|
8
|
+
DATABASE_URL: z.string().optional(),
|
|
8
9
|
POSTGRES_HOST: z.string().default("localhost"),
|
|
9
10
|
POSTGRES_PORT: z.coerce.number().default(5432),
|
|
10
11
|
POSTGRES_USER: z.string().default("podokit"),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderSelections } from "@podosoft/podokit-runtime";
|
|
2
|
+
|
|
3
|
+
export const PROVIDERS = {
|
|
4
|
+
database: "{{databaseProvider}}",
|
|
5
|
+
cache: "{{cacheProvider}}",
|
|
6
|
+
"object-storage": "{{objectStorageProvider}}",
|
|
7
|
+
events: "{{eventsProvider}}",
|
|
8
|
+
jobs: "{{jobsProvider}}",
|
|
9
|
+
} as const satisfies ProviderSelections;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
2
|
import pino, { type Logger } from "pino";
|
|
3
|
+
import {
|
|
4
|
+
DATABASE as RUNTIME_DATABASE,
|
|
5
|
+
type ServiceKey,
|
|
6
|
+
} from "@podosoft/podokit-runtime";
|
|
3
7
|
import type { AppEnv } from "../config/env.validation";
|
|
4
8
|
import { Database } from "../database/database";
|
|
5
9
|
import { ReadinessService } from "../health/readiness.service";
|
|
6
10
|
|
|
7
|
-
export type
|
|
11
|
+
export type { ServiceKey } from "@podosoft/podokit-runtime";
|
|
8
12
|
|
|
9
13
|
export class ServiceRegistry {
|
|
10
14
|
private readonly values = new Map<symbol, unknown>();
|
|
@@ -142,7 +146,7 @@ export class OpenApiRegistry {
|
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
export const DATABASE =
|
|
149
|
+
export const DATABASE = RUNTIME_DATABASE as ServiceKey<Database>;
|
|
146
150
|
export const READINESS = Symbol("readiness") as ServiceKey<ReadinessService>;
|
|
147
151
|
export const LOGGER = Symbol("logger") as ServiceKey<Logger>;
|
|
148
152
|
export const ACCESS_POLICY = Symbol("access-policy") as ServiceKey<AccessPolicy>;
|
|
@@ -185,6 +189,7 @@ export function createCoreServices(env: AppEnv): ServiceRegistry {
|
|
|
185
189
|
accessPolicy.register("*", "/api-docs-json", "public");
|
|
186
190
|
accessPolicy.register("*", "/api-docs-elysia-json", "public");
|
|
187
191
|
services.register(DATABASE, database, () => database.close());
|
|
192
|
+
services.onStart(() => database.connect());
|
|
188
193
|
services.register(READINESS, readiness);
|
|
189
194
|
services.register(LOGGER, logger);
|
|
190
195
|
services.register(ACCESS_POLICY, accessPolicy);
|
|
@@ -2,23 +2,45 @@ import "dotenv/config";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DataSource, type DataSourceOptions } from "typeorm";
|
|
5
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
6
|
+
import { PROVIDERS } from "../config/providers";
|
|
7
|
+
import { validateEnv } from "../config/env.validation";
|
|
8
|
+
import { databaseUrl, sqliteDatabasePath } from "./database";
|
|
5
9
|
|
|
6
10
|
const compiledMigrations = join(process.cwd(), "dist", "migrations");
|
|
7
11
|
const migrations = existsSync(compiledMigrations)
|
|
8
12
|
? [join(compiledMigrations, "[0-9]*.js")]
|
|
9
13
|
: [join(process.cwd(), "src", "migrations", "[0-9]*.ts")];
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
type: "postgres",
|
|
13
|
-
host: process.env.POSTGRES_HOST ?? "localhost",
|
|
14
|
-
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
15
|
-
username: process.env.POSTGRES_USER ?? "podokit",
|
|
16
|
-
password: process.env.POSTGRES_PASSWORD ?? "podokit",
|
|
17
|
-
database: process.env.POSTGRES_DB ?? "podokit",
|
|
15
|
+
const commonOptions = {
|
|
18
16
|
entities: [],
|
|
19
17
|
migrations,
|
|
20
18
|
synchronize: false,
|
|
21
19
|
};
|
|
22
20
|
|
|
21
|
+
const env = validateEnv(process.env);
|
|
22
|
+
function databaseProvider(): DatabaseProviderName {
|
|
23
|
+
return PROVIDERS.database;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const dataSourceOptions: DataSourceOptions = databaseProvider() === "sqlite"
|
|
27
|
+
? {
|
|
28
|
+
type: "better-sqlite3",
|
|
29
|
+
database: sqliteDatabasePath(databaseUrl(env)),
|
|
30
|
+
enableWAL: true,
|
|
31
|
+
...commonOptions,
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
type: "postgres",
|
|
35
|
+
...(process.env.DATABASE_URL ? { url: process.env.DATABASE_URL } : {}),
|
|
36
|
+
host: env.POSTGRES_HOST,
|
|
37
|
+
port: env.POSTGRES_PORT,
|
|
38
|
+
username: env.POSTGRES_USER,
|
|
39
|
+
password: env.POSTGRES_PASSWORD,
|
|
40
|
+
database: env.POSTGRES_DB,
|
|
41
|
+
...commonOptions,
|
|
42
|
+
};
|
|
43
|
+
|
|
23
44
|
// Used by the TypeORM CLI for migrations (see package.json scripts).
|
|
24
|
-
export
|
|
45
|
+
export const appDataSource = new DataSource(dataSourceOptions);
|
|
46
|
+
export default appDataSource;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import type { AppEnv } from "../config/env.validation";
|
|
6
|
+
import { PROVIDERS } from "../config/providers";
|
|
7
|
+
import { databaseUrl, sqliteDatabasePath } from "./database";
|
|
8
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
9
|
+
|
|
10
|
+
const env: AppEnv = {
|
|
11
|
+
NODE_ENV: "test",
|
|
12
|
+
PORT: 5002,
|
|
13
|
+
POSTGRES_HOST: "localhost",
|
|
14
|
+
POSTGRES_PORT: 5432,
|
|
15
|
+
POSTGRES_USER: "podokit",
|
|
16
|
+
POSTGRES_PASSWORD: "podokit",
|
|
17
|
+
POSTGRES_DB: "podokit",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function databaseProvider(): DatabaseProviderName {
|
|
21
|
+
return PROVIDERS.database;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("database provider", () => {
|
|
25
|
+
it("uses DATABASE_URL as the canonical connection setting", () => {
|
|
26
|
+
const provider = databaseProvider();
|
|
27
|
+
const DATABASE_URL = provider === "sqlite"
|
|
28
|
+
? "sqlite://:memory:"
|
|
29
|
+
: "postgres://podokit:podokit@localhost:5432/podokit";
|
|
30
|
+
expect(databaseUrl({ ...env, DATABASE_URL })).toBe(DATABASE_URL);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("keeps the PostgreSQL fields as a fallback and creates a SQLite default", () => {
|
|
34
|
+
const url = databaseUrl(env);
|
|
35
|
+
const provider = databaseProvider();
|
|
36
|
+
if (provider === "sqlite") expect(url).toMatch(/^sqlite:\/\//);
|
|
37
|
+
else expect(url).toBe("postgres://podokit:podokit@localhost:5432/podokit");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rejects a connection URL for the other database dialect", () => {
|
|
41
|
+
const provider = databaseProvider();
|
|
42
|
+
const DATABASE_URL = provider === "sqlite"
|
|
43
|
+
? "postgres://podokit:podokit@localhost:5432/podokit"
|
|
44
|
+
: "sqlite://:memory:";
|
|
45
|
+
expect(() => databaseUrl({ ...env, DATABASE_URL })).toThrow("DATABASE_URL must use");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("creates the parent directory for an explicit SQLite database", () => {
|
|
49
|
+
const root = mkdtempSync(join(tmpdir(), "podokit-sqlite-"));
|
|
50
|
+
try {
|
|
51
|
+
const path = join(root, "nested", "app.sqlite");
|
|
52
|
+
expect(sqliteDatabasePath(`sqlite://${path}`)).toBe(path);
|
|
53
|
+
expect(existsSync(dirname(path))).toBe(true);
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(root, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,22 +1,78 @@
|
|
|
1
1
|
import { SQL } from "bun";
|
|
2
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
2
5
|
import type { AppEnv } from "../config/env.validation";
|
|
6
|
+
import { PROVIDERS } from "../config/providers";
|
|
7
|
+
|
|
8
|
+
function isSqliteUrl(url: string): boolean {
|
|
9
|
+
return url === ":memory:" || /^(?:sqlite|file):/.test(url);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function selectedDatabaseProvider(): DatabaseProviderName {
|
|
13
|
+
return PROVIDERS.database;
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
export function databaseUrl(env: AppEnv): string {
|
|
17
|
+
const provider = selectedDatabaseProvider();
|
|
18
|
+
if (env.DATABASE_URL) {
|
|
19
|
+
if (provider === "sqlite" && !isSqliteUrl(env.DATABASE_URL)) {
|
|
20
|
+
throw new Error("DATABASE_URL must use sqlite:, file:, or :memory: for the SQLite provider");
|
|
21
|
+
}
|
|
22
|
+
if (provider === "postgres" && isSqliteUrl(env.DATABASE_URL)) {
|
|
23
|
+
throw new Error("DATABASE_URL must use postgres: for the PostgreSQL provider");
|
|
24
|
+
}
|
|
25
|
+
return env.DATABASE_URL;
|
|
26
|
+
}
|
|
27
|
+
if (provider === "sqlite") {
|
|
28
|
+
const path = resolve("./data/podokit.sqlite");
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
30
|
+
return `sqlite://${path}`;
|
|
31
|
+
}
|
|
5
32
|
const username = encodeURIComponent(env.POSTGRES_USER);
|
|
6
33
|
const password = encodeURIComponent(env.POSTGRES_PASSWORD);
|
|
7
34
|
const database = encodeURIComponent(env.POSTGRES_DB);
|
|
8
35
|
return `postgres://${username}:${password}@${env.POSTGRES_HOST}:${env.POSTGRES_PORT}/${database}`;
|
|
9
36
|
}
|
|
10
37
|
|
|
38
|
+
export function sqliteDatabasePath(url: string): string {
|
|
39
|
+
if (url === ":memory:" || url === "sqlite://:memory:") return ":memory:";
|
|
40
|
+
const path = url.startsWith("sqlite://")
|
|
41
|
+
? url.slice("sqlite://".length)
|
|
42
|
+
: url.startsWith("file:")
|
|
43
|
+
? decodeURIComponent(new URL(url).pathname)
|
|
44
|
+
: undefined;
|
|
45
|
+
if (!path) throw new Error("SQLite database URL must use sqlite:, file:, or :memory:");
|
|
46
|
+
mkdirSync(dirname(resolve(path)), { recursive: true, mode: 0o700 });
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
|
|
11
50
|
export class Database {
|
|
51
|
+
readonly provider: DatabaseProviderName = selectedDatabaseProvider();
|
|
12
52
|
readonly sql: SQL;
|
|
53
|
+
private ready?: Promise<void>;
|
|
13
54
|
|
|
14
55
|
constructor(env: AppEnv) {
|
|
15
|
-
|
|
56
|
+
const url = databaseUrl(env);
|
|
57
|
+
this.sql = this.provider === "sqlite"
|
|
58
|
+
? new SQL(url, { adapter: "sqlite", create: true, readwrite: true, strict: true })
|
|
59
|
+
: new SQL(url, { max: 20 });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
connect(): Promise<void> {
|
|
63
|
+
this.ready ??= (async () => {
|
|
64
|
+
if (this.provider === "sqlite") {
|
|
65
|
+
await this.sql`PRAGMA journal_mode = WAL`;
|
|
66
|
+
await this.sql`PRAGMA foreign_keys = ON`;
|
|
67
|
+
await this.sql`PRAGMA busy_timeout = 5000`;
|
|
68
|
+
}
|
|
69
|
+
await this.sql`SELECT 1`;
|
|
70
|
+
})();
|
|
71
|
+
return this.ready;
|
|
16
72
|
}
|
|
17
73
|
|
|
18
74
|
async ping(): Promise<void> {
|
|
19
|
-
await this.
|
|
75
|
+
await this.connect();
|
|
20
76
|
}
|
|
21
77
|
|
|
22
78
|
async close(): Promise<void> {
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
2
|
+
import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { validateEnv } from "../config/env.validation";
|
|
4
|
+
import { PROVIDERS } from "../config/providers";
|
|
5
|
+
import { databaseUrl, sqliteDatabasePath } from "./database";
|
|
6
|
+
import { revertLatestSqliteMigration, runSqliteMigrations } from "./sqlite-migrator";
|
|
7
|
+
|
|
8
|
+
function databaseProvider(): DatabaseProviderName {
|
|
9
|
+
return PROVIDERS.database;
|
|
10
|
+
}
|
|
2
11
|
|
|
3
12
|
async function migrate(): Promise<void> {
|
|
13
|
+
if (databaseProvider() === "sqlite") {
|
|
14
|
+
const env = validateEnv(process.env);
|
|
15
|
+
const database = new SqliteDatabase(sqliteDatabasePath(databaseUrl(env)), { create: true });
|
|
16
|
+
try {
|
|
17
|
+
if (process.argv.includes("--revert")) await revertLatestSqliteMigration(database);
|
|
18
|
+
else await runSqliteMigrations(database);
|
|
19
|
+
} finally {
|
|
20
|
+
database.close();
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { appDataSource: dataSource } = await import("./data-source.js");
|
|
4
25
|
await dataSource.initialize();
|
|
5
26
|
try {
|
|
6
|
-
await dataSource.
|
|
27
|
+
if (process.argv.includes("--revert")) await dataSource.undoLastMigration({ transaction: "all" });
|
|
28
|
+
else await dataSource.runMigrations({ transaction: "all" });
|
|
7
29
|
} finally {
|
|
8
30
|
await dataSource.destroy();
|
|
9
31
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
3
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
4
|
+
import {
|
|
5
|
+
revertLatestSqliteMigration,
|
|
6
|
+
runSqliteMigrations,
|
|
7
|
+
type SqliteMigrationDefinition,
|
|
8
|
+
} from "./sqlite-migrator";
|
|
9
|
+
|
|
10
|
+
class CreateExample1000 implements MigrationInterface {
|
|
11
|
+
name = "CreateExample1000";
|
|
12
|
+
|
|
13
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
14
|
+
await queryRunner.query('CREATE TABLE "example" ("id" text PRIMARY KEY)');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
18
|
+
await queryRunner.query('DROP TABLE "example"');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const definitions: readonly SqliteMigrationDefinition[] = [
|
|
23
|
+
{ timestamp: 1_000, migration: new CreateExample1000() },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
describe("SQLite migrator", () => {
|
|
27
|
+
it("runs each migration once and records it atomically", async () => {
|
|
28
|
+
const database = new SqliteDatabase(":memory:");
|
|
29
|
+
try {
|
|
30
|
+
await runSqliteMigrations(database, definitions);
|
|
31
|
+
await runSqliteMigrations(database, definitions);
|
|
32
|
+
expect(database.query<{ count: number }, []>('SELECT count(*) AS "count" FROM "migrations"').get()?.count).toBe(1);
|
|
33
|
+
expect(database.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE name = 'example'").get()?.name).toBe("example");
|
|
34
|
+
} finally {
|
|
35
|
+
database.close();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("reverts the latest applied migration", async () => {
|
|
40
|
+
const database = new SqliteDatabase(":memory:");
|
|
41
|
+
try {
|
|
42
|
+
await runSqliteMigrations(database, definitions);
|
|
43
|
+
await expect(revertLatestSqliteMigration(database, definitions)).resolves.toBe(true);
|
|
44
|
+
expect(database.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE name = 'example'").get()).toBeNull();
|
|
45
|
+
await expect(revertLatestSqliteMigration(database, definitions)).resolves.toBe(false);
|
|
46
|
+
} finally {
|
|
47
|
+
database.close();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Database as SqliteDatabase } from "bun:sqlite";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
6
|
+
|
|
7
|
+
export interface SqliteMigrationDefinition {
|
|
8
|
+
timestamp: number;
|
|
9
|
+
migration: MigrationInterface;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AppliedMigration {
|
|
13
|
+
id: number;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type MigrationConstructor = new () => MigrationInterface;
|
|
19
|
+
|
|
20
|
+
function isMigration(value: unknown): value is MigrationConstructor {
|
|
21
|
+
if (typeof value !== "function") return false;
|
|
22
|
+
const prototype = (value as { prototype?: unknown }).prototype;
|
|
23
|
+
if (!prototype || typeof prototype !== "object") return false;
|
|
24
|
+
const record = prototype as Record<string, unknown>;
|
|
25
|
+
return typeof record.up === "function" && typeof record.down === "function";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function discoverMigrations(): Promise<SqliteMigrationDefinition[]> {
|
|
29
|
+
const compiled = join(process.cwd(), "dist", "migrations");
|
|
30
|
+
const source = join(process.cwd(), "src", "migrations");
|
|
31
|
+
const directory = process.env.NODE_ENV === "production" && existsSync(compiled)
|
|
32
|
+
? compiled
|
|
33
|
+
: source;
|
|
34
|
+
if (!existsSync(directory)) return [];
|
|
35
|
+
const files = readdirSync(directory)
|
|
36
|
+
.filter((file) => /^\d+-.+\.(?:js|ts)$/.test(file))
|
|
37
|
+
.sort();
|
|
38
|
+
const definitions: SqliteMigrationDefinition[] = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const loaded = await import(pathToFileURL(join(directory, file)).href) as Record<string, unknown>;
|
|
41
|
+
const constructors = Object.values(loaded).filter(isMigration);
|
|
42
|
+
if (constructors.length !== 1) {
|
|
43
|
+
throw new Error(`SQLite migration ${file} must export exactly one migration class`);
|
|
44
|
+
}
|
|
45
|
+
const timestamp = Number(file.slice(0, file.indexOf("-")));
|
|
46
|
+
if (!Number.isSafeInteger(timestamp)) throw new Error(`SQLite migration ${file} has an invalid timestamp`);
|
|
47
|
+
const Constructor = constructors[0];
|
|
48
|
+
if (!Constructor) throw new Error(`SQLite migration ${file} has no migration class`);
|
|
49
|
+
definitions.push({ timestamp, migration: new Constructor() });
|
|
50
|
+
}
|
|
51
|
+
return definitions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function queryRunner(database: SqliteDatabase): QueryRunner {
|
|
55
|
+
return {
|
|
56
|
+
connection: { options: { type: "better-sqlite3" } },
|
|
57
|
+
query: (sql: string): Promise<unknown> => Promise.resolve(database.run(sql)),
|
|
58
|
+
} as unknown as QueryRunner;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function prepare(database: SqliteDatabase): void {
|
|
62
|
+
database.run("PRAGMA journal_mode = WAL");
|
|
63
|
+
database.run("PRAGMA foreign_keys = ON");
|
|
64
|
+
database.run("PRAGMA busy_timeout = 5000");
|
|
65
|
+
database.run(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS "migrations" (
|
|
67
|
+
"id" integer PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
"timestamp" bigint NOT NULL,
|
|
69
|
+
"name" text NOT NULL UNIQUE
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function migrationName(migration: MigrationInterface): string {
|
|
75
|
+
if (typeof migration.name !== "string" || migration.name.length === 0) {
|
|
76
|
+
throw new Error("SQLite migration classes must declare a non-empty name");
|
|
77
|
+
}
|
|
78
|
+
return migration.name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runSqliteMigrations(
|
|
82
|
+
database: SqliteDatabase,
|
|
83
|
+
supplied?: readonly SqliteMigrationDefinition[],
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
prepare(database);
|
|
86
|
+
const definitions = supplied ? [...supplied] : await discoverMigrations();
|
|
87
|
+
definitions.sort((left, right) => left.timestamp - right.timestamp);
|
|
88
|
+
const applied = new Set(
|
|
89
|
+
database.query<Pick<AppliedMigration, "name">, []>(
|
|
90
|
+
'SELECT "name" FROM "migrations"',
|
|
91
|
+
).all().map((row) => row.name),
|
|
92
|
+
);
|
|
93
|
+
const runner = queryRunner(database);
|
|
94
|
+
for (const definition of definitions) {
|
|
95
|
+
const name = migrationName(definition.migration);
|
|
96
|
+
if (applied.has(name)) continue;
|
|
97
|
+
database.run("BEGIN IMMEDIATE");
|
|
98
|
+
try {
|
|
99
|
+
await definition.migration.up(runner);
|
|
100
|
+
database.query(
|
|
101
|
+
'INSERT INTO "migrations" ("timestamp", "name") VALUES (?1, ?2)',
|
|
102
|
+
).run(definition.timestamp, name);
|
|
103
|
+
database.run("COMMIT");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
database.run("ROLLBACK");
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function revertLatestSqliteMigration(
|
|
112
|
+
database: SqliteDatabase,
|
|
113
|
+
supplied?: readonly SqliteMigrationDefinition[],
|
|
114
|
+
): Promise<boolean> {
|
|
115
|
+
prepare(database);
|
|
116
|
+
const latest = database.query<AppliedMigration, []>(
|
|
117
|
+
'SELECT "id", "timestamp", "name" FROM "migrations" ORDER BY "id" DESC LIMIT 1',
|
|
118
|
+
).get();
|
|
119
|
+
if (!latest) return false;
|
|
120
|
+
const definitions = supplied ? [...supplied] : await discoverMigrations();
|
|
121
|
+
const definition = definitions.find((item) => migrationName(item.migration) === latest.name);
|
|
122
|
+
if (!definition) throw new Error(`Cannot find SQLite migration ${latest.name} to revert`);
|
|
123
|
+
const runner = queryRunner(database);
|
|
124
|
+
database.run("BEGIN IMMEDIATE");
|
|
125
|
+
try {
|
|
126
|
+
await definition.migration.down(runner);
|
|
127
|
+
database.query('DELETE FROM "migrations" WHERE "id" = ?1').run(latest.id);
|
|
128
|
+
database.run("COMMIT");
|
|
129
|
+
return true;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
database.run("ROLLBACK");
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -102,7 +102,7 @@ services:
|
|
|
102
102
|
|
|
103
103
|
minio:
|
|
104
104
|
profiles: [storage]
|
|
105
|
-
image:
|
|
105
|
+
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
|
|
106
106
|
command: server /data --console-address ":9001"
|
|
107
107
|
environment:
|
|
108
108
|
MINIO_ROOT_USER: podokit
|
|
@@ -113,7 +113,7 @@ services:
|
|
|
113
113
|
|
|
114
114
|
minio-init:
|
|
115
115
|
profiles: [storage]
|
|
116
|
-
image:
|
|
116
|
+
image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z
|
|
117
117
|
depends_on:
|
|
118
118
|
minio: { condition: service_started }
|
|
119
119
|
entrypoint:
|
|
@@ -60,9 +60,14 @@ it at a different host invalidates the profile instead of deploying somewhere el
|
|
|
60
60
|
The fields that differ from the Kubernetes driver:
|
|
61
61
|
|
|
62
62
|
- `target`: Docker context, endpoint fingerprint, Compose project name
|
|
63
|
-
- `exposure`: `bindAddress` and `port
|
|
64
|
-
`
|
|
65
|
-
|
|
63
|
+
- `exposure`: `bindAddress` and `port`, plus an exact `webSocketPaths` allowlist,
|
|
64
|
+
`trustedProxyCidrs`, and `gatewayImage`. With an empty allowlist, web owns the
|
|
65
|
+
published port. With paths, an internal gateway owns it, sends only exact listed
|
|
66
|
+
paths to API, and sends every other request to web. Forwarding headers are trusted
|
|
67
|
+
only from the direct proxy CIDRs in `trustedProxyCidrs`; leave it empty without an
|
|
68
|
+
external proxy. The bind address defaults to `127.0.0.1`, because the expected
|
|
69
|
+
front door is a trusted reverse proxy — binding every interface publishes the app
|
|
70
|
+
before TLS exists.
|
|
66
71
|
- `workloads`: replicas plus Compose `cpus` and `memory` limits
|
|
67
72
|
- `dependencies`: `managed`, `external`, or `disabled`, each with a named Docker
|
|
68
73
|
volume and the path of its env file on the target host
|
|
@@ -86,3 +91,5 @@ Required keys:
|
|
|
86
91
|
Data volumes are declared `external`, so `docker compose down -v` cannot delete the
|
|
87
92
|
database. Editing an env file changes the containers' rollout label, so the next
|
|
88
93
|
apply actually restarts them instead of leaving stale environment values running.
|
|
94
|
+
The gateway image is resolved to an immutable digest during planning. The external
|
|
95
|
+
TLS proxy keeps one upstream; do not duplicate the WebSocket path split there.
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
NODE_ENV=development
|
|
5
5
|
PORT=5002
|
|
6
6
|
|
|
7
|
+
# Canonical database connection. Set this for PostgreSQL or SQLite; POSTGRES_* remains a fallback.
|
|
8
|
+
# DATABASE_URL=postgres://podokit:podokit@localhost:5432/podokit
|
|
7
9
|
POSTGRES_HOST=localhost
|
|
8
10
|
POSTGRES_PORT=5432
|
|
9
11
|
POSTGRES_USER=podokit
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
PROFILE_IMAGE_REQUIRED,
|
|
4
4
|
PROFILE_IMAGE_TOO_LARGE,
|
|
5
5
|
} from "@podosoft/podokit-contracts";
|
|
6
|
+
import { OBJECT_STORAGE } from "@podosoft/podokit-runtime";
|
|
6
7
|
import { Elysia, t } from "elysia";
|
|
7
8
|
import { AUTH } from "./auth/auth.module";
|
|
8
9
|
import {
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
SiteSettingsService,
|
|
21
22
|
} from "./site-settings/site-settings.service";
|
|
22
23
|
import { validateSiteSetting } from "./site-settings/site-settings.validation";
|
|
23
|
-
import { STORAGE } from "./storage/storage.module";
|
|
24
24
|
|
|
25
25
|
export const SITE_SETTINGS = Symbol("site-settings") as ServiceKey<SiteSettingsService>;
|
|
26
26
|
export const PROFILE_IMAGES = Symbol("profile-images") as ServiceKey<ProfileImageService>;
|
|
@@ -138,7 +138,7 @@ const adminPlugin: AppPlugin = ({ services }) => {
|
|
|
138
138
|
export const adminModule: PodokitModule = {
|
|
139
139
|
name: "admin-dashboard",
|
|
140
140
|
configure: (_env, services): void => {
|
|
141
|
-
const storage = services.resolve(
|
|
141
|
+
const storage = services.resolve(OBJECT_STORAGE);
|
|
142
142
|
const site = new SiteSettingsService(services.resolve(DATABASE).sql, storage);
|
|
143
143
|
const profileImages = new ProfileImageService(storage, services.resolve(LOGGER));
|
|
144
144
|
services.register(SITE_SETTINGS, site);
|