4

i am using storage components in loopback 3.0 to accessing the cloud storage's. but i need how do implement in loopback 4.0.The below link to make it sample in 3.0.

https://github.com/strongloop/loopback-example-storage

Salitha
  • 1,022
  • 1
  • 12
  • 32
Santhosh
  • 140
  • 2
  • 13

2 Answers2

5

I will share my implementation with the community, because even though the answer of Miroslav Bajtoš is the key to do that, the documentation suggested is not very clear, I refer to Calling other APIS and web services.

  • In the first place I create a new DataSource with the lb4 command cli tool and I filled the json file with the same keys that I used in loopback 3 datasource.
// storage.datasource.ts

import { inject } from '@loopback/core';
import { juggler } from '@loopback/service-proxy';
import * as config from './storage-gc.datasource.json';


export class StorageGCDataSource extends juggler.DataSource {
  static dataSourceName = 'StorageGC';

  constructor(
    @inject('datasources.config.StorageGC', { optional: true })
    dsConfig: object = config,
  ) {
    super(dsConfig);
  }
}
// storage.datasource.json
{
  "name": "Storage",
  "connector": "loopback-component-storage",
  "provider": "google",
  "keyFilename": "your-project-key.json",
  "projectId": "your-project-id",
  "nameConflict": "makeUnique"
}
// Note: your-project-key.json is in the root folder at the same level that tsconfig.json

Remember that you need to install loopback-component-storage running

npm install --save loopback-component-storage

  • In the second place I create two new models with lb4 command cli tool. These models are: Container and File.
// container.model.ts

import {Entity, model, property} from '@loopback/repository';

@model()
export class Container extends Entity {
  @property({
    type: 'string',
    required: true,
  })
  name: string;


  constructor(data?: Partial<Container>) {
    super(data);
  }
}

// file.model.ts

import { Entity, model, property } from '@loopback/repository';

@model()
export class File extends Entity {
  @property({
    type: 'string',
    required: true,
  })
  name: string;

  @property({
    type: 'string',
  })
  type?: string;

  @property({
    type: 'string',
  })
  url?: string;


  constructor(data?: Partial<File>) {
    super(data);
  }
}

  • In the third place I create a new folder in src/ directory called interfaces. In this new folder I create an index.ts (mostly to follow the export convention) and a storage.interface.ts (this is the important file)
// index.ts

export * from './storage.interface';
// storage.interface.ts

import { Container, File } from "../models";


export type Callback<T> = (err: Error | null, reply: T) => void;

export interface IStorageService {
  // container methods
  createContainer(container: Partial<Container>, cb: Callback<Container>): void;
  destroyContainer(containerName: string, cb: Callback<boolean>): void;
  getContainers(cb: Callback<Container[]>): void;
  getContainer(containerName: string, cb: Callback<Container>): void;
  // file methods
  getFiles(containerName: string, options: Object, cb: Callback<File[]>): void;
  getFile(containerName: string, fileName: string, cb: Callback<File>): void;
  removeFile(containerName: string, fileName: string, cb: Callback<boolean>): void;
  // main methods
  upload(containerName: string, req: any, res: any, options: Object, cb: Callback<any>): void;
  download(containerName: string, fileName: string, req: any, res: any, cb: Callback<any>): void;
}
  • In the fourth place I create the container.controller.ts file, but previously to this step you need to install @loopback/service-proxy running the following command:

npm install --save @loopback/service-proxy

// storage-gc.controller.ts

import { inject } from '@loopback/core';
import { serviceProxy } from '@loopback/service-proxy';
import { post, requestBody, del, param, get, getFilterSchemaFor, Request, Response, RestBindings } from '@loopback/rest';
import { Filter } from '@loopback/repository';
import { promisify } from 'util';

import { IStorageService } from '../interfaces';
import { Container, File } from '../models';


export class StorageGcController {
  @serviceProxy('StorageGC') // StorageGC is the name of the datasoruce
  private storageGcSvc: IStorageService;

  constructor(@inject(RestBindings.Http.REQUEST) public request: Request,
    @inject(RestBindings.Http.RESPONSE) public response: Response) { }

  @post('/containers', {
    responses: {
      '200': {
        description: 'Container model instance',
        content: { 'application/json': { schema: { 'x-ts-type': Container } } },
      },
    },
  })
  async createContainer(@requestBody() container: Container): Promise<Container> {
    const createContainer = promisify(this.storageGcSvc.createContainer);
    return await createContainer(container);
  }

  @get('/containers', {
    responses: {
      '200': {
        description: 'Array of Containers model instances',
        content: {
          'application/json': {
            schema: { type: 'array', items: { 'x-ts-type': Container } },
          },
        },
      },
    },
  })
  async findContainer(@param.query.object('filter', getFilterSchemaFor(Container)) filter?: Filter): Promise<Container[]> {
    const getContainers = promisify(this.storageGcSvc.getContainers);
    return await getContainers();
  }

