@mybuildex/pkg
v0.5.0
Published
Infra comum das APIs MyBuilder: multitenancy Prisma (pool único), middleware de tenant e CORS
Maintainers
Readme
@mybuildex/pkg
Infra comum das APIs MyBuilder (fonte única): multitenancy Prisma com pool único compartilhado entre escolas, middleware de tenant (:id_school da URL decide o schema) e CORS padrão.
Módulos
createMultitenantPrisma— fábrica: 1 pool mariadb para a instância inteira; client por escola é leve e só qualifica o database no SQL gerado. Pool sem database default (raw sem qualificar falha alto — Error 1046 — em vez de vazar para o tenant errado). Suporta?socket=/cloudsql/...(prod) ehost:porta(dev/túnel).createTenantMiddleware— hookpreValidationque injetareq.prismaresolvendoparams.id_school → query.id_school → header x-id-school(só inteiro positivo; inválido cai no default).resolveTenantId— a regra de resolução, exportada para testes/usos avulsos.createCorsPlugin— CORS padrão das APIs.
Uso (numa API)
import mariadb from "mariadb";
import {PrismaMariaDb} from "@prisma/adapter-mariadb";
import {PrismaClient} from "./generated/client";
import {createMultitenantPrisma, createTenantMiddleware} from "@mybuildex/pkg";
const mt = createMultitenantPrisma({
databaseUrl: process.env.DATABASE_URL!,
PrismaClient, PrismaMariaDb, createPool: mariadb.createPool,
});
// fastify.register(mt.plugin) + fastify.register(createTenantMiddleware(mt))A lib não depende de @prisma/client/mariadb: cada API injeta o client gerado e o driver nas versões dela.
0.4.0 — concorrência e isolamento entre escolas
Quatro módulos novos, todos opt-in (nada muda numa API que não os usar):
createRequestScopePlugin()+requestScoped(Ctor, ...args)— escopo por request viaAsyncLocalStorage. Resolve o padrãoconst service = new XService(fastify)+service.setRequest(request)no preHandler, que compartilhavathis.request(e orequest.prismado tenant) entre requests concorrentes. Troca mecânica:new XService(fastify)→requestScoped(XService, fastify); registrar o plugin antes dos controllers. Uso fora de request lançaRequestScopeError.createTenantMembershipAssert({getTenantClient})— oassertTenantpronto: consultausers.firebase_id = uidno schema do:id_schoolda URL; sem vínculo → 403. Fail-closed em erro de banco; cache positivo porescola:uid(120 s).createTenantMiddleware({..., strictTenant: true})—id_schoolpresente mas inválido responde 400 (InvalidTenantError) em vez de cair no client default, que é a escola doDATABASE_URL. Semid_schoolnenhum, o default continua.createAuthMiddleware({..., internalAuth: {routes: [publicRoute("emailsend", "", {methods: ["POST"]})]}})(0.4.1) — autenticação de serviço entre APIs dentro do próprio auth: headerx-internal-secretcomINTERNAL_API_SECRET, aceito só nas rotas listadas; o request virainternale não passa porassertTenant. Segredo errado/rota fora da lista caem no fluxo de token.createCompressPlugin({threshold, encodings, brotliQuality, level})(0.5.0) —@fastify/compress(global, br/gzip/deflate, threshold 1 KB, brotli qualidade 4) + guardonRouteque tornareply.send()semreturnem handlerasyncequivalente areturn reply. Sem o guard, o compress (stream) fecha a resposta comcontent-length: 0nesse padrão (reproduzido em socket real e na ponte do Functions Framework). Registrar emloadPluginslogo após o CORS, antes dos controllers.createAsyncSendGuard()também é exportado.createSharedSecretGuard({env: "INTERNAL_API_SECRET", header: "x-internal-secret"})—preHandlerpara rotas chamadas entre APIs (ex:emailsend): compara o segredo do env em tempo constante; 401 se inválido, 503 (fail-closed) se a variável não existe em produção.publicRoute(recurso, sufixo, {methods, prefix}),publicPath,isPublicRoute,safeEqual— rotas públicas ancoradas (^/v1/\d+/recurso/sufixo$, com filtro de método) no lugar de regex de substring, e comparação de segredo/HMAC em tempo constante.createAuthMiddleware.publicRoutesaceita as regras e continua aceitandoRegExp.
import {createRequestScopePlugin, requestScoped, createTenantMembershipAssert, publicRoute} from "@mybuildex/pkg";
await createRequestScopePlugin()(fastify); // antes do autoload dos módulos
const service = requestScoped(PayService, fastify); // em cada controller
createAuthMiddleware({
publicRoutes: [publicRoute("pay_checkouts", "public/new", {methods: ["POST"]})],
assertTenant: createTenantMembershipAssert({getTenantClient}),
...
});
createTenantMiddleware({getTenantClient, defaultClient, strictTenant: true});Consumo pelas APIs (npmjs público)
O pacote é publicado público no npmjs — só o dist/ compilado vai no tarball (files: ["dist"]); testes e repo continuam privados. Zero token: npm i funciona no dev, no Actions e no build do GCP.
npm i @mybuildex/pkgRelease
- Bump da versão no
package.json(npm version patch|minorlocal, sem push automático). - Commit + tag
vX.Y.Z+ push da tag → o workflow publish.yml roda testes e publica (secretNPM_TOKEN). - Nas APIs:
npm update @mybuildex/pkg(ou bump da range no package.json).
Testes
npm testInclui o teste de isolamento ponta a ponta (test/isolation.spec.ts): URL → middleware → prisma, com 50 requests concorrentes intercalando escolas — dados de uma escola nunca aparecem na resposta de outra.
