@tdk-landscape/tdk-cli-core 1.3.76 → 1.3.77
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 +58 -12
- package/cli/bin/tdk.js +22 -2
- package/cli/dist/cli.js +9 -2
- package/cli/dist/commands/completion.js +30 -3
- package/cli/dist/commands/config.js +59 -1
- package/cli/dist/commands/doctor.js +286 -98
- package/cli/dist/commands/eject.js +77 -0
- package/cli/dist/commands/networks.js +17 -4
- package/cli/dist/commands/project.js +28 -5
- package/cli/dist/commands/resource.js +118 -10
- package/cli/dist/commands/up.js +93 -21
- package/cli/dist/commands/upgrade.js +163 -68
- package/cli/dist/generator/template-engine.js +77 -13
- package/cli/dist/types/index.js +6 -1
- package/cli/dist/utils/cold-preflight.js +198 -0
- package/cli/dist/utils/constants.js +4 -0
- package/cli/dist/utils/docker.js +15 -1
- package/cli/dist/utils/doctor-runtime.js +48 -16
- package/cli/dist/utils/doctor-wiring.js +15 -7
- package/cli/dist/utils/errors.js +5 -5
- package/cli/dist/utils/exec-async.js +30 -0
- package/cli/dist/utils/generated-paths.js +49 -0
- package/cli/dist/utils/platform.js +53 -0
- package/cli/dist/utils/port-assignment.js +13 -20
- package/cli/dist/utils/project-templates.js +3 -2
- package/cli/dist/utils/resource-features.js +6 -2
- package/cli/dist/utils/service-manifest.js +73 -0
- package/cli/dist/utils/services.js +8 -1
- package/cli/dist/utils/tar.js +27 -5
- package/cli/dist/utils/tilt-process.js +53 -0
- package/cli/dist/utils/tilt.js +3 -1
- package/cli/dist/utils/which.js +26 -0
- package/cli/dist/utils/windows-doctor.js +175 -0
- package/cli/package.json +1 -1
- package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +1 -2
- package/cli/templates/TILT_TECH_STACK.star.hbs +0 -1
- package/cli/templates/Tiltfile.hbs +0 -1
- package/cli/templates/spec.master.hbs +1 -2
- package/discovery/AGENTS.md +1 -1
- package/discovery/constants.star +2 -2
- package/discovery/discovery_orchestrator.star +2 -2
- package/discovery/manifest/constants.star +1 -1
- package/discovery/manifest/loading.star +7 -3
- package/discovery/registry.star +17 -3
- package/engine/README.md +2 -0
- package/engine/schemas/service-schema.json +47 -21
- package/engine/scripts/generate-manifest.sh +2 -1
- package/engine/topologies/platform/docker/compose/compose.star +37 -5
- package/engine/topologies/tilt/common/utils.star +54 -11
- package/engine/topologies/tilt/manifest/AGENTS.md +1 -1
- package/engine/topologies/tilt/manifest/constants.star +3 -0
- package/engine/topologies/tilt/manifest/schema.star +2 -4
- package/engine/topologies/tilt/manifest/validator.star +1 -1
- package/engine/topologies/tilt/resources/infra-loader.star +14 -3
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +15 -11
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +15 -6
- package/examples/tdk-example/LICENSE +21 -0
- package/examples/tdk-example/README.md +23 -0
- package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
- package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
- package/examples/tdk-example/Tiltfile +25 -0
- package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
- package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
- package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
- package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
- package/examples/tdk-example/services/shop/orders-app/package.json +20 -0
- package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
- package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
- package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
- package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
- package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
- package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
- package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
- package/package.json +2 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# TILT_SERVICE_DEFAULTS.star - Platform Runtime Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
BASE_PORT_FRONTEND = 3000
|
|
6
|
+
BASE_PORT_BACKEND = 4000
|
|
7
|
+
|
|
8
|
+
HEALTH_CHECK_PATH = "/health"
|
|
9
|
+
HEALTH_CHECK_PATH_LIVE = "/health/live"
|
|
10
|
+
HEALTH_CHECK_PATH_READY = "/health/ready"
|
|
11
|
+
HEALTH_CHECK_INTERVAL = 10
|
|
12
|
+
HEALTH_CHECK_TIMEOUT = 5
|
|
13
|
+
HEALTH_CHECK_RETRIES = 3
|
|
14
|
+
|
|
15
|
+
MEMORY_LIMITS = {
|
|
16
|
+
"frontend": 512,
|
|
17
|
+
"backend": 1024,
|
|
18
|
+
"worker": 768,
|
|
19
|
+
"infra": 256,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DOCKER_BASE_IMAGES = {
|
|
23
|
+
"bun": "oven/bun:1.2",
|
|
24
|
+
"node": "node:20-alpine",
|
|
25
|
+
"nginx": "nginx:alpine",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
NETWORK_PREFIX = "tdk"
|
|
29
|
+
|
|
30
|
+
exports = {
|
|
31
|
+
"BASE_PORT_FRONTEND": BASE_PORT_FRONTEND,
|
|
32
|
+
"BASE_PORT_BACKEND": BASE_PORT_BACKEND,
|
|
33
|
+
"HEALTH_CHECK_PATH": HEALTH_CHECK_PATH,
|
|
34
|
+
"HEALTH_CHECK_PATH_LIVE": HEALTH_CHECK_PATH_LIVE,
|
|
35
|
+
"HEALTH_CHECK_PATH_READY": HEALTH_CHECK_PATH_READY,
|
|
36
|
+
"HEALTH_CHECK_INTERVAL": HEALTH_CHECK_INTERVAL,
|
|
37
|
+
"HEALTH_CHECK_TIMEOUT": HEALTH_CHECK_TIMEOUT,
|
|
38
|
+
"HEALTH_CHECK_RETRIES": HEALTH_CHECK_RETRIES,
|
|
39
|
+
"MEMORY_LIMITS": MEMORY_LIMITS,
|
|
40
|
+
"DOCKER_BASE_IMAGES": DOCKER_BASE_IMAGES,
|
|
41
|
+
"NETWORK_PREFIX": NETWORK_PREFIX,
|
|
42
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# TILT_TECH_STACK.star - Technology Stack Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
RUNTIME = "bun"
|
|
6
|
+
RUNTIME_VERSION = "1.2"
|
|
7
|
+
|
|
8
|
+
BUNDLER = "vite"
|
|
9
|
+
BUNDLER_VERSION = "5"
|
|
10
|
+
|
|
11
|
+
ORM = "prisma"
|
|
12
|
+
ORM_VERSION = "7"
|
|
13
|
+
|
|
14
|
+
MESSAGING = "nats"
|
|
15
|
+
MESSAGING_VERSION = "2"
|
|
16
|
+
NATS_SERVER = "nats://nats:4222"
|
|
17
|
+
|
|
18
|
+
TESTING = "vitest"
|
|
19
|
+
TESTING_VERSION = "1"
|
|
20
|
+
|
|
21
|
+
LINTING = "biome"
|
|
22
|
+
LINTING_VERSION = "1.5"
|
|
23
|
+
|
|
24
|
+
WEB_FRAMEWORK = "hono"
|
|
25
|
+
WEB_FRAMEWORK_VERSION = "4"
|
|
26
|
+
|
|
27
|
+
CONTAINER_PLATFORM = "docker"
|
|
28
|
+
COMPOSE_VERSION = "3.8"
|
|
29
|
+
|
|
30
|
+
def assert_tech_stack(loaded_stack):
|
|
31
|
+
required = {
|
|
32
|
+
"bundler": BUNDLER,
|
|
33
|
+
"runtime": RUNTIME,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for key, expected in required.items():
|
|
37
|
+
actual = loaded_stack.get(key)
|
|
38
|
+
if actual != expected:
|
|
39
|
+
fail("Tech stack mismatch: {} should be '{}' but got '{}'".format(
|
|
40
|
+
key, expected, actual
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
print("✅ Tech stack validated: {} / {} / {}".format(
|
|
44
|
+
BUNDLER, RUNTIME, TESTING
|
|
45
|
+
))
|
|
46
|
+
|
|
47
|
+
exports = {
|
|
48
|
+
"RUNTIME": RUNTIME,
|
|
49
|
+
"RUNTIME_VERSION": RUNTIME_VERSION,
|
|
50
|
+
"BUNDLER": BUNDLER,
|
|
51
|
+
"BUNDLER_VERSION": BUNDLER_VERSION,
|
|
52
|
+
"ORM": ORM,
|
|
53
|
+
"ORM_VERSION": ORM_VERSION,
|
|
54
|
+
"MESSAGING": MESSAGING,
|
|
55
|
+
"MESSAGING_VERSION": MESSAGING_VERSION,
|
|
56
|
+
"NATS_SERVER": NATS_SERVER,
|
|
57
|
+
"TESTING": TESTING,
|
|
58
|
+
"TESTING_VERSION": TESTING_VERSION,
|
|
59
|
+
"LINTING": LINTING,
|
|
60
|
+
"LINTING_VERSION": LINTING_VERSION,
|
|
61
|
+
"WEB_FRAMEWORK": WEB_FRAMEWORK,
|
|
62
|
+
"WEB_FRAMEWORK_VERSION": WEB_FRAMEWORK_VERSION,
|
|
63
|
+
"CONTAINER_PLATFORM": CONTAINER_PLATFORM,
|
|
64
|
+
"COMPOSE_VERSION": COMPOSE_VERSION,
|
|
65
|
+
"assert_tech_stack": assert_tech_stack,
|
|
66
|
+
"TECH_STACK": exports,
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# TDK Example Tiltfile
|
|
2
|
+
# Minimal Tiltfile demonstrating TDK PSR model
|
|
3
|
+
|
|
4
|
+
# Must be set before the extension loads: discovery/constants.star reads it to
|
|
5
|
+
# find this project's spec.master (services/identity, services/appointment)
|
|
6
|
+
# instead of falling back to the beauty-crm monorepo's default scan roots.
|
|
7
|
+
os.environ['TDK_PROJECT_ROOT'] = config.main_dir
|
|
8
|
+
|
|
9
|
+
# Load TDK extension
|
|
10
|
+
v1alpha1.extension_repo(name='tdk-cli', url='https://github.com/tdk-landscape/tdk-cli')
|
|
11
|
+
v1alpha1.extension(name='tdk-cli', repo_name='tdk-cli', repo_path='')
|
|
12
|
+
|
|
13
|
+
# Load TDK utilities
|
|
14
|
+
load('ext://tdk-cli', 'Utils', 'Manifest', 'Config')
|
|
15
|
+
|
|
16
|
+
print("🚀 TDK Example Project")
|
|
17
|
+
print(" Project → Stack → Resource")
|
|
18
|
+
print("")
|
|
19
|
+
|
|
20
|
+
# Default to all services if no args
|
|
21
|
+
config.define_string_list("to-run", args=True)
|
|
22
|
+
cfg = config.parse()
|
|
23
|
+
|
|
24
|
+
# Let TDK handle service discovery and registration
|
|
25
|
+
# Services are discovered from service.json files automatically
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
nats:
|
|
3
|
+
image: nats:2.10-alpine
|
|
4
|
+
container_name: tdk_example_nats
|
|
5
|
+
command: ["--jetstream", "--store_dir=/data/jetstream", "--http_port", "8222"]
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
networks:
|
|
8
|
+
tdk_example_backend:
|
|
9
|
+
aliases:
|
|
10
|
+
- tdk_example_nats
|
|
11
|
+
volumes:
|
|
12
|
+
- tdk_example_nats_data:/data
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8222/healthz"]
|
|
15
|
+
interval: 5s
|
|
16
|
+
timeout: 3s
|
|
17
|
+
start_period: 5s
|
|
18
|
+
retries: 20
|
|
19
|
+
|
|
20
|
+
networks:
|
|
21
|
+
tdk_example_backend:
|
|
22
|
+
external: true
|
|
23
|
+
name: tdk_example_backend
|
|
24
|
+
|
|
25
|
+
volumes:
|
|
26
|
+
tdk_example_nats_data:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orders-api",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
7
|
+
"start": "bun run dist/index.js",
|
|
8
|
+
"dev": "bun --watch src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.19.0",
|
|
12
|
+
"hono": "^4.9.0",
|
|
13
|
+
"nats": "^2.29.3",
|
|
14
|
+
"pg": "^8.16.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^20.0.0",
|
|
18
|
+
"@types/pg": "^8.15.5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://tdk-landscape.github.io/schema.service.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"appName": "orders-api",
|
|
5
|
+
"appType": "backend",
|
|
6
|
+
"stack": "shop",
|
|
7
|
+
"port": 4000,
|
|
8
|
+
"featuresEnabled": ["nats"],
|
|
9
|
+
"params": {
|
|
10
|
+
"NATS_URL": "nats://tdk_example_nats:4222"
|
|
11
|
+
},
|
|
12
|
+
"healthCheckPath": "/health",
|
|
13
|
+
"dependsOn": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { connect, type NatsConnection } from "nats";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { Pool } from "pg";
|
|
5
|
+
|
|
6
|
+
const port = Number(process.env.PORT ?? 4000);
|
|
7
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
8
|
+
const natsUrl = process.env.NATS_URL ?? "nats://tdk_example_nats:4222";
|
|
9
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
|
10
|
+
|
|
11
|
+
const pgUrl = new URL(databaseUrl);
|
|
12
|
+
pgUrl.searchParams.delete("schema");
|
|
13
|
+
const db = new Pool({ connectionString: pgUrl.toString() });
|
|
14
|
+
let nats: NatsConnection;
|
|
15
|
+
let workerReady = false;
|
|
16
|
+
|
|
17
|
+
async function connectDependencies(): Promise<void> {
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
await db.query(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS demo_orders (
|
|
22
|
+
id bigserial PRIMARY KEY,
|
|
23
|
+
item text NOT NULL,
|
|
24
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
25
|
+
worker_seen_at timestamptz
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
nats = await connect({ servers: natsUrl });
|
|
29
|
+
return;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error("waiting for PostgreSQL and NATS", error);
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
app.use("*", async (c, next) => {
|
|
39
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
40
|
+
c.header("Access-Control-Allow-Headers", "content-type");
|
|
41
|
+
c.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
42
|
+
await next();
|
|
43
|
+
});
|
|
44
|
+
app.options("*", (c) => c.body(null, 204));
|
|
45
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
46
|
+
app.get("/worker-ready", (c) => c.json({ ready: workerReady }, workerReady ? 200 : 503));
|
|
47
|
+
app.get("/", async (c) => {
|
|
48
|
+
const result = await db.query("SELECT id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\" FROM demo_orders ORDER BY id DESC LIMIT 20");
|
|
49
|
+
return c.json({ orders: result.rows });
|
|
50
|
+
});
|
|
51
|
+
app.post("/", async (c) => {
|
|
52
|
+
if (!workerReady) return c.json({ error: "orders worker is not ready" }, 503);
|
|
53
|
+
const body = await c.req.json<{ item?: unknown }>();
|
|
54
|
+
if (typeof body.item !== "string" || body.item.trim().length === 0) {
|
|
55
|
+
return c.json({ error: "item is required" }, 400);
|
|
56
|
+
}
|
|
57
|
+
const result = await db.query(
|
|
58
|
+
"INSERT INTO demo_orders (item) VALUES ($1) RETURNING id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\"",
|
|
59
|
+
[body.item.trim()],
|
|
60
|
+
);
|
|
61
|
+
const order = result.rows[0];
|
|
62
|
+
nats.publish("demo.orders.created", new TextEncoder().encode(String(order.id)));
|
|
63
|
+
return c.json({ order }, 201);
|
|
64
|
+
});
|
|
65
|
+
app.get("/:id", async (c) => {
|
|
66
|
+
const result = await db.query(
|
|
67
|
+
"SELECT id, item, created_at AS \"createdAt\", worker_seen_at AS \"workerSeenAt\" FROM demo_orders WHERE id = $1",
|
|
68
|
+
[c.req.param("id")],
|
|
69
|
+
);
|
|
70
|
+
if (!result.rows[0]) return c.json({ error: "order not found" }, 404);
|
|
71
|
+
return c.json({ order: result.rows[0] });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await connectDependencies();
|
|
75
|
+
const readySubscription = nats.subscribe("demo.orders.worker-ready");
|
|
76
|
+
await nats.flush();
|
|
77
|
+
void (async () => {
|
|
78
|
+
for await (const _message of readySubscription) workerReady = true;
|
|
79
|
+
})();
|
|
80
|
+
serve({ fetch: app.fetch, port });
|
|
81
|
+
console.log(`orders-api listening on ${port}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TDK Orders</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orders-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "vite build",
|
|
7
|
+
"dev": "vite --host 0.0.0.0"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
11
|
+
"vite": "^7.1.7",
|
|
12
|
+
"react": "^19.1.1",
|
|
13
|
+
"react-dom": "^19.1.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/react": "^19.1.16",
|
|
17
|
+
"@types/react-dom": "^19.1.9",
|
|
18
|
+
"typescript": "^5.9.2"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import "./style.css";
|
|
3
|
+
|
|
4
|
+
type Order = { id: number; item: string; workerSeenAt: string | null };
|
|
5
|
+
const api = `${window.location.protocol}//api.${window.location.hostname.replace(/^app\./, "")}/api/orders`;
|
|
6
|
+
|
|
7
|
+
export function App() {
|
|
8
|
+
const [item, setItem] = useState("coffee");
|
|
9
|
+
const [orders, setOrders] = useState<Order[]>([]);
|
|
10
|
+
const [message, setMessage] = useState("Loading orders…");
|
|
11
|
+
|
|
12
|
+
async function refresh() {
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(api);
|
|
15
|
+
const data = (await response.json()) as { orders: Order[] };
|
|
16
|
+
setOrders(data.orders);
|
|
17
|
+
setMessage("API and database are connected");
|
|
18
|
+
} catch {
|
|
19
|
+
setMessage("Waiting for the orders API");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
void refresh();
|
|
25
|
+
const timer = window.setInterval(() => void refresh(), 1500);
|
|
26
|
+
return () => window.clearInterval(timer);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
async function createOrder(event: React.FormEvent) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
const response = await fetch(api, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({ item }),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
setMessage(`Could not create order (${response.status})`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setMessage("Order saved; waiting for NATS worker observation…");
|
|
41
|
+
await refresh();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<main>
|
|
46
|
+
<p className="eyebrow">TDK · Hono · PostgreSQL · NATS · Vite · Traefik</p>
|
|
47
|
+
<h1>Order desk</h1>
|
|
48
|
+
<p className="status">{message}</p>
|
|
49
|
+
<form onSubmit={createOrder}>
|
|
50
|
+
<label htmlFor="item">What should the worker process?</label>
|
|
51
|
+
<div className="row">
|
|
52
|
+
<input id="item" value={item} onChange={(event) => setItem(event.target.value)} />
|
|
53
|
+
<button type="submit">Create order</button>
|
|
54
|
+
</div>
|
|
55
|
+
</form>
|
|
56
|
+
<h2>Persisted orders</h2>
|
|
57
|
+
<ul>
|
|
58
|
+
{orders.map((order) => (
|
|
59
|
+
<li key={order.id}>
|
|
60
|
+
<span>#{order.id} · {order.item}</span>
|
|
61
|
+
<strong>{order.workerSeenAt ? "Worker observed" : "Queued"}</strong>
|
|
62
|
+
</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
</main>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #13231e; background: #edf2ee; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; min-width: 320px; }
|
|
4
|
+
main { width: min(720px, calc(100% - 32px)); margin: 12vh auto; padding: 40px; background: white; border: 1px solid #d8e1dc; border-radius: 18px; box-shadow: 0 20px 60px #163c2512; }
|
|
5
|
+
.eyebrow { color: #537363; font-size: 12px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; }
|
|
6
|
+
h1 { margin: 12px 0; font-size: clamp(38px, 8vw, 64px); letter-spacing: -.06em; }
|
|
7
|
+
.status { color: #587064; }
|
|
8
|
+
label { display: block; margin: 28px 0 8px; font-weight: 650; }
|
|
9
|
+
.row { display: flex; gap: 10px; }
|
|
10
|
+
input { min-width: 0; flex: 1; padding: 12px 14px; border: 1px solid #cad7cf; border-radius: 8px; font: inherit; }
|
|
11
|
+
button { padding: 12px 18px; border: 0; border-radius: 8px; color: white; background: #176447; font: inherit; font-weight: 700; cursor: pointer; }
|
|
12
|
+
h2 { margin-top: 34px; font-size: 18px; }
|
|
13
|
+
ul { display: grid; gap: 8px; padding: 0; list-style: none; }
|
|
14
|
+
li { display: flex; justify-content: space-between; gap: 12px; padding: 12px; background: #f4f7f4; border-radius: 8px; }
|
|
15
|
+
li strong { color: #176447; }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orders-worker",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
7
|
+
"start": "bun run dist/index.js",
|
|
8
|
+
"dev": "bun --watch src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"nats": "^2.29.3",
|
|
12
|
+
"pg": "^8.16.3"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^20.0.0",
|
|
16
|
+
"@types/pg": "^8.15.5"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://tdk-landscape.github.io/schema.service.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"appName": "orders-worker",
|
|
5
|
+
"appType": "worker",
|
|
6
|
+
"stack": "shop",
|
|
7
|
+
"port": 4001,
|
|
8
|
+
"featuresEnabled": ["nats"],
|
|
9
|
+
"params": {
|
|
10
|
+
"NATS_URL": "nats://tdk_example_nats:4222"
|
|
11
|
+
},
|
|
12
|
+
"dependsOn": ["orders-api"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { connect, type NatsConnection } from "nats";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
|
|
4
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
5
|
+
const natsUrl = process.env.NATS_URL ?? "nats://tdk_example_nats:4222";
|
|
6
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
|
7
|
+
|
|
8
|
+
const pgUrl = new URL(databaseUrl);
|
|
9
|
+
pgUrl.searchParams.delete("schema");
|
|
10
|
+
const db = new Pool({ connectionString: pgUrl.toString() });
|
|
11
|
+
let nc: NatsConnection | undefined;
|
|
12
|
+
while (!nc) {
|
|
13
|
+
try {
|
|
14
|
+
nc = await connect({ servers: natsUrl });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("waiting for NATS", error);
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const subscription = nc.subscribe("demo.orders.created", { queue: "orders-workers" });
|
|
21
|
+
await nc.flush();
|
|
22
|
+
nc.publish("demo.orders.worker-ready", new Uint8Array());
|
|
23
|
+
console.log("orders-worker subscribed to demo.orders.created");
|
|
24
|
+
|
|
25
|
+
for await (const message of subscription) {
|
|
26
|
+
const id = new TextDecoder().decode(message.data);
|
|
27
|
+
const result = await db.query(
|
|
28
|
+
"UPDATE demo_orders SET worker_seen_at = now() WHERE id = $1 RETURNING id, item",
|
|
29
|
+
[id],
|
|
30
|
+
);
|
|
31
|
+
if (result.rows[0]) console.log(`worker observed order ${id}: ${result.rows[0].item}`);
|
|
32
|
+
else console.error(`worker received unknown order ${id}`);
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdk-landscape/tdk-cli-core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.77",
|
|
4
4
|
"description": "Run your whole microservice landscape locally without Kubernetes. Scaffold services and run them with hot reload, built on Tilt.",
|
|
5
5
|
"homepage": "https://tdk-landscape.github.io/tdk-website",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"specs",
|
|
27
27
|
"ext",
|
|
28
28
|
"shared-platform-engineering/docker-templates",
|
|
29
|
+
"examples/tdk-example",
|
|
29
30
|
"cli/bin",
|
|
30
31
|
"cli/dist/**/*.js",
|
|
31
32
|
"cli/templates",
|