  @get('/containers/{containerName}', {
    responses: {
      '200': {
        description: 'Container model instance',
        content: { 'application/json': { schema: { 'x-ts-type': Container } } },
      },
    },
  })
  async findContainerByName(@param.path.string('containerName') containerName: string): Promise<Container> {
    const getContainer = promisify(this.storageGcSvc.getContainer);
    return await getContainer(containerName);
  }

  @del('/containers/{containerName}', {
    responses: {
      '204': {
        description: 'Container DELETE success',
      },
    },
  })
  async deleteContainerByName(@param.path.string('containerName') containerName: string): Promise<boolean> {
    const destroyContainer = promisify(this.storageGcSvc.destroyContainer);
    return await destroyContainer(containerName);
  }

  @get('/containers/{containerName}/files', {
    responses: {
      '200': {
        description: 'Array of Files model instances belongs to container',
        content: {
          'application/json': {
            schema: { type: 'array', items: { 'x-ts-type': File } },
          },
        },
      },
    },
  })
  async findFilesInContainer(@param.path.string('containerName') containerName: string,
    @param.query.object('filter', getFilterSchemaFor(Container)) filter?: Filter): Promise<File[]> {
    const getFiles = promisify(this.storageGcSvc.getFiles);
    return await getFiles(containerName, {});
  }

