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.
-
Could you solve it? if not please share the code of your implementation to analyse it. – Diego Alberto Zapata Häntsch Mar 14 '19 at 03:23
2 Answers
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 thejson
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
andFile
.
// 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 calledinterfaces
. In this new folder I create anindex.ts
(mostly to follow the export convention) and astorage.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.

- 1,088
- 16
- 23
-
I followed this and facing some issues. Do you have working sample app on github? – Himesh Dec 24 '19 at 08:03
-
I have this code working well on my project, but I not update my loopback 4, maybe the new version has some difference; which version you are using? which is the error message? – Diego Alberto Zapata Häntsch Dec 25 '19 at 17:41
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.

- 10,667
- 1
- 41
- 99
-
any example to follow it? Specially to build the controller. Thanks. – Diego Alberto Zapata Häntsch Mar 05 '19 at 16:29