Na primeira parte dessa série, a gente construiu uma Function App que recebe uma imagem por HTTP e salva no Azure Blob Storage de forma segura, usando identidade gerenciada.
Agora vamos dar o próximo passo e registrar no banco de dados os metadados dessas imagens. Spoiler: vamos usar Azure Postgres SQL, arquitetura em camadas e boas práticas como SOLID e separação de responsabilidades.
Por que salvar no banco?
O Blob Storage é ótimo para guardar arquivos, mas e se você quiser saber:
- Quando um arquivo foi enviado?
- Qual é a URL pública da imagem?
- Qual é o status de processamento OCR (pendente, processado, com erro)?
- Quais imagens são receitas médicas, por exemplo?
Paara isso que entra o Azure Postgres SQL no jogo. A ideia aqui é manter o controle de tudo o que está rolando com cada imagem.
Atualização da arquitetura do projeto
Adicionei o IImageRepository.ts
para ajustar o dominio relacionado ao banco de dados e sua implementação em OcrImageRepository.ts
:
/ocr-function-app ├── application/ │ └── UploadImageService.ts ├── domain/ │ └── IImageStorage.ts │ └── IImageRepository.ts ├── infrastructure/ │ └── AzureBlobStorage.ts │ └── OcrImageRepository.ts ├── validations/ │ └── ContentTypeValidator.ts ├── HttpAddToBlob/ │ └── index.ts │ └── function.json ├── constants.ts ├── host.json ├── local.settings.json └── package.json
A ideia é manter a pegada de DDD light, delegando responsabilidades para camadas mais específicas.
Validando o tipo de conteúdo
Antes de sair processando tudo o que chega na Function, bora validar se o conteúdo é uma imagem de verdade.
primeiro criamos um enum para guardar os tipos utilizados:
export enum AllowedContentTypes { JPEG = 'image/jpeg', PNG = 'image/png', JPG = 'image/jpg', }
E criamos uma classe simples para isso:
import { AllowedContentTypes } from "../constants"; export class ContentTypeValidator { private static allowedTypes = Object.values(AllowedContentTypes); static validate(contentType?: AllowedContentTypes): void { if (!contentType || !this.allowedTypes.includes(contentType)) { throw new Error('Tipo de conteúdo não suportado. Envie uma imagem JPEG ou PNG.'); } } }
E lá dentro da Function:
const contentType = req.headers['content-type']; ContentTypeValidator.validate(contentType);
Criando a tabela no banco
Com o banco de dados criado, execute esse script via Azure Data Studio ou SSMS pra criar a tabela:
CREATE TABLE OcrImages ( Id INT IDENTITY PRIMARY KEY, FileName NVARCHAR(200) NOT NULL, Url NVARCHAR(MAX) NOT NULL, UploadDate DATETIME NOT NULL DEFAULT GETDATE(), Status NVARCHAR(50) NOT NULL DEFAULT 'pending', IsPrescription Boolean NOT NULL DEFAULT false );
📦 Pacotes utilizados
Você vai precisar instalar os seguintes pacotes no seu projeto TypeScript com Azure Functions:
npm install pg
Esses pacotes serão usados para:
- Conectar ao banco Azure Postgres SQL (
pg
)
🔗Conectando a Function ao Azure Postgres SQL
Pra não usar string de conexão com usuário e senha no código (o famoso hardcoded), vamos usar Microsoft Entra ID (identidade gerenciada) via o recurso Service Connector do portal Azure.
Como conectar:
- Vá até a sua Function App no portal Azure
- Clique em Service Connector > + Adicionar
- Selecione o Azure Postgres SQL como destino
- Escolha Used Assigned
- Escolha Firewall
- Clique em Próximo: Revisar + Criar
- Clique em Criar
Definindo o contrato do repositório
Criamos a interface IImageRepository
pra definir o que a persistência precisa saber fazer, sem se preocupar com o tipo de banco de dados utilizado:
export interface IImageRepository { save(fileName: string, url: string): Promise<void>; }
⚙️Implementando com Azure Postgres SQL
Agora sim, a implementação real, vai lá para o infrastructure
:
import { IImageRepository } from "../domain/IImageRepository"; import { Pool } from "pg"; export class OcrImageRepository implements IImageRepository { constructor( private readonly pool: Pool, ) { } async save(fileName: string, url: string): Promise<void> { try { await this.pool.query( ` INSERT INTO OcrImages (FileName, Url) VALUES ($1, $2) `, [fileName, url] ); } catch (err) { throw new Error(`Error inserting image: ${(err as Error).message}`); } } }
Note como a classe só cuida da conexão e do insert. Ela não sabe nada sobre HTTP, OCR ou validação. Isso é SOLID na veia.
Agora é atualizar o serviço de aplicação com a dependência e execução:
export class UploadImageService { constructor( private readonly imageStorage: IImageStorage private readonly imageStorage: IImageRepository ) {} async handleUpload(buffer: Buffer): Promise<{ url: string; fileName: string }> { const fileName = `${uuidv4()}.png`; const url = await this.imageStorage.uploadImage(buffer, fileName); const result = await this.imageRepository.save(filename, url) return { url, fileName }; } }
Após criar as camadas de infraestrutura e aplicação, chegou a hora de integrar tudo na Function em si.
Estrutura atual da Function
A função HTTP é responsável por:
- Validar o tipo de conteúdo (
Content-Type
) - Validar o tamanho da imagem
- Salvar a imagem no Azure Blob Storage
- Registrar o nome da imagem e URL no Azure Postgres SQL
A seguir, o código completo da função com todos esses elementos:
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> { try { if (!req.body) { context.res = { status: 400, body: "Imagem inválida ou ausente" }; return; } const contentType = req.headers['content-type']; ContentTypeValidator.validate(contentType as AllowedContentTypes); const buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body); if (buffer.length > 15 * 1024 * 1024) { throw new Error("Imagem excede o tamanho máximo de 15MB."); } const credential = new DefaultAzureCredential({ managedIdentityClientId: managedIdentityClientId, }); const storage = new AzureBlobStorage( accountUrl, containerName, credential, ); const { token: password } = await credential.getToken('https://ossrdbms-aad.database.windows.net/.default'); const pool = new Pool({ host, user, password, database, port, ssl, }); const repository = new OcrImageRepository(pool); const uploadService = new UploadImageService( storage, repository, ); const { url, fileName } = await uploadService.handleUpload(buffer); context.res = { status: 200, body: { message: "Imagem armazenada com sucesso", url, fileName } }; } catch (error) { context.log.error("Erro ao armazenar imagem", error); context.res = { status: 500, body: "Erro ao armazenar imagem", error }; } }; export default httpTrigger;
🧠 Note como a responsabilidade de cada etapa foi distribuída em classes específicas, respeitando o princípio da responsabilidade única (SRP), deixando a Function mais limpa e testável.
Na próxima parte vamos criar uma Function que processa o OCR de verdade, extrai o texto da imagem e atualiza o banco com o resultado e a informação se o conteúdo é uma receita médica.
Se ficou com dúvida ou curtiu o formato, comenta aqui embaixo e bora trocar uma ideia!
🚀 Próximo passo: Parte 3 - Processando o OCR e atualizando o banco
Top comments (0)