Integra Products e adapters S3/SMTP; corrige desvios pós-merge dos agentes
- Wire ProductsModule em app.module.ts; troca o import "deep path" que o agente usou (packages/database/dist/...) pela exportação correta via barrel (@eden/database), já que packages/database/src/index.ts é meu para editar durante a integração. - S3/SMTP: troca leitura direta de process.env por loadEnv()/@eden/config (que não existia ainda quando o agente rodou no worktree dele). - Resolve conflito de pnpm-lock.yaml (regenerado via pnpm install). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* External adapters (Focus NFe, SaperX, Control iD, S3, SMTP) — each
|
||||
* isolated behind a port per Master Prompt §13. Empty placeholder; the
|
||||
* first real adapter (S3 or SMTP) lands with the rest of Fase 1's
|
||||
* platform bootstrap.
|
||||
* isolated behind a port per Master Prompt §13. S3 and SMTP adapters land
|
||||
* first (platform bootstrap); the others follow per-domain as their
|
||||
* modules are built.
|
||||
*/
|
||||
export const INTEGRATIONS_PACKAGE_VERSION = "0.1.0";
|
||||
export * from "./s3/client.js";
|
||||
export * from "./smtp/mailer.js";
|
||||
|
||||
128
packages/integrations/src/s3/client.ts
Normal file
128
packages/integrations/src/s3/client.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* S3 adapter — port of the legacy `server/src/lib/s3.js` (eden.md §5
|
||||
* "Armazenamento S3"). Compatible with any S3-like endpoint (MinIO, AWS S3),
|
||||
* not exclusive to AWS. Storage failures are never swallowed: every
|
||||
* function throws a clear error when S3 isn't configured, unlike the SMTP
|
||||
* adapter (see `../smtp/mailer.ts`) which fails silently by design.
|
||||
*/
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import type { Readable } from "node:stream";
|
||||
import { loadEnv } from "@eden/config";
|
||||
|
||||
const NOT_CONFIGURED_MESSAGE = "S3 não configurado (S3_ENDPOINT ausente)";
|
||||
|
||||
interface S3Config {
|
||||
endpoint: string;
|
||||
region: string;
|
||||
forcePathStyle: boolean;
|
||||
accessKeyId: string | undefined;
|
||||
secretAccessKey: string | undefined;
|
||||
bucket: string | undefined;
|
||||
}
|
||||
|
||||
let client: S3Client | undefined;
|
||||
let config: S3Config | undefined;
|
||||
|
||||
function loadConfig(): S3Config | undefined {
|
||||
const env = loadEnv();
|
||||
if (!env.S3_ENDPOINT) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
endpoint: env.S3_ENDPOINT,
|
||||
region: env.S3_REGION,
|
||||
forcePathStyle: env.S3_FORCE_PATH_STYLE,
|
||||
accessKeyId: env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
||||
bucket: env.S3_BUCKET,
|
||||
};
|
||||
}
|
||||
|
||||
function requireConfig(): S3Config {
|
||||
if (!config) {
|
||||
config = loadConfig();
|
||||
}
|
||||
if (!config) {
|
||||
throw new Error(NOT_CONFIGURED_MESSAGE);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw S3 client, exported for callers (e.g. a future Backup module) that
|
||||
* need multipart streaming upload via `@aws-sdk/lib-storage`'s `Upload`
|
||||
* class directly instead of `uploadObject` (which buffers the whole file
|
||||
* in memory — fine for small files, not for a Postgres dump).
|
||||
*/
|
||||
export function getClient(): S3Client {
|
||||
const cfg = requireConfig();
|
||||
if (!client) {
|
||||
client = new S3Client({
|
||||
endpoint: cfg.endpoint,
|
||||
region: cfg.region,
|
||||
forcePathStyle: cfg.forcePathStyle,
|
||||
...(cfg.accessKeyId && cfg.secretAccessKey
|
||||
? {
|
||||
credentials: {
|
||||
accessKeyId: cfg.accessKeyId,
|
||||
secretAccessKey: cfg.secretAccessKey,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Single bucket used for all data (no bucket-per-data-type). */
|
||||
export function getBucket(): string {
|
||||
const cfg = requireConfig();
|
||||
return cfg.bucket ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple PUT, whole buffer in memory — adequate for small/medium files
|
||||
* (images, PDFs, attachments), not for large streaming uploads.
|
||||
*/
|
||||
export async function uploadObject(
|
||||
key: string,
|
||||
buffer: Buffer,
|
||||
mimeType: string,
|
||||
): Promise<void> {
|
||||
const s3 = getClient();
|
||||
const bucket = getBucket();
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: buffer,
|
||||
ContentType: mimeType,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET, returning the object body as a Node stream — used both to serve a
|
||||
* download and to pipe directly to an HTTP response. On Node.js, the SDK
|
||||
* v3 response `.Body` is already a `Readable` (with extra helper mixins);
|
||||
* this just types it correctly for callers.
|
||||
*/
|
||||
export async function getObjectStream(key: string): Promise<Readable> {
|
||||
const s3 = getClient();
|
||||
const bucket = getBucket();
|
||||
const response = await s3.send(
|
||||
new GetObjectCommand({ Bucket: bucket, Key: key }),
|
||||
);
|
||||
return response.Body as Readable;
|
||||
}
|
||||
|
||||
export async function deleteObject(key: string): Promise<void> {
|
||||
const s3 = getClient();
|
||||
const bucket = getBucket();
|
||||
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
||||
}
|
||||
76
packages/integrations/src/smtp/mailer.ts
Normal file
76
packages/integrations/src/smtp/mailer.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* SMTP adapter — port of the legacy `server/src/lib/mailer.js` (eden.md §4
|
||||
* "Envio de e-mail"). Generic SMTP via `nodemailer`, not a direct
|
||||
* integration with SES/SendGrid/etc.
|
||||
*
|
||||
* Deliberately fail-soft, unlike the S3 adapter (`../s3/client.ts`): no
|
||||
* business flow may ever fail because of email. If SMTP isn't configured,
|
||||
* or if sending fails, `sendMail` logs and returns normally — it never
|
||||
* throws into the caller.
|
||||
*/
|
||||
import nodemailer, { type Transporter } from "nodemailer";
|
||||
import { loadEnv } from "@eden/config";
|
||||
|
||||
export interface SendMailInput {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
from?: string;
|
||||
}
|
||||
|
||||
let transporter: Transporter | undefined;
|
||||
let transporterBuilt = false;
|
||||
|
||||
function buildTransporter(): Transporter | undefined {
|
||||
const env = loadEnv();
|
||||
if (!env.SMTP_HOST) {
|
||||
return undefined;
|
||||
}
|
||||
return nodemailer.createTransport({
|
||||
host: env.SMTP_HOST,
|
||||
port: env.SMTP_PORT,
|
||||
// secure: true automatically when the port is 465 (implicit SMTPS).
|
||||
secure: env.SMTP_PORT === 465,
|
||||
...(env.SMTP_USER ? { auth: { user: env.SMTP_USER, pass: env.SMTP_PASS } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function getTransporter(): Transporter | undefined {
|
||||
// Built once, lazily, on first use — not rebuilt afterwards even if env
|
||||
// vars change at runtime (simplification acceptable for this MVP).
|
||||
if (!transporterBuilt) {
|
||||
transporter = buildTransporter();
|
||||
transporterBuilt = true;
|
||||
}
|
||||
return transporter;
|
||||
}
|
||||
|
||||
function defaultFrom(): string | undefined {
|
||||
const env = loadEnv();
|
||||
return env.SMTP_FROM ?? env.SMTP_USER;
|
||||
}
|
||||
|
||||
export async function sendMail({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
from,
|
||||
}: SendMailInput): Promise<void> {
|
||||
const t = getTransporter();
|
||||
if (!t) {
|
||||
console.log(
|
||||
`[mailer] SMTP não configurado — e-mail para ${to} não enviado`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await t.sendMail({
|
||||
from: from ?? defaultFrom(),
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[mailer] falha ao enviar e-mail para ${to}:`, err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user