API LoopBack 4- API com Upload e Download de ficheiros

Para começar, como sempre, vamos criar um novo repositório.
mkdir API_LoopBack
npm init
Após a criação de um repositorio estamos prontos para começar a trabalhar com o Loopback.
Após a instalação do loopback podemos prosseguir para a criação da API através de:
lb4 example todo

O passo seguinte é a criação do controlador através de:
lb4 controller

O passo seguinte é alterar o codigo do ficheiro application que se encontra na pasta src.
// Copyright IBM Corp. 2018,2020. All Rights Reserved.// Node module: @loopback/example-todo// This file is licensed under the MIT License.// License text available at https://opensource.org/licenses/MITimport {AuthenticationComponent} from '@loopback/authentication';import {JWTAuthenticationComponent,SECURITY_SCHEME_SPEC,UserServiceBindings,} from '@loopback/authentication-jwt';import {DbDataSource} from './datasources';import {BootMixin} from '@loopback/boot';import {ApplicationConfig} from '@loopback/core';import {RepositoryMixin} from '@loopback/repository';import {Request, Response, RestApplication} from '@loopback/rest';import {RestExplorerBindings,RestExplorerComponent,} from '@loopback/rest-explorer';import {ServiceMixin} from '@loopback/service-proxy';import morgan from 'morgan';import path from 'path';import {MySequence} from './sequence';export {ApplicationConfig};export class TodoListApplication extends BootMixin(ServiceMixin(RepositoryMixin(RestApplication)),) {constructor(options: ApplicationConfig = {}) {super(options);// Set up the custom sequencethis.sequence(MySequence);// Set up default home pagethis.static('/', path.join(__dirname, '../public'));// Customize @loopback/rest-explorer configuration herethis.configure(RestExplorerBindings.COMPONENT).to({path: '/explorer',});this.component(RestExplorerComponent);this.projectRoot = __dirname;// Customize @loopback/boot Booter Conventions herethis.bootOptions = {controllers: {// Customize ControllerBooter Conventions heredirs: ['controllers'],extensions: ['.controller.js'],nested: true,},};// Mount authentication systemthis.component(AuthenticationComponent);// Mount jwt componentthis.component(JWTAuthenticationComponent);// Bind datasourcethis.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME);this.setupLogging();}private setupLogging() {// Register `morgan` express middleware// Create a middleware factory wrapper for `morgan(format, options)`const morganFactory = (config?: morgan.Options<Request, Response>) => {this.debug('Morgan configuration', config);return morgan('combined', config);};// Print out logs using `debug`const defaultConfig: morgan.Options<Request, Response> = {stream: {write: str => {this._debug(str);},},};this.expressMiddleware(morganFactory, defaultConfig, {injectConfiguration: 'watch',key: 'middleware.morgan',});}}
Também na pasta src vamos alterar o controllers:
// Copyright IBM Corp. 2020. All Rights Reserved.// Node module: @loopback/example-todo-jwt// This file is licensed under the MIT License.// License text available at https://opensource.org/licenses/MITimport {authenticate, TokenService} from '@loopback/authentication';import {Credentials,MyUserService,TokenServiceBindings,User,UserRepository,UserServiceBindings,} from '@loopback/authentication-jwt';import {inject} from '@loopback/core';import {model, property, repository} from '@loopback/repository';import {get,getModelSchemaRef,post,requestBody,SchemaObject,} from '@loopback/rest';import {SecurityBindings, securityId, UserProfile} from '@loopback/security';import {genSalt, hash} from 'bcryptjs';import _ from 'lodash';@model()export class NewUserRequest extends User {@property({type: 'string',required: true,})password: string;}const CredentialsSchema: SchemaObject = {type: 'object',required: ['email', 'password'],properties: {email: {type: 'string',format: 'email',},password: {type: 'string',minLength: 8,},},};export const CredentialsRequestBody = {description: 'The input of login function',required: true,content: {'application/json': {schema: CredentialsSchema},},};export class UserController {constructor(@inject(TokenServiceBindings.TOKEN_SERVICE)public jwtService: TokenService,@inject(UserServiceBindings.USER_SERVICE)public userService: MyUserService,@inject(SecurityBindings.USER, {optional: true})public user: UserProfile,@repository(UserRepository) protected userRepository: UserRepository,) {}@post('/users/login', {responses: {'200': {description: 'Token',content: {'application/json': {schema: {type: 'object',properties: {token: {type: 'string',},},},},},},},})async login(@requestBody(CredentialsRequestBody) credentials: Credentials,): Promise<{token: string}> {// ensure the user exists, and the password is correctconst user = await this.userService.verifyCredentials(credentials);// convert a User object into a UserProfile object (reduced set of properties)const userProfile = this.userService.convertToUserProfile(user);// create a JSON Web Token based on the user profileconst token = await this.jwtService.generateToken(userProfile);return {token};}@authenticate('jwt')@get('/whoAmI', {responses: {'200': {description: 'Return current user',content: {'application/json': {schema: {type: 'string',},},},},},})async whoAmI(@inject(SecurityBindings.USER)currentUserProfile: UserProfile,): Promise<string> {return currentUserProfile[securityId];}@post('/signup', {responses: {'200': {description: 'User',content: {'application/json': {schema: {'x-ts-type': User,},},},},},})async signUp(@requestBody({content: {'application/json': {schema: getModelSchemaRef(NewUserRequest, {title: 'NewUser',}),},},})newUserRequest: NewUserRequest,): Promise<User> {const password = await hash(newUserRequest.password, await genSalt());const savedUser = await this.userRepository.create(_.omit(newUserRequest, 'password'),);await this.userRepository.userCredentials(savedUser.id).create({password});return savedUser;}}
O ficheiro todo.controller.
// Copyright IBM Corp. 2018,2020. All Rights Reserved.// Node module: @loopback/example-todo// This file is licensed under the MIT License.// License text available at https://opensource.org/licenses/MITimport {inject} from '@loopback/core';import {Count,CountSchema,Filter,FilterExcludingWhere,repository,Where,} from '@loopback/repository';import {del,get,getModelSchemaRef,HttpErrors,param,patch,post,put,requestBody,} from '@loopback/rest';import {Todo} from '../models';import {TodoRepository} from '../repositories';import {Geocoder} from '../services';import {authenticate} from '@loopback/authentication';// ------------------------------------@authenticate('jwt') // <---- Apply the @authenticate decorator at the class levelexport class TodoController {constructor(@repository(TodoRepository)public todoRepository: TodoRepository,@inject('services.Geocoder') protected geoService: Geocoder,) {}@post('/todos', {responses: {'200': {description: 'Todo model instance',content: {'application/json': {schema: getModelSchemaRef(Todo)}},},},})async create(@requestBody({content: {'application/json': {schema: getModelSchemaRef(Todo, {title: 'NewTodo',exclude: ['id'],}),},},})todo: Omit<Todo, 'id'>,): Promise<Todo> {if (todo.remindAtAddress) {const geo = await this.geoService.geocode(todo.remindAtAddress);// ignoring because if the service is down, the following section will// not be covered/* istanbul ignore next */if (!geo[0]) {// address not foundthrow new HttpErrors.BadRequest(`Address not found: ${todo.remindAtAddress}`,);}// Encode the coordinates as "lat,lng" (Google Maps API format). See also// https://stackoverflow.com/q/7309121/69868// https://gis.stackexchange.com/q/7379todo.remindAtGeo = `${geo[0].y},${geo[0].x}`;}return this.todoRepository.create(todo);}@get('/todos/{id}', {responses: {'200': {description: 'Todo model instance',content: {'application/json': {schema: getModelSchemaRef(Todo, {includeRelations: true}),},},},},})async findById(@param.path.number('id') id: number,@param.filter(Todo, {exclude: 'where'}) filter?: FilterExcludingWhere<Todo>,): Promise<Todo> {return this.todoRepository.findById(id, filter);}@get('/todos', {responses: {'200': {description: 'Array of Todo model instances',content: {'application/json': {schema: {type: 'array',items: getModelSchemaRef(Todo, {includeRelations: true}),},},},},},})async find(@param.filter(Todo) filter?: Filter<Todo>): Promise<Todo[]> {return this.todoRepository.find(filter);}@put('/todos/{id}', {responses: {'204': {description: 'Todo PUT success',},},})async replaceById(@param.path.number('id') id: number,@requestBody() todo: Todo,): Promise<void> {await this.todoRepository.replaceById(id, todo);}@patch('/todos/{id}', {responses: {'204': {description: 'Todo PATCH success',},},})async updateById(@param.path.number('id') id: number,@requestBody({content: {'application/json': {schema: getModelSchemaRef(Todo, {partial: true}),},},})todo: Todo,): Promise<void> {await this.todoRepository.updateById(id, todo);}@del('/todos/{id}', {responses: {'204': {description: 'Todo DELETE success',},},})async deleteById(@param.path.number('id') id: number): Promise<void> {await this.todoRepository.deleteById(id);}@get('/todos/count', {responses: {'200': {description: 'Todo model count',content: {'application/json': {schema: CountSchema}},},},})async count(@param.where(Todo) where?: Where<Todo>): Promise<Count> {return this.todoRepository.count(where);}@patch('/todos', {responses: {'200': {description: 'Todo PATCH success count',content: {'application/json': {schema: CountSchema}},},},})async updateAll(@requestBody({content: {'application/json': {schema: getModelSchemaRef(Todo, {partial: true}),},},})todo: Todo,@param.where(Todo) where?: Where<Todo>,): Promise<Count> {return this.todoRepository.updateAll(todo, where);}}
E está tudo pronto para dar inicio ao nosso projeto:


O resultado deve ser algo semelhante a isto.
constructor(@inject(FILE_UPLOAD_SERVICE) private handler: FileUploadHandler,) {}@post('/files', {responses: {200: {content: {'application/json': {schema: {type: 'object',},},},description: 'Files and fields',},},})async fileUpload(@requestBody.file()request: Request,@inject(RestBindings.Http.RESPONSE) response: Response,): Promise<object> {return new Promise<object>((resolve, reject) => {this.handler(request, response, (err: unknown) => {if (err) reject(err);else {resolve(FileUploadController.getFilesAndFields(request));}});});}/*** Get files and fields for the request* @param request - Http request*/private static getFilesAndFields(request: Request) {const uploadedFiles = request.files;const mapper = (f: globalThis.Express.Multer.File) => ({fieldname: f.fieldname,originalname: f.originalname,encoding: f.encoding,mimetype: f.mimetype,size: f.size,});let files: object[] = [];if (Array.isArray(uploadedFiles)) {files = uploadedFiles.map(mapper);} else {for (const filename in uploadedFiles) {files.push(...uploadedFiles[filename].map(mapper));}}return {files, fields: request.body};}}
Na pasta public encontra-se o index.html, então vamos alterá-lo da maneira que seja possivel verificar os passos anteriores.

E podemos usar o seguinte layout.
<!DOCTYPE html><html lang="en"><head><title>File upload and download</title><meta charset="utf-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1" /><linkrel="shortcut icon"type="image/x-icon"href="https://loopback.io/favicon.ico"/><style>h3 {margin-left: 25px;text-align: center;}a,a:visited {color: #3f5dff;}h3 a {margin-left: 10px;}a:hover,a:focus,a:active {color: #001956;}.power {position: absolute;bottom: 25px;left: 50%;transform: translateX(-50%);}.info {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);}.info h1 {text-align: center;margin-bottom: 0;}.info p {text-align: center;margin-bottom: 3em;margin-top: 1em;}</style><script>/*** Submit the upload form*/function setupUploadForm() {const formElem = document.getElementById('uploadForm');formElem.onsubmit = async e => {e.preventDefault();const res = await fetch('/files', {method: 'POST',body: new FormData(formElem),});const body = await res.json();console.log('Response from upload', body);await fetchFiles();};}/*** List uploaded files*/async function fetchFiles() {const res = await fetch('/files');const files = await res.json();console.log('Response from list', files);const list = files.map(f => `<li><a href="/files/${f}">${f}</a></li>\n`,);document.getElementById('fileList').innerHTML = list.join('');}async function init() {setupUploadForm();await fetchFiles();}</script></head><body onload="init();"><div class="info"><h1>File upload and download</h1><div id="upload"><h3>Upload files</h3><form id="uploadForm"><label for="files">Select files:</label><input type="file" id="files" name="files" multiple /><br /><br /><label for="note">Note:</label><inputtype="text"name="note"id="note"placeholder="Note about the files"/><br /><br /><input type="submit" /></form></div><div id="download"><h3>Download files</h3><ul id="fileList"></ul><button onclick="fetchFiles()">Refresh</button></div><h3>OpenAPI spec: <a href="/openapi.json">/openapi.json</a></h3><h3>API Explorer: <a href="/explorer">/explorer</a></h3></div><footer class="power"><a href="https://loopback.io" target="_blank"><imgsrc="https://loopback.io/images/branding/powered-by-loopback/blue/powered-by-loopback-sm.png"/></a></footer></body></html>
Agora só nos resta inicializar a nossa aplicação e verificar. Para tal basta aceder-mos à linha de comandos e usar o comando npm start.

E já está, se obter o que o pretendido podemos concluir que o nosso toturial foi concluído com sucesso.