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:

  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>;
}

⚙️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