@bhooai/nexus-examples 2.0.9 → 2.0.11

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.
@@ -39,7 +39,7 @@ export default defineRoutes([
39
39
  method: 'GET',
40
40
  path: '/api/posts',
41
41
  handler: async (ctx) => {
42
- const posts = await Post.find({}, { sort: { createdAt: -1 } });
42
+ const posts = await Post.find().sort({ createdAt: -1 });
43
43
  ctx.json({ posts: posts.map(toJson) });
44
44
  },
45
45
  },
@@ -75,7 +75,7 @@ export default defineRoutes([
75
75
  handler: async (ctx) => {
76
76
  const post = await Post.findById(ctx.params.id);
77
77
  if (!post) throw new NotFoundError('post not found');
78
- const comments = await Comment.find({ postId: ctx.params.id }, { sort: { createdAt: 1 } });
78
+ const comments = await Comment.find({ postId: ctx.params.id }).sort({ createdAt: 1 });
79
79
  ctx.json({ post: toJson(post), comments: comments.map((c) => (c as unknown as { toObject(): Record<string, unknown> }).toObject?.() ?? c) });
80
80
  },
81
81
  },
@@ -7,10 +7,20 @@ export default defineRoutes([
7
7
  path: '/api/chat/rooms/:roomId/messages',
8
8
  handler: async (ctx) => {
9
9
  const limit = Math.min(parseInt((ctx.query.limit as string) ?? '50', 10), 200);
10
- const messages = await Message.find(
11
- { roomId: ctx.params.roomId },
12
- { sort: { createdAt: -1 }, limit },
13
- );
10
+ const docs = await Message.find({ roomId: ctx.params.roomId }).sort({ createdAt: -1 }).limit(limit);
11
+ // Normalize to frontend shape {from, text, at} — Message has {username, text, createdAt}
12
+ const messages = docs.map((d: any) => {
13
+ const o = (d as any).toObject?.() ?? (d as any)._doc ?? d;
14
+ return {
15
+ _id: o._id,
16
+ roomId: o.roomId,
17
+ from: o.username ?? o.from ?? 'anon',
18
+ username: o.username ?? o.from ?? 'anon',
19
+ text: o.text,
20
+ at: o.createdAt ? new Date(o.createdAt).toISOString() : o.at ?? new Date().toISOString(),
21
+ createdAt: o.createdAt,
22
+ };
23
+ });
14
24
  ctx.json({ messages: messages.reverse() });
15
25
  },
16
26
  },
@@ -12,45 +12,60 @@
12
12
  export default {
13
13
  name: 'chat',
14
14
 
15
- async onJoin(socket: any, payload: { roomId: string; username: string }) {
15
+ async onJoin(socket: any, payload: { roomId?: string; room?: string; username: string }) {
16
+ const roomId = payload.roomId ?? payload.room ?? 'lobby';
17
+ socket.data = socket.data ?? {};
16
18
  socket.data.username = payload.username;
17
- socket.join(`chat:${payload.roomId}`);
18
- socket.to(`chat:${payload.roomId}`).emit('presence', {
19
- type: 'join',
20
- username: payload.username,
21
- });
19
+ // Support both legacy Socket.IO style (join/to) and RealtimeServer (broadcast)
20
+ try { socket.join?.(`chat:${roomId}`); } catch {}
21
+ try { socket.join?.(roomId); } catch {}
22
+ const emitPresence = (type: 'join' | 'leave') => {
23
+ const data = { type, username: payload.username };
24
+ try { socket.to?.(`chat:${roomId}`)?.emit?.('presence', data); } catch {}
25
+ try { socket.to?.(roomId)?.emit?.('presence', data); } catch {}
26
+ };
27
+ emitPresence('join');
22
28
  },
23
29
 
24
- async onLeave(socket: any, payload: { roomId: string }) {
25
- socket.to(`chat:${payload.roomId}`).emit('presence', {
26
- type: 'leave',
27
- username: socket.data?.username,
28
- });
30
+ async onLeave(socket: any, payload: { roomId?: string; room?: string }) {
31
+ const roomId = payload.roomId ?? payload.room ?? 'lobby';
32
+ const data = { type: 'leave', username: socket.data?.username };
33
+ try { socket.to?.(`chat:${roomId}`)?.emit?.('presence', data); } catch {}
34
+ try { socket.to?.(roomId)?.emit?.('presence', data); } catch {}
29
35
  },
30
36
 
31
37
  onMessage: {
32
- 'msg': async (socket: any, payload: { roomId: string; text: string }, ctx: any) => {
38
+ 'msg': async (socket: any, payload: { roomId?: string; room?: string; text: string }, ctx: any) => {
39
+ const roomId = payload.roomId ?? payload.room ?? 'lobby';
33
40
  const msg = {
34
41
  from: socket.data?.username ?? 'anon',
35
42
  text: payload.text,
36
43
  at: new Date().toISOString(),
37
44
  };
38
- // Persist (optional; async so we don't block broadcast)
39
45
  try {
40
46
  const { Message } = await import('../models/Message.js');
41
47
  await Message.create({
42
- roomId: payload.roomId,
48
+ roomId,
43
49
  userId: socket.data?.userId ?? 'anon',
44
50
  username: msg.from,
45
51
  text: msg.text,
46
52
  });
47
53
  } catch { /* swallow */ }
48
- ctx.server.to(`chat:${payload.roomId}`).emit('msg', msg);
54
+ // Broadcast via both legacy and RealtimeServer APIs for compatibility
55
+ try { ctx.server.to?.(`chat:${roomId}`)?.emit?.('msg', msg); } catch {}
56
+ try { ctx.server.to?.(roomId)?.emit?.('msg', msg); } catch {}
57
+ try { ctx.server.broadcast?.(roomId, 'msg', msg); } catch {}
58
+ try { ctx.server.broadcast?.(`chat:${roomId}`, 'msg', msg); } catch {}
59
+ // Also generic broadcast with event for RealtimeServer clients
60
+ try { ctx.server.broadcast?.(roomId, 'broadcast', { event: 'msg', data: msg }); } catch {}
49
61
  },
50
- 'typing': (socket: any, payload: { roomId: string }, ctx: any) => {
51
- socket.to(`chat:${payload.roomId}`).emit('typing', {
52
- username: socket.data?.username,
53
- });
62
+ 'typing': (socket: any, payload: { roomId?: string; room?: string }, ctx: any) => {
63
+ const roomId = payload.roomId ?? payload.room ?? 'lobby';
64
+ const data = { username: socket.data?.username };
65
+ try { socket.to?.(`chat:${roomId}`)?.emit?.('typing', data); } catch {}
66
+ try { socket.to?.(roomId)?.emit?.('typing', data); } catch {}
67
+ try { ctx.server.broadcast?.(roomId, 'typing', data); } catch {}
68
+ try { ctx.server.broadcast?.(roomId, 'broadcast', { event: 'typing', data }); } catch {}
54
69
  },
55
70
  },
56
71
  };
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
 
4
- interface Msg { from: string; text: string; at: string }
4
+ interface Msg { from: string; text: string; at: string; username?: string; createdAt?: string }
5
5
 
6
6
  function App() {
7
7
  const [roomId, setRoomId] = useState('lobby');
@@ -21,45 +21,81 @@ function App() {
21
21
 
22
22
  ws.onopen = () => {
23
23
  setConnected(true);
24
- ws.send(JSON.stringify({ type: 'join', roomId, username }));
24
+ // Send both room and roomId for compatibility (generic RealtimeServer uses room, chat.room uses roomId)
25
+ ws.send(JSON.stringify({ type: 'join', room: roomId, roomId, username }));
25
26
  };
26
27
  ws.onclose = () => setConnected(false);
27
28
  ws.onmessage = (ev) => {
28
29
  try {
29
30
  const data = JSON.parse(ev.data);
30
- if (data.type === 'msg') setMessages((m) => [...m, data.payload]);
31
+ // Legacy chat protocol
32
+ if (data.type === 'msg') setMessages((m) => [...m, data.payload ?? data]);
31
33
  else if (data.type === 'presence') {
32
34
  setPresence((p) => {
33
- if (data.payload.type === 'join') return [...new Set([...p, data.payload.username])];
34
- return p.filter((u) => u !== data.payload.username);
35
+ if (data.payload?.type === 'join' || data.type === 'join') return [...new Set([...p, data.payload?.username ?? data.username])];
36
+ return p.filter((u) => u !== (data.payload?.username ?? data.username));
35
37
  });
36
38
  } else if (data.type === 'typing') {
37
- setTypingUsers((t) => [...new Set([...t, data.payload.username])]);
38
- setTimeout(() => setTypingUsers((t) => t.filter((u) => u !== data.payload.username)), 2000);
39
+ const u = data.payload?.username ?? data.username;
40
+ if (!u) return;
41
+ setTypingUsers((t) => [...new Set([...t, u])]);
42
+ setTimeout(() => setTypingUsers((t) => t.filter((x) => x !== u)), 2000);
43
+ }
44
+ // Generic RealtimeServer protocol
45
+ else if (data.type === 'joined') {
46
+ if (Array.isArray(data.peers)) setPresence((p) => [...new Set([...p, ...data.peers])]);
47
+ } else if (data.type === 'peer-joined') {
48
+ if (data.peer) setPresence((p) => [...new Set([...p, data.peer])]);
49
+ } else if (data.type === 'peer-left') {
50
+ if (data.peer) setPresence((p) => p.filter((u) => u !== data.peer));
51
+ } else if (data.type === 'broadcast') {
52
+ const ev = data.event;
53
+ const payload = data.data ?? data.payload;
54
+ if (ev === 'msg' && payload) setMessages((m) => [...m, { from: payload.from ?? data.from, text: payload.text, at: payload.at ?? new Date().toISOString() }]);
55
+ else if (ev === 'typing' && payload?.username) {
56
+ const u2 = payload.username;
57
+ setTypingUsers((t) => [...new Set([...t, u2])]);
58
+ setTimeout(() => setTypingUsers((t) => t.filter((x) => x !== u2)), 2000);
59
+ } else if (ev === 'presence' && payload) {
60
+ setPresence((p) => {
61
+ if (payload.type === 'join') return [...new Set([...p, payload.username])];
62
+ return p.filter((u) => u !== payload.username);
63
+ });
64
+ }
39
65
  }
40
66
  } catch { /* ignore */ }
41
67
  };
42
68
  return () => ws.close();
43
69
  }, [roomId, username]);
44
70
 
45
- // Load history on room change
71
+ // Load history on room change — normalize createdAt→at and username→from for legacy docs
46
72
  useEffect(() => {
47
73
  fetch(`/api/chat/rooms/${roomId}/messages`)
48
74
  .then((r) => r.json())
49
- .then((data) => setMessages(data.messages ?? []))
75
+ .then((data) => {
76
+ const list = (data.messages ?? []) as any[];
77
+ const normalized = list.map((m: any) => ({
78
+ from: m.from ?? m.username ?? 'anon',
79
+ text: m.text,
80
+ at: m.at ?? (m.createdAt ? new Date(m.createdAt).toISOString() : new Date().toISOString()),
81
+ username: m.username ?? m.from,
82
+ createdAt: m.createdAt ?? m.at,
83
+ }));
84
+ setMessages(normalized);
85
+ })
50
86
  .catch(() => {});
51
87
  }, [roomId]);
52
88
 
53
89
  const send = () => {
54
90
  if (!draft.trim() || !wsRef.current) return;
55
- wsRef.current.send(JSON.stringify({ type: 'msg', roomId, text: draft }));
91
+ wsRef.current.send(JSON.stringify({ type: 'msg', roomId, room: roomId, text: draft, username }));
56
92
  setDraft('');
57
93
  };
58
94
 
59
95
  const onType = (v: string) => {
60
96
  setDraft(v);
61
97
  if (wsRef.current && v.length % 3 === 0) {
62
- wsRef.current.send(JSON.stringify({ type: 'typing', roomId }));
98
+ wsRef.current.send(JSON.stringify({ type: 'typing', roomId, room: roomId }));
63
99
  }
64
100
  };
65
101
 
@@ -78,11 +114,17 @@ function App() {
78
114
  </div>
79
115
 
80
116
  <div style={{ border: '1px solid #ccc', borderRadius: 8, padding: '1rem', minHeight: 300, maxHeight: 400, overflow: 'auto', marginBottom: '1rem', background: '#fafafa' }}>
81
- {messages.map((m, i) => (
82
- <div key={i} style={{ marginBottom: '0.5rem' }}>
83
- <strong>{m.from}</strong>: {m.text} <span style={{ color: '#999', fontSize: '0.75rem' }}>{new Date(m.at).toLocaleTimeString()}</span>
84
- </div>
85
- ))}
117
+ {messages.map((m, i) => {
118
+ const from = (m as any).from ?? (m as any).username ?? 'anon';
119
+ const at = (m as any).at ?? (m as any).createdAt;
120
+ const time = at ? new Date(at).toLocaleTimeString() : '';
121
+ const isInvalid = !at || time === 'Invalid Date';
122
+ return (
123
+ <div key={i} style={{ marginBottom: '0.5rem' }}>
124
+ <strong>{from}</strong>: {m.text} <span style={{ color: '#999', fontSize: '0.75rem' }}>{isInvalid ? '' : time}</span>
125
+ </div>
126
+ );
127
+ })}
86
128
  </div>
87
129
 
88
130
  <div style={{ display: 'flex', gap: '0.5rem' }}>
@@ -2,13 +2,13 @@
2
2
  <html>
3
3
  <body style="font-family: system-ui, sans-serif; color: #222;">
4
4
  <h1>Thanks for your order!</h1>
5
- <p>Order <strong><%= orderId %></strong> has been confirmed.</p>
5
+ <p>Order <strong><%= typeof orderId !== 'undefined' ? orderId : '' %></strong> has been confirmed.</p>
6
6
  <table cellpadding="6" style="border-collapse: collapse;">
7
7
  <thead>
8
8
  <tr><th align="left">Item</th><th align="right">Qty</th><th align="right">Price</th></tr>
9
9
  </thead>
10
10
  <tbody>
11
- <% items.forEach(function (item) { %>
11
+ <% if (typeof items !== 'undefined' && Array.isArray(items)) items.forEach(function (item) { %>
12
12
  <tr>
13
13
  <td><%= item.name %></td>
14
14
  <td align="right"><%= item.qty %></td>
@@ -19,7 +19,7 @@
19
19
  <tfoot>
20
20
  <tr>
21
21
  <td colspan="2" align="right"><strong>Total</strong></td>
22
- <td align="right"><strong><%= currency %> <%= total %></strong></td>
22
+ <td align="right"><strong><%= typeof currency !== 'undefined' ? currency : '' %> <%= typeof total !== 'undefined' ? total : '' %></strong></td>
23
23
  </tr>
24
24
  </tfoot>
25
25
  </table>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * multi-app example — setup script run by `nexus init --example multi-app`
3
+ * BEFORE the overlay is copied (`init.ts` runs this before `renderTemplateTree`).
4
+ *
5
+ * Scaffolds the extra backends/frontends so the subsequent overlay can drop
6
+ * products.ts / users.ts into already-existing app folders.
7
+ *
8
+ * Each add* is idempotent — if the app dir already exists but is incomplete,
9
+ * missing files are filled and port is ensured.
10
+ *
11
+ * @param {import('../../../nexus-cli/src/commands/init.js').SetupContext} ctx
12
+ */
13
+ export default async function setup(ctx) {
14
+ const { utils, vars } = ctx;
15
+ const log = utils.log ?? console.log;
16
+
17
+ log(' → multi-app setup: scaffolding extra apps...');
18
+
19
+ // Two extra backends (default backend already exists from base template)
20
+ const shop = await utils.addBackend('shop');
21
+ log(` backend-shop :${shop.port} ${shop.created ? '(created)' : '(repaired/exists)'}`);
22
+
23
+ const api = await utils.addBackend('api');
24
+ log(` backend-api :${api.port} ${api.created ? '(created)' : '(repaired/exists)'}`);
25
+
26
+ // Two extra frontends wired to their backends
27
+ const feShop = await utils.addFrontend('shop', { for: 'backend-shop' });
28
+ log(` frontend-shop :${feShop.port} → ${feShop.backend}:${feShop.backendPort} ${feShop.created ? '(created)' : '(repaired)'}`);
29
+
30
+ const feWeb = await utils.addFrontend('web', { for: 'backend' });
31
+ log(` frontend-web :${feWeb.port} → ${feWeb.backend}:${feWeb.backendPort} ${feWeb.created ? '(created)' : '(repaired)'}`);
32
+
33
+ // Overlay will now copy:
34
+ // apps/backend-shop/src/routes/products.ts
35
+ // apps/backend-api/src/routes/users.ts
36
+ // on top of these freshly scaffolded apps.
37
+ log(' → multi-app setup done. Overlay routes will be applied next.');
38
+ void vars;
39
+ }
@@ -1,11 +1,17 @@
1
1
  import { defineRoutes } from '@bhooai/nexus-core';
2
+ import { z } from 'zod';
2
3
 
3
- // Mounted on the "api" backend (apps/backend-api) — shared identity/service API.
4
+ // Mounted on the "api" backend (apps/backend-api) — shared identity/service API. In-memory for smoke test.
4
5
  const USERS = [
5
6
  { id: 'u-1', name: 'Ada Lovelace', email: 'ada@example.com' },
6
7
  { id: 'u-2', name: 'Grace Hopper', email: 'grace@example.com' },
7
8
  ];
8
9
 
10
+ const createUserSchema = z.object({
11
+ name: z.string().min(1, 'name required').max(80),
12
+ email: z.string().email('invalid email'),
13
+ });
14
+
9
15
  export default defineRoutes([
10
16
  {
11
17
  method: 'GET',
@@ -23,4 +29,15 @@ export default defineRoutes([
23
29
  ctx.json({ user });
24
30
  },
25
31
  },
32
+ {
33
+ method: 'POST',
34
+ path: '/api/users',
35
+ handler: async (ctx) => {
36
+ const parsed = createUserSchema.safeParse(ctx.body);
37
+ if (!parsed.success) return ctx.json({ error: 'validation failed', details: parsed.error.flatten() }, 400);
38
+ const user = { id: `u-${Date.now()}`, ...parsed.data };
39
+ USERS.push(user);
40
+ ctx.json({ user }, 201);
41
+ },
42
+ },
26
43
  ]);
@@ -1,12 +1,18 @@
1
1
  import { defineRoutes } from '@bhooai/nexus-core';
2
+ import { z } from 'zod';
2
3
 
3
- // Mounted on the "shop" backend (apps/backend-shop) — the storefront API.
4
+ // Mounted on the "shop" backend (apps/backend-shop) — the storefront API. In-memory for smoke test.
4
5
  const PRODUCTS = [
5
6
  { id: 'sku-1', name: 'Nexus mug', priceCents: 49900 },
6
7
  { id: 'sku-2', name: 'Nexus tee', priceCents: 89900 },
7
8
  { id: 'sku-3', name: 'Sticker pack', priceCents: 14900 },
8
9
  ];
9
10
 
11
+ const createProductSchema = z.object({
12
+ name: z.string().min(1, 'name required').max(100),
13
+ priceCents: z.number().int().min(0).max(1_000_000).optional().default(0),
14
+ });
15
+
10
16
  export default defineRoutes([
11
17
  {
12
18
  method: 'GET',
@@ -24,4 +30,15 @@ export default defineRoutes([
24
30
  ctx.json({ product });
25
31
  },
26
32
  },
33
+ {
34
+ method: 'POST',
35
+ path: '/api/products',
36
+ handler: async (ctx) => {
37
+ const parsed = createProductSchema.safeParse(ctx.body);
38
+ if (!parsed.success) return ctx.json({ error: 'validation failed', details: parsed.error.flatten() }, 400);
39
+ const product = { id: `sku-${Date.now()}`, ...parsed.data };
40
+ PRODUCTS.push(product);
41
+ ctx.json({ product }, 201);
42
+ },
43
+ },
27
44
  ]);
@@ -0,0 +1,63 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ type Product = { id: string; name: string; priceCents: number };
5
+
6
+ function App() {
7
+ const [products, setProducts] = useState<Product[]>([]);
8
+ const [name, setName] = useState('');
9
+ const [price, setPrice] = useState('');
10
+ const [msg, setMsg] = useState('');
11
+
12
+ const fetchProducts = async () => {
13
+ try {
14
+ const r = await fetch('/api/products');
15
+ const data = await r.json();
16
+ setProducts(data.products ?? []);
17
+ } catch (e) { setMsg(String(e)); }
18
+ };
19
+
20
+ useEffect(() => { fetchProducts(); }, []);
21
+
22
+ const createProduct = async (e: React.FormEvent) => {
23
+ e.preventDefault();
24
+ setMsg('');
25
+ const priceCents = price ? parseInt(price, 10) : 0;
26
+ const res = await fetch('/api/products', {
27
+ method: 'POST',
28
+ headers: { 'content-type': 'application/json' },
29
+ body: JSON.stringify({ name, priceCents }),
30
+ });
31
+ const data = await res.json();
32
+ if (!res.ok) {
33
+ setMsg(`Error ${res.status}: ${JSON.stringify(data)}`);
34
+ return;
35
+ }
36
+ setMsg(`Created ${data.product.id}`);
37
+ setName(''); setPrice('');
38
+ fetchProducts();
39
+ };
40
+
41
+ return <main style={{ fontFamily: 'system-ui', padding: '2rem', maxWidth: 640 }}>
42
+ <h1>shop — backend-shop via proxy</h1>
43
+ <p style={{ color: '#666' }}>Served by apps/frontend-shop; proxying /api to backend-shop.</p>
44
+
45
+ <h2>Products (GET /api/products)</h2>
46
+ <ul>
47
+ {products.map(p => <li key={p.id}><b>{p.name}</b> — {(p.priceCents/100).toFixed(2)} ({p.id})</li>)}
48
+ {products.length===0 && <li style={{color:'#999'}}>no products</li>}
49
+ </ul>
50
+ <button onClick={fetchProducts} style={{ marginBottom: 16 }}>Refresh</button>
51
+
52
+ <h2>Create product (POST /api/products) — zod validated</h2>
53
+ <form onSubmit={createProduct} style={{ display:'grid', gap: 8, maxWidth: 360 }}>
54
+ <input placeholder="name (required)" value={name} onChange={e=>setName(e.target.value)} />
55
+ <input placeholder="priceCents (int)" value={price} onChange={e=>setPrice(e.target.value)} />
56
+ <button type="submit">Create</button>
57
+ </form>
58
+ {msg && <pre style={{ background:'#f5f5f5', padding:8, marginTop:12 }}>{msg}</pre>}
59
+ </main>;
60
+ }
61
+
62
+ const el = document.getElementById('root');
63
+ if (el) createRoot(el).render(<App />);
@@ -0,0 +1,70 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ type User = { id: string; name: string; email: string };
5
+
6
+ function App() {
7
+ const [users, setUsers] = useState<User[]>([]);
8
+ const [me, setMe] = useState<any>(null);
9
+ const [name, setName] = useState('');
10
+ const [email, setEmail] = useState('');
11
+ const [msg, setMsg] = useState('');
12
+
13
+ const fetchMe = async () => {
14
+ try {
15
+ const r = await fetch('/me');
16
+ setMe(await r.json());
17
+ } catch {}
18
+ };
19
+
20
+ const fetchUsers = async () => {
21
+ try {
22
+ const r = await fetch('/api/users');
23
+ const data = await r.json();
24
+ setUsers(data.users ?? []);
25
+ } catch (e) { setMsg(String(e)); }
26
+ };
27
+
28
+ useEffect(() => { fetchUsers(); fetchMe(); }, []);
29
+
30
+ const createUser = async (e: React.FormEvent) => {
31
+ e.preventDefault();
32
+ setMsg('');
33
+ const res = await fetch('/api/users', {
34
+ method: 'POST',
35
+ headers: { 'content-type': 'application/json' },
36
+ body: JSON.stringify({ name, email }),
37
+ });
38
+ const data = await res.json();
39
+ if (!res.ok) { setMsg(`Error ${res.status}: ${JSON.stringify(data)}`); return; }
40
+ setMsg(`Created ${data.user.id}`);
41
+ setName(''); setEmail('');
42
+ fetchUsers();
43
+ };
44
+
45
+ return <main style={{ fontFamily: 'system-ui', padding: '2rem', maxWidth: 640 }}>
46
+ <h1>web — demos both backends</h1>
47
+ <p style={{ color:'#666' }}>frontend-web :3002 proxies /api/users → backend-api:4002, /me → backend:4000.</p>
48
+
49
+ <h2>GET /me (backend :4000)</h2>
50
+ <pre style={{ background:'#f5f5f5', padding:8 }}>{me ? JSON.stringify(me,null,2) : 'loading...'}</pre>
51
+
52
+ <h2>Users — backend-api :4002 (GET /api/users)</h2>
53
+ <ul>
54
+ {users.map(u => <li key={u.id}><b>{u.name}</b> — {u.email} ({u.id})</li>)}
55
+ {users.length===0 && <li style={{color:'#999'}}>no users</li>}
56
+ </ul>
57
+ <button onClick={fetchUsers} style={{ marginBottom:16 }}>Refresh users</button>
58
+
59
+ <h2>Create user (POST /api/users) — zod validated</h2>
60
+ <form onSubmit={createUser} style={{ display:'grid', gap:8, maxWidth:360 }}>
61
+ <input placeholder="name (required)" value={name} onChange={e=>setName(e.target.value)} />
62
+ <input placeholder="email (required, must be email)" value={email} onChange={e=>setEmail(e.target.value)} />
63
+ <button type="submit">Create</button>
64
+ </form>
65
+ {msg && <pre style={{ background:'#f5f5f5', padding:8, marginTop:12 }}>{msg}</pre>}
66
+ </main>;
67
+ }
68
+
69
+ const el = document.getElementById('root');
70
+ if (el) createRoot(el).render(<App />);
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ const backendHost = process.env.NEXUS_BACKEND_HOST ?? 'localhost';
4
+
5
+ export default defineConfig({
6
+ server: {
7
+ port: 3002,
8
+ proxy: {
9
+ '/api/users': { target: `http://${backendHost}:4002`, changeOrigin: true },
10
+ '/api/products': { target: `http://${backendHost}:4001`, changeOrigin: true },
11
+ '/api': { target: `http://${backendHost}:4000`, changeOrigin: true },
12
+ '/me': { target: `http://${backendHost}:4000`, changeOrigin: true },
13
+ '/uploads': { target: `http://${backendHost}:4000`, changeOrigin: true },
14
+ '/ws': { target: `ws://${backendHost}:4000`, ws: true },
15
+ },
16
+ },
17
+ });
@@ -5,10 +5,11 @@ import { AuthenticationError } from '@bhooai/nexus-core';
5
5
  * Demo auth middleware: trusts an `x-user-id` header instead of verifying a
6
6
  * real session/JWT. Swap for nexus-auth's jwt middleware in production.
7
7
  */
8
- export const requireAuth: Middleware = async (ctx) => {
8
+ export const requireAuth: Middleware = async (ctx, next) => {
9
9
  const userId = ctx.headers['x-user-id'];
10
10
  if (typeof userId !== 'string' || userId.length === 0) {
11
11
  throw new AuthenticationError('Sign in required (x-user-id header in this demo)');
12
12
  }
13
13
  ctx.state.userId = userId;
14
+ await next();
14
15
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-examples",
3
- "version": "2.0.9",
4
- "description": "Working examples for BhooAI Nexus v2 — each demos a slice of the framework.",
3
+ "version": "2.0.11",
4
+ "description": "Working examples for BhooAI Nexus v2 each demos a slice of the framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",