@bhooai/nexus-cli 2.0.2 → 2.0.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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/commands/add.ts +1 -1
  3. package/src/commands/dev.ts +74 -125
  4. package/src/commands/init.ts +87 -136
  5. package/src/devPanel.ts +696 -0
  6. package/src/devServiceManager.ts +229 -0
  7. package/src/dispatcher.ts +22 -1
  8. package/src/examples.ts +90 -0
  9. package/src/features.ts +261 -0
  10. package/src/launcher.ts +164 -0
  11. package/src/layout.ts +101 -0
  12. package/src/templating/tree.ts +66 -0
  13. package/src/tui.ts +170 -0
  14. package/src/wizard.ts +691 -0
  15. package/templates/base/Dockerfile.ejs +1 -0
  16. package/templates/base/apps/admin/nginx.conf.ejs +30 -1
  17. package/templates/base/apps/admin/package.json.ejs +7 -2
  18. package/templates/base/apps/admin/postcss.config.js +5 -0
  19. package/templates/base/apps/admin/src/App.tsx +4127 -0
  20. package/templates/base/apps/admin/src/alertCenter.tsx +150 -0
  21. package/templates/base/apps/admin/src/api.ts +474 -0
  22. package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +25 -0
  23. package/templates/base/apps/admin/src/index.css +3481 -0
  24. package/templates/base/apps/admin/src/main.tsx.ejs +3 -3
  25. package/templates/base/apps/admin/src/vite-env.d.ts +19 -0
  26. package/templates/base/apps/admin/tailwind.config.js +9 -0
  27. package/templates/base/apps/admin/vite.config.ts.ejs +21 -2
  28. package/templates/base/apps/ai-server/main.py.ejs +94 -6
  29. package/templates/base/apps/backend/package.json.ejs +27 -0
  30. package/templates/base/apps/frontend/package.json.ejs +7 -0
  31. package/templates/base/apps/frontend/vite.config.ts.ejs +0 -1
  32. package/templates/base/docker-compose.yml.ejs +6 -1
  33. package/templates/base/nexus.config.ts.ejs +4 -4
  34. package/templates/features/auth/apps/backend/src/models/User.ts +21 -0
  35. package/templates/features/auth/apps/backend/src/routes/auth.ts +95 -0
  36. package/templates/features/email/apps/backend/src/mail/mailables/WelcomeMail.ts +25 -0
  37. package/templates/features/email/apps/backend/src/mail/templates/welcome.ejs.ejs +10 -0
  38. package/templates/features/graphql/apps/backend/src/graphql/post.graph.ts +61 -0
  39. package/templates/features/graphql/apps/backend/src/models/Post.ts +15 -0
  40. package/templates/features/payments/apps/backend/src/routes/payments.ts +45 -0
  41. package/templates/features/queue/apps/backend/src/events/JobQueued.ts +14 -0
  42. package/templates/features/queue/apps/backend/src/jobs/ExampleJob.ts +18 -0
  43. package/templates/features/queue/apps/backend/src/listeners/OnJobQueued.ts +12 -0
  44. package/templates/features/realtime/apps/backend/src/models/Message.ts +14 -0
  45. package/templates/features/realtime/apps/backend/src/ws/chat.room.ts +56 -0
  46. package/templates/features/storage/apps/backend/src/routes/uploads.ts +91 -0
@@ -1,7 +1,7 @@
1
- import '@bhooai/nexus-admin/style.css';
2
- import { App as NexusAdmin } from '@bhooai/nexus-admin';
1
+ import './index.css';
2
+ import { App as NexusAdmin } from './App.js';
3
3
  import React from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
5
 
6
6
  const el = document.getElementById('root');
