@podosoft/podokit 0.1.1 → 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.
Files changed (153) hide show
  1. package/README.md +17 -3
  2. package/dist/add.d.ts +40 -0
  3. package/dist/add.js +114 -0
  4. package/dist/create.d.ts +2 -1
  5. package/dist/create.js +3 -2
  6. package/dist/index.js +39 -1
  7. package/dist/prompt.d.ts +0 -1
  8. package/dist/prompt.js +6 -7
  9. package/dist/templates/fullstack-nest-svelte/README.md +20 -8
  10. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +14 -2
  11. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +11 -1
  12. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +20 -15
  13. package/dist/templates/fullstack-nest-svelte/apps/api/src/database/data-source.ts +19 -0
  14. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +15 -5
  15. package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +8 -0
  16. package/dist/templates/fullstack-nest-svelte/apps/web/components.json +1 -1
  17. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +9 -2
  18. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +72 -8
  19. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/button.svelte +82 -0
  20. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/button/index.ts +17 -0
  21. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
  22. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/card.svelte +22 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/card/index.ts +25 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
  30. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/index.ts +7 -0
  32. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/input/input.svelte +48 -0
  33. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/index.ts +7 -0
  34. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/label/label.svelte +20 -0
  35. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/utils.ts +11 -0
  36. package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +19 -12
  37. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.controller.ts +31 -0
  38. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.module.ts +22 -0
  39. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.service.ts +44 -0
  40. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/login.dto.ts +12 -0
  41. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/register.dto.ts +13 -0
  42. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt-auth.guard.ts +5 -0
  43. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt.strategy.ts +23 -0
  44. package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/user.entity.ts +16 -0
  45. package/dist/templates/modules/auth-jwt/files/apps/api/src/migrations/1720200000000-InitUsers.ts +23 -0
  46. package/dist/templates/modules/auth-jwt/module.manifest.json +31 -0
  47. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/demo.processor.ts +12 -0
  48. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/dto/create-job.dto.ts +9 -0
  49. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.controller.ts +29 -0
  50. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +15 -0
  51. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +8 -0
  52. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +20 -0
  53. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +14 -0
  54. package/dist/templates/modules/bullmq/files/infra/docker/worker.compose.example.yml +18 -0
  55. package/dist/templates/modules/bullmq/files/infra/k3s/worker-deployment.yaml +22 -0
  56. package/dist/templates/modules/bullmq/module.manifest.json +28 -0
  57. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.controller.ts +29 -0
  58. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -0
  59. package/dist/templates/modules/file-upload/module.manifest.json +19 -0
  60. package/dist/templates/modules/job-progress/files/apps/api/src/progress/dto/start-job.dto.ts +11 -0
  61. package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +13 -0
  62. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +19 -0
  63. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.controller.ts +17 -0
  64. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +25 -0
  65. package/dist/templates/modules/job-progress/module.manifest.json +23 -0
  66. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/dto/put-object.dto.ts +8 -0
  67. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.config.ts +31 -0
  68. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.controller.ts +29 -0
  69. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +11 -0
  70. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +37 -0
  71. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +33 -0
  72. package/dist/templates/modules/object-storage-s3/module.manifest.json +31 -0
  73. package/dist/templates/modules/redis/files/apps/api/src/redis/cache.controller.ts +21 -0
  74. package/dist/templates/modules/redis/files/apps/api/src/redis/dto/set-cache.dto.ts +14 -0
  75. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +12 -0
  76. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +43 -0
  77. package/dist/templates/modules/redis/module.manifest.json +21 -0
  78. package/dist/templates/modules/sse/files/apps/api/src/events/dto/publish-event.dto.ts +8 -0
  79. package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +25 -0
  80. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +12 -0
  81. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +17 -0
  82. package/dist/templates/modules/sse/module.manifest.json +16 -0
  83. package/dist/templates/todo/README.md +40 -0
  84. package/dist/templates/todo/apps/api/Dockerfile +22 -0
  85. package/dist/templates/todo/apps/api/nest-cli.json +5 -0
  86. package/dist/templates/todo/apps/api/package.json +44 -0
  87. package/dist/templates/todo/apps/api/src/app.module.ts +19 -0
  88. package/dist/templates/todo/apps/api/src/common/all-exceptions.filter.ts +43 -0
  89. package/dist/templates/todo/apps/api/src/common/app-exception.ts +12 -0
  90. package/dist/templates/todo/apps/api/src/config/env.validation.ts +23 -0
  91. package/dist/templates/todo/apps/api/src/database/data-source.ts +19 -0
  92. package/dist/templates/todo/apps/api/src/health/health.controller.ts +23 -0
  93. package/dist/templates/todo/apps/api/src/health/health.module.ts +7 -0
  94. package/dist/templates/todo/apps/api/src/main.ts +29 -0
  95. package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +22 -0
  96. package/dist/templates/todo/apps/api/src/todos/dto/create-todo.dto.ts +10 -0
  97. package/dist/templates/todo/apps/api/src/todos/dto/update-todo.dto.ts +10 -0
  98. package/dist/templates/todo/apps/api/src/todos/todo.entity.ts +16 -0
  99. package/dist/templates/todo/apps/api/src/todos/todos.controller.ts +38 -0
  100. package/dist/templates/todo/apps/api/src/todos/todos.module.ts +12 -0
  101. package/dist/templates/todo/apps/api/src/todos/todos.service.ts +41 -0
  102. package/dist/templates/todo/apps/api/test/health.e2e-spec.ts +23 -0
  103. package/dist/templates/todo/apps/api/tsconfig.json +21 -0
  104. package/dist/templates/todo/apps/web/Dockerfile +22 -0
  105. package/dist/templates/todo/apps/web/components.json +15 -0
  106. package/dist/templates/todo/apps/web/package.json +35 -0
  107. package/dist/templates/todo/apps/web/src/app.css +81 -0
  108. package/dist/templates/todo/apps/web/src/app.d.ts +11 -0
  109. package/dist/templates/todo/apps/web/src/app.html +11 -0
  110. package/dist/templates/todo/apps/web/src/lib/components/ui/button/button.svelte +82 -0
  111. package/dist/templates/todo/apps/web/src/lib/components/ui/button/index.ts +17 -0
  112. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-action.svelte +23 -0
  113. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-content.svelte +20 -0
  114. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-description.svelte +20 -0
  115. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-footer.svelte +20 -0
  116. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-header.svelte +23 -0
  117. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card-title.svelte +20 -0
  118. package/dist/templates/todo/apps/web/src/lib/components/ui/card/card.svelte +22 -0
  119. package/dist/templates/todo/apps/web/src/lib/components/ui/card/index.ts +25 -0
  120. package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/checkbox.svelte +39 -0
  121. package/dist/templates/todo/apps/web/src/lib/components/ui/checkbox/index.ts +6 -0
  122. package/dist/templates/todo/apps/web/src/lib/components/ui/input/index.ts +7 -0
  123. package/dist/templates/todo/apps/web/src/lib/components/ui/input/input.svelte +48 -0
  124. package/dist/templates/todo/apps/web/src/lib/components/ui/label/index.ts +7 -0
  125. package/dist/templates/todo/apps/web/src/lib/components/ui/label/label.svelte +20 -0
  126. package/dist/templates/todo/apps/web/src/lib/i18n/README.md +7 -0
  127. package/dist/templates/todo/apps/web/src/lib/i18n/en.ts +10 -0
  128. package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +8 -0
  129. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +16 -0
  130. package/dist/templates/todo/apps/web/src/lib/utils.ts +11 -0
  131. package/dist/templates/todo/apps/web/src/routes/+layout.svelte +9 -0
  132. package/dist/templates/todo/apps/web/src/routes/+page.svelte +95 -0
  133. package/dist/templates/todo/apps/web/src/routes/api/health/+server.ts +12 -0
  134. package/dist/templates/todo/apps/web/src/routes/api/todos/+server.ts +24 -0
  135. package/dist/templates/todo/apps/web/src/routes/api/todos/[id]/+server.ts +24 -0
  136. package/dist/templates/todo/apps/web/static/.gitkeep +0 -0
  137. package/dist/templates/todo/apps/web/svelte.config.js +15 -0
  138. package/dist/templates/todo/apps/web/tsconfig.json +9 -0
  139. package/dist/templates/todo/apps/web/vite.config.ts +7 -0
  140. package/dist/templates/todo/dot-env.example +16 -0
  141. package/dist/templates/todo/dot-gitignore +9 -0
  142. package/dist/templates/todo/infra/docker/docker-compose.yml +29 -0
  143. package/dist/templates/todo/infra/k3s/api-deployment.yaml +24 -0
  144. package/dist/templates/todo/infra/k3s/configmap.yaml +10 -0
  145. package/dist/templates/todo/infra/k3s/ingress.yaml +18 -0
  146. package/dist/templates/todo/infra/k3s/namespace.yaml +4 -0
  147. package/dist/templates/todo/infra/k3s/secret.example.yaml +10 -0
  148. package/dist/templates/todo/infra/k3s/services.yaml +21 -0
  149. package/dist/templates/todo/infra/k3s/web-deployment.yaml +20 -0
  150. package/dist/templates/todo/package.json +13 -0
  151. package/dist/templates.d.ts +10 -0
  152. package/dist/templates.js +33 -0
  153. package/package.json +2 -2
@@ -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,8 @@
1
+ import { ApiProperty } from "@nestjs/swagger";
2
+ import { IsString } from "class-validator";
3
+
4
+ export class PublishEventDto {
5
+ @ApiProperty({ example: "hello" })
6
+ @IsString()
7
+ message!: string;
8
+ }
@@ -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,5 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src"
5
+ }
@@ -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,7 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { HealthController } from "./health.controller";
3
+
4
+ @Module({
5
+ controllers: [HealthController],
6
+ })
7
+ export class HealthModule {}
@@ -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 { ApiProperty } from "@nestjs/swagger";
2
+ import { IsNotEmpty, IsString, MaxLength } from "class-validator";
3
+
4
+ export class CreateTodoDto {
5
+ @ApiProperty({ example: "Buy milk" })
6
+ @IsString()
7
+ @IsNotEmpty()
8
+ @MaxLength(500)
9
+ title!: string;
10
+ }
@@ -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
+ }