@podosoft/podokit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/add.d.ts +40 -0
- package/dist/add.js +114 -0
- package/dist/create.d.ts +2 -1
- package/dist/create.js +3 -2
- package/dist/index.js +39 -1
- package/dist/prompt.d.ts +0 -1
- package/dist/prompt.js +6 -7
- package/dist/templates/fullstack-nest-svelte/README.md +20 -8
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +14 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +11 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +20 -15
- package/dist/templates/fullstack-nest-svelte/apps/api/src/database/data-source.ts +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +15 -5
- package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +8 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/components.json +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +9 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +72 -8
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/button.svelte +82 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/index.ts +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card.svelte +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/index.ts +25 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +48 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/label.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/utils.ts +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +19 -12
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.controller.ts +31 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.module.ts +22 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.service.ts +44 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/login.dto.ts +12 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/register.dto.ts +13 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt-auth.guard.ts +5 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt.strategy.ts +23 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/user.entity.ts +16 -0
- package/dist/templates/modules/auth-jwt/files/apps/api/src/migrations/1720200000000-InitUsers.ts +23 -0
- package/dist/templates/modules/auth-jwt/module.manifest.json +31 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/demo.processor.ts +12 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/dto/create-job.dto.ts +9 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.controller.ts +29 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +15 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +8 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +20 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +14 -0
- package/dist/templates/modules/bullmq/files/infra/docker/worker.compose.example.yml +18 -0
- package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +22 -0
- package/dist/templates/modules/bullmq/module.manifest.json +28 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.controller.ts +29 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -0
- package/dist/templates/modules/file-upload/module.manifest.json +19 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/dto/start-job.dto.ts +11 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +13 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +19 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.controller.ts +17 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +25 -0
- package/dist/templates/modules/job-progress/module.manifest.json +23 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/dto/put-object.dto.ts +8 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +31 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.controller.ts +29 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +11 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +37 -0
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +33 -0
- package/dist/templates/modules/object-storage-s3/module.manifest.json +31 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/cache.controller.ts +21 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/dto/set-cache.dto.ts +14 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +12 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +43 -0
- package/dist/templates/modules/redis/module.manifest.json +21 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/dto/publish-event.dto.ts +8 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +25 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +12 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +17 -0
- package/dist/templates/modules/sse/module.manifest.json +16 -0
- package/dist/templates/todo/README.md +40 -0
- package/dist/templates/todo/apps/api/Dockerfile +22 -0
- package/dist/templates/todo/apps/api/nest-cli.json +5 -0
- package/dist/templates/todo/apps/api/package.json +44 -0
- package/dist/templates/todo/apps/api/src/app.module.ts +19 -0
- package/dist/templates/todo/apps/api/src/common/all-exceptions.filter.ts +43 -0
- package/dist/templates/todo/apps/api/src/common/app-exception.ts +12 -0
- package/dist/templates/todo/apps/api/src/config/env.validation.ts +23 -0
- package/dist/templates/todo/apps/api/src/database/data-source.ts +19 -0
- package/dist/templates/todo/apps/api/src/health/health.controller.ts +23 -0
- package/dist/templates/todo/apps/api/src/health/health.module.ts +7 -0
- package/dist/templates/todo/apps/api/src/main.ts +29 -0
- package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +22 -0
- package/dist/templates/todo/apps/api/src/todos/dto/create-todo.dto.ts +10 -0
- package/dist/templates/todo/apps/api/src/todos/dto/update-todo.dto.ts +10 -0
- package/dist/templates/todo/apps/api/src/todos/todo.entity.ts +16 -0
- package/dist/templates/todo/apps/api/src/todos/todos.controller.ts +38 -0
- package/dist/templates/todo/apps/api/src/todos/todos.module.ts +12 -0
- package/dist/templates/todo/apps/api/src/todos/todos.service.ts +41 -0
- package/dist/templates/todo/apps/api/test/health.e2e-spec.ts +23 -0
- package/dist/templates/todo/apps/api/tsconfig.json +21 -0
- package/dist/templates/todo/apps/web/Dockerfile +22 -0
- package/dist/templates/todo/apps/web/components.json +15 -0
- package/dist/templates/todo/apps/web/package.json +35 -0
- package/dist/templates/todo/apps/web/src/app.css +81 -0
- package/dist/templates/todo/apps/web/src/app.d.ts +11 -0
- package/dist/templates/todo/apps/web/src/app.html +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/button/button.svelte +82 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/button/index.ts +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/card.svelte +22 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/card/index.ts +25 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/input/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/input/input.svelte +48 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/label/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/label/label.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +10 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +8 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +16 -0
- package/dist/templates/todo/apps/web/src/lib/utils.ts +11 -0
- package/dist/templates/todo/apps/web/src/routes/+layout.svelte +9 -0
- package/dist/templates/todo/apps/web/src/routes/+page.svelte +95 -0
- package/dist/templates/todo/apps/web/src/routes/api/health/+server.ts +12 -0
- package/dist/templates/todo/apps/web/src/routes/api/todos/+server.ts +24 -0
- package/dist/templates/todo/apps/web/src/routes/api/todos/[id]/+server.ts +24 -0
- package/dist/templates/todo/apps/web/static/.gitkeep +0 -0
- package/dist/templates/todo/apps/web/svelte.config.js +15 -0
- package/dist/templates/todo/apps/web/tsconfig.json +9 -0
- package/dist/templates/todo/apps/web/vite.config.ts +7 -0
- package/dist/templates/todo/dot-env.example +16 -0
- package/dist/templates/todo/dot-gitignore +9 -0
- package/dist/templates/todo/infra/docker/docker-compose.yml +29 -0
- package/dist/templates/todo/infra/k3s/api-deployment.yaml +24 -0
- package/dist/templates/todo/infra/k3s/configmap.yaml +10 -0
- package/dist/templates/todo/infra/k3s/ingress.yaml +18 -0
- package/dist/templates/todo/infra/k3s/namespace.yaml +4 -0
- package/dist/templates/todo/infra/k3s/secret.example.yaml +10 -0
- package/dist/templates/todo/infra/k3s/services.yaml +21 -0
- package/dist/templates/todo/infra/k3s/web-deployment.yaml +20 -0
- package/dist/templates/todo/package.json +13 -0
- package/dist/templates.d.ts +10 -0
- package/dist/templates.js +33 -0
- package/package.json +14 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# MinIO for local S3-compatible object storage (dev).
|
|
2
|
+
# The default docker-compose.yml is postgres + redis; add MinIO with:
|
|
3
|
+
#
|
|
4
|
+
# docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml up -d
|
|
5
|
+
#
|
|
6
|
+
# Console: http://localhost:9001 (S3 API: http://localhost:9000)
|
|
7
|
+
services:
|
|
8
|
+
minio:
|
|
9
|
+
image: minio/minio:latest
|
|
10
|
+
command: server /data --console-address ":9001"
|
|
11
|
+
environment:
|
|
12
|
+
MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-podokit}
|
|
13
|
+
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-podokitsecret}
|
|
14
|
+
ports:
|
|
15
|
+
- "${S3_PORT:-9000}:9000"
|
|
16
|
+
- "${S3_CONSOLE_PORT:-9001}:9001"
|
|
17
|
+
volumes:
|
|
18
|
+
- minio-data:/data
|
|
19
|
+
|
|
20
|
+
# Waits for MinIO, then creates the bucket and exits.
|
|
21
|
+
minio-init:
|
|
22
|
+
image: minio/mc:latest
|
|
23
|
+
depends_on:
|
|
24
|
+
- minio
|
|
25
|
+
entrypoint: >
|
|
26
|
+
/bin/sh -c "
|
|
27
|
+
until mc alias set local http://minio:9000 ${S3_ACCESS_KEY_ID:-podokit} ${S3_SECRET_ACCESS_KEY:-podokitsecret}; do echo 'waiting for minio'; sleep 1; done;
|
|
28
|
+
mc mb -p local/${S3_BUCKET:-podokit};
|
|
29
|
+
echo 'bucket ready';
|
|
30
|
+
exit 0"
|
|
31
|
+
|
|
32
|
+
volumes:
|
|
33
|
+
minio-data:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "object-storage-s3",
|
|
3
|
+
"description": "S3-compatible object storage (AWS S3 or MinIO, chosen by STORAGE_PROVIDER) with a StorageService and presigned URLs.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@aws-sdk/client-s3": "^3.700.0",
|
|
8
|
+
"@aws-sdk/s3-request-presigner": "^3.700.0"
|
|
9
|
+
},
|
|
10
|
+
"env": [
|
|
11
|
+
"# object storage — STORAGE_PROVIDER: minio | aws",
|
|
12
|
+
"STORAGE_PROVIDER=minio",
|
|
13
|
+
"S3_ENDPOINT=http://localhost:9000",
|
|
14
|
+
"S3_REGION=us-east-1",
|
|
15
|
+
"S3_BUCKET=podokit",
|
|
16
|
+
"S3_ACCESS_KEY_ID=podokit",
|
|
17
|
+
"S3_SECRET_ACCESS_KEY=podokitsecret",
|
|
18
|
+
"S3_FORCE_PATH_STYLE=true"
|
|
19
|
+
],
|
|
20
|
+
"inject": [
|
|
21
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { StorageModule } from \"./storage/storage.module\";" },
|
|
22
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "StorageModule," }
|
|
23
|
+
],
|
|
24
|
+
"instructions": [
|
|
25
|
+
"Start MinIO for local dev: docker compose -f infra/docker/docker-compose.yml -f infra/docker/minio.compose.yml up -d",
|
|
26
|
+
"Store an object: curl -XPUT localhost:3000/storage/hello -H 'content-type: application/json' -d '{\"content\":\"hi\"}'",
|
|
27
|
+
"Read it back: curl localhost:3000/storage/hello",
|
|
28
|
+
"Presigned URL: curl localhost:3000/storage/hello/presigned",
|
|
29
|
+
"For AWS S3: set STORAGE_PROVIDER=aws, remove S3_ENDPOINT, set S3_FORCE_PATH_STYLE=false and real credentials/bucket."
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Body, Controller, Get, Param, Put } from "@nestjs/common";
|
|
2
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
3
|
+
import { RedisService } from "./redis.service";
|
|
4
|
+
import { SetCacheDto } from "./dto/set-cache.dto";
|
|
5
|
+
|
|
6
|
+
@ApiTags("cache")
|
|
7
|
+
@Controller("cache")
|
|
8
|
+
export class CacheController {
|
|
9
|
+
constructor(private readonly redis: RedisService) {}
|
|
10
|
+
|
|
11
|
+
@Put(":key")
|
|
12
|
+
async put(@Param("key") key: string, @Body() dto: SetCacheDto): Promise<{ key: string }> {
|
|
13
|
+
await this.redis.set(key, dto.value, dto.ttl);
|
|
14
|
+
return { key };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Get(":key")
|
|
18
|
+
async get(@Param("key") key: string): Promise<{ key: string; value: string | null }> {
|
|
19
|
+
return { key, value: await this.redis.get(key) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
|
2
|
+
import { IsInt, IsOptional, IsString, Min } from "class-validator";
|
|
3
|
+
|
|
4
|
+
export class SetCacheDto {
|
|
5
|
+
@ApiProperty({ example: "hello" })
|
|
6
|
+
@IsString()
|
|
7
|
+
value!: string;
|
|
8
|
+
|
|
9
|
+
@ApiPropertyOptional({ example: 60, description: "TTL in seconds" })
|
|
10
|
+
@IsOptional()
|
|
11
|
+
@IsInt()
|
|
12
|
+
@Min(1)
|
|
13
|
+
ttl?: number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { CacheController } from "./cache.controller";
|
|
3
|
+
import { RedisService } from "./redis.service";
|
|
4
|
+
|
|
5
|
+
// Global so any module can inject RedisService (cache, pub/sub, etc.).
|
|
6
|
+
@Global()
|
|
7
|
+
@Module({
|
|
8
|
+
controllers: [CacheController],
|
|
9
|
+
providers: [RedisService],
|
|
10
|
+
exports: [RedisService],
|
|
11
|
+
})
|
|
12
|
+
export class RedisModule {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Injectable, type OnModuleDestroy } from "@nestjs/common";
|
|
2
|
+
import Redis from "ioredis";
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class RedisService implements OnModuleDestroy {
|
|
6
|
+
readonly client = new Redis({
|
|
7
|
+
host: process.env.REDIS_HOST ?? "localhost",
|
|
8
|
+
port: Number(process.env.REDIS_PORT ?? 6379),
|
|
9
|
+
maxRetriesPerRequest: null,
|
|
10
|
+
});
|
|
11
|
+
private readonly subscribers: Redis[] = [];
|
|
12
|
+
|
|
13
|
+
get(key: string): Promise<string | null> {
|
|
14
|
+
return this.client.get(key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async set(key: string, value: string, ttlSeconds?: number): Promise<void> {
|
|
18
|
+
if (ttlSeconds) await this.client.set(key, value, "EX", ttlSeconds);
|
|
19
|
+
else await this.client.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
del(key: string): Promise<number> {
|
|
23
|
+
return this.client.del(key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
publish(channel: string, message: string): Promise<number> {
|
|
27
|
+
return this.client.publish(channel, message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Subscribe on a dedicated connection (ioredis can't mix subscribe + commands).
|
|
31
|
+
subscribe(channel: string, handler: (message: string) => void): void {
|
|
32
|
+
const sub = this.client.duplicate();
|
|
33
|
+
this.subscribers.push(sub);
|
|
34
|
+
void sub.subscribe(channel);
|
|
35
|
+
sub.on("message", (ch, message) => {
|
|
36
|
+
if (ch === channel) handler(message);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async onModuleDestroy(): Promise<void> {
|
|
41
|
+
await Promise.allSettled([this.client.quit(), ...this.subscribers.map((s) => s.quit())]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redis",
|
|
3
|
+
"description": "Redis client (ioredis) with get/set/del and publish/subscribe, plus demo cache endpoints.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"ioredis": "^5.4.1"
|
|
8
|
+
},
|
|
9
|
+
"env": [
|
|
10
|
+
"# redis (REDIS_HOST / REDIS_PORT already defined above)"
|
|
11
|
+
],
|
|
12
|
+
"inject": [
|
|
13
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { RedisModule } from \"./redis/redis.module\";" },
|
|
14
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "RedisModule," }
|
|
15
|
+
],
|
|
16
|
+
"instructions": [
|
|
17
|
+
"Start Redis: docker compose -f infra/docker/docker-compose.yml up -d",
|
|
18
|
+
"Set a value: curl -XPUT localhost:3000/cache/greeting -H 'content-type: application/json' -d '{\"value\":\"hi\",\"ttl\":60}'",
|
|
19
|
+
"Read it back: curl localhost:3000/cache/greeting"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Body, Controller, Post, Sse, type MessageEvent } from "@nestjs/common";
|
|
2
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
3
|
+
import { interval, map, merge, type Observable } from "rxjs";
|
|
4
|
+
import { EventsService } from "./events.service";
|
|
5
|
+
import { PublishEventDto } from "./dto/publish-event.dto";
|
|
6
|
+
|
|
7
|
+
@ApiTags("events")
|
|
8
|
+
@Controller("events")
|
|
9
|
+
export class EventsController {
|
|
10
|
+
constructor(private readonly events: EventsService) {}
|
|
11
|
+
|
|
12
|
+
@Sse("stream")
|
|
13
|
+
stream(): Observable<MessageEvent> {
|
|
14
|
+
const heartbeat = interval(5000).pipe(
|
|
15
|
+
map((n): MessageEvent => ({ data: { type: "heartbeat", n } })),
|
|
16
|
+
);
|
|
17
|
+
return merge(this.events.asObservable(), heartbeat);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Post()
|
|
21
|
+
publish(@Body() dto: PublishEventDto): { ok: true } {
|
|
22
|
+
this.events.publish({ type: "message", message: dto.message });
|
|
23
|
+
return { ok: true };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { EventsController } from "./events.controller";
|
|
3
|
+
import { EventsService } from "./events.service";
|
|
4
|
+
|
|
5
|
+
// Global so any module can inject EventsService to broadcast updates.
|
|
6
|
+
@Global()
|
|
7
|
+
@Module({
|
|
8
|
+
controllers: [EventsController],
|
|
9
|
+
providers: [EventsService],
|
|
10
|
+
exports: [EventsService],
|
|
11
|
+
})
|
|
12
|
+
export class EventsModule {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Injectable, type MessageEvent } from "@nestjs/common";
|
|
2
|
+
import { Observable, Subject } from "rxjs";
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class EventsService {
|
|
6
|
+
private readonly subject = new Subject<MessageEvent>();
|
|
7
|
+
|
|
8
|
+
// Push an event to all connected clients. Inject this service anywhere
|
|
9
|
+
// (e.g. a queue processor) to broadcast progress or notifications.
|
|
10
|
+
publish(data: unknown): void {
|
|
11
|
+
this.subject.next({ data } as MessageEvent);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
asObservable(): Observable<MessageEvent> {
|
|
15
|
+
return this.subject.asObservable();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sse",
|
|
3
|
+
"description": "Server-Sent Events: a /events/stream endpoint (heartbeat + published messages) and a POST /events publisher.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"env": [],
|
|
7
|
+
"inject": [
|
|
8
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { EventsModule } from \"./events/events.module\";" },
|
|
9
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "EventsModule," }
|
|
10
|
+
],
|
|
11
|
+
"instructions": [
|
|
12
|
+
"Stream events: curl -N localhost:3000/events/stream",
|
|
13
|
+
"Publish a message (in another terminal): curl -XPOST localhost:3000/events -H 'content-type: application/json' -d '{\"message\":\"hello\"}'",
|
|
14
|
+
"Inject EventsService anywhere to push updates (e.g. job progress)."
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Full-stack TypeScript app generated with [PodoKit](https://github.com/podosoft-dev/podokit).
|
|
4
|
+
|
|
5
|
+
- `apps/api` — NestJS API: schema-validated env, `/health` + `/health/ready`, a `todos` CRUD resource (TypeORM + PostgreSQL), Swagger docs at `/api-docs`, and a standard error envelope.
|
|
6
|
+
- `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) with a todo UI that talks to the API through a server-side proxy.
|
|
7
|
+
- `infra/` — Docker Compose (PostgreSQL, Redis) and k3s manifests.
|
|
8
|
+
|
|
9
|
+
## Getting started
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
{{packageManager}} install
|
|
13
|
+
cp .env.example .env
|
|
14
|
+
|
|
15
|
+
# start local PostgreSQL + Redis
|
|
16
|
+
docker compose -f infra/docker/docker-compose.yml up -d
|
|
17
|
+
|
|
18
|
+
# run database migrations
|
|
19
|
+
{{packageManager}} run migration:run -w {{projectName}}-api
|
|
20
|
+
|
|
21
|
+
# run api + web
|
|
22
|
+
{{packageManager}} run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- API: http://localhost:3000 — health at `/health`, docs at `/api-docs`
|
|
26
|
+
- Web: http://localhost:5173
|
|
27
|
+
|
|
28
|
+
## Database & migrations
|
|
29
|
+
|
|
30
|
+
The API uses TypeORM with PostgreSQL. A sample `Todo` entity and an initial migration are included — replace them with your own domain model.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
{{packageManager}} run migration:run -w {{projectName}}-api # apply migrations
|
|
34
|
+
{{packageManager}} run migration:revert -w {{projectName}}-api # roll back the last one
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Deploy
|
|
38
|
+
|
|
39
|
+
Docker Compose in `infra/docker`; example k3s manifests in `infra/k3s`
|
|
40
|
+
(use `secret.example.yaml` as a template — never commit real secrets).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:20-alpine AS deps
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY package.json ./
|
|
5
|
+
RUN npm install --omit=dev=false --no-audit --no-fund
|
|
6
|
+
|
|
7
|
+
FROM node:20-alpine AS build
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN npm run build
|
|
12
|
+
|
|
13
|
+
FROM node:20-alpine AS runtime
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
RUN addgroup -S app && adduser -S app -G app
|
|
17
|
+
COPY --from=build /app/dist ./dist
|
|
18
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
19
|
+
COPY package.json ./
|
|
20
|
+
USER app
|
|
21
|
+
EXPOSE 3000
|
|
22
|
+
CMD ["node", "dist/main"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-api",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "nest start --watch",
|
|
7
|
+
"build": "nest build",
|
|
8
|
+
"start": "node dist/main",
|
|
9
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"typeorm": "typeorm-ts-node-commonjs -d src/database/data-source.ts",
|
|
12
|
+
"migration:run": "npm run typeorm -- migration:run",
|
|
13
|
+
"migration:revert": "npm run typeorm -- migration:revert",
|
|
14
|
+
"migration:generate": "npm run typeorm -- migration:generate"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@nestjs/common": "^10.4.0",
|
|
18
|
+
"@nestjs/config": "^3.2.3",
|
|
19
|
+
"@nestjs/core": "^10.4.0",
|
|
20
|
+
"@nestjs/platform-express": "^10.4.0",
|
|
21
|
+
"@nestjs/swagger": "^7.4.0",
|
|
22
|
+
"@nestjs/typeorm": "^10.0.2",
|
|
23
|
+
"class-transformer": "^0.5.1",
|
|
24
|
+
"class-validator": "^0.14.1",
|
|
25
|
+
"dotenv": "^16.4.5",
|
|
26
|
+
"pg": "^8.12.0",
|
|
27
|
+
"reflect-metadata": "^0.2.2",
|
|
28
|
+
"rxjs": "^7.8.1",
|
|
29
|
+
"typeorm": "^0.3.20",
|
|
30
|
+
"zod": "^3.23.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@nestjs/cli": "^10.4.0",
|
|
34
|
+
"@nestjs/testing": "^10.4.0",
|
|
35
|
+
"@types/express": "^4.17.21",
|
|
36
|
+
"@types/node": "^20.17.0",
|
|
37
|
+
"@types/supertest": "^6.0.2",
|
|
38
|
+
"jest": "^29.7.0",
|
|
39
|
+
"supertest": "^7.0.0",
|
|
40
|
+
"ts-jest": "^29.2.5",
|
|
41
|
+
"ts-node": "^10.9.2",
|
|
42
|
+
"typescript": "^5.6.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { ConfigModule } from "@nestjs/config";
|
|
3
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
4
|
+
import { validateEnv } from "./config/env.validation";
|
|
5
|
+
import { dataSourceOptions } from "./database/data-source";
|
|
6
|
+
import { HealthModule } from "./health/health.module";
|
|
7
|
+
import { TodosModule } from "./todos/todos.module";
|
|
8
|
+
// podokit:imports
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }),
|
|
13
|
+
TypeOrmModule.forRoot(dataSourceOptions),
|
|
14
|
+
HealthModule,
|
|
15
|
+
TodosModule,
|
|
16
|
+
// podokit:module-imports
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
export class AppModule {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
|
|
2
|
+
import type { Request, Response } from "express";
|
|
3
|
+
import { AppException } from "./app-exception";
|
|
4
|
+
|
|
5
|
+
interface ErrorBody {
|
|
6
|
+
success: false;
|
|
7
|
+
error: {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
path: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Catch()
|
|
17
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
18
|
+
catch(exception: unknown, host: ArgumentsHost): void {
|
|
19
|
+
const ctx = host.switchToHttp();
|
|
20
|
+
const response = ctx.getResponse<Response>();
|
|
21
|
+
const request = ctx.getRequest<Request>();
|
|
22
|
+
|
|
23
|
+
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
24
|
+
let code = "INTERNAL_ERROR";
|
|
25
|
+
let message = "Internal server error";
|
|
26
|
+
|
|
27
|
+
if (exception instanceof AppException) {
|
|
28
|
+
statusCode = exception.statusCode;
|
|
29
|
+
code = exception.code;
|
|
30
|
+
message = exception.message;
|
|
31
|
+
} else if (exception instanceof HttpException) {
|
|
32
|
+
statusCode = exception.getStatus();
|
|
33
|
+
code = "HTTP_ERROR";
|
|
34
|
+
message = exception.message;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body: ErrorBody = {
|
|
38
|
+
success: false,
|
|
39
|
+
error: { code, message, statusCode, path: request.url, timestamp: new Date().toISOString() },
|
|
40
|
+
};
|
|
41
|
+
response.status(statusCode).json(body);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Stable, language-independent error codes. The frontend branches on `code`,
|
|
2
|
+
// not on the human-readable message.
|
|
3
|
+
export class AppException extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly code: string,
|
|
6
|
+
message: string,
|
|
7
|
+
readonly statusCode = 400,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AppException";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Schema-validated environment. Fails fast at boot if something is wrong.
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
NODE_ENV: z.string().default("development"),
|
|
6
|
+
PORT: z.coerce.number().default(3000),
|
|
7
|
+
CORS_ORIGIN: z.string().optional(),
|
|
8
|
+
POSTGRES_HOST: z.string().default("localhost"),
|
|
9
|
+
POSTGRES_PORT: z.coerce.number().default(5432),
|
|
10
|
+
POSTGRES_USER: z.string().default("podokit"),
|
|
11
|
+
POSTGRES_PASSWORD: z.string().default("podokit"),
|
|
12
|
+
POSTGRES_DB: z.string().default("podokit"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type AppEnv = z.infer<typeof schema>;
|
|
16
|
+
|
|
17
|
+
export function validateEnv(config: Record<string, unknown>): AppEnv {
|
|
18
|
+
const parsed = schema.safeParse(config);
|
|
19
|
+
if (!parsed.success) {
|
|
20
|
+
throw new Error(`Invalid environment:\n${parsed.error.toString()}`);
|
|
21
|
+
}
|
|
22
|
+
return parsed.data;
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DataSource, type DataSourceOptions } from "typeorm";
|
|
4
|
+
|
|
5
|
+
export const dataSourceOptions: DataSourceOptions = {
|
|
6
|
+
type: "postgres",
|
|
7
|
+
host: process.env.POSTGRES_HOST ?? "localhost",
|
|
8
|
+
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
|
9
|
+
username: process.env.POSTGRES_USER ?? "podokit",
|
|
10
|
+
password: process.env.POSTGRES_PASSWORD ?? "podokit",
|
|
11
|
+
database: process.env.POSTGRES_DB ?? "podokit",
|
|
12
|
+
// Entities are auto-discovered by file name (*.entity.ts / .js).
|
|
13
|
+
entities: [join(__dirname, "..", "**", "*.entity{.ts,.js}")],
|
|
14
|
+
migrations: [join(__dirname, "..", "migrations", "*{.ts,.js}")],
|
|
15
|
+
synchronize: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Used by the TypeORM CLI for migrations (see package.json scripts).
|
|
19
|
+
export default new DataSource(dataSourceOptions);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { InjectDataSource } from "@nestjs/typeorm";
|
|
3
|
+
import { DataSource } from "typeorm";
|
|
4
|
+
|
|
5
|
+
@Controller("health")
|
|
6
|
+
export class HealthController {
|
|
7
|
+
constructor(@InjectDataSource() private readonly dataSource: DataSource) {}
|
|
8
|
+
|
|
9
|
+
@Get()
|
|
10
|
+
liveness(): { status: string; uptime: number; timestamp: string } {
|
|
11
|
+
return { status: "ok", uptime: process.uptime(), timestamp: new Date().toISOString() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Get("ready")
|
|
15
|
+
async readiness(): Promise<{ status: string; db: string }> {
|
|
16
|
+
try {
|
|
17
|
+
await this.dataSource.query("SELECT 1");
|
|
18
|
+
return { status: "ready", db: "up" };
|
|
19
|
+
} catch {
|
|
20
|
+
return { status: "degraded", db: "down" };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NestFactory } from "@nestjs/core";
|
|
2
|
+
import { ValidationPipe } from "@nestjs/common";
|
|
3
|
+
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
|
4
|
+
import { AppModule } from "./app.module";
|
|
5
|
+
import { AllExceptionsFilter } from "./common/all-exceptions.filter";
|
|
6
|
+
|
|
7
|
+
async function bootstrap(): Promise<void> {
|
|
8
|
+
const app = await NestFactory.create(AppModule);
|
|
9
|
+
|
|
10
|
+
app.useGlobalPipes(
|
|
11
|
+
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
|
12
|
+
);
|
|
13
|
+
app.useGlobalFilters(new AllExceptionsFilter());
|
|
14
|
+
|
|
15
|
+
const corsOrigin = process.env.CORS_ORIGIN?.split(",").map((o) => o.trim());
|
|
16
|
+
app.enableCors({ origin: corsOrigin ?? true, credentials: true });
|
|
17
|
+
|
|
18
|
+
const config = new DocumentBuilder()
|
|
19
|
+
.setTitle("{{projectName}} API")
|
|
20
|
+
.setDescription("Generated with PodoKit")
|
|
21
|
+
.setVersion("0.0.0")
|
|
22
|
+
.build();
|
|
23
|
+
SwaggerModule.setup("api-docs", app, SwaggerModule.createDocument(app, config));
|
|
24
|
+
|
|
25
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
26
|
+
await app.listen(port);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void bootstrap();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class InitTodos1720100000000 implements MigrationInterface {
|
|
4
|
+
name = "InitTodos1720100000000";
|
|
5
|
+
|
|
6
|
+
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "pgcrypto"`);
|
|
8
|
+
await queryRunner.query(`
|
|
9
|
+
CREATE TABLE "todos" (
|
|
10
|
+
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
11
|
+
"title" character varying(500) NOT NULL,
|
|
12
|
+
"completed" boolean NOT NULL DEFAULT false,
|
|
13
|
+
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
|
14
|
+
CONSTRAINT "PK_todos_id" PRIMARY KEY ("id")
|
|
15
|
+
)
|
|
16
|
+
`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async down(queryRunner: QueryRunner): Promise<void> {
|
|
20
|
+
await queryRunner.query(`DROP TABLE "todos"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
|
2
|
+
import { IsBoolean, IsOptional } from "class-validator";
|
|
3
|
+
import { CreateTodoDto } from "./create-todo.dto";
|
|
4
|
+
|
|
5
|
+
export class UpdateTodoDto extends PartialType(CreateTodoDto) {
|
|
6
|
+
@ApiPropertyOptional({ example: true })
|
|
7
|
+
@IsOptional()
|
|
8
|
+
@IsBoolean()
|
|
9
|
+
completed?: boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm";
|
|
2
|
+
|
|
3
|
+
@Entity("todos")
|
|
4
|
+
export class Todo {
|
|
5
|
+
@PrimaryGeneratedColumn("uuid")
|
|
6
|
+
id!: string;
|
|
7
|
+
|
|
8
|
+
@Column({ type: "varchar", length: 500 })
|
|
9
|
+
title!: string;
|
|
10
|
+
|
|
11
|
+
@Column({ type: "boolean", default: false })
|
|
12
|
+
completed!: boolean;
|
|
13
|
+
|
|
14
|
+
@CreateDateColumn({ type: "timestamptz" })
|
|
15
|
+
createdAt!: Date;
|
|
16
|
+
}
|