bhooai-nexus 2.0.12 → 2.0.13

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 (48) hide show
  1. package/package.json +1 -1
  2. package/packages/nexus-admin/package.json +1 -1
  3. package/packages/nexus-admin/src/App.tsx +23 -0
  4. package/packages/nexus-ads/package.json +1 -1
  5. package/packages/nexus-ai-client/package.json +1 -1
  6. package/packages/nexus-auth/package.json +1 -1
  7. package/packages/nexus-cache/package.json +1 -1
  8. package/packages/nexus-cli/package.json +1 -1
  9. package/packages/nexus-cli/src/commands/init.ts +92 -48
  10. package/packages/nexus-cli/templates/base/apps/frontend/vite.config.ts.ejs +23 -1
  11. package/packages/nexus-cli/templates/base/nexus.config.ts.ejs +19 -16
  12. package/packages/nexus-core/package.json +1 -1
  13. package/packages/nexus-core/src/app/createNexusApp.ts +28 -4
  14. package/packages/nexus-core/src/config/runtimeJson.ts +6 -0
  15. package/packages/nexus-crypto/package.json +1 -1
  16. package/packages/nexus-data/package.json +1 -1
  17. package/packages/nexus-email/package.json +1 -1
  18. package/packages/nexus-examples/examples/blog-crud/README.md +8 -6
  19. package/packages/nexus-examples/examples/blog-crud/apps/frontend/src/main.tsx +45 -16
  20. package/packages/nexus-examples/examples/chat/apps/backend/src/graphql/app.graph.ts +39 -4
  21. package/packages/nexus-examples/examples/chat/apps/backend/src/routes/index.ts +0 -11
  22. package/packages/nexus-examples/examples/chat/apps/backend/src/ws/chat.room.ts +5 -0
  23. package/packages/nexus-examples/examples/chat/apps/frontend/src/main.tsx +192 -70
  24. package/packages/nexus-examples/examples/checkout/apps/backend/src/graphql/app.graph.ts +57 -12
  25. package/packages/nexus-examples/examples/checkout/apps/backend/src/main.ts +28 -0
  26. package/packages/nexus-examples/examples/checkout/apps/backend/src/routes/index.ts +30 -0
  27. package/packages/nexus-examples/examples/checkout/apps/frontend/index.html +13 -0
  28. package/packages/nexus-examples/examples/checkout/apps/frontend/src/main.tsx +190 -18
  29. package/packages/nexus-examples/examples/dashboard/README.md +3 -2
  30. package/packages/nexus-examples/examples/dashboard/apps/backend/src/routes/metrics.ts +1 -1
  31. package/packages/nexus-examples/examples/dashboard/apps/frontend/src/main.tsx +8 -2
  32. package/packages/nexus-examples/examples/file-storage/README.md +4 -2
  33. package/packages/nexus-examples/examples/file-storage/apps/frontend/src/main.tsx +24 -7
  34. package/packages/nexus-examples/examples/multi-app/apps/frontend-shop/src/main.tsx +2 -1
  35. package/packages/nexus-examples/examples/saas-starter/apps/backend/src/graphql/app.graph.ts +9 -8
  36. package/packages/nexus-examples/examples/saas-starter/apps/frontend/src/main.tsx +18 -13
  37. package/packages/nexus-examples/package.json +1 -1
  38. package/packages/nexus-graphql/package.json +1 -1
  39. package/packages/nexus-graphql/src/explorer/explorerHtml.ts +278 -131
  40. package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +6 -1
  41. package/packages/nexus-graphql/src/federation/executor.ts +61 -6
  42. package/packages/nexus-payments/package.json +1 -1
  43. package/packages/nexus-payments/src/providers/PayUProvider.ts +1 -0
  44. package/packages/nexus-plugins/package.json +1 -1
  45. package/packages/nexus-postcss/package.json +1 -1
  46. package/packages/nexus-realtime/package.json +1 -1
  47. package/packages/nexus-safe-goto/package.json +1 -1
  48. package/packages/nexus-telemetry/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bhooai-nexus",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "BhooAI Nexus - a Node-based full-stack framework (websockets, GraphQL federation, inbuilt security, custom ODM, OAuth, payments, WebRTC, AI, plugins). v2: multi-app, Laravel-style folders, Docker-based scaling.",
