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.
- package/package.json +1 -1
- package/packages/nexus-admin/package.json +1 -1
- package/packages/nexus-admin/src/App.tsx +23 -0
- package/packages/nexus-ads/package.json +1 -1
- package/packages/nexus-ai-client/package.json +1 -1
- package/packages/nexus-auth/package.json +1 -1
- package/packages/nexus-cache/package.json +1 -1
- package/packages/nexus-cli/package.json +1 -1
- package/packages/nexus-cli/src/commands/init.ts +92 -48
- package/packages/nexus-cli/templates/base/apps/frontend/vite.config.ts.ejs +23 -1
- package/packages/nexus-cli/templates/base/nexus.config.ts.ejs +19 -16
- package/packages/nexus-core/package.json +1 -1
- package/packages/nexus-core/src/app/createNexusApp.ts +28 -4
- package/packages/nexus-core/src/config/runtimeJson.ts +6 -0
- package/packages/nexus-crypto/package.json +1 -1
- package/packages/nexus-data/package.json +1 -1
- package/packages/nexus-email/package.json +1 -1
- package/packages/nexus-examples/examples/blog-crud/README.md +8 -6
- package/packages/nexus-examples/examples/blog-crud/apps/frontend/src/main.tsx +45 -16
- package/packages/nexus-examples/examples/chat/apps/backend/src/graphql/app.graph.ts +39 -4
- package/packages/nexus-examples/examples/chat/apps/backend/src/routes/index.ts +0 -11
- package/packages/nexus-examples/examples/chat/apps/backend/src/ws/chat.room.ts +5 -0
- package/packages/nexus-examples/examples/chat/apps/frontend/src/main.tsx +192 -70
- package/packages/nexus-examples/examples/checkout/apps/backend/src/graphql/app.graph.ts +57 -12
- package/packages/nexus-examples/examples/checkout/apps/backend/src/main.ts +28 -0
- package/packages/nexus-examples/examples/checkout/apps/backend/src/routes/index.ts +30 -0
- package/packages/nexus-examples/examples/checkout/apps/frontend/index.html +13 -0
- package/packages/nexus-examples/examples/checkout/apps/frontend/src/main.tsx +190 -18
- package/packages/nexus-examples/examples/dashboard/README.md +3 -2
- package/packages/nexus-examples/examples/dashboard/apps/backend/src/routes/metrics.ts +1 -1
- package/packages/nexus-examples/examples/dashboard/apps/frontend/src/main.tsx +8 -2
- package/packages/nexus-examples/examples/file-storage/README.md +4 -2
- package/packages/nexus-examples/examples/file-storage/apps/frontend/src/main.tsx +24 -7
- package/packages/nexus-examples/examples/multi-app/apps/frontend-shop/src/main.tsx +2 -1
- package/packages/nexus-examples/examples/saas-starter/apps/backend/src/graphql/app.graph.ts +9 -8
- package/packages/nexus-examples/examples/saas-starter/apps/frontend/src/main.tsx +18 -13
- package/packages/nexus-examples/package.json +1 -1
- package/packages/nexus-graphql/package.json +1 -1
- package/packages/nexus-graphql/src/explorer/explorerHtml.ts +278 -131
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +6 -1
- package/packages/nexus-graphql/src/federation/executor.ts +61 -6
- package/packages/nexus-payments/package.json +1 -1
- package/packages/nexus-payments/src/providers/PayUProvider.ts +1 -0
- package/packages/nexus-plugins/package.json +1 -1
- package/packages/nexus-postcss/package.json +1 -1
- package/packages/nexus-realtime/package.json +1 -1
- package/packages/nexus-safe-goto/package.json +1 -1
- 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.
|
|
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",
|
|
@@ -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>
|
|
@@ -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/*
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
for (const
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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,14 +1,16 @@
|
|
|
1
1
|
# Blog CRUD example
|
|
2
2
|
|
|
3
|
-
Posts + authors + comments
|
|
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
|
-
- `
|
|
10
|
-
|
|
11
|
-
- `
|
|
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
|
-
`/
|
|
47
|
+
`/graphql`.
|