  @get('/containers/{containerName}/files/{fileName}', {
    responses: {
      '200': {
        description: 'File model instances belongs to container',
        content: { 'application/json': { schema: { 'x-ts-type': File } } },
      },
    },
  })
  async findFileInContainer(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<File> {
    const getFile = promisify(this.storageGcSvc.getFile);
    return await getFile(containerName, fileName);
  }

  @del('/containers/{containerName}/files/{fileName}', {
    responses: {
      '204': {
        description: 'File DELETE from Container success',
      },
    },
  })
  async deleteFileInContainer(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<boolean> {
    const removeFile = promisify(this.storageGcSvc.removeFile);
    return await removeFile(containerName, fileName);
  }

  @post('/containers/{containerName}/upload', {
    responses: {
      '200': {
        description: 'Upload a Files model instances into Container',
        content: { 'application/json': { schema: { 'x-ts-type': File } } },
      },
    },
  })
  async upload(@param.path.string('containerName') containerName: string): Promise<File> {
    const upload = promisify(this.storageGcSvc.upload);
    return await upload(containerName, this.request, this.response, {});
  }

  @get('/containers/{containerName}/download/{fileName}', {
    responses: {
      '200': {
        description: 'Download a File within specified Container',
        content: { 'application/json': { schema: { 'x-ts-type': Object } } },
      },
    },
  })
  async download(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<any> {
    const download = promisify(this.storageGcSvc.download);
    return await download(containerName, fileName, this.request, this.response);
  }
}

With this steps your storage component, or connector most precisely, should work as expected. But following the instructions of Calling other APIS and web services guide to improve this implementation for integration tests you should use a provider instead of @serviceProxy decorator in the controller, to do that I create a new folder called providers inside a src/ folder with the following two files:

// index.ts

export * from './storage-service.provider';
// storage-gc-service.provider.ts

import { getService, juggler } from '@loopback/service-proxy';
import { Provider } from '@loopback/core';
import { StorageGCDataSource } from '../datasources/storage-gc.datasource';
import { IStorageService } from '../interfaces';


export class StorageGCServiceProvider implements Provider<IStorageService> {
  constructor(
    protected dataSource: juggler.DataSource = new StorageGCDataSource()

    /* I try to change the line above in the same way that documentation show,
    as follows:

    @inject('datasources.StorageGC')
    protected dataSource: juggler.DataSource = new StorageGCDataSource()

    and also, in the same way that repositories

    @inject('datasources.StorageGC')
    protected dataSource: StorageGCDataSource

    but always return:

    `Error: Cannot resolve injected arguments for StorageGCServiceProvider.[0]:
    The arguments[0] is not decorated for dependency injection, but a value is
    not supplied`
     */
  ) { }

  value(): Promise<IStorageService> {
    return getService(this.dataSource);
  }
}

After that, you need to modify your src/index.ts and your storage-gc.controller.ts in the following way

// src/index.ts

import { ApplicationConfig } from '@loopback/core';
import { HocicosCuriososApp } from './application';
import { StorageGCServiceProvider } from './providers';

export { HocicosCuriososApp };

export async function main(options: ApplicationConfig = {}) {
  const app = new HocicosCuriososApp(options);

  /* Add this line, it add a service to the app after that you can
  call them in the controller with dependency injection, like:
  @inject('services.StorageGCService') */
  app.serviceProvider(StorageGCServiceProvider);

  await app.boot();
  await app.start();

  const url = app.restServer.url;
  console.log(`Server is running at ${url}`);
  console.log(`Try ${url}/ping`);

  return app;
}
// storage-gc.controller.ts

import { inject } from '@loopback/core';
import { post, requestBody, del, param, get, getFilterSchemaFor, Request, Response, RestBindings } from '@loopback/rest';
import { Filter } from '@loopback/repository';
import { promisify } from 'util';

import { IStorageService } from '../interfaces';
import { Container, File } from '../models';


export class StorageGcController {
  @inject('services.StorageGCService')
  private storageGcSvc: IStorageService;

  constructor(@inject(RestBindings.Http.REQUEST) public request: Request,
    @inject(RestBindings.Http.RESPONSE) public response: Response) { }

  @post('/containers', {
    responses: {
      '200': {
        description: 'Container model instance',
        content: { 'application/json': { schema: { 'x-ts-type': Container } } },
      },
    },
  })
  async createContainer(@requestBody() container: Container): Promise<Container> {
    const createContainer = promisify(this.storageGcSvc.createContainer);
    return await createContainer(container);
  }

  @get('/containers', {
    responses: {
      '200': {
        description: 'Array of Containers model instances',
        content: {
          'application/json': {
            schema: { type: 'array', items: { 'x-ts-type': Container } },
          },
        },
      },
    },
  })
  async findContainer(@param.query.object('filter', getFilterSchemaFor(Container)) filter?: Filter): Promise<Container[]> {
    const getContainers = promisify(this.storageGcSvc.getContainers);
    return await getContainers();
  }

  @get('/containers/{containerName}', {
    responses: {
      '200': {
        description: 'Container model instance',
        content: { 'application/json': { schema: { 'x-ts-type': Container } } },
      },
    },
  })
  async findContainerByName(@param.path.string('containerName') containerName: string): Promise<Container> {
    const getContainer = promisify(this.storageGcSvc.getContainer);
    return await getContainer(containerName);
  }

  @del('/containers/{containerName}', {
    responses: {
      '204': {
        description: 'Container DELETE success',
      },
    },
  })
  async deleteContainerByName(@param.path.string('containerName') containerName: string): Promise<boolean> {
    const destroyContainer = promisify(this.storageGcSvc.destroyContainer);
    return await destroyContainer(containerName);
  }

  @get('/containers/{containerName}/files', {
    responses: {
      '200': {
        description: 'Array of Files model instances belongs to container',
        content: {
          'application/json': {
            schema: { type: 'array', items: { 'x-ts-type': File } },
          },
        },
      },
    },
  })
  async findFilesInContainer(@param.path.string('containerName') containerName: string,
    @param.query.object('filter', getFilterSchemaFor(Container)) filter?: Filter): Promise<File[]> {
    const getFiles = promisify(this.storageGcSvc.getFiles);
    return await getFiles(containerName, {});
  }

  @get('/containers/{containerName}/files/{fileName}', {
    responses: {
      '200': {
        description: 'File model instances belongs to container',
        content: { 'application/json': { schema: { 'x-ts-type': File } } },
      },
    },
  })
  async findFileInContainer(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<File> {
    const getFile = promisify(this.storageGcSvc.getFile);
    return await getFile(containerName, fileName);
  }

  @del('/containers/{containerName}/files/{fileName}', {
    responses: {
      '204': {
        description: 'File DELETE from Container success',
      },
    },
  })
  async deleteFileInContainer(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<boolean> {
    const removeFile = promisify(this.storageGcSvc.removeFile);
    return await removeFile(containerName, fileName);
  }

  @post('/containers/{containerName}/upload', {
    responses: {
      '200': {
        description: 'Upload a Files model instances into Container',
        content: { 'application/json': { schema: { 'x-ts-type': File } } },
      },
    },
  })
  async upload(@param.path.string('containerName') containerName: string): Promise<File> {
    const upload = promisify(this.storageGcSvc.upload);
    return await upload(containerName, this.request, this.response, {});
  }

  @get('/containers/{containerName}/download/{fileName}', {
    responses: {
      '200': {
        description: 'Download a File within specified Container',
        content: { 'application/json': { schema: { 'x-ts-type': Object } } },
      },
    },
  })
  async download(@param.path.string('containerName') containerName: string,
    @param.path.string('fileName') fileName: string): Promise<any> {
    const download = promisify(this.storageGcSvc.download);
    return await download(containerName, fileName, this.request, this.response);
  }
}

This is all, following this steps you must have storage component working very well. Good luck!

Regards.

2

Hello from the LoopBack team!

We haven't looked into integration of loopback-component-storage with LoopBack version 4.

Since the storage component behaves more like a connector than a component, I think it should be possible to use it in LoopBack 4 via @loopback/service-proxy layer. See Calling other APIS and web services for documentation. The code snippets on that page are using loopback-connector-rest as an example, you will use loopback-component-storage instead.

Once you can access the the storage component from JavaScript/TypeScript, you will also need to expose REST API. LoopBack 4 does not provide any built-in controllers, you will have to write one yourself. Please see the definition of LB3 remote methods in storage-service.js, you will need to build controller methods to accept the same arguments and then call out to the service proxy under the hood.

If you are willing to look into this integration, then it's probably best to open a new GitHub issue in loopback-next where we can have a structured conversation.

Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99