Implementa o MVP do Adaptador de Planilhas no Elo Pro.

O módulo usa a identidade Supabase, isola cada workspace e registra storage, TTL, runner e biblioteca XLSX como decisões ainda abertas.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
camilafagundesandCursor committed 2026-10-02 23:02:00 -03:00
commit 6e23f1533b
144 files changed
+19060

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { AdapterError } from "@/domain/spreadsheet-adapter/errors";
import { assertStorageKey } from "@/domain/spreadsheet-adapter/filenames";
import type { ObjectStorage } from "./memory";
export class LocalPrivateStorage implements ObjectStorage {
constructor(private readonly root: string) {}
private resolve(key: string): string {
assertStorageKey(key);
const full = path.resolve(this.root, ...key.split("/"));
const root = path.resolve(this.root);
if (full !== root && !full.startsWith(root + path.sep)) {
throw new AdapterError("PATH_TRAVERSAL", "Chave de armazenamento rejeitada.");
}
return full;
}
async put(key: string, body: Buffer): Promise<void> {
const full = this.resolve(key);
await mkdir(path.dirname(full), { recursive: true });
await writeFile(full, body);
}
async get(key: string): Promise<Buffer> {
try {
return await readFile(this.resolve(key));
} catch (error) {
if (error instanceof AdapterError) throw error;
throw new AdapterError("NOT_FOUND", "Arquivo não encontrado.", 404);
}
}
async delete(key: string): Promise<void> {
try {
await rm(this.resolve(key), { force: true });
} catch (error) {
if (error instanceof AdapterError) throw error;
}
}
async list(prefix: string): Promise<string[]> {
const normalized = prefix.replace(/\/$/, "");
const start = this.resolve(normalized.includes("/") ? normalized : `${normalized}/noop`.replace("/noop", ""));
const root = path.resolve(this.root);
const found: string[] = [];
const walk = async (dir: string) => {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) await walk(full);
else found.push(path.relative(root, full).split(path.sep).join("/"));
}
};
await walk(start);
return found.filter((key) => key.startsWith(prefix.replace(/\/$/, "")));
}
}