bhooai-nexus 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-email",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@bhooai/nexus-core": "^0.1.0",
|
|
14
|
+
"nodemailer": "^6.9.16"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.5.0",
|
|
18
|
+
"@types/nodemailer": "^6.4.17",
|
|
19
|
+
"typescript": "^5.6.2",
|
|
20
|
+
"vitest": "^2.1.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { EmailProvider, EmailInput, EmailResult, QueueBackend } from './types.js';
|
|
2
|
+
import { renderTemplate } from './providers.js';
|
|
3
|
+
import { TemplateEngine } from './TemplateEngine.js';
|
|
4
|
+
|
|
5
|
+
export interface EmailServiceOptions {
|
|
6
|
+
provider: EmailProvider;
|
|
7
|
+
templates?: TemplateEngine;
|
|
8
|
+
/** Optional queue; when set, `queue()` enqueues and a worker drains it. */
|
|
9
|
+
queue?: QueueBackend;
|
|
10
|
+
/** Max send attempts per queued job (exponential backoff). */
|
|
11
|
+
maxAttempts?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* EmailService ties a provider + template engine + optional queue together.
|
|
16
|
+
* `send()` renders (if a template is named) and sends immediately.
|
|
17
|
+
* `queue()` enqueues for background delivery; `startWorker()` drains the queue
|
|
18
|
+
* with retries (exponential backoff) until success or max attempts exhausted.
|
|
19
|
+
*/
|
|
20
|
+
export class EmailService {
|
|
21
|
+
readonly provider: EmailProvider;
|
|
22
|
+
private readonly templates?: TemplateEngine;
|
|
23
|
+
private readonly queueBackend?: QueueBackend;
|
|
24
|
+
private readonly maxAttempts: number;
|
|
25
|
+
private running = false;
|
|
26
|
+
|
|
27
|
+
constructor(opts: EmailServiceOptions) {
|
|
28
|
+
this.provider = opts.provider;
|
|
29
|
+
this.templates = opts.templates;
|
|
30
|
+
this.queueBackend = opts.queue;
|
|
31
|
+
this.maxAttempts = opts.maxAttempts ?? 3;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async send(input: EmailInput): Promise<EmailResult> {
|
|
35
|
+
const rendered = this.templates ? renderTemplate(input, this.templates) : input;
|
|
36
|
+
return this.provider.send(rendered);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async queue(input: EmailInput): Promise<void> {
|
|
40
|
+
if (!this.queueBackend) throw new Error('[nexus-email] no queue configured');
|
|
41
|
+
const rendered = this.templates ? renderTemplate(input, this.templates) : input;
|
|
42
|
+
await this.queueBackend.push(rendered);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Start the background worker (idempotent). */
|
|
46
|
+
startWorker(): void {
|
|
47
|
+
if (!this.queueBackend || this.running) return;
|
|
48
|
+
this.running = true;
|
|
49
|
+
(this.queueBackend as any).subscribe?.((job: EmailInput) => this.processWithRetry(job));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Process a queued job with exponential backoff up to maxAttempts. */
|
|
53
|
+
private async processWithRetry(job: EmailInput): Promise<void> {
|
|
54
|
+
let attempt = 0;
|
|
55
|
+
let lastErr: unknown;
|
|
56
|
+
while (attempt < this.maxAttempts) {
|
|
57
|
+
try {
|
|
58
|
+
await this.provider.send(job);
|
|
59
|
+
return;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
lastErr = err;
|
|
62
|
+
attempt++;
|
|
63
|
+
if (attempt >= this.maxAttempts) break;
|
|
64
|
+
const backoff = 250 * 2 ** (attempt - 1);
|
|
65
|
+
await new Promise<void>((r) => setTimeout(r, backoff));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Give up; in production this should go to a dead-letter store.
|
|
69
|
+
console.error(`[nexus-email] delivery failed after ${this.maxAttempts} attempts:`, (lastErr as Error)?.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async close(): Promise<void> {
|
|
73
|
+
this.running = false;
|
|
74
|
+
await this.queueBackend?.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal template engine for emails: `{{name}}` interpolation, `{{#if cond}}…{{/if}}`
|
|
3
|
+
* conditionals (with optional `{{else}}`), and `{{#each items}}…{{this}}…{{/each}}`
|
|
4
|
+
* iteration over arrays. No external dependency (no Handlebars) — sufficient for
|
|
5
|
+
* transactional email templates. Each transformation is applied in sequence
|
|
6
|
+
* (each → if → interpolate); `#each` bodies are rendered recursively per item with
|
|
7
|
+
* `this` merged into the vars.
|
|
8
|
+
*/
|
|
9
|
+
export class TemplateEngine {
|
|
10
|
+
private readonly templates = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
register(name: string, body: string): void {
|
|
13
|
+
this.templates.set(name, body);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
has(name: string): boolean {
|
|
17
|
+
return this.templates.has(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render(name: string, vars: Record<string, unknown> = {}): string {
|
|
21
|
+
const body = this.templates.get(name);
|
|
22
|
+
if (body == null) throw new Error(`[nexus-email] unknown template: ${name}`);
|
|
23
|
+
return this.renderString(body, vars);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
renderString(tpl: string, vars: Record<string, unknown>): string {
|
|
27
|
+
return interpolate(expandIf(expandEach(tpl, vars), vars), vars);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Expand `{{#each key}}…{{/each}}` by rendering the body recursively per item. */
|
|
32
|
+
function expandEach(tpl: string, vars: Record<string, unknown>): string {
|
|
33
|
+
return tpl.replace(/\{\{#each\s+(\w+(?:\.\w+)*)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g, (_m, key: string, body: string) => {
|
|
34
|
+
const arr = lookup(vars, key.trim());
|
|
35
|
+
if (!Array.isArray(arr)) return '';
|
|
36
|
+
return arr.map((item) => interpolate(expandIf(expandEach(body, { ...vars, this: item }), { ...vars, this: item }), { ...vars, this: item })).join('');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Expand `{{#if cond}}…{{else}}…{{/if}}` (cond truthy; empty arrays are falsy). */
|
|
41
|
+
function expandIf(tpl: string, vars: Record<string, unknown>): string {
|
|
42
|
+
return tpl.replace(/\{\{#if\s+([^}]+?)\s*\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_m, cond: string, then: string, els?: string) => {
|
|
43
|
+
const val = lookup(vars, cond.trim());
|
|
44
|
+
const truthy = Array.isArray(val) ? val.length > 0 : !!val;
|
|
45
|
+
return truthy ? then : (els ?? '');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Replace `{{key}}` (and `{{ a.b.c }}`) with values; unknown keys → ''. */
|
|
50
|
+
function interpolate(tpl: string, vars: Record<string, unknown>): string {
|
|
51
|
+
return tpl.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_m, expr: string) => {
|
|
52
|
+
const val = lookup(vars, expr.trim());
|
|
53
|
+
return val == null ? '' : String(val);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function lookup(vars: Record<string, unknown>, path: string): unknown {
|
|
58
|
+
return path.split('.').reduce<unknown>((acc, key) => {
|
|
59
|
+
if (acc == null || typeof acc !== 'object') return undefined;
|
|
60
|
+
return (acc as Record<string, unknown>)[key];
|
|
61
|
+
}, vars);
|
|
62
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './TemplateEngine.js';
|
|
3
|
+
export * from './providers.js';
|
|
4
|
+
export * from './queue.js';
|
|
5
|
+
export * from './EmailService.js';
|
|
6
|
+
|
|
7
|
+
import { SmtpProvider, LogProvider } from './providers.js';
|
|
8
|
+
import { TemplateEngine } from './TemplateEngine.js';
|
|
9
|
+
import { EmailService, type EmailServiceOptions } from './EmailService.js';
|
|
10
|
+
import type { EmailProvider, QueueBackend } from './types.js';
|
|
11
|
+
|
|
12
|
+
export interface EmailConfigLike {
|
|
13
|
+
provider: 'smtp' | 'log';
|
|
14
|
+
smtp?: { host: string; port: number; secure: boolean; user: string; pass: string };
|
|
15
|
+
from: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateEmailOptions {
|
|
19
|
+
/** Inject a custom provider (tests). */
|
|
20
|
+
provider?: EmailProvider;
|
|
21
|
+
/** Inject a queue backend (default: none → send is synchronous only). */
|
|
22
|
+
queue?: QueueBackend;
|
|
23
|
+
templates?: TemplateEngine;
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build an EmailService from the `email` config section. `provider: 'log'`
|
|
29
|
+
* never sends (dev/test); `provider: 'smtp'` uses nodemailer with the smtp config.
|
|
30
|
+
*/
|
|
31
|
+
export function createEmail(config: EmailConfigLike, opts: CreateEmailOptions = {}): EmailService {
|
|
32
|
+
const provider: EmailProvider = opts.provider
|
|
33
|
+
?? (config.provider === 'smtp' && config.smtp
|
|
34
|
+
? new SmtpProvider(config.smtp)
|
|
35
|
+
: new LogProvider());
|
|
36
|
+
return new EmailService({ provider, templates: opts.templates, queue: opts.queue, maxAttempts: opts.maxAttempts });
|
|
37
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import nodemailer, { type Transport } from 'nodemailer';
|
|
2
|
+
import type { EmailProvider, EmailInput, EmailResult } from './types.js';
|
|
3
|
+
import { TemplateEngine } from './TemplateEngine.js';
|
|
4
|
+
|
|
5
|
+
/** SMTP provider backed by nodemailer. The transport is injectable for tests. */
|
|
6
|
+
export class SmtpProvider implements EmailProvider {
|
|
7
|
+
readonly name = 'smtp';
|
|
8
|
+
private readonly transport: { sendMail: (opts: any) => Promise<unknown> };
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
opts: {
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
secure: boolean;
|
|
15
|
+
user: string;
|
|
16
|
+
pass: string;
|
|
17
|
+
},
|
|
18
|
+
/** Inject a custom nodemailer Transport (tests use `jsonTransport`). */
|
|
19
|
+
customTransport?: Transport,
|
|
20
|
+
) {
|
|
21
|
+
this.transport = customTransport
|
|
22
|
+
? nodemailer.createTransport(customTransport as any)
|
|
23
|
+
: nodemailer.createTransport({ host: opts.host, port: opts.port, secure: opts.secure, auth: { user: opts.user, pass: opts.pass } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async send(input: EmailInput): Promise<EmailResult> {
|
|
27
|
+
const res = (await this.transport.sendMail({
|
|
28
|
+
to: input.to,
|
|
29
|
+
cc: input.cc,
|
|
30
|
+
bcc: input.bcc,
|
|
31
|
+
subject: input.subject,
|
|
32
|
+
html: input.html,
|
|
33
|
+
text: input.text,
|
|
34
|
+
attachments: input.attachments,
|
|
35
|
+
replyTo: input.replyTo,
|
|
36
|
+
headers: input.headers,
|
|
37
|
+
})) as { messageId?: string; envelope?: unknown };
|
|
38
|
+
return { messageId: res.messageId, envelope: res.envelope, raw: res, input };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Log provider: writes the email to the logger instead of sending it. Used when
|
|
44
|
+
* `config.email.provider === 'log'` (dev/test) so the app runs without SMTP.
|
|
45
|
+
*/
|
|
46
|
+
export class LogProvider implements EmailProvider {
|
|
47
|
+
readonly name = 'log';
|
|
48
|
+
private readonly log: (line: string) => void;
|
|
49
|
+
constructor(log?: (line: string) => void) {
|
|
50
|
+
this.log = log ?? ((line) => console.log(`[nexus-email] ${line}`));
|
|
51
|
+
}
|
|
52
|
+
async send(input: EmailInput): Promise<EmailResult> {
|
|
53
|
+
this.log(JSON.stringify({ name: input.to, subject: input.subject, text: input.text ?? input.html }));
|
|
54
|
+
return { messageId: `log-${Date.now()}`, input };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Render an EmailInput's template (if any) against the provided engine. */
|
|
59
|
+
export function renderTemplate(input: EmailInput, engine: TemplateEngine): EmailInput {
|
|
60
|
+
if (!input.template) return input;
|
|
61
|
+
const html = engine.render(input.template, input.vars ?? {});
|
|
62
|
+
return { ...input, html: input.html ?? html, subject: /\{\{/.test(input.subject) ? engine.renderString(input.subject, input.vars ?? {}) : input.subject };
|
|
63
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { EmailInput, QueueBackend } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory queue backend: push appends to an array; the consumer drains it on a
|
|
5
|
+
* poll interval. Sufficient for single-instance apps and tests; RedisQueue is the
|
|
6
|
+
* horizontal-scale option (below). The queue never blocks the sender.
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryQueue implements QueueBackend {
|
|
9
|
+
private readonly jobs: EmailInput[] = [];
|
|
10
|
+
private readonly consumers = new Set<(job: EmailInput) => Promise<void>>();
|
|
11
|
+
private readonly interval: ReturnType<typeof setInterval>;
|
|
12
|
+
private closed = false;
|
|
13
|
+
|
|
14
|
+
constructor(pollMs = 50) {
|
|
15
|
+
this.interval = setInterval(() => void this.drain(), pollMs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async push(job: EmailInput): Promise<void> {
|
|
19
|
+
if (this.closed) throw new Error('[nexus-email] queue closed');
|
|
20
|
+
this.jobs.push(job);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
subscribe(consumer: (job: EmailInput) => Promise<void>): void {
|
|
24
|
+
this.consumers.add(consumer);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async drain(): Promise<void> {
|
|
28
|
+
while (this.jobs.length > 0 && this.consumers.size > 0) {
|
|
29
|
+
const job = this.jobs.shift();
|
|
30
|
+
if (!job) break;
|
|
31
|
+
for (const c of this.consumers) {
|
|
32
|
+
await c(job);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get pending(): number {
|
|
38
|
+
return this.jobs.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close(): Promise<void> {
|
|
42
|
+
this.closed = true;
|
|
43
|
+
clearInterval(this.interval);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Redis-backed queue using LPUSH (producer) + RPOP (consumer poll). Uses a
|
|
49
|
+
* dedicated redis client (the `redis` package). Jobs are JSON-serialized
|
|
50
|
+
* EmailInputs. Gracefully degrades to no-op if a client can't connect — callers
|
|
51
|
+
* that need guaranteed delivery should check connectivity at boot.
|
|
52
|
+
*/
|
|
53
|
+
export class RedisQueue implements QueueBackend {
|
|
54
|
+
private readonly key: string;
|
|
55
|
+
private client: any;
|
|
56
|
+
private connected = false;
|
|
57
|
+
private readonly consumers = new Set<(job: EmailInput) => Promise<void>>();
|
|
58
|
+
private interval: ReturnType<typeof setInterval> | null = null;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly createClient: () => any,
|
|
62
|
+
key = 'nexus:email:queue',
|
|
63
|
+
pollMs = 100,
|
|
64
|
+
) {
|
|
65
|
+
this.key = key;
|
|
66
|
+
this.interval = setInterval(() => void this.drain(), pollMs);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async ensureConnected(): Promise<void> {
|
|
70
|
+
if (this.connected) return;
|
|
71
|
+
this.client = this.createClient();
|
|
72
|
+
await this.client.connect();
|
|
73
|
+
this.connected = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async push(job: EmailInput): Promise<void> {
|
|
77
|
+
await this.ensureConnected();
|
|
78
|
+
await this.client.lPush(this.key, JSON.stringify(job));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
subscribe(consumer: (job: EmailInput) => Promise<void>): void {
|
|
82
|
+
this.consumers.add(consumer);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async drain(): Promise<void> {
|
|
86
|
+
if (!this.connected || this.consumers.size === 0) return;
|
|
87
|
+
try {
|
|
88
|
+
while (true) {
|
|
89
|
+
const raw = (await this.client.rPop(this.key)) as string | null;
|
|
90
|
+
if (!raw) break;
|
|
91
|
+
const job = JSON.parse(raw) as EmailInput;
|
|
92
|
+
for (const c of this.consumers) await c(job);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// transient redis errors shouldn't kill the poller
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async close(): Promise<void> {
|
|
100
|
+
if (this.interval) clearInterval(this.interval);
|
|
101
|
+
if (this.connected) await this.client?.quit();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Email-domain types shared by providers, templates, and the service. */
|
|
2
|
+
|
|
3
|
+
export interface EmailAttachment {
|
|
4
|
+
filename: string;
|
|
5
|
+
content: string | Buffer;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
/** Path to a file on disk (alternative to inline `content`). */
|
|
8
|
+
path?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EmailInput {
|
|
12
|
+
to: string | string[];
|
|
13
|
+
cc?: string | string[];
|
|
14
|
+
bcc?: string | string[];
|
|
15
|
+
subject: string;
|
|
16
|
+
/** HTML body. */
|
|
17
|
+
html?: string;
|
|
18
|
+
/** Plain-text body. */
|
|
19
|
+
text?: string;
|
|
20
|
+
/** Named template to render (used by EmailService, not providers directly). */
|
|
21
|
+
template?: string;
|
|
22
|
+
/** Variables for the template. */
|
|
23
|
+
vars?: Record<string, unknown>;
|
|
24
|
+
attachments?: EmailAttachment[];
|
|
25
|
+
replyTo?: string;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EmailResult {
|
|
30
|
+
/** Provider message id. */
|
|
31
|
+
messageId?: string;
|
|
32
|
+
/** Provider response envelope (nodemailer). */
|
|
33
|
+
envelope?: unknown;
|
|
34
|
+
/** Raw response for debugging. */
|
|
35
|
+
raw?: unknown;
|
|
36
|
+
/** The input that was sent (for queue/audit). */
|
|
37
|
+
input: EmailInput;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EmailProvider {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
send(input: EmailInput): Promise<EmailResult>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Generic send-queue backend (memory or Redis). */
|
|
46
|
+
export interface QueueBackend {
|
|
47
|
+
push(job: EmailInput): Promise<void>;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-graphql",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@bhooai/nexus-core": "^0.1.0",
|
|
14
|
+
"@bhooai/nexus-auth": "^0.1.0",
|
|
15
|
+
"graphql": "^16.9.0",
|
|
16
|
+
"ws": "^8.18.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.5.0",
|
|
20
|
+
"@types/ws": "^8.5.13",
|
|
21
|
+
"typescript": "^5.6.2",
|
|
22
|
+
"vitest": "^2.1.1"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { buildSchema, isObjectType, isInterfaceType, type GraphQLSchema, type GraphQLObjectType, type GraphQLInterfaceType } from 'graphql';
|
|
2
|
+
import type { Subgraph } from '../types.js';
|
|
3
|
+
import { parseFederationMetadata, type TypeMeta } from './parseMetadata.js';
|
|
4
|
+
import { buildPublicSchema } from '../gateway/publicSchema.js';
|
|
5
|
+
|
|
6
|
+
export interface EntitySource {
|
|
7
|
+
subgraph: string;
|
|
8
|
+
keyFields: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface RequiresRecord {
|
|
11
|
+
subgraph: string;
|
|
12
|
+
requiresFields: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Supergraph {
|
|
16
|
+
subgraphs: Subgraph[];
|
|
17
|
+
/** typeName -> fieldName -> owning subgraph name. */
|
|
18
|
+
ownership: Map<string, Map<string, string>>;
|
|
19
|
+
/** typeName -> subgraphs that can resolve it via _entities (have @key). */
|
|
20
|
+
entities: Map<string, EntitySource[]>;
|
|
21
|
+
/** typeName -> fieldName -> subgraph that @provides the field. */
|
|
22
|
+
provides: Map<string, Map<string, string>>;
|
|
23
|
+
/** typeName -> fieldName -> { subgraph, requiresFields }. */
|
|
24
|
+
requires: Map<string, Map<string, RequiresRecord>>;
|
|
25
|
+
/** Client-facing merged SDL (no federation directives). */
|
|
26
|
+
mergedSdl: string;
|
|
27
|
+
/** Schema built from mergedSdl (for query planning/type lookup only). */
|
|
28
|
+
schema: GraphQLSchema;
|
|
29
|
+
/** Per-subgraph public schemas (type info). */
|
|
30
|
+
subgraphSchemas: Map<string, GraphQLSchema>;
|
|
31
|
+
/** Per-subgraph federation metadata. */
|
|
32
|
+
metadata: Map<string, Map<string, TypeMeta>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ROOT_TYPES = ['Query', 'Mutation', 'Subscription'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compose multiple subgraphs into a Supergraph: collect field ownership,
|
|
39
|
+
* entity @key sources, @provides and @requires records, and a merged public SDL.
|
|
40
|
+
*
|
|
41
|
+
* Ownership rule (v1): a field is owned by a subgraph if that subgraph's SDL
|
|
42
|
+
* defines the field on the type and it is NOT `@external`. The first subgraph
|
|
43
|
+
* to claim a non-external field wins; a conflicting claim by a second subgraph
|
|
44
|
+
* is recorded as a composition warning (real federation would arbitrate via
|
|
45
|
+
* `@shareable`/`@override` — out of scope for v1).
|
|
46
|
+
*/
|
|
47
|
+
export function composeSupergraph(subgraphs: Subgraph[]): Supergraph {
|
|
48
|
+
if (subgraphs.length === 0) throw new Error('[nexus-graphql] composeSupergraph requires subgraphs');
|
|
49
|
+
|
|
50
|
+
const ownership = new Map<string, Map<string, string>>();
|
|
51
|
+
const entities = new Map<string, EntitySource[]>();
|
|
52
|
+
const provides = new Map<string, Map<string, string>>();
|
|
53
|
+
const requires = new Map<string, Map<string, RequiresRecord>>();
|
|
54
|
+
const subgraphSchemas = new Map<string, GraphQLSchema>();
|
|
55
|
+
const metadata = new Map<string, Map<string, TypeMeta>>();
|
|
56
|
+
const conflicts: string[] = [];
|
|
57
|
+
|
|
58
|
+
for (const sub of subgraphs) {
|
|
59
|
+
const pub = buildPublicSchema(sub.sdl);
|
|
60
|
+
subgraphSchemas.set(sub.name, pub);
|
|
61
|
+
const meta = parseFederationMetadata(sub.sdl);
|
|
62
|
+
metadata.set(sub.name, meta);
|
|
63
|
+
|
|
64
|
+
for (const [typeName, typeMeta] of meta) {
|
|
65
|
+
// Entity sources: every @key on the type makes the subgraph a resolver.
|
|
66
|
+
if (typeMeta.keys.length) {
|
|
67
|
+
const list = entities.get(typeName) ?? [];
|
|
68
|
+
for (const keyFields of typeMeta.keys) list.push({ subgraph: sub.name, keyFields });
|
|
69
|
+
entities.set(typeName, list);
|
|
70
|
+
}
|
|
71
|
+
for (const [fieldName, fieldMeta] of typeMeta.fields) {
|
|
72
|
+
if (fieldMeta.external) continue; // resolved elsewhere — not owned here
|
|
73
|
+
const typeOwn = ownership.get(typeName) ?? new Map<string, string>();
|
|
74
|
+
if (typeOwn.has(fieldName) && typeOwn.get(fieldName) !== sub.name) {
|
|
75
|
+
conflicts.push(`${typeName}.${fieldName} claimed by ${typeOwn.get(fieldName)} and ${sub.name} (v1: first wins)`);
|
|
76
|
+
} else {
|
|
77
|
+
typeOwn.set(fieldName, sub.name);
|
|
78
|
+
}
|
|
79
|
+
ownership.set(typeName, typeOwn);
|
|
80
|
+
if (fieldMeta.provides) {
|
|
81
|
+
const tp = provides.get(typeName) ?? new Map<string, string>();
|
|
82
|
+
tp.set(fieldName, sub.name);
|
|
83
|
+
provides.set(typeName, tp);
|
|
84
|
+
}
|
|
85
|
+
if (fieldMeta.requires) {
|
|
86
|
+
const tr = requires.get(typeName) ?? new Map<string, RequiresRecord>();
|
|
87
|
+
tr.set(fieldName, { subgraph: sub.name, requiresFields: fieldMeta.requires });
|
|
88
|
+
requires.set(typeName, tr);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mergedSdl = mergeSdl(subgraphs, subgraphSchemas, metadata, ownership);
|
|
95
|
+
const schema = buildSchema(mergedSdl);
|
|
96
|
+
|
|
97
|
+
return { subgraphs, ownership, entities, provides, requires, mergedSdl, schema, subgraphSchemas, metadata };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Build the merged public SDL from all subgraphs' public types. */
|
|
101
|
+
function mergeSdl(
|
|
102
|
+
subgraphs: Subgraph[],
|
|
103
|
+
subgraphSchemas: Map<string, GraphQLSchema>,
|
|
104
|
+
_metadata: Map<string, Map<string, TypeMeta>>,
|
|
105
|
+
ownership: Map<string, Map<string, string>>,
|
|
106
|
+
): string {
|
|
107
|
+
// Collect each type's fields (name -> SDL type string) from the owning subgraph's public schema.
|
|
108
|
+
const typeFields = new Map<string, Map<string, string>>();
|
|
109
|
+
const rootFields = new Map<string, Map<string, string>>(); // Query/Mutation/Subscription
|
|
110
|
+
const kinds = new Map<string, 'type' | 'interface'>();
|
|
111
|
+
|
|
112
|
+
for (const sub of subgraphs) {
|
|
113
|
+
const pub = subgraphSchemas.get(sub.name)!;
|
|
114
|
+
const typeMap = pub.getTypeMap();
|
|
115
|
+
for (const [typeName, gqlType] of Object.entries(typeMap)) {
|
|
116
|
+
if (typeName.startsWith('__') || typeName.startsWith('_')) continue;
|
|
117
|
+
if (isObjectType(gqlType)) {
|
|
118
|
+
const isRoot = ROOT_TYPES.includes(typeName);
|
|
119
|
+
const target = isRoot ? (rootFields.get(typeName) ?? new Map<string, string>()) : (typeFields.get(typeName) ?? new Map<string, string>());
|
|
120
|
+
kinds.set(typeName, 'type');
|
|
121
|
+
for (const field of Object.values(gqlType.getFields())) {
|
|
122
|
+
const owner = ownership.get(typeName)?.get(field.name);
|
|
123
|
+
// Include the field if this subgraph owns it (or it's a root field here).
|
|
124
|
+
if (isRoot || owner === sub.name) {
|
|
125
|
+
target.set(field.name, field.type.toString());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (isRoot) rootFields.set(typeName, target); else typeFields.set(typeName, target);
|
|
129
|
+
} else if (isInterfaceType(gqlType)) {
|
|
130
|
+
const target = typeFields.get(typeName) ?? new Map<string, string>();
|
|
131
|
+
kinds.set(typeName, 'interface');
|
|
132
|
+
for (const field of Object.values(gqlType.getFields())) {
|
|
133
|
+
if (ownership.get(typeName)?.get(field.name) === sub.name) target.set(field.name, field.type.toString());
|
|
134
|
+
}
|
|
135
|
+
typeFields.set(typeName, target);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const parts: string[] = [];
|
|
141
|
+
for (const [typeName, fields] of rootFields) {
|
|
142
|
+
parts.push(`type ${typeName} {\n${fieldLines(fields)}\n}`);
|
|
143
|
+
}
|
|
144
|
+
for (const [typeName, fields] of typeFields) {
|
|
145
|
+
const kind = kinds.get(typeName) ?? 'type';
|
|
146
|
+
parts.push(`${kind} ${typeName} {\n${fieldLines(fields)}\n}`);
|
|
147
|
+
}
|
|
148
|
+
return parts.join('\n\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function fieldLines(fields: Map<string, string>): string {
|
|
152
|
+
return [...fields.entries()].map(([name, type]) => ` ${name}: ${type}`).join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parse, type DocumentNode, type ExecutionResult } from 'graphql';
|
|
2
|
+
import type { Subgraph, GraphQLContext, ExecuteParams } from '../types.js';
|
|
3
|
+
import { composeSupergraph, type Supergraph } from './composeSupergraph.js';
|
|
4
|
+
import { executeFederated, planQuery, type FetchPlan } from './executor.js';
|
|
5
|
+
|
|
6
|
+
export interface FederatedGateway {
|
|
7
|
+
/** Client-facing merged schema. */
|
|
8
|
+
schema: Supergraph['schema'];
|
|
9
|
+
supergraph: Supergraph;
|
|
10
|
+
subgraphs: Subgraph[];
|
|
11
|
+
execute(params: Omit<ExecuteParams, 'schema' | 'resolvers'>): Promise<ExecutionResult>;
|
|
12
|
+
/** Inspect the fetch plan for a query (golden tests / debugging). */
|
|
13
|
+
plan(document: DocumentNode): FetchPlan;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compose multiple subgraphs into a federated gateway. The gateway exposes the
|
|
18
|
+
* merged public schema and executes queries via the naive sequential federated
|
|
19
|
+
* executor (root fetches + iterative entity-join passes). Subscriptions are not
|
|
20
|
+
* federated in v1 — route them to the owning subgraph's `SubscriptionServer`.
|
|
21
|
+
*/
|
|
22
|
+
export function createFederatedGateway(subgraphs: Subgraph[]): FederatedGateway {
|
|
23
|
+
const supergraph = composeSupergraph(subgraphs);
|
|
24
|
+
return {
|
|
25
|
+
schema: supergraph.schema,
|
|
26
|
+
supergraph,
|
|
27
|
+
subgraphs,
|
|
28
|
+
async execute(params) {
|
|
29
|
+
const document: DocumentNode = params.document;
|
|
30
|
+
return executeFederated(supergraph, document, params.variableValues, (params.contextValue as GraphQLContext) ?? {});
|
|
31
|
+
},
|
|
32
|
+
plan(document) {
|
|
33
|
+
return planQuery(supergraph, document);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|