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:
2026-09-03 09:44:25 -03:00
8 changed files with 545 additions and 17 deletions

View 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 }));
}