DEV Community

Cover image for Criando uma API OCR com FaaS na Azure - Parte 2: Persistindo dados no Azure Postgres SQL com Boas Práticas
Cláudio Filipe Lima Rapôso
Cláudio Filipe Lima Rapôso

Posted on

Criando uma API OCR com FaaS na Azure - Parte 2: Persistindo dados no Azure Postgres SQL com Boas Práticas

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 
Enter fullscreen mode Exit fullscreen mode

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', } 
Enter fullscreen mode Exit fullscreen mode

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.'); } } } 
Enter fullscreen mode Exit fullscreen mode

E lá dentro da Function:

const contentType = req.headers['content-type']; ContentTypeValidator.validate(contentType); 
Enter fullscreen mode Exit fullscreen mode

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 ); 
Enter fullscreen mode Exit fullscreen mode

📦 Pacotes utilizados

Você vai precisar instalar os seguintes pacotes no seu projeto TypeScript com Azure Functions:

npm install pg 
Enter fullscreen mode Exit fullscreen mode

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:

  1. Vá até a sua Function App no portal Azure
  2. Clique em Service Connector > + Adicionar
  3. Selecione o Azure Postgres SQL como destino
  4. Escolha Used Assigned
  5. Escolha Firewall
  6. Clique em Próximo: Revisar + Criar
  7. 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>; } 
Enter fullscreen mode Exit fullscreen mode

⚙️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}`); } } } 
Enter fullscreen mode Exit fullscreen mode

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 }; } } 
Enter fullscreen mode Exit fullscreen mode

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; 
Enter fullscreen mode Exit fullscreen mode

🧠 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)