5
5
  "license": "MIT",
6
6
  "author": "BhooAI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-admin",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "description": "BhooAI Nexus v2 admin SPA — Apps-tab-first, live registry/health/logs/config/users/payments/AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -3276,11 +3276,34 @@ function GraphQLSettings({ onNavigate }: { onNavigate?: (tab: Tab) => void }) {
3276
3276
  const path = (g.path as string) ?? '/graphql';
3277
3277
  const introspection = g.introspection !== false;
3278
3278
 
3279
+ // Build a directly-openable explorer URL (backend origin + path). The admin
3280
+ // and backend share a host in local dev, so use the browser origin + the
3281
+ // backend port from the merged config.
3282
+ const serverCfg = (cfg?.config?.server ?? {}) as { port?: number };
3283
+ const backendPort = serverCfg.port ?? 4000;
3284
+ const explorerUrl = `${window.location.protocol}//${window.location.hostname}:${backendPort}/graphiql`;
3285
+
3279
3286
  return (
3280
3287
  <div className="space-y-6">
3281
3288
  <PageHead title="GraphQL" />
3282
3289
  <PageHero kicker="GRAPHQL WORKSPACE" title={<>Explore your <em>schema.</em></>} desc="Control the GraphiQL explorer and gate it behind an admin access token." glyph="gitFork" art="grid" />
3283
3290
 
3291
+ <section className="glass-card space-y-3">
3292
+ <h3 className="glass-h3">Open the explorer</h3>
3293
+ <p className="text-xs text-slate-500">Jump straight into the interactive GraphiQL sandbox on the backend.</p>
3294
+ <div className="flex flex-wrap items-center gap-3">
3295
+ <a
3296
+ href={explorerUrl}
3297
+ target="_blank"
3298
+ rel="noreferrer"
3299
+ className="glass-chip-btn-primary inline-flex items-center gap-2 px-4 py-2 text-sm"
3300
+ >
3301
+ <Icon name="gitFork" size={14} /> Open GraphQL explorer <Icon name="externalLink" size={12} />
3302
+ </a>
3303
+ <code className="font-mono text-xs text-slate-400">{explorerUrl}</code>
3304
+ </div>
3305
+ </section>
3306
+
3284
3307
  <section className="glass-card space-y-3">
3285
3308
  <h3 className="glass-h3">GraphiQL explorer</h3>
3286
3309
  <p className="text-xs text-slate-500">The interactive sandbox is served at <code className="font-mono text-slate-300">{path}</code> (page) and <code className="font-mono text-slate-300">/graphiql</code>. The <code className="font-mono text-slate-300">/graphql</code> JSON endpoint is always public.</p>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-ads",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-ai-client",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-auth",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-cache",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-cli",
3
- "version": "2.0.13",
3
+ "version": "2.0.14",
4
4
  "description": "BhooAI Nexus v2 CLI — init, dev, build, test, scaffolding, plugins, queue, db.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,10 +8,11 @@
8
8
  * Always interactive (the wizard). Requires a TTY; in a non-interactive
9
9
  * context it prints a message and exits non-zero. `--no-install` skips the
10
10
  * dependency install, `--force` overwrites an existing target directory.
11
- * `--dev` rewrites bhooai-nexus + @bhooai/* to file:../../PACKAGES/bhooai-nexus
11
+ * `--dev` rewrites bhooai-nexus + @bhooai/* (incl. the admin app's
12
+ * @bhooai/nexus-admin + @bhooai/nexus-safe-goto) to file:../../PACKAGES/bhooai-nexus
12
13
  * (relative) and runs `npm link` for live symlink.
13
14
  */
14
- import { mkdir, writeFile, readFile } from 'node:fs/promises';
15
+ import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
15
16
  import { existsSync } from 'node:fs';
16
17
  import { spawn } from 'node:child_process';
17
18
  import { join, resolve, dirname, relative, sep } from 'node:path';
@@ -204,7 +205,7 @@ export async function run(ctx: CommandContext): Promise<void> {
204
205
  if (devMode) {
205
206
  console.log(`\n[dev] Linking local packages for live symlink...\n`);
206
207
  const linkCode = await runNpmLinkForDev(targetDir);
207
- if (linkCode === 0) console.log(`\n[dev] Live symlink ready — edits in ${resolveLocalBhooaiNexusRoot() ?? 'PACKAGES/bhooai-nexus'} reflect instantly.`);
208
+ if (linkCode === 0) console.log(`\n[dev] Live symlink ready — edits in ${resolveLocalBhooaiNexusRoot() ?? 'PACKAGES/bhooai-nexus'} (incl. admin) reflect instantly.`);
208
209
  else console.warn(`\n[dev] npm link failed (code ${linkCode}) — fallback to file: copy (re-run npm install after edits).`);
209
210
  }
210
211
  }
@@ -260,47 +261,79 @@ function resolveLocalBhooaiNexusRoot(): string | null {
260
261
  return null;
261
262
  }
262
263
 
263
- async function rewriteToLocalFileDeps(targetDir: string): Promise<{ count: number; root: string; entries: Array<{ name: string; spec: string }> }> {
264
- const localRoot = resolveLocalBhooaiNexusRoot();
264
+ /**
265
+ * Rewrite bhooai-nexus + @bhooai/* deps to local `file:` specs in the root
266
+ * package.json AND every app workspace package.json (notably apps/admin, which
267
+ * depends on @bhooai/nexus-admin + @bhooai/nexus-safe-goto). This ensures a
268
+ * `--dev` scaffold serves the local admin source, not the published npm copy.
269
+ *
270
+ * `localRootOverride` lets tests pin the local repo root; when omitted it is
271
+ * resolved automatically.
272
+ */
273
+ export async function rewriteToLocalFileDeps(
274
+ targetDir: string,
275
+ localRootOverride?: string,
276
+ ): Promise<{ count: number; root: string; entries: Array<{ name: string; spec: string }> }> {
277
+ const localRoot = localRootOverride ?? resolveLocalBhooaiNexusRoot();
265
278
  if (!localRoot) return { count: 0, root: '', entries: [] };
266
- const pkgPath = join(targetDir, 'package.json');
267
- let raw: string;
268
- try { raw = await readFile(pkgPath, 'utf8'); } catch { return { count: 0, root: localRoot, entries: [] }; }
269
- let pkg: any;
270
- try { pkg = JSON.parse(raw); } catch { return { count: 0, root: localRoot, entries: [] }; }
271
279
  const entries: Array<{ name: string; spec: string }> = [];
272
- const toFileSpec = (abs: string) => {
273
- let rel = relative(targetDir, abs).replace(/\\/g, '/');
274
- if (!rel.startsWith('.')) rel = './' + rel;
275
- // Use forward slashes for file: spec (npm supports both, but posix is more portable)
276
- return `file:${rel}`;
277
- };
278
- const maybeRewrite = (obj: Record<string, string> | undefined) => {
279
- if (!obj) return;
280
- for (const name of Object.keys(obj)) {
281
- if (name === 'bhooai-nexus') {
282
- const spec = toFileSpec(localRoot);
283
- obj[name] = spec;
284
- entries.push({ name, spec });
285
- } else if (name.startsWith('@bhooai/')) {
286
- const short = name.replace('@bhooai/', '');
287
- // Map @bhooai/nexus-* -> packages/nexus-<short> ; @bhooai/nexus -> packages/nexus-core? but we keep generic
288
- const pkgDir = join(localRoot, 'packages', short === 'nexus' ? 'nexus-core' : short.startsWith('nexus-') ? short : `nexus-${short}`);
289
- // Fallback: try packages/<name without @bhooai/> and packages/nexus-<name>
290
- const candidates = [join(localRoot, 'packages', name.replace('@bhooai/', '')), join(localRoot, 'packages', `nexus-${short}`), pkgDir];
291
- let found: string | null = null;
292
- for (const c of candidates) if (existsSync(join(c, 'package.json'))) { found = c; break; }
293
- if (found) {
294
- const spec = toFileSpec(found);
280
+
281
+ const rewritePackageJson = async (pkgPath: string): Promise<void> => {
282
+ const appDir = dirname(pkgPath);
283
+ let raw: string;
284
+ try { raw = await readFile(pkgPath, 'utf8'); } catch { return; }
285
+ let pkg: any;
286
+ try { pkg = JSON.parse(raw); } catch { return; }
287
+ const toFileSpec = (abs: string) => {
288
+ let rel = relative(appDir, abs).replace(/\\/g, '/');
289
+ if (!rel.startsWith('.')) rel = './' + rel;
290
+ // Use forward slashes for file: spec (npm supports both, but posix is more portable)
291
+ return `file:${rel}`;
292
+ };
293
+ let changed = false;
294
+ const maybeRewrite = (obj: Record<string, string> | undefined) => {
295
+ if (!obj) return;
296
+ for (const name of Object.keys(obj)) {
297
+ if (name === 'bhooai-nexus') {
298
+ const spec = toFileSpec(localRoot);
295
299
  obj[name] = spec;
296
300
  entries.push({ name, spec });
301
+ changed = true;
302
+ } else if (name.startsWith('@bhooai/')) {
303
+ const short = name.replace('@bhooai/', '');
304
+ // Map @bhooai/nexus-* -> packages/nexus-<short> ; @bhooai/nexus -> packages/nexus-core
305
+ const pkgDir = join(localRoot, 'packages', short === 'nexus' ? 'nexus-core' : short.startsWith('nexus-') ? short : `nexus-${short}`);
306
+ // Fallback: try packages/<name without @bhooai/> and packages/nexus-<name>
307
+ const candidates = [join(localRoot, 'packages', name.replace('@bhooai/', '')), join(localRoot, 'packages', `nexus-${short}`), pkgDir];
308
+ let found: string | null = null;
309
+ for (const c of candidates) if (existsSync(join(c, 'package.json'))) { found = c; break; }
310
+ if (found) {
311
+ const spec = toFileSpec(found);
312
+ obj[name] = spec;
313
+ entries.push({ name, spec });
314
+ changed = true;
315
+ }
297
316
  }
298
317
  }
299
- }
318
+ };
319
+ maybeRewrite(pkg.dependencies);
320
+ maybeRewrite(pkg.devDependencies);
321
+ if (changed) await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
300
322
  };
301
- maybeRewrite(pkg.dependencies);
302
- maybeRewrite(pkg.devDependencies);
303
- if (entries.length) await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
323
+
324
+ // Root package.json first, then every app workspace package.json.
325
+ await rewritePackageJson(join(targetDir, 'package.json'));
326
+ const appsDir = join(targetDir, 'apps');
327
+ if (existsSync(appsDir)) {
328
+ let appEntries: import('node:fs').Dirent[] = [];
329
+ try { appEntries = await readdir(appsDir, { withFileTypes: true }); } catch {}
330
+ for (const e of appEntries) {
331
+ if (!e.isDirectory()) continue;
332
+ const appPkg = join(appsDir, e.name, 'package.json');
333
+ if (existsSync(appPkg)) await rewritePackageJson(appPkg);
334
+ }
335
+ }
336
+
304
337
  return { count: entries.length, root: localRoot, entries };
305
338
  }
306
339
 
@@ -320,18 +353,29 @@ async function runNpmLinkForDev(targetDir: string): Promise<number> {
320
353
  c.on('error', () => resolve(1));
321
354
  c.on('exit', (code) => resolve(code ?? 1));
322
355
  });
323
- // Also link each @bhooai/* that we rewrote to file: (best-effort, ignore failures)
324
- try {
325
- const pkgRaw = await readFile(join(targetDir, 'package.json'), 'utf8');
326
- const pkg = JSON.parse(pkgRaw);
327
- const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
328
- for (const name of Object.keys(allDeps)) if (name.startsWith('@bhooai/')) {
329
- await new Promise<void>((r) => {
330
- const c = spawn('npm', ['link', name], { cwd: targetDir, stdio: 'inherit', shell: process.platform === 'win32' });
331
- c.on('exit', () => r()); c.on('error', () => r());
332
- });
356
+ // Also link each @bhooai/* that we rewrote to file: in the root AND app
357
+ // workspace package.json (e.g. apps/admin's @bhooai/nexus-admin) — best-effort.
358
+ const collectBhooaiDeps = async (): Promise<string[]> => {
359
+ const names = new Set<string>();
360
+ const roots = [join(targetDir, 'package.json'), join(targetDir, 'apps', 'admin', 'package.json')];
361
+ for (const p of roots) {
362
+ try {
363
+ const raw = await readFile(p, 'utf8');
364
+ const pkg = JSON.parse(raw);
365
+ for (const obj of [pkg.dependencies ?? {}, pkg.devDependencies ?? {}]) {
366
+ for (const name of Object.keys(obj)) if (name.startsWith('@bhooai/')) names.add(name);
367
+ }
368
+ } catch {}
333
369
  }
334
- } catch {}
370
+ return [...names];
371
+ };
372
+ const bhooaiDeps = await collectBhooaiDeps();
373
+ for (const name of bhooaiDeps) {
374
+ await new Promise<void>((r) => {
375
+ const c = spawn('npm', ['link', name], { cwd: targetDir, stdio: 'inherit', shell: process.platform === 'win32' });
376
+ c.on('exit', () => r()); c.on('error', () => r());
377
+ });
378
+ }
335
379
  return linkTarget;
336
380
  }
337
381
 
@@ -1,11 +1,20 @@
1
1
  import { defineConfig } from 'vite';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
 
3
5
  // Backend host the dev proxy targets. Override with NEXUS_BACKEND_HOST when
4
6
  // deploying to a VPS (or running the backend on another machine). Defaults to
5
7
  // localhost for local development.
6
8
  const backendHost = process.env.NEXUS_BACKEND_HOST ?? 'localhost';
7
9
 
10
+ // Project root (where .env lives) = 2 levels up from this config file.
11
+ const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
12
+
8
13
  export default defineConfig({
14
+ // Expose NEXUS_PAYMENTS_* vars (from the project-root .env) to the browser
15
+ // via import.meta.env, alongside the default VITE_ prefix.
16
+ envDir: projectRoot,
17
+ envPrefix: ['VITE_', 'NEXUS_PAYMENTS_'],
9
18
  server: {
10
19
  port: <%= frontendPort %>,
11
20
  proxy: {
@@ -25,6 +34,19 @@ export default defineConfig({
25
34
  target: `ws://${backendHost}:<%= backendPort %>`,
26
35
  ws: true,
27
36
  },
37
+ '/graphql/ws': {
38
+ target: `ws://${backendHost}:<%= backendPort %>`,
39
+ ws: true,
40
+ changeOrigin: true,
41
+ },
42
+ '/graphql': {
43
+ target: `http://${backendHost}:<%= backendPort %>`,
44
+ changeOrigin: true,
45
+ },
46
+ '/csrf-token': {
47
+ target: `http://${backendHost}:<%= backendPort %>`,
48
+ changeOrigin: true,
49
+ },
28
50
  },
29
51
  },
30
- });
52
+ });
@@ -19,6 +19,10 @@ const config: Partial<NexusConfig> = {
19
19
  bodyLimit: 12 * 1024 * 1024,
20
20
  },
21
21
 
22
+ frontend: { port: <%= frontendPort %>, host: 'localhost', enabled: true },
23
+
24
+ admin: { port: <%= adminPort %>, host: 'localhost', enabled: true },
25
+
22
26
  uploads: {
23
27
  dir: 'storage/uploads',
24
28
  path: '/uploads',
@@ -27,17 +31,19 @@ const config: Partial<NexusConfig> = {
27
31
  allowedTypes: [],
28
32
  },
29
33
 
34
+ ai: {
35
+ serverUrl: process.env.NEXUS_AI_URL ?? `http://localhost:${process.env.AI_PORT ?? <%= aiPort %>}/v1`,
36
+ timeoutMs: 60_000,
37
+ defaultProvider: process.env.NEXUS_AI_PROVIDER ?? 'ollama',
38
+ schemaModel: process.env.NEXUS_AI_MODEL ?? 'llama3.1:8b',
39
+ },
40
+
30
41
  db: {
31
42
  uri: process.env.NEXUS_DB_URI ?? '<%= mongoUri %>',
32
43
  maxPoolSize: 10,
33
44
  autoIndex: true,
34
45
  },
35
46
 
36
- redis: {
37
- url: process.env.NEXUS_REDIS_URL ?? '<%= redisUrl %>',
38
- keyPrefix: '<%= nameSlug %>:',
39
- },
40
-
41
47
  graphql: {
42
48
  path: '/graphql',
43
49
  federation: 'in-process',
@@ -51,6 +57,11 @@ const config: Partial<NexusConfig> = {
51
57
  requireCsrf: true,
52
58
  },
53
59
 
60
+ redis: {
61
+ url: process.env.NEXUS_REDIS_URL ?? '<%= redisUrl %>',
62
+ keyPrefix: '<%= nameSlug %>:',
63
+ },
64
+
54
65
  auth: {
55
66
  jwt: {
56
67
  accessTtl: 60 * 15,
@@ -68,6 +79,9 @@ const config: Partial<NexusConfig> = {
68
79
  currency: 'INR',
69
80
  razorpay: { enabled: false, sandbox: true },
70
81
  paypal: { enabled: false, sandbox: true },
82
+ payoneer: { enabled: false, sandbox: true },
83
+ payu: { enabled: false, sandbox: true },
84
+ skrill: { enabled: false, sandbox: true },
71
85
  },
72
86
 
73
87
  email: { provider: 'log', from: 'no-reply@<%= nameSlug %>.local' },
@@ -78,20 +92,9 @@ const config: Partial<NexusConfig> = {
78
92
 
79
93
  webrtc: { rtcMinPort: 40000, rtcMaxPort: 40100, announceIp: '127.0.0.1' },
80
94
 
81
- ai: {
82
- serverUrl: process.env.NEXUS_AI_URL ?? `http://localhost:${process.env.AI_PORT ?? <%= aiPort %>}/v1`,
83
- timeoutMs: 60_000,
84
- defaultProvider: process.env.NEXUS_AI_PROVIDER ?? 'ollama',
85
- schemaModel: process.env.NEXUS_AI_MODEL ?? 'llama3.1:8b',
86
- },
87
-
88
95
  logging: { level: 'info', format: 'pretty', console: true, dir: 'logs', maxFileSize: 10 * 1024 * 1024, maxFiles: 7 },
89
96
 
90
97
  plugins: { dir: 'plugins', entries: [] },
91
-
92
- frontend: { port: <%= frontendPort %>, host: 'localhost', enabled: true },
93
-
94
- admin: { port: <%= adminPort %>, host: 'localhost', enabled: true },
95
98
  };
96
99
 
97
100
  export default config;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-core",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,8 +36,8 @@ import { createLazyDb, registerAdminRoutes, RequestLogBuffer } from './adminModu
36
36
  import { registerAuthRoutes } from './authModule.js';
37
37
  import { issueCsrfToken, getCsrfToken, csrf, authToken, requireRole } from '@bhooai/nexus-auth';
38
38
  import { connect } from '@bhooai/nexus-data';
39
- import { createGateway, createFederatedGateway, graphqlHttpHandler, getExplorerHtml, helloSubgraph } from '@bhooai/nexus-graphql';
40
- import type { Subgraph } from '@bhooai/nexus-graphql';
39
+ import { createGateway, createFederatedGateway, graphqlHttpHandler, getExplorerHtml, helloSubgraph, SubscriptionServer } from '@bhooai/nexus-graphql';
40
+ import type { Subgraph, GraphQLContext } from '@bhooai/nexus-graphql';
41
41
 
42
42
  export interface CreateNexusAppOptions {
43
43
  /** Identifier for this backend, used in admin, telemetry, logs. */
@@ -59,6 +59,11 @@ export interface CreateNexusAppOptions {
59
59
  /** Hooks for extending boot. */
60
60
  beforeStart?: (app: NexusApp) => Promise<void> | void;
61
61
  afterStart?: (app: NexusApp) => Promise<void> | void;
62
+ /**
63
+ * Extra values injected into every GraphQL resolver context (e.g. a payments
64
+ * service). Merged with the default `{ request, user }` context.
65
+ */
66
+ graphqlContext?: Record<string, unknown>;
62
67
  }
63
68
 
64
69
  export interface NexusApp {
@@ -249,6 +254,8 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
249
254
  // GraphQL gateway — builtin hello + discovered subgraphs (federated)
250
255
  // ------------------------------------------------------------------
251
256
  let graphqlMounted = false;
257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
+ let graphqlGateway: any = null;
252
259
  {
253
260
  const explorerEnabled = (config.graphql as unknown as { explorer?: boolean })?.explorer ?? true;
254
261
  const helloEnabled = (config.graphql as unknown as { hello?: boolean })?.hello !== false;
@@ -295,6 +302,7 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
295
302
  gateway,
296
303
  introspection: config.graphql?.introspection ?? true,
297
304
  requireMutationCsrf: config.graphql?.requireMutationCsrf ?? true,
305
+ context: (ctx) => ({ request: ctx, user: (ctx.state.user as GraphQLContext['user']), ...opts.graphqlContext }),
298
306
  });
299
307
  if (explorerEnabled) {
300
308
  const explorerHtml = getExplorerHtml({
@@ -314,6 +322,7 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
314
322
  }
315
323
  router.add('POST', graphqlPath, handler);
316
324
  graphqlMounted = true;
325
+ graphqlGateway = gateway;
317
326
  if (subgraphs.length === 1) {
318
327
  console.log(`[${name}] GraphQL gateway mounted at ${graphqlPath} (${subgraphs[0]!.name} subgraph${explorerEnabled ? ' + explorer at /graphiql' : ''})`);
319
328
  } else {
@@ -340,7 +349,7 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
340
349
  if (helloEnabled) {
341
350
  // hello is available even in fallback — route through its gateway
342
351
  const gw = createGateway({ subgraph: helloSubgraph });
343
- const h = graphqlHttpHandler({ gateway: gw, introspection: config.graphql?.introspection ?? true, requireMutationCsrf: config.graphql?.requireMutationCsrf ?? true });
352
+ const h = graphqlHttpHandler({ gateway: gw, introspection: config.graphql?.introspection ?? true, requireMutationCsrf: config.graphql?.requireMutationCsrf ?? true, context: (ctx) => ({ request: ctx, user: (ctx.state.user as GraphQLContext['user']), ...opts.graphqlContext }) });
344
353
  return h(ctx);
345
354
  }
346
355
  ctx.json({ errors: [{ message: 'No GraphQL subgraph found. Add one with: npx nexus make:subgraph <name> (then restart)' }] }, 404);
@@ -350,7 +359,7 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
350
359
  try {
351
360
  if (helloEnabled) {
352
361
  const gw = createGateway({ subgraph: helloSubgraph });
353
- router.add('POST', graphqlPath, graphqlHttpHandler({ gateway: gw, introspection: config.graphql?.introspection ?? true, requireMutationCsrf: config.graphql?.requireMutationCsrf ?? true }));
362
+ router.add('POST', graphqlPath, graphqlHttpHandler({ gateway: gw, introspection: config.graphql?.introspection ?? true, requireMutationCsrf: config.graphql?.requireMutationCsrf ?? true, context: (ctx) => ({ request: ctx, user: (ctx.state.user as GraphQLContext['user']), ...opts.graphqlContext }) }));
354
363
  } else {
355
364
  router.add('POST', graphqlPath, async (ctx) => ctx.json({ errors: [{ message: 'No GraphQL subgraph found. Add one with: npx nexus make:subgraph <name>' }] }, 404));
356
365
  }
@@ -526,6 +535,21 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
526
535
  if ((e as any)?.code !== 'ERR_MODULE_NOT_FOUND') console.warn('[realtime] ws init failed:', e);
527
536
  }
528
537
 
538
+ // ------------------------------------------------------------------
539
+ // GraphQL subscriptions over WebSocket — mount when a gateway exists
540
+ // and config.graphql.subscriptions is enabled.
541
+ // ------------------------------------------------------------------
542
+ const graphqlSubscriptions = (config.graphql as unknown as { subscriptions?: boolean })?.subscriptions ?? true;
543
+ if (graphqlGateway && graphqlSubscriptions && typeof graphqlGateway.subscribe === 'function') {
544
+ try {
545
+ const subPath = `${config.graphql?.path ?? '/graphql'}/ws`;
546
+ new SubscriptionServer({ httpServer: server.httpServer, gateway: graphqlGateway, path: subPath });
547
+ console.log(`[${name}] GraphQL subscriptions mounted at ${subPath}`);
548
+ } catch (err) {
549
+ console.warn(`[${name}] failed to mount GraphQL subscriptions:`, err);
550
+ }
551
+ }
552
+
529
553
  // Public uploads: serve GET /uploads/* from storage/uploads (file-storage example).
530
554
  // This was missing — 404 `No route for GET /uploads/media/...`.
531
555
  server.use(serveStatic(join(projectRoot, 'storage', 'uploads'), { prefix: '/uploads' }));
@@ -74,5 +74,11 @@ export async function mergeRuntimeJson(projectRoot: string, patch: Record<string
74
74
  if (patch.ai && typeof patch.ai === 'object' && existing.ai && typeof existing.ai === 'object') {
75
75
  merged.ai = { ...(existing as { ai: Record<string, unknown> }).ai, ...(patch.ai as Record<string, unknown>) };
76
76
  }
77
+ // Deep-merge `payments` so toggling one provider's enabled/sandbox state does
78
+ // NOT wipe the other providers' persisted state (shallow merge would replace
79
+ // the whole payments object).
80
+ if (patch.payments && typeof patch.payments === 'object' && existing.payments && typeof existing.payments === 'object') {
81
+ merged.payments = { ...(existing as { payments: Record<string, unknown> }).payments, ...(patch.payments as Record<string, unknown>) };
82
+ }
77
83
  await writeRuntimeJson(projectRoot, stripRedactionMask(merged) as Record<string, unknown>);
78
84
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-crypto",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-data",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-email",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,14 +1,16 @@
1
1
  # Blog CRUD example
2
2
 
3
- Posts + authors + comments with a REST API and an equivalent GraphQL subgraph.
4
- Best starting point for the `nexus-data` ODM.
3
+ Posts + authors + comments, exposed through a GraphQL subgraph served at
4
+ `/graphql`. Best starting point for the `nexus-data` ODM.
5
5
 
6
6
  ## What's here
7
7
 
8
8
  - `models/Post.ts`, `models/Author.ts`, `models/Comment.ts` — ODM documents
9
- - `routes/posts.ts` — full REST CRUD (`GET/POST /api/posts`, `GET/PUT/DELETE /api/posts/:id`, `POST /api/posts/:id/comments`)
10
- - `graphql/post.graph.ts` — federation subgraph: `typeDefs` + resolvers for posts, nested author + comments, and a `publishPost` mutation
11
- - `apps/frontend/src/main.tsx` — post list + create form (REST)
9
+ - `graphql/app.graph.ts` — subgraph: `typeDefs` + resolvers for posts, nested
10
+ author + comments, and create/update/delete/publish mutations
11
+ - `handlers/blog.handler.ts` — handler helpers extracted from the original REST
12
+ route (kept for reference / REST additions)
13
+ - `apps/frontend/src/main.tsx` — post list + create form (GraphQL client)
12
14
 
13
15
  ## Conventions shown
14
16
 
@@ -42,4 +44,4 @@ npx nexus dev
42
44
  ```
43
45
 
44
46
  Create a post, toggle publish, delete — all through the UI or directly against
45
- `/api/posts`.
47
+ `/graphql`.