Originalmente publicado em 2019-10-11 10:00 no blog Patinete a vela.

A idéia é simples, e bem comum:

Tenho um objeto do tipo Product que pode ter várias Images associadas.

O modelo Image salva o nome do container e o nome do arquivo dessa imagem.

// Image.json
{
  "name": "Image",
  "base": "PersistedModel",
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {
    "container": {
      "type": "string",
      "required": true
    },
    "filename": {
      "type": "string",
      "required": true
    }
  },
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": {}
}

O modelo Product tem uma relação do tipo hasMany com o modelo Image.

// Product.json
{
  "name": "Product",
  "base": "PersistedModel",
  "idInjection": true,
  "options": {
    "validateUpsert": true
  },
  "properties": {
    "name": {
      "type": "string",
      "required": true
    }
  },
  "validations": [],
  "relations": {
    "images": {
      "type": "hasMany",
      "model": "Image",
      "foreignKey": "",
      "options": {
        "nestRemoting": true
      }
    }
  },
  "acls": [],
  "methods": {}
}

Usando o api explorer do loopback, vemos que as imagens retornam apenas container e filename, porém o ideal seria que fossem retornadas urls que apontassem direto para as imagens e pudessem ser abertas no navegador.

// GET api/images
[
  {
    "container": "container1",
    "filename": "f1.txt",
    "id": 1,
    "productId": 1
  },
  {
    "container": "container1",
    "filename": "f2.txt",
    "id": 2,
    "productId": 1
  }
]

Temos que fazer duas coisas. A primeira é disponibilizar uma api rest para acessar arquivos armazenados no servidor. A segunda é construir a url toda vez que alguém quiser acessar um objeto do tipo Image.

Disponibilizar uma api rest é muito simples. Criamos um datasource do tipo filesystem apontando para um diretório local do servidor:

// datasources.json
...
    "storage": {
        "name": "storage",
        "connector": "loopback-component-storage",
        "provider": "filesystem",
        "root": "./server/storage"
    }
...
}

Precisamos também criar um modelo, que chamaremos de container, que representa o local onde arquivos são armazenados.

// container.json
{
  "name": "container",
  "base": "Model",
  "properties": {},
  "validations": [],
  "relations": {},
  "acls": [],
  "methods": {}
}

Precisamos também associar o modelo ao datasource:

// model-config.json
...
    "container": {
        "dataSource": "storage",
        "public": true
  },
...

Com este modelo container agora está disponível na api explorer do loopback uma série de métodos que permitem listar containers, imagens dentro de containers, fazer upload e download de imagens. Esta postagem no blog do loopback é muito útil e lista os métodos disponíveis.

Com a configuração acima, na pasta server/storage em nosso servidor poderemos criar pastas (onde cada pasta será um container) e dentro das pastas podemos colocar arquivos. Podemos acessar estes arquivos com a seguinte url:

https://myserver.com/api/containers/{container_name}/download/{filename}

Por padrão, nosso modelo Image já tem o nome do container e o nome do arquivo, então o cliente, ao consumir nossa api, já tem a informação suficiente para construir a URL para download das imagens.

Mas é muito inconveniente passar para o cliente a responsabilidade de construir a url toda vez que ele precisar acessar uma imagem.

Para evitar isto, vamos criar um operation hook que nos permitirá gerar uma url toda vez que uma imagem for buscada no nosso backend.

Fazemos isto adicionando código ao nosso modelo Image:

// Image.js
'use strict';

module.exports = function(Image) {
  Image.observe('loaded', function(ctx, next) {
    const app = require('../../server/server.js');
    const { filename, container } = ctx.data;

    // Get the server url and remove a trailing slash, if present.
    const serverUrl = app.locals.settings.url.replace(/\/$/, "");
    const restApiRoot = app.locals.settings.restApiRoot;
    ctx.data.url =
      `${serverUrl}${restApiRoot}/containers/${container}/download/${filename}`;
    next();
  });
};

Este código adiciona um “observador” à operação loaded. Esta operação representa o momento em que um dado já foi buscado no banco de dados e ainda não foi construído o objeto que será devolvido ao cliente que requisitou tal dado.

Algumas coisas importantes: o arquivo server.js, que é o principal ponto de entrada do servidor e é criado automaticamente ao iniciar um projeto loopback, pode ser importado e ele conterá uma série de informações globais úteis. Se importarmos na variável app, encontraremos em app.locals.settings.url a url do servidor (por exemplo http://localhost:3000/ caso estejamos executando no servidor local) e em app.locals.settings.restApiRoot encontaremos o caminho para a api rest (por exemplo /api, caso você esteja usando o padrão).

No código acima nós removemos a barra no final da url e combinamos os diferentes elementos para gerar a url que aponta para a imagem em função de seu container e de seu nome de arquivo. Salvamos esta url na variável ctx.data.url.

Com isto, quando baixamos a lista de imagens obteremos:

// GET images
[
  {
    "container": "container1",
    "filename": "f1.txt",
    "id": 1,
    "productId": 1,
    "url": "http://localhost:3000/api/containers/container1/download/f1.txt"
  },
  {
    "container": "container1",
    "filename": "f1.txt",
    "id": 2,
    "productId": 1,
    "url": "http://localhost:3000/api/containers/container1/download/f1.txt"
  }
]

O código completo deste exemplo está disponível no GitHub.

Referências

Operation hooks, Loopback Documentation

Storage connector, Loopback Documentation

Managing Objects in LoopBack with the Storage Provider of Your Choice, Strongblog

felipeferri/loopback-images, projeto completo no GitHub.