@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
package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { CACHE, type CacheStore } from "@podosoft/podokit-runtime";
|
|
2
2
|
import { AppException } from "../common/app-exception";
|
|
3
3
|
import { API_KEY_VERIFIER } from "../api-key/api-key.module";
|
|
4
4
|
import {
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
type RequestGuardContext,
|
|
8
8
|
type ServiceKey,
|
|
9
9
|
} from "../core/services";
|
|
10
|
-
import { REDIS } from "../redis/redis.module";
|
|
11
10
|
import { rateLimitConfig, type RateLimitConfig } from "./rate-limit.config";
|
|
12
11
|
import { RateLimitIdentity } from "./rate-limit.identity";
|
|
13
12
|
|
|
@@ -20,39 +19,18 @@ export interface RateLimitStorage {
|
|
|
20
19
|
increment(key: string, ttlSeconds: number): Promise<RateLimitIncrement>;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
|
|
26
|
-
local ttl = redis.call('TTL', KEYS[1])
|
|
27
|
-
return { count, ttl }
|
|
28
|
-
`;
|
|
29
|
-
|
|
30
|
-
function resultNumber(value: unknown): number | undefined {
|
|
31
|
-
if (typeof value === "number") return value;
|
|
32
|
-
if (typeof value === "string") {
|
|
33
|
-
const parsed = Number(value);
|
|
34
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class RedisRateLimitStorage implements RateLimitStorage {
|
|
40
|
-
constructor(private readonly client: RedisClient) {}
|
|
22
|
+
export class CacheRateLimitStorage implements RateLimitStorage {
|
|
23
|
+
constructor(private readonly cache: CacheStore) {}
|
|
41
24
|
|
|
42
25
|
async increment(key: string, ttlSeconds: number): Promise<RateLimitIncrement> {
|
|
43
|
-
const result = await this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const retryAfterSeconds = resultNumber(result[1]);
|
|
52
|
-
if (count === undefined || retryAfterSeconds === undefined) {
|
|
53
|
-
throw new Error("Redis returned an invalid rate-limit counter");
|
|
54
|
-
}
|
|
55
|
-
return { count, retryAfterSeconds: Math.max(1, retryAfterSeconds) };
|
|
26
|
+
const result = await this.cache.incrementFixedWindow(key, {
|
|
27
|
+
windowMs: ttlSeconds * 1_000,
|
|
28
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
count: result.count,
|
|
32
|
+
retryAfterSeconds: Math.max(1, Math.ceil((result.resetAt - Date.now()) / 1_000)),
|
|
33
|
+
};
|
|
56
34
|
}
|
|
57
35
|
}
|
|
58
36
|
|
|
@@ -117,12 +95,15 @@ export class RateLimiter {
|
|
|
117
95
|
}
|
|
118
96
|
|
|
119
97
|
private profile(method: string, path: string): LimitProfile {
|
|
98
|
+
if (
|
|
99
|
+
method === "GET" &&
|
|
100
|
+
(path === "/api/auth/get-session" || path === "/site/settings")
|
|
101
|
+
) {
|
|
102
|
+
return { name: "runtime", ttlSeconds: this.config.ttlSeconds, limit: this.config.runtimeLimit };
|
|
103
|
+
}
|
|
120
104
|
if (path === "/api/auth" || path.startsWith("/api/auth/")) {
|
|
121
105
|
return { name: "auth", ttlSeconds: this.config.authTtlSeconds, limit: this.config.authLimit };
|
|
122
106
|
}
|
|
123
|
-
if (method === "GET" && path === "/site/settings") {
|
|
124
|
-
return { name: "runtime", ttlSeconds: this.config.ttlSeconds, limit: this.config.runtimeLimit };
|
|
125
|
-
}
|
|
126
107
|
return { name: "general", ttlSeconds: this.config.ttlSeconds, limit: this.config.limit };
|
|
127
108
|
}
|
|
128
109
|
|
|
@@ -152,7 +133,7 @@ export const rateLimitModule: PodokitModule = {
|
|
|
152
133
|
const limiter = new RateLimiter(
|
|
153
134
|
rateLimitConfig(),
|
|
154
135
|
new RateLimitIdentity(services.resolve(API_KEY_VERIFIER), services),
|
|
155
|
-
new
|
|
136
|
+
new CacheRateLimitStorage(services.resolve(CACHE)),
|
|
156
137
|
);
|
|
157
138
|
services.register(RATE_LIMITER, limiter);
|
|
158
139
|
services.resolve(REQUEST_GUARDS).register((context) => limiter.enforce(context));
|
|
@@ -12,6 +12,7 @@ const origin = { origin: base };
|
|
|
12
12
|
// value so the window is crossed in a handful of requests); otherwise fall back
|
|
13
13
|
// to the module default and allow a little headroom.
|
|
14
14
|
const limit = Number(process.env.RATE_LIMIT_MAX ?? 300);
|
|
15
|
+
const authLimit = Number(process.env.RATE_LIMIT_AUTH_MAX ?? 20);
|
|
15
16
|
|
|
16
17
|
async function session(
|
|
17
18
|
playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"],
|
|
@@ -63,3 +64,14 @@ test("rate limit: health probes stay available while ordinary routes return 429
|
|
|
63
64
|
});
|
|
64
65
|
await ctx.dispose();
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
test("rate limit: session reads do not consume the strict authentication budget", async ({
|
|
69
|
+
playwright,
|
|
70
|
+
}) => {
|
|
71
|
+
const ctx = await session(playwright);
|
|
72
|
+
for (let i = 0; i < authLimit + 5; i++) {
|
|
73
|
+
const response = await ctx.get("/api/auth/get-session");
|
|
74
|
+
expect(response.status(), `session request ${i + 1}`).toBe(200);
|
|
75
|
+
}
|
|
76
|
+
await ctx.dispose();
|
|
77
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rate-limit",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"manifestVersion": 3,
|
|
4
|
+
"description": "Provider-neutral Elysia request limiting by authenticated user, validated API key, or trusted-proxy client address.",
|
|
5
|
+
"requires": ["api-key-auth"],
|
|
6
|
+
"requiresCapabilities": ["cache"],
|
|
5
7
|
"targetApp": "api",
|
|
6
8
|
"managedGlobs": [
|
|
7
9
|
"apps/api/src/rate-limit/**",
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
"# stricter authentication endpoint ceiling",
|
|
16
18
|
"RATE_LIMIT_AUTH_TTL=60",
|
|
17
19
|
"RATE_LIMIT_AUTH_MAX=20",
|
|
18
|
-
"# higher ceiling for SSR runtime settings reads",
|
|
20
|
+
"# higher ceiling for SSR runtime settings and session reads",
|
|
19
21
|
"RATE_LIMIT_RUNTIME_MAX=1000",
|
|
20
22
|
"# trust no proxy hop by default; set the exact production proxy depth",
|
|
21
23
|
"RATE_LIMIT_TRUSTED_PROXY_HOPS=0",
|
|
@@ -36,14 +38,14 @@
|
|
|
36
38
|
}
|
|
37
39
|
],
|
|
38
40
|
"instructions": [
|
|
39
|
-
"
|
|
41
|
+
"Configure the selected cache provider before enabling request limits.",
|
|
40
42
|
"Tune the general, authentication, and runtime limits in .env.",
|
|
41
43
|
"Give each deployment that shares Redis a distinct RATE_LIMIT_KEY_PREFIX.",
|
|
42
44
|
"Set RATE_LIMIT_TRUSTED_PROXY_HOPS to the exact number of trusted reverse proxies; keep 0 for direct API access.",
|
|
43
45
|
"Authenticated users and validated X-API-Key clients receive independent opaque counters; other traffic uses the trusted client address.",
|
|
44
|
-
"Exceeded limits return RATE_LIMIT_EXCEEDED with HTTP 429 and Retry-After.
|
|
46
|
+
"Exceeded limits return RATE_LIMIT_EXCEEDED with HTTP 429 and Retry-After. Cache failures return RATE_LIMIT_UNAVAILABLE with HTTP 503.",
|
|
45
47
|
"Register a RateLimitIdentityExtension under RATE_LIMIT_IDENTITY_EXTENSION in app.extensions.ts for another validated machine-key source.",
|
|
46
48
|
"GET /health and GET /health/ready bypass request quotas so orchestrator probes remain reliable.",
|
|
47
|
-
"
|
|
49
|
+
"Redis counters are shared across replicas; memory counters require one API replica."
|
|
48
50
|
]
|
|
49
51
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { RedisClient } from "bun";
|
|
2
|
+
import type {
|
|
3
|
+
CacheSetOptions,
|
|
4
|
+
CacheStore,
|
|
5
|
+
FixedWindowOptions,
|
|
6
|
+
FixedWindowResult,
|
|
7
|
+
} from "@podosoft/podokit-runtime";
|
|
8
|
+
import type { RedisService } from "./redis.service";
|
|
9
|
+
|
|
10
|
+
const FIXED_WINDOW_SCRIPT = `
|
|
11
|
+
local count = redis.call('INCR', KEYS[1])
|
|
12
|
+
if count == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[1]) end
|
|
13
|
+
local ttl = redis.call('PTTL', KEYS[1])
|
|
14
|
+
return { count, ttl }
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function resultNumber(value: unknown): number | undefined {
|
|
18
|
+
if (typeof value === "number") return value;
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
const parsed = Number(value);
|
|
21
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RedisCacheStore implements CacheStore {
|
|
27
|
+
private readonly client: RedisClient;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly redis: RedisService) {
|
|
30
|
+
this.client = redis.client;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get(key: string): Promise<string | null> {
|
|
34
|
+
return this.redis.get(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async set(key: string, value: string, options: CacheSetOptions = {}): Promise<void> {
|
|
38
|
+
await this.client.set(key, value);
|
|
39
|
+
if (options.ttlMs !== undefined) {
|
|
40
|
+
if (!Number.isSafeInteger(options.ttlMs) || options.ttlMs < 1) {
|
|
41
|
+
throw new Error("ttlMs must be a positive integer");
|
|
42
|
+
}
|
|
43
|
+
await this.client.send("PEXPIRE", [key, String(options.ttlMs)]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async delete(key: string): Promise<boolean> {
|
|
48
|
+
return (await this.redis.del(key)) > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async incrementFixedWindow(
|
|
52
|
+
key: string,
|
|
53
|
+
options: FixedWindowOptions,
|
|
54
|
+
): Promise<FixedWindowResult> {
|
|
55
|
+
if (!Number.isSafeInteger(options.windowMs) || options.windowMs < 1) {
|
|
56
|
+
throw new Error("windowMs must be a positive integer");
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1) {
|
|
59
|
+
throw new Error("limit must be a positive integer");
|
|
60
|
+
}
|
|
61
|
+
const result = await this.client.send("EVAL", [
|
|
62
|
+
FIXED_WINDOW_SCRIPT,
|
|
63
|
+
"1",
|
|
64
|
+
key,
|
|
65
|
+
String(options.windowMs),
|
|
66
|
+
]);
|
|
67
|
+
if (!Array.isArray(result)) throw new Error("Redis returned an invalid cache counter");
|
|
68
|
+
const count = resultNumber(result[0]);
|
|
69
|
+
const ttlMs = resultNumber(result[1]);
|
|
70
|
+
if (count === undefined || ttlMs === undefined) {
|
|
71
|
+
throw new Error("Redis returned an invalid cache counter");
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
allowed: count <= options.limit,
|
|
75
|
+
count,
|
|
76
|
+
remaining: Math.max(0, options.limit - count),
|
|
77
|
+
resetAt: Date.now() + Math.max(1, ttlMs),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
close(): void {}
|
|
82
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Elysia, t } from "elysia";
|
|
2
|
+
import { CACHE } from "@podosoft/podokit-runtime";
|
|
3
|
+
import { PROVIDERS } from "../config/providers";
|
|
2
4
|
import {
|
|
3
5
|
type AppPlugin,
|
|
4
6
|
type PodokitModule,
|
|
@@ -6,10 +8,12 @@ import {
|
|
|
6
8
|
type ServiceKey,
|
|
7
9
|
} from "../core/services";
|
|
8
10
|
import { RedisService } from "./redis.service";
|
|
11
|
+
import { RedisCacheStore } from "./redis-cache.store";
|
|
9
12
|
|
|
10
13
|
export const REDIS = Symbol("redis") as ServiceKey<RedisService>;
|
|
11
14
|
|
|
12
15
|
const cachePlugin: AppPlugin = ({ services }) => {
|
|
16
|
+
if (PROVIDERS.cache !== "redis") return new Elysia({ name: "podokit.redis.inactive" });
|
|
13
17
|
const redis = services.resolve(REDIS);
|
|
14
18
|
return new Elysia({ name: "podokit.redis" })
|
|
15
19
|
.put("/cache/:key", async ({ params, body }) => {
|
|
@@ -35,8 +39,10 @@ const cachePlugin: AppPlugin = ({ services }) => {
|
|
|
35
39
|
export const redisModule: PodokitModule = {
|
|
36
40
|
name: "redis",
|
|
37
41
|
configure: (_env, services): void => {
|
|
42
|
+
if (PROVIDERS.cache !== "redis") return;
|
|
38
43
|
const redis = new RedisService(services.resolve(READINESS));
|
|
39
44
|
services.register(REDIS, redis, () => redis.close());
|
|
45
|
+
services.register(CACHE, new RedisCacheStore(redis));
|
|
40
46
|
services.onStart(() => redis.connect());
|
|
41
47
|
},
|
|
42
48
|
plugin: cachePlugin,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"manifestVersion": 3,
|
|
2
3
|
"name": "redis",
|
|
3
4
|
"description": "Bun native Redis client with get/set/del and publish/subscribe, plus demo cache endpoints.",
|
|
4
5
|
"requires": [],
|
|
6
|
+
"provides": { "cache": "redis" },
|
|
5
7
|
"requiredProjectFiles": [
|
|
6
8
|
"apps/api/src/config/redis.connection.ts",
|
|
7
9
|
"apps/api/src/health/readiness.service.ts"
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { AppException } from "@podosoft/podokit-contracts";
|
|
2
|
+
import { EVENTS } from "@podosoft/podokit-runtime";
|
|
2
3
|
import { Elysia, sse, t } from "elysia";
|
|
3
4
|
import {
|
|
4
5
|
type AppPlugin,
|
|
5
6
|
type PodokitModule,
|
|
6
7
|
READINESS,
|
|
7
|
-
type ServiceKey,
|
|
8
8
|
} from "../core/services";
|
|
9
9
|
import { EventsService } from "./events.service";
|
|
10
|
-
import { createEventsTransport } from "./events.transport";
|
|
11
10
|
|
|
12
|
-
export const
|
|
11
|
+
export const SSE_EVENTS = Symbol("sse-events") as import("../core/services").ServiceKey<EventsService>;
|
|
13
12
|
|
|
14
13
|
const eventsPlugin: AppPlugin = ({ services }) => {
|
|
15
|
-
const events = services.resolve(
|
|
14
|
+
const events = services.resolve(SSE_EVENTS);
|
|
16
15
|
return new Elysia({ name: "podokit.events" })
|
|
17
16
|
.get("/events/stream", async function* ({ request }) {
|
|
18
17
|
for await (const data of events.stream(request.signal)) yield sse({ data });
|
|
@@ -40,8 +39,8 @@ const eventsPlugin: AppPlugin = ({ services }) => {
|
|
|
40
39
|
export const eventsModule: PodokitModule = {
|
|
41
40
|
name: "sse",
|
|
42
41
|
configure: (_env, services): void => {
|
|
43
|
-
const events = new EventsService(
|
|
44
|
-
services.register(
|
|
42
|
+
const events = new EventsService(services.resolve(EVENTS), services.resolve(READINESS));
|
|
43
|
+
services.register(SSE_EVENTS, events, () => events.close());
|
|
45
44
|
services.onStart(() => events.connect());
|
|
46
45
|
},
|
|
47
46
|
plugin: eventsPlugin,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { ReadinessService } from "../health/readiness.service";
|
|
3
|
+
import { MemoryEventBus } from "@podosoft/podokit-runtime";
|
|
3
4
|
import { EventsService } from "./events.service";
|
|
4
|
-
import { MemoryEventsTransport } from "./events.transport";
|
|
5
5
|
|
|
6
6
|
describe("EventsService", () => {
|
|
7
7
|
it("publishes through the transport and delivers locally", async () => {
|
|
8
8
|
const readiness = new ReadinessService();
|
|
9
|
-
const service = new EventsService(new
|
|
9
|
+
const service = new EventsService(new MemoryEventBus(), readiness);
|
|
10
10
|
const received: unknown[] = [];
|
|
11
11
|
service.subscribe((data) => received.push(data));
|
|
12
12
|
await service.connect();
|
|
@@ -18,12 +18,12 @@ describe("EventsService", () => {
|
|
|
18
18
|
|
|
19
19
|
it("rejects events larger than the configured limit", async () => {
|
|
20
20
|
const service = new EventsService(
|
|
21
|
-
new
|
|
21
|
+
new MemoryEventBus({ maxEventBytes: 8 }),
|
|
22
22
|
new ReadinessService(),
|
|
23
23
|
);
|
|
24
24
|
await service.connect();
|
|
25
25
|
await expect(service.publishAsync({ message: "too large" })).rejects.toThrow(
|
|
26
|
-
"Event
|
|
26
|
+
"Event exceeds maxEventBytes",
|
|
27
27
|
);
|
|
28
28
|
await service.close();
|
|
29
29
|
});
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { ReadinessService } from "../health/readiness.service";
|
|
2
|
-
import type {
|
|
2
|
+
import type { EventBus, Unsubscribe } from "@podosoft/podokit-runtime";
|
|
3
3
|
|
|
4
4
|
type LocalHandler = (data: unknown) => void;
|
|
5
5
|
|
|
6
6
|
export class EventsService {
|
|
7
7
|
private readonly handlers = new Set<LocalHandler>();
|
|
8
8
|
private unregisterReadiness?: () => void;
|
|
9
|
+
private unsubscribe?: Unsubscribe;
|
|
9
10
|
|
|
10
11
|
constructor(
|
|
11
|
-
private readonly transport:
|
|
12
|
+
private readonly transport: EventBus,
|
|
12
13
|
private readonly readiness: ReadinessService,
|
|
13
14
|
) {}
|
|
14
15
|
|
|
15
16
|
async connect(): Promise<void> {
|
|
16
|
-
await this.transport.
|
|
17
|
+
this.unsubscribe = await this.transport.subscribe("sse", (data) => this.publishLocal(data));
|
|
17
18
|
this.unregisterReadiness = this.readiness.register("events", () => this.transport.ready());
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -25,7 +26,7 @@ export class EventsService {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
publishAsync(data: unknown): Promise<void> {
|
|
28
|
-
return this.transport.publish(data);
|
|
29
|
+
return this.transport.publish("sse", data);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
publishLocal(data: unknown): void {
|
|
@@ -66,7 +67,7 @@ export class EventsService {
|
|
|
66
67
|
|
|
67
68
|
async close(): Promise<void> {
|
|
68
69
|
this.unregisterReadiness?.();
|
|
70
|
+
await this.unsubscribe?.();
|
|
69
71
|
this.handlers.clear();
|
|
70
|
-
await this.transport.close();
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"manifestVersion": 3,
|
|
2
3
|
"name": "sse",
|
|
3
4
|
"description": "Server-Sent Events with memory or Redis transport for multi-replica delivery.",
|
|
4
|
-
"
|
|
5
|
+
"requiresCapabilities": ["events"],
|
|
5
6
|
"requiredProjectFiles": [
|
|
6
|
-
"apps/api/src/config/redis.connection.ts",
|
|
7
7
|
"apps/api/src/health/readiness.service.ts"
|
|
8
8
|
],
|
|
9
9
|
"targetApp": "api",
|
|
10
10
|
"env": [
|
|
11
|
-
"# Server-Sent
|
|
12
|
-
"SSE_TRANSPORT=memory",
|
|
13
|
-
"SSE_REDIS_CHANNEL=podokit:events",
|
|
11
|
+
"# Maximum encoded Server-Sent Event payload",
|
|
14
12
|
"SSE_MAX_EVENT_BYTES=65536"
|
|
15
13
|
],
|
|
16
14
|
"inject": [
|
|
@@ -20,7 +18,7 @@
|
|
|
20
18
|
"instructions": [
|
|
21
19
|
"Stream events: curl -N localhost:5002/events/stream",
|
|
22
20
|
"Publish a message (in another terminal): curl -XPOST localhost:5002/events -H 'content-type: application/json' -d '{\"message\":\"hello\"}'",
|
|
23
|
-
"
|
|
21
|
+
"Select events=redis for multiple replicas or events=memory for a single process.",
|
|
24
22
|
"Inject EventsService and call publishAsync for confirmed delivery, publish for best-effort delivery, or publishLocal for events already fanned out to every replica."
|
|
25
23
|
]
|
|
26
24
|
}
|
|
@@ -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
|
|
14
|
-
- `.podokit/` —
|
|
13
|
+
- `infra/` — Docker Compose references for external services and example k3s manifests.
|
|
14
|
+
- `.podokit/` — generation metadata plus application-owned development and deployment profiles.
|
|
15
15
|
- **Todo example**: `apps/api/src/todos/` (Elysia plugin, Bun.SQL repository, and migration) and the todo UI in `apps/web/src/routes/` show the end-to-end patterns to follow.
|
|
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,
|
|
@@ -69,11 +87,12 @@ in `.podokit/files.lock` — respect it:
|
|
|
69
87
|
|
|
70
88
|
Useful commands:
|
|
71
89
|
|
|
90
|
+
- `podo provider list` / `podo provider set <capability> <provider> [--apply]` — inspect or switch database, cache, storage, events, and jobs; set previews by default.
|
|
72
91
|
- `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.
|
|
73
92
|
- `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
|
|
74
93
|
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
75
94
|
- `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
|
|
76
|
-
- `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`.
|
|
95
|
+
- `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`.
|
|
77
96
|
- `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`.
|
|
78
97
|
|
|
79
98
|
## Commit conventions
|
|
@@ -2,9 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Full-stack TypeScript app 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`, a Bun.SQL Todo CRUD resource backed by PostgreSQL, merged OpenAPI at `/api-docs`, and a standard error envelope.
|
|
5
|
+
- `apps/api` — Bun 1.4 + Elysia API: schema-validated env, `/health` + `/health/ready`, a Bun.SQL Todo CRUD resource backed by PostgreSQL or SQLite, merged OpenAPI at `/api-docs`, and a standard error envelope.
|
|
6
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
|
|
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 @@ In a second terminal, apply the included Todo migration and use the lifecycle he
|
|
|
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,17 +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:
|
|
61
100
|
|
|
62
101
|
```bash
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# run database migrations
|
|
102
|
+
{{packageManager}} install
|
|
103
|
+
cp .env.example .env
|
|
67
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:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
{{apiRun}} dev # terminal 1
|
|
115
|
+
```
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
{{
|
|
117
|
+
```bash
|
|
118
|
+
{{webRun}} dev # terminal 2
|
|
71
119
|
```
|
|
72
120
|
|
|
73
121
|
- API: http://localhost:5002 — health at `/health`, docs at `/api-docs`
|
|
@@ -78,18 +126,28 @@ tunnel, see the PodoKit [development guide](https://github.com/podosoft-dev/podo
|
|
|
78
126
|
|
|
79
127
|
## Database & migrations
|
|
80
128
|
|
|
81
|
-
The API uses Bun.SQL with
|
|
82
|
-
|
|
129
|
+
The API uses Bun.SQL with the selected PostgreSQL or SQLite database. TypeORM
|
|
130
|
+
runs PostgreSQL migrations; SQLite uses the local migration runner. A sample
|
|
131
|
+
Todo repository and initial migration are included.
|
|
83
132
|
|
|
84
133
|
```bash
|
|
85
134
|
{{apiRun}} migration:run # apply migrations
|
|
86
135
|
{{apiRun}} migration:revert # roll back the last one
|
|
87
136
|
```
|
|
88
137
|
|
|
138
|
+
When `auth` is installed, use `{{apiRun}} migrate:all` to create or update both
|
|
139
|
+
Better Auth and application tables. Set the module's required environment
|
|
140
|
+
values before migrating.
|
|
141
|
+
|
|
89
142
|
## Deploy
|
|
90
143
|
|
|
91
144
|
Docker Compose lives in `infra/docker`; the basic k3s manifests in `infra/k3s`
|
|
92
145
|
route every public path through the web proxy. Use `podo deploy init`, `doctor`,
|
|
93
|
-
and `plan` for an exact-image
|
|
94
|
-
rolling back requires the exact confirmation hash from a fresh plan. See the
|
|
146
|
+
and `plan` for an exact-image release on Kubernetes or one Docker host. Applying
|
|
147
|
+
or rolling back requires the exact confirmation hash from a fresh plan. See the
|
|
95
148
|
PodoKit [deployment guide](https://github.com/podosoft-dev/podokit/blob/main/docs/deployment.md).
|
|
149
|
+
|
|
150
|
+
For a Docker Compose deployment with API WebSockets, list their exact paths under
|
|
151
|
+
`exposure.webSocketPaths`. The driver keeps one published port, routes those paths
|
|
152
|
+
to API, and sends every other request to web. If an external reverse proxy supplies
|
|
153
|
+
forwarding headers, list only its direct CIDR blocks in `exposure.trustedProxyCidrs`.
|
|
@@ -4,6 +4,17 @@ export class InitTodos1720100000000 implements MigrationInterface {
|
|
|
4
4
|
name = "InitTodos1720100000000";
|
|
5
5
|
|
|
6
6
|
async up(queryRunner: QueryRunner): Promise<void> {
|
|
7
|
+
if (queryRunner.connection.options.type === "better-sqlite3") {
|
|
8
|
+
await queryRunner.query(`
|
|
9
|
+
CREATE TABLE "todos" (
|
|
10
|
+
"id" text NOT NULL PRIMARY KEY,
|
|
11
|
+
"title" character varying(500) NOT NULL,
|
|
12
|
+
"completed" boolean NOT NULL DEFAULT false,
|
|
13
|
+
"createdAt" text NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
14
|
+
)
|
|
15
|
+
`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
7
18
|
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "pgcrypto"`);
|
|
8
19
|
await queryRunner.query(`
|
|
9
20
|
CREATE TABLE "todos" (
|