7
- if (el) createRoot(el).render(<NexusAdmin />);
7
+ if (el) createRoot(el).render(<NexusAdmin />);
@@ -0,0 +1,19 @@
1
+ // Ambient declarations that keep `@bhooai/admin` self-contained: it does
2
+ // not depend on the consumer having `vite/client` types installed. Vite /
3
+ // esbuild inject `import.meta.env` and resolve asset imports at runtime; the
4
+ // host project's build (vite build) handles actual bundling.
5
+ interface ImportMetaEnv {
6
+ readonly VITE_SUPERVISOR_URL?: string;
7
+ readonly [key: string]: string | undefined;
8
+ }
9
+ interface ImportMeta {
10
+ readonly env: ImportMetaEnv;
11
+ }
12
+ declare module '*.svg' {
13
+ const src: string;
14
+ export default src;
15
+ }
16
+ declare module '*.png' {
17
+ const src: string;
18
+ export default src;
19
+ }
@@ -0,0 +1,9 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ './index.html',
5
+ './src/**/*.{ts,tsx}',
6
+ ],
7
+ theme: { extend: {} },
8
+ plugins: [],
9
+ };
@@ -8,15 +8,34 @@ export default defineConfig({
8
8
  target: 'http://localhost:<%= backendPort %>',
9
9
  changeOrigin: true,
10
10
  },
11
+ '/ai': {
12
+ target: 'http://localhost:<%= backendPort %>',
13
+ changeOrigin: true,
14
+ },
11
15
  '/api': {
12
16
  target: 'http://localhost:<%= backendPort %>',
13
17
  changeOrigin: true,
14
- rewrite: (p: string) => p.replace(/^\/api/, ''),
18
+ },
19
+ '/auth': {
20
+ target: 'http://localhost:<%= backendPort %>',
21
+ changeOrigin: true,
22
+ },
23
+ '/csrf-token': {
24
+ target: 'http://localhost:<%= backendPort %>',
25
+ changeOrigin: true,
15
26
  },
16
27
  '/health': {
17
28
  target: 'http://localhost:<%= backendPort %>',
18
29
  changeOrigin: true,
19
30
  },
31
+ '/uploads': {
32
+ target: 'http://localhost:<%= backendPort %>',
33
+ changeOrigin: true,
34
+ },
35
+ '/ws': {
36
+ target: 'ws://localhost:<%= backendPort %>',
37
+ ws: true,
38
+ },
20
39
  },
21
40
  },
22
- });
41
+ });
@@ -1,9 +1,25 @@
1
- """AI server (FastAPI) — provides /health and OpenAI-compatible /v1/chat/completions stub.
1
+ """AI server (FastAPI) — OpenAI-compatible proxy to Ollama by default.
2
2
 
