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