@podosoft/podokit 1.0.2 → 1.1.4
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 +74 -6
- 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 +1 -0
- package/dist/deploy-compose-render.js +46 -1
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.js +18 -8
- 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/base/AGENTS.md +9 -3
- package/dist/templates/base/README.md +12 -8
- package/dist/templates/fullstack/AGENTS.md +24 -5
- package/dist/templates/fullstack/README.md +75 -11
- package/dist/templates/fullstack/apps/api/package.json +4 -3
- 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/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 +88 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.ts +123 -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 +24 -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 +6 -6
- 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 +25 -6
- package/dist/templates/todo/README.md +72 -14
- package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +11 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.spec.ts +76 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.ts +9 -4
- package/dist/templates/todo/tests/api/todo.api.spec.ts +32 -0
- package/dist/templates/todo/tests/ui/todo.ui.spec.ts +31 -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
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Generated with [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
4
4
|
|
|
5
|
-
A minimal Bun 1.4 TypeScript workspace
|
|
6
|
-
|
|
5
|
+
A minimal Bun 1.4 TypeScript workspace with placeholder API/web scripts. Choose
|
|
6
|
+
`fullstack` or `todo` when creating a project that needs the working Elysia API,
|
|
7
|
+
SvelteKit web app, database integration, or feature modules.
|
|
8
|
+
|
|
9
|
+
For a simple working app, those templates support SQLite, memory cache/events,
|
|
10
|
+
local files, and local jobs without external PostgreSQL, Redis, or S3 services.
|
|
11
|
+
`base` is for building your own foundation; it does not include the provider
|
|
12
|
+
consumers or module integration points. See
|
|
13
|
+
[Getting Started](https://github.com/podosoft-dev/podokit/blob/main/docs/getting-started.md).
|
|
7
14
|
|
|
8
15
|
## Getting started
|
|
9
16
|
|
|
10
17
|
```bash
|
|
11
18
|
{{packageManager}} install
|
|
12
|
-
{{packageManager}} run dev
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Copy the example environment file before running:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
19
|
cp .env.example .env
|
|
19
20
|
```
|
|
21
|
+
|
|
22
|
+
Implement the workspace scripts before running the application. The generated
|
|
23
|
+
`dev` commands are placeholders and do not start services.
|
|
@@ -8,26 +8,44 @@ 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
|
|
|
17
17
|
## Setup & commands
|
|
18
18
|
|
|
19
|
+
Inspect active infrastructure with `podo provider list`. New-project defaults
|
|
20
|
+
are `database=postgres`, `cache=redis`, `object-storage=s3`, `events=redis`, and
|
|
21
|
+
`jobs=bullmq`. A simple single-process app can instead use `sqlite`, `memory`
|
|
22
|
+
cache/events, `local` files, and `local` jobs. Only the database has a creation
|
|
23
|
+
flag (`--database`); apply other choices using
|
|
24
|
+
`podo provider set <capability> <provider> --apply` before adding features.
|
|
25
|
+
Provider selection does not migrate or delete existing data.
|
|
26
|
+
|
|
19
27
|
```bash
|
|
20
28
|
{{packageManager}} install
|
|
21
29
|
cp .env.example .env
|
|
22
|
-
|
|
30
|
+
{{apiRun}} migration:run
|
|
23
31
|
{{rootRun}} dev # api on http://localhost:5002, web on http://localhost:5001
|
|
24
32
|
```
|
|
25
33
|
|
|
34
|
+
For PostgreSQL, start `docker compose -f infra/docker/docker-compose.yml up -d`
|
|
35
|
+
before migrating. Add `--profile cache` for Redis; Silo uses the overlay from
|
|
36
|
+
`object-storage-s3`. Skip external services entirely for the complete local
|
|
37
|
+
combination. `podo dev watch` is a separate container workflow and requires
|
|
38
|
+
Docker. SQLite and local files default to `./data/podokit.sqlite` and
|
|
39
|
+
`./data/files` relative to the API working directory; migrations and runtime
|
|
40
|
+
must use the same database path. Keep local providers at one API process.
|
|
41
|
+
Merge added module settings from `.env.example` into an existing `.env`.
|
|
42
|
+
|
|
26
43
|
- **Ports: web = 5001, api = 5002.** The web app reaches the API only through its
|
|
27
44
|
SvelteKit server proxy (`/api/*`), never with a direct browser `fetch`.
|
|
28
45
|
- Build: `{{rootRun}} build` · Type-check/lint: `{{rootRun}} lint` · Unit tests: `{{rootRun}} test`
|
|
29
46
|
- Per workspace: `{{apiRun}} build` / `{{webRun}} build`.
|
|
30
|
-
-
|
|
47
|
+
- Application migrations (PostgreSQL or SQLite): `{{apiRun}} migration:run`.
|
|
48
|
+
- When `auth` is installed, run `{{apiRun}} migrate:all` for Better Auth and application tables.
|
|
31
49
|
- e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`.
|
|
32
50
|
- Playwright officially runs on Node, so `bunx playwright` respects its Node
|
|
33
51
|
shebang. Node LTS is a test-tool dependency only; API, web, worker, migrations,
|
|
@@ -74,9 +92,10 @@ Useful commands:
|
|
|
74
92
|
exact API WebSocket allowlist; never use a prefix or wildcard for that allowlist.
|
|
75
93
|
- `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
94
|
- `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
|
|
95
|
+
- `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
96
|
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
78
97
|
- `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`.
|
|
98
|
+
- `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
99
|
- `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
100
|
|
|
82
101
|
## Commit conventions
|
|
@@ -2,9 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Full-stack TypeScript starter generated with [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
4
4
|
|
|
5
|
-
- `apps/api` — Bun 1.4 + Elysia API: schema-validated env, `/health` + `/health/ready`, merged OpenAPI at `/api-docs`, a standard error envelope, and Bun.SQL
|
|
5
|
+
- `apps/api` — Bun 1.4 + Elysia API: schema-validated env, `/health` + `/health/ready`, merged OpenAPI at `/api-docs`, a standard error envelope, and Bun.SQL with PostgreSQL or SQLite. TypeORM is used only for PostgreSQL migrations; SQLite has its own migration runner.
|
|
6
6
|
- `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) that talks to the API through a server-side proxy.
|
|
7
|
-
- `infra/` — Docker Compose
|
|
7
|
+
- `infra/` — Docker Compose references for external services and k3s manifests.
|
|
8
|
+
|
|
9
|
+
## Infrastructure choices
|
|
10
|
+
|
|
11
|
+
Inspect the current selections with `{{packageExecutor}} @podosoft/podokit provider list`.
|
|
12
|
+
New projects use these defaults unless configured otherwise:
|
|
13
|
+
|
|
14
|
+
| Capability | Default | Lightweight alternative |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Database | `postgres` | `sqlite` |
|
|
17
|
+
| Cache | `redis` | `memory` |
|
|
18
|
+
| Object storage | `s3` | `local` |
|
|
19
|
+
| Events | `redis` | `memory` |
|
|
20
|
+
| Jobs | `bullmq` | `local` |
|
|
21
|
+
|
|
22
|
+
For a simple program, prototype, or desktop app, SQLite, memory cache/events,
|
|
23
|
+
local files, and local jobs can run inside one API process without PostgreSQL,
|
|
24
|
+
Redis, or S3 services. `create --database sqlite` selects only the database;
|
|
25
|
+
choose the other providers after creation:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
{{packageExecutor}} @podosoft/podokit provider set cache memory --apply
|
|
29
|
+
{{packageExecutor}} @podosoft/podokit provider set object-storage local --apply
|
|
30
|
+
{{packageExecutor}} @podosoft/podokit provider set events memory --apply
|
|
31
|
+
{{packageExecutor}} @podosoft/podokit provider set jobs local --apply
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Apply providers before adding features, then install dependencies and merge new
|
|
35
|
+
`.env.example` entries into `.env`. These commands install implementation modules
|
|
36
|
+
and configure them; they do not move or delete data. SQLite defaults to
|
|
37
|
+
`./data/podokit.sqlite`, and local files to `LOCAL_STORAGE_PATH=./data/files`,
|
|
38
|
+
relative to the API working directory. Memory cache/events are lost on restart;
|
|
39
|
+
the database, files, and local job records persist. Use absolute paths and back
|
|
40
|
+
up the database and files together for packaged or production apps.
|
|
41
|
+
|
|
42
|
+
Use the host-process setup below to avoid Docker. The containerized loop still
|
|
43
|
+
requires Docker. See the PodoKit
|
|
44
|
+
[runtime provider guide](https://github.com/podosoft-dev/podokit/blob/main/docs/providers.md)
|
|
45
|
+
for server settings, Silo/AWS S3 configuration, and provider switching.
|
|
8
46
|
|
|
9
47
|
## Getting started
|
|
10
48
|
|
|
@@ -40,7 +78,7 @@ Run lifecycle and container commands from a second terminal:
|
|
|
40
78
|
```
|
|
41
79
|
|
|
42
80
|
`dev watch` reads `.podokit/manifest.json` and automatically activates `cache`,
|
|
43
|
-
`storage`, and `queue` when installed modules require Redis,
|
|
81
|
+
`storage`, and `queue` when installed modules require Redis, Silo, or a worker.
|
|
44
82
|
`dev up` uses the same shared gateway and module profiles without keeping Compose
|
|
45
83
|
Watch attached.
|
|
46
84
|
You can still activate an additional Compose profile explicitly:
|
|
@@ -57,13 +95,27 @@ through Compose Watch, including Vite HMR on the same portless browser origin.
|
|
|
57
95
|
|
|
58
96
|
### Alternative: host processes
|
|
59
97
|
|
|
60
|
-
Use this loop
|
|
98
|
+
Use this loop for an app with SQLite and local providers, or when you want the
|
|
99
|
+
web and API processes on the host. Prepare the project first:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
{{packageManager}} install
|
|
103
|
+
cp .env.example .env
|
|
104
|
+
{{apiRun}} migration:run
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For PostgreSQL, start `docker compose -f infra/docker/docker-compose.yml up -d`
|
|
108
|
+
before migrating. Redis needs `--profile cache`; Silo needs the storage overlay
|
|
109
|
+
added by `object-storage-s3`. Skip those services for the complete local setup.
|
|
110
|
+
|
|
111
|
+
Run each service in a separate terminal from the project root:
|
|
61
112
|
|
|
62
113
|
```bash
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
{{apiRun}} dev # terminal 1
|
|
115
|
+
```
|
|
65
116
|
|
|
66
|
-
|
|
117
|
+
```bash
|
|
118
|
+
{{webRun}} dev # terminal 2
|
|
67
119
|
```
|
|
68
120
|
|
|
69
121
|
- API: http://localhost:5002 — health at `/health`, docs at `/api-docs`
|
|
@@ -71,13 +123,18 @@ docker compose -f infra/docker/docker-compose.yml up -d
|
|
|
71
123
|
|
|
72
124
|
## Database & migrations
|
|
73
125
|
|
|
74
|
-
The API uses Bun.SQL
|
|
75
|
-
|
|
126
|
+
The API uses Bun.SQL with the selected PostgreSQL or SQLite database. TypeORM
|
|
127
|
+
runs PostgreSQL migrations; SQLite uses the local migration runner. Add a
|
|
128
|
+
numbered migration under `apps/api/src/migrations`, then run it:
|
|
76
129
|
|
|
77
130
|
```bash
|
|
78
131
|
{{apiRun}} migration:run
|
|
79
132
|
```
|
|
80
133
|
|
|
134
|
+
When `auth` is installed, use `{{apiRun}} migrate:all` to create or update both
|
|
135
|
+
Better Auth and application tables. Set the module's required environment
|
|
136
|
+
values before migrating.
|
|
137
|
+
|
|
81
138
|
Want a worked example? Generate the `todo` template instead:
|
|
82
139
|
`{{packageExecutor}} @podosoft/podokit create my-app --template todo`.
|
|
83
140
|
|
|
@@ -118,5 +175,12 @@ docker build --build-arg PODOKIT_BUILD_ORIGIN=https://example.com -f apps/web/Do
|
|
|
118
175
|
|
|
119
176
|
For a Docker Compose deployment behind a trusted reverse proxy, set
|
|
120
177
|
`PROTOCOL_HEADER=x-forwarded-proto` and `HOST_HEADER=x-forwarded-host` in the
|
|
121
|
-
deployment profile runtime configuration
|
|
122
|
-
headers on a
|
|
178
|
+
deployment profile runtime configuration and list the proxy's direct CIDR blocks in
|
|
179
|
+
`exposure.trustedProxyCidrs`. Never trust client-controlled forwarded headers on a
|
|
180
|
+
directly exposed container.
|
|
181
|
+
|
|
182
|
+
If the production API exposes WebSocket endpoints, list their exact paths under
|
|
183
|
+
`exposure.webSocketPaths` in the deployment profile. The Compose driver then keeps a
|
|
184
|
+
single published port, sends only those paths to API, and sends all other traffic to
|
|
185
|
+
web. The external TLS proxy continues to use the same single upstream. Keep
|
|
186
|
+
`trustedProxyCidrs` empty unless that upstream is a known reverse proxy.
|
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
"dev": "bun --watch src/main.ts",
|
|
7
7
|
"build": "bun scripts/build.mjs",
|
|
8
8
|
"start": "bun dist/main.js",
|
|
9
|
-
"lint": "bunx tsc -p tsconfig.json --noEmit",
|
|
9
|
+
"lint": "bunx --bun tsc -p tsconfig.json --noEmit",
|
|
10
10
|
"test": "bun test src",
|
|
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
|
+
});
|