3
- Extend with real provider integrations (Ollama, OpenAI, Anthropic, etc.).
3
+ Routes:
4
+ GET /health — liveness probe
5
+ GET /v1/models — list Ollama models (OpenAI format)
6
+ POST /v1/chat/completions — chat completions (stream + non-stream) via Ollama
7
+
8
+ Env:
9
+ OLLAMA_BASE_URL Ollama server URL (default http://ollama:11434)
10
+ OLLAMA_MODEL Default model if not specified in request (default llama3.2)
4
11
  """
5
- from fastapi import FastAPI
12
+ import json
13
+ import os
14
+ from typing import Any
15
+
16
+ import httpx
17
+ from fastapi import FastAPI, Request
6
18
  from fastapi.middleware.cors import CORSMiddleware
19
+ from fastapi.responses import JSONResponse, StreamingResponse
20
+
21
+ OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://ollama:11434")
22
+ DEFAULT_MODEL = os.getenv("OLLAMA_MODEL", "llama3.2")
7
23
 
8
24
  app = FastAPI(title="<%= name %> AI server")
9
25
 
@@ -17,14 +33,86 @@ app.add_middleware(
17
33
 
18
34
  @app.get("/health")
19
35
  async def health():
20
- return {"ok": True, "service": "ai", "app": "<%= nameSlug %>"}
36
+ return {"ok": True, "service": "ai", "app": "<%= nameSlug %>", "provider": "ollama"}
21
37
 
22
38
 
23
39
  @app.get("/v1/models")
24
40
  async def models():
25
- return {"data": [{"id": "default", "object": "model"}]}
41
+ async with httpx.AsyncClient(timeout=30) as client:
42
+ try:
43
+ resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
44
+ resp.raise_for_status()
45
+ data = resp.json()
46
+ except Exception:
47
+ return {"data": [{"id": DEFAULT_MODEL, "object": "model"}]}
48
+ models_list = [
49
+ {"id": m["name"], "object": "model"}
50
+ for m in data.get("models", [])
51
+ ]
52
+ return {"data": models_list or [{"id": DEFAULT_MODEL, "object": "model"}]}
53
+
54
+
55
+ @app.post("/v1/chat/completions")
56
+ async def chat_completions(req: Request):
57
+ body: dict[str, Any] = await req.json()
58
+ model = body.get("model") or DEFAULT_MODEL
59
+ messages = body.get("messages", [])
60
+ stream = body.get("stream", False)
61
+
62
+ ollama_payload = {"model": model, "messages": messages, "stream": stream}
63
+
64
+ if not stream:
65
+ async with httpx.AsyncClient(timeout=120) as client:
66
+ resp = await client.post(
67
+ f"{OLLAMA_BASE_URL}/api/chat", json=ollama_payload
68
+ )
69
+ resp.raise_for_status()
70
+ data = resp.json()
71
+
72
+ return JSONResponse({
73
+ "id": "chatcmpl-ollama",
74
+ "object": "chat.completion",
75
+ "model": model,
76
+ "choices": [
77
+ {
78
+ "index": 0,
79
+ "message": {
80
+ "role": data.get("message", {}).get("role", "assistant"),
81
+ "content": data.get("message", {}).get("content", ""),
82
+ },
83
+ "finish_reason": data.get("done", True) and "stop" or None,
84
+ }
85
+ ],
86
+ "usage": {
87
+ "prompt_tokens": data.get("prompt_eval_count", 0),
88
+ "completion_tokens": data.get("eval_count", 0),
89
+ "total_tokens": data.get("prompt_eval_count", 0)
90
+ + data.get("eval_count", 0),
91
+ },
92
+ })
93
+
94
+ async def stream_generator():
95
+ async with httpx.AsyncClient(timeout=120) as client:
96
+ async with client.stream(
97
+ "POST", f"{OLLAMA_BASE_URL}/api/chat", json=ollama_payload
98
+ ) as resp:
99
+ resp.raise_for_status()
100
+ async for line in resp.aiter_lines():
101
+ if not line:
102
+ continue
103
+ try:
104
+ chunk = json.loads(line)
105
+ except json.JSONDecodeError:
106
+ continue
107
+ content = chunk.get("message", {}).get("content", "")
108
+ if content:
109
+ yield f"data: {json.dumps({'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})}\n\n"
110
+ if chunk.get("done"):
111
+ yield "data: [DONE]\n\n"
112
+
113
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
26
114
 
27
115
 
28
116
  if __name__ == "__main__":
29
117
  import uvicorn
30
- uvicorn.run(app, host="127.0.0.1", port=<%= aiPort %>)
118
+ uvicorn.run(app, host="0.0.0.0", port=<%= aiPort %>)
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@<%= nameSlug %>/backend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/main.ts",
8
+ "start": "tsx src/main.ts"
9
+ },
10
+ "dependencies": {
11
+ "@bhooai/nexus-core": "^2.0.1",
12
+ "@bhooai/nexus-auth": "^2.0.1",
13
+ "@bhooai/nexus-data": "^2.0.1",
14
+ "@bhooai/nexus-cache": "^2.0.1",
15
+ "@bhooai/nexus-realtime": "^2.0.1",
16
+ "@bhooai/nexus-payments": "^2.0.1",
17
+ "@bhooai/nexus-email": "^2.0.1",
18
+ "@bhooai/nexus-ai-client": "^2.0.1",
19
+ "@bhooai/nexus-graphql": "^2.0.1",
20
+ "@bhooai/nexus-telemetry": "^2.0.1",
21
+ "tsx": "^4.19.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.6.2",
25
+ "@types/node": "^22.5.0"
26
+ }
27
+ }
@@ -7,5 +7,12 @@
7
7
  "dev": "vite --port <%= frontendPort %>",
8
8
  "build": "vite build",
9
9
  "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1"
14
+ },
15
+ "devDependencies": {
16
+ "vite": "^5.4.10"
10
17
  }
11
18
  }
@@ -7,7 +7,6 @@ export default defineConfig({
7
7
  '/api': {
8
8
  target: 'http://localhost:<%= backendPort %>',
9
9
  changeOrigin: true,
10
- rewrite: (p: string) => p.replace(/^\/api/, ''),
11
10
  },
12
11
  '/uploads': {
13
12
  target: 'http://localhost:<%= backendPort %>',
@@ -15,6 +15,9 @@ services:
15
15
  NODE_ENV: production
16
16
  NEXUS_DB_URI: ${NEXUS_DB_URI:-mongodb://mongo:27017/<%= nameSlug %>}
17
17
  NEXUS_REDIS_URL: ${NEXUS_REDIS_URL:-redis://redis:6379}
18
+ NEXUS_AI_URL: http://ai-server:<%= aiPort %>/v1
19
+ NEXUS_AI_PROVIDER: ollama
20
+ NEXUS_AI_MODEL: ${OLLAMA_MODEL:-llama3.2}
18
21
  volumes:
19
22
  - ./apps/backend/storage:/app/apps/backend/storage
20
23
  depends_on:
@@ -56,9 +59,11 @@ services:
56
59
  context: .
57
60
  dockerfile: Dockerfile
58
61
  target: ai-server
59
- profiles: ["ai"]
60
62
  ports:
61
63
  - "<%= aiPort %>:<%= aiPort %>"
64
+ environment:
65
+ OLLAMA_BASE_URL: http://host.docker.internal:11434
66
+ OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3.2}
62
67
  restart: unless-stopped
63
68
 
64
69
  mongo:
@@ -13,7 +13,7 @@ const config: Partial<NexusConfig> = {
13
13
 
14
14
  server: {
15
15
  port: <%= backendPort %>,
16
- host: '127.0.0.1',
16
+ host: process.env.NEXUS_SERVER_HOST ?? '0.0.0.0',
17
17
  https: false,
18
18
  trustProxy: false,
19
19
  bodyLimit: 12 * 1024 * 1024,
@@ -79,10 +79,10 @@ const config: Partial<NexusConfig> = {
79
79
  webrtc: { rtcMinPort: 40000, rtcMaxPort: 40100, announceIp: '127.0.0.1' },
80
80
 
81
81
  ai: {
82
- serverUrl: `http://localhost:${process.env.AI_PORT ?? <%= aiPort %>}`,
82
+ serverUrl: process.env.NEXUS_AI_URL ?? `http://localhost:${process.env.AI_PORT ?? <%= aiPort %>}/v1`,
83
83
  timeoutMs: 60_000,
84
- defaultProvider: 'auto',
85
- schemaModel: 'llama3:latest',
84
+ defaultProvider: process.env.NEXUS_AI_PROVIDER ?? 'ollama',
85
+ schemaModel: process.env.NEXUS_AI_MODEL ?? 'llama3.1:8b',
86
86
  },
87
87
 
88
88
  logging: { level: 'info', format: 'pretty', console: true, dir: 'logs', maxFileSize: 10 * 1024 * 1024, maxFiles: 7 },
@@ -0,0 +1,21 @@
1
+ import { model, Schema } from '@bhooai/nexus-data';
2
+
3
+ /**
4
+ * Auth starter — User model.
5
+ *
6
+ * passwordHash is set by POST /auth/register (see routes/auth.ts).
7
+ * roles is an array of strings used by the RBAC middleware.
8
+ */
9
+ const userSchema = new Schema({
10
+ email: { type: String, required: true, unique: true, index: true },
11
+ name: { type: String, default: '' },
12
+ passwordHash: { type: String },
13
+ roles: { type: Array, default: ['user'] },
14
+ createdAt: { type: Date, default: () => new Date() },
15
+ updatedAt: { type: Date, default: () => new Date() },
16
+ });
17
+
18
+ export const User = model('users', userSchema);
19
+
20
+ /** A hydrated User document (property reads proxy to the underlying doc). */
21
+ export type UserDoc = InstanceType<typeof User>;
@@ -0,0 +1,95 @@
1
+ import { defineRoutes } from '@bhooai/nexus-core';
2
+ import type { RequestContext } from '@bhooai/nexus-core';
3
+ import {
4
+ AuthService,
5
+ MemorySessionStore,
6
+ authToken,
7
+ hashPassword,
8
+ verifyPassword,
9
+ } from '@bhooai/nexus-auth';
10
+ import { User } from '../models/User.js';
11
+
12
+ /**
13
+ * Auth starter — register / login / me.
14
+ *
15
+ * Tokens are JWT pairs (access + rotating refresh). The MemorySessionStore is
16
+ * per-process; swap in a Redis-backed store for horizontal scaling.
17
+ */
18
+ const jwt = {
19
+ secret: process.env.NEXUS_AUTH_JWT_SECRET ?? 'dev-secret-change-me',
20
+ accessTtl: 60 * 15,
21
+ refreshTtl: 60 * 60 * 24 * 30,
22
+ algorithm: 'HS256' as const,
23
+ issuer: 'nexus-app',
24
+ audience: 'nexus-app-client',
25
+ };
26
+
27
+ const sessions = new MemorySessionStore();
28
+ const auth = new AuthService(jwt, sessions);
29
+
30
+ interface UserShape {
31
+ _id: unknown;
32
+ email: string;
33
+ name?: string;
34
+ roles?: string[];
35
+ }
36
+
37
+ function toUser(doc: unknown) {
38
+ const u = doc as UserShape;
39
+ return { id: String(u._id), email: u.email, name: u.name ?? '', roles: u.roles ?? ['user'] };
40
+ }
41
+
42
+ export default defineRoutes([
43
+ {
44
+ method: 'POST',
45
+ path: '/auth/register',
46
+ handler: async (ctx) => {
47
+ const body = (ctx.body ?? {}) as { email?: string; password?: string; name?: string };
48
+ if (!body.email?.trim() || !body.password || body.password.length < 6) {
49
+ return ctx.json({ error: 'email and password (min 6 chars) are required' }, 422);
50
+ }
51
+ const email = body.email.trim().toLowerCase();
52
+ const existing = await User.findOne({ email });
53
+ if (existing) return ctx.json({ error: 'email already registered' }, 409);
54
+
55
+ const passwordHash = await hashPassword(body.password);
56
+ const [user] = await User.create({
57
+ email,
58
+ name: body.name?.trim() ?? '',
59
+ passwordHash,
60
+ roles: ['user'],
61
+ });
62
+
63
+ const pair = await auth.login({ userId: String((user as UserShape)._id), roles: ['user'] });
64
+ ctx.json({ user: toUser(user), accessToken: pair.accessToken, refreshToken: pair.refreshToken }, 201);
65
+ },
66
+ },
67
+ {
68
+ method: 'POST',
69
+ path: '/auth/login',
70
+ handler: async (ctx) => {
71
+ const body = (ctx.body ?? {}) as { email?: string; password?: string };
72
+ const email = body.email?.trim().toLowerCase() ?? '';
73
+ const user = await User.findOne({ email });
74
+ const hash = (user as { passwordHash?: string } | null)?.passwordHash;
75
+ if (!user || !hash || !(await verifyPassword(body.password ?? '', hash))) {
76
+ return ctx.json({ error: 'invalid credentials' }, 401);
77
+ }
78
+ const userId = String((user as UserShape)._id);
79
+ const pair = await auth.login({ userId, roles: (user as UserShape).roles ?? ['user'] });
80
+ ctx.json({ user: toUser(user), accessToken: pair.accessToken, refreshToken: pair.refreshToken });
81
+ },
82
+ },
83
+ {
84
+ method: 'GET',
85
+ path: '/auth/me',
86
+ middleware: [authToken(auth)],
87
+ handler: async (ctx: RequestContext) => {
88
+ const sub = (ctx.state.user as { id?: string })?.id;
89
+ if (!sub) return ctx.json({ error: 'not authenticated' }, 401);
90
+ const user = await User.findById(sub);
91
+ if (!user) return ctx.json({ error: 'user not found' }, 404);
92
+ ctx.json({ user: toUser(user) });
93
+ },
94
+ },
95
+ ]);
@@ -0,0 +1,25 @@
1
+ import { Mailable } from '@bhooai/nexus-core';
2
+
3
+ /**
4
+ * Email feature — Welcome Mailable. Template: mail/templates/welcome.ejs.
5
+ *
6
+ * await WelcomeMail.to('someone@example.com').send();
7
+ * await WelcomeMail.to('someone@example.com').queue();
8
+ *
9
+ * The dev driver logs to the console (see config.email.provider = 'log').
10
+ */
11
+ export default class WelcomeMail extends Mailable {
12
+ subject = 'Welcome aboard';
13
+ template = 'welcome';
14
+
15
+ constructor(
16
+ private user: { name: string; email: string },
17
+ private appName = 'Nexus',
18
+ ) {
19
+ super();
20
+ }
21
+
22
+ data() {
23
+ return { name: this.user.name, email: this.user.email, app: this.appName };
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <body style="font-family: system-ui, sans-serif; color: #222;">
4
+ <h1>Welcome aboard!</h1>
5
+ <p>Your account is ready. Customize this template and the
6
+ <code>WelcomeMail</code> mailable to fit your product.</p>
7
+ <p style="color: #666; font-size: 0.85rem;">Rendered by the BhooAI Nexus
8
+ mail system (dev driver logs to the console).</p>
9
+ </body>
10
+ </html>
@@ -0,0 +1,61 @@
1
+ /**
2
+ * GraphQL feature — posts subgraph. Auto-discovered from graphql/*.graph.ts
3
+ * and composed into the federated gateway under /graphql.
4
+ */
5
+ import { defineSubgraph } from '@bhooai/nexus-graphql';
6
+ import { Post } from '../models/Post.js';
7
+
8
+ export const typeDefs = /* GraphQL */ `
9
+ type Post @key(fields: "id") {
10
+ id: ID!
11
+ title: String!
12
+ slug: String!
13
+ body: String!
14
+ published: Boolean!
15
+ createdAt: String!
16
+ }
17
+
18
+ type Query {
19
+ posts(publishedOnly: Boolean): [Post!]!
20
+ post(id: ID!): Post
21
+ }
22
+
23
+ type Mutation {
24
+ publishPost(id: ID!): Post
25
+ }
26
+ `;
27
+
28
+ const toPost = (p: Record<string, any>) => ({
29
+ id: String(p._id),
30
+ title: p.title,
31
+ slug: p.slug,
32
+ body: p.body,
33
+ published: !!p.published,
34
+ createdAt: p.createdAt instanceof Date ? p.createdAt.toISOString() : String(p.createdAt),
35
+ });
36
+
37
+ export default defineSubgraph({
38
+ name: 'posts',
39
+ typeDefs,
40
+ resolvers: {
41
+ Query: {
42
+ posts: async (_parent, args) => {
43
+ const filter = args.publishedOnly ? { published: true } : {};
44
+ return (await Post.find(filter, { sort: { createdAt: -1 } })).map(toPost);
45
+ },
46
+ post: async (_parent, args) => {
47
+ const post = await Post.findById(args.id);
48
+ return post ? toPost(post) : null;
49
+ },
50
+ },
51
+ Mutation: {
52
+ publishPost: async (_parent, args) => {
53
+ const post = await Post.findOneAndUpdate(
54
+ { _id: args.id },
55
+ { $set: { published: true, updatedAt: new Date() } },
56
+ );
57
+ return post ? toPost(post) : null;
58
+ },
59
+ },
60
+ },
61
+ });
@@ -0,0 +1,15 @@
1
+ import { model, Schema } from '@bhooai/nexus-data';
2
+
3
+ /**
4
+ * GraphQL feature — Post model backing the posts subgraph.
5
+ */
6
+ const postSchema = new Schema({
7
+ title: { type: String, required: true },
8
+ slug: { type: String, required: true, unique: true, index: true },
9
+ body: { type: String, required: true },
10
+ published: { type: Boolean, default: false },
11
+ createdAt: { type: Date, default: () => new Date(), index: true },
12
+ updatedAt: { type: Date, default: () => new Date() },
13
+ });
14
+
15
+ export const Post = model('posts', postSchema);
@@ -0,0 +1,45 @@
1
+ import { defineRoutes } from '@bhooai/nexus-core';
2
+
3
+ /**
4
+ * Payments feature — checkout + webhook routes.
5
+ *
6
+ * This is a wiring skeleton: provider SDK calls (Razorpay / PayPal) are
7
+ * commented where your keys go. Config lives in nexus.config.ts under
8
+ * `payments` and provider secrets come from .env:
9
+ * NEXUS_PAYMENTS_RAZORPAY_KEY_ID / KEY_SECRET
10
+ * NEXUS_PAYMENTS_PAYPAL_CLIENT_ID / CLIENT_SECRET
11
+ */
12
+ export default defineRoutes([
13
+ {
14
+ method: 'POST',
15
+ path: '/payments/order',
16
+ handler: async (ctx) => {
17
+ const body = (ctx.body ?? {}) as { amount?: number; currency?: string; metadata?: Record<string, unknown> };
18
+ const amount = Number(body.amount);
19
+ if (!Number.isFinite(amount) || amount <= 0) {
20
+ return ctx.json({ error: 'amount is required and must be > 0' }, 422);
21
+ }
22
+ const orderId = `ord_${Date.now().toString(36)}`;
23
+ ctx.json({
24
+ orderId,
25
+ amount,
26
+ currency: body.currency ?? 'INR',
27
+ status: 'created',
28
+ // providerIntent: await razorpay.orders.create({ amount, currency }),
29
+ metadata: body.metadata ?? {},
30
+ }, 201);
31
+ },
32
+ },
33
+ {
34
+ method: 'POST',
35
+ path: '/payments/webhook/:provider',
36
+ handler: async (ctx) => {
37
+ const provider = ctx.params.provider;
38
+ const raw = ctx.body;
39
+ // Verify the signature using the provider's webhook secret from .env,
40
+ // then reconcile the order status.
41
+ console.log(`[payments] webhook from ${provider}:`, raw);
42
+ ctx.json({ ok: true });
43
+ },
44
+ },
45
+ ]);
@@ -0,0 +1,14 @@
1
+ import { DomainEvent } from '@bhooai/nexus-core';
2
+
3
+ /**
4
+ * Queue feature — event emitted when an ExampleJob is dispatched.
5
+ * Demo of the events + listeners convention.
6
+ */
7
+ export default class JobQueued extends DomainEvent {
8
+ constructor(
9
+ public readonly message: string,
10
+ public readonly at: number = Date.now(),
11
+ ) {
12
+ super();
13
+ }
14
+ }
@@ -0,0 +1,18 @@
1
+ import { Job } from '@bhooai/nexus-core';
2
+
3
+ /**
4
+ * Queue feature — example job.
5
+ *
6
+ * await ExampleJob.dispatch('hello');
7
+ *
8
+ * Run a worker with: nexus queue:work
9
+ */
10
+ export default class ExampleJob extends Job<[message: string]> {
11
+ queue = 'default';
12
+ retries = 3;
13
+ backoff = 'exponential' as const;
14
+
15
+ async handle(message: string): Promise<void> {
16
+ console.log(`[queue] ExampleJob handled: ${message}`);
17
+ }
18
+ }
@@ -0,0 +1,12 @@
1
+ import { Listener } from '@bhooai/nexus-core';
2
+ import type JobQueued from '../events/JobQueued.js';
3
+
4
+ /**
5
+ * Queue feature — auto-wired listener (On<EventName>.ts convention).
6
+ * Fires when the ExampleJob is enqueued.
7
+ */
8
+ export default class OnJobQueued extends Listener<JobQueued> {
9
+ async handle(event: JobQueued): Promise<void> {
10
+ console.log(`[events] job queued: ${event.message}`);
11
+ }
12
+ }
@@ -0,0 +1,14 @@
1
+ import { model, Schema } from '@bhooai/nexus-data';
2
+
3
+ /**
4
+ * Realtime feature — chat message model persisted by ws/chat.room.ts.
5
+ */
6
+ const messageSchema = new Schema({
7
+ roomId: { type: String, required: true, index: true },
8
+ userId: { type: String, required: true },
9
+ username: { type: String, required: true },
10
+ text: { type: String, required: true },
11
+ createdAt: { type: Date, default: () => new Date(), index: true },
12
+ });
13
+
14
+ export const Message = model('messages', messageSchema);