diff --git a/packages/service/.npmrc b/packages/service/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/packages/service/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/service/LICENSE b/packages/service/LICENSE new file mode 100644 index 000000000000..97ef00dd73ad --- /dev/null +++ b/packages/service/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018. All Rights Reserved. +Node module: @loopback/service +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/service/README.md b/packages/service/README.md new file mode 100644 index 000000000000..5d1051449eee --- /dev/null +++ b/packages/service/README.md @@ -0,0 +1,39 @@ +# @loopback/service + +This module provides a common set of interfaces for interacting with service +oriented backends such as REST APIs, SOAP Web Services, and gRPC microservices. + +## Overview + +## Installation + +``` +$ npm install --save @loopback/service +``` + +## Basic use + +## Concepts + +### Service + +## Use Service + +### Define legacy data sources + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/wiki/Contributing##guidelines) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +run 'npm test' from the root folder. + +## Contributors + +See [all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/service/docs.json b/packages/service/docs.json new file mode 100644 index 000000000000..165c11d06478 --- /dev/null +++ b/packages/service/docs.json @@ -0,0 +1,8 @@ +{ + "content": [ + "./index.ts", + "./src/index.ts", + "./src/service-proxy.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/service/index.d.ts b/packages/service/index.d.ts new file mode 100644 index 000000000000..6b7503d3691c --- /dev/null +++ b/packages/service/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/service/index.js b/packages/service/index.js new file mode 100644 index 000000000000..4ad59645b590 --- /dev/null +++ b/packages/service/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/service/index.ts b/packages/service/index.ts new file mode 100644 index 000000000000..b77c8c152cd0 --- /dev/null +++ b/packages/service/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/service/package.json b/packages/service/package.json new file mode 100644 index 000000000000..848475cecd45 --- /dev/null +++ b/packages/service/package.json @@ -0,0 +1,44 @@ +{ + "name": "@loopback/service", + "version": "0.2.0", + "description": "Service integration for LoopBack 4", + "engines": { + "node": ">=8" + }, + "main": "index", + "scripts": { + "acceptance": "lb-mocha \"DIST/test/acceptance/**/*.js\"", + "build": "lb-tsc es2017", + "build:apidocs": "lb-apidocs", + "clean": "lb-clean loopback-service*.tgz dist package api-docs", + "pretest": "npm run build", + "test": "lb-mocha \"DIST/test/unit/**/*.js\" \"DIST/test/acceptance/**/*.js\"", + "unit": "lb-mocha \"DIST/test/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-service*.tgz && tree package && npm run clean" + }, + "author": "IBM", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "devDependencies": { + "@loopback/build": "^0.2.0", + "@loopback/testlab": "^0.2.0", + "loopback-connector-rest": "^3.1.0" + }, + "dependencies": { + "@loopback/context": "^0.8.0", + "@loopback/core": "^0.6.0", + "loopback-datasource-juggler": "^3.15.2" + }, + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist/src", + "api-docs", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/service/src/decorators/service.decorator.ts b/packages/service/src/decorators/service.decorator.ts new file mode 100644 index 000000000000..534921c37f34 --- /dev/null +++ b/packages/service/src/decorators/service.decorator.ts @@ -0,0 +1,75 @@ +import { + DecoratorFactory, + ParameterDecoratorFactory, + MetadataAccessor, + inject, + Context, + Injection, + InjectionMetadata, +} from '@loopback/context'; +import {getService, juggler} from '..'; + +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * Type definition for decorators returned by `@serviceProxy` decorator factory + */ +export type ServiceProxyDecorator = PropertyDecorator | ParameterDecorator; + +export const SERVICE_PROXY_KEY = MetadataAccessor.create< + string, + ServiceProxyDecorator +>('service.proxy'); + +/** + * Metadata for a service proxy + */ +export class ServiceProxyMetadata implements InjectionMetadata { + decorator = '@serviceProxy'; + dataSourceName?: string; + dataSource?: juggler.DataSource; + + constructor(dataSource: string | juggler.DataSource) { + if (typeof dataSource === 'string') { + this.dataSourceName = dataSource; + } else { + this.dataSource = dataSource; + } + } +} + +export function serviceProxy(dataSource: string | juggler.DataSource) { + return function( + target: Object, + key: symbol | string, + parameterIndex?: number, + ) { + if (key || typeof parameterIndex === 'number') { + const meta = new ServiceProxyMetadata(dataSource); + inject('', meta, resolve)(target, key, parameterIndex); + } else { + throw new Error( + '@serviceProxy can only be applied to properties or method parameters', + ); + } + }; +} + +/** + * Resolve the @repository injection + * @param ctx Context + * @param injection Injection metadata + */ +async function resolve(ctx: Context, injection: Injection) { + const meta = injection.metadata as ServiceProxyMetadata; + if (meta.dataSource) return getService(meta.dataSource); + if (meta.dataSourceName) { + const ds = await ctx.get( + 'datasources.' + meta.dataSourceName, + ); + return getService(ds); + } +} diff --git a/packages/service/src/index.ts b/packages/service/src/index.ts new file mode 100644 index 000000000000..2a84ad39528c --- /dev/null +++ b/packages/service/src/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './legacy-juggler-bridge'; +export * from './loopback-datasource-juggler'; +export * from './service-proxy'; +export * from './decorators/service.decorator'; diff --git a/packages/service/src/legacy-juggler-bridge.ts b/packages/service/src/legacy-juggler-bridge.ts new file mode 100644 index 000000000000..77818c36f05d --- /dev/null +++ b/packages/service/src/legacy-juggler-bridge.ts @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const jugglerModule = require('loopback-datasource-juggler'); + +import {juggler} from './loopback-datasource-juggler'; + +export const DataSourceConstructor = jugglerModule.DataSource as typeof juggler.DataSource; + +export function getService(ds: juggler.DataSource): T { + return ds.DataAccessObject as T; +} diff --git a/packages/service/src/loopback-datasource-juggler.ts b/packages/service/src/loopback-datasource-juggler.ts new file mode 100644 index 000000000000..058d29263fcb --- /dev/null +++ b/packages/service/src/loopback-datasource-juggler.ts @@ -0,0 +1,20 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// tslint:disable:no-any + +export declare namespace juggler { + /** + * DataSource instance properties/operations + */ + export class DataSource { + name: string; + settings: {[name: string]: any}; + DataAccessObject: {[name: string]: any}; + + constructor(name?: string, settings?: {[name: string]: any}); + constructor(settings?: {[name: string]: any}); + } +} diff --git a/packages/service/src/service-proxy.ts b/packages/service/src/service-proxy.ts new file mode 100644 index 000000000000..2555cd8da128 --- /dev/null +++ b/packages/service/src/service-proxy.ts @@ -0,0 +1,77 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// tslint:disable:no-any + +/** + * Invocable represents a weakly-typed object that has a generic `invoke` method + * to handle method invocations + */ +export interface Invocable { + /** + * Properties or methods + */ + [property: string]: any; + /** + * Invoke an operation with an array of arguments + * @param operationName Name of the operation + * @param args An array of arguments + */ + invoke(operationName: string, ...args: any[]): any; +} + +/** + * An invoker is responsible for implementing method invocations on the given + * target + */ +export interface Invoker { + invoke(target: any, operationName: string, ...args: any[]): any; +} + +function isInvocable(obj: any): obj is Invocable { + return obj != null && typeof obj.invoke === 'function'; +} + +class InvocableProxyHandler + implements ProxyHandler { + get(target: T, propKey: PropertyKey, receiver: any) { + if (propKey in target) { + return target[propKey]; + } + if (isInvocable(target)) { + return function(...args: any[]) { + return target.invoke(propKey.toString(), ...args); + }; + } else { + return undefined; + } + } +} + +class InvokerProxyHandler implements ProxyHandler { + constructor(private invoker: Invoker) {} + get(target: T, propKey: PropertyKey, receiver: any) { + return (...args: any[]) => + this.invoker.invoke(target, propKey.toString(), ...args); + } +} + +const DEFAULT_HANDLER = new InvocableProxyHandler(); + +/** + * Create a proxy for the given object + * @param obj The source object + * @param invoker The optional invoker that handles invocation of methods that + * do not exist on the source object. If not provided, the proxy will try to + * use the `invoke` method if it is implemented by the source object. If + * neither an invoker nor the invoke method is present, the proxy only allows + * existing properties and methods on the source object. + */ +export function proxify(obj: T, invoker?: Invoker): T { + if (invoker) { + return new Proxy(obj, new InvokerProxyHandler(invoker)); + } + return new Proxy(obj, DEFAULT_HANDLER); +} diff --git a/packages/service/test/unit/decorators/service-proxy.decorator.unit.ts b/packages/service/test/unit/decorators/service-proxy.decorator.unit.ts new file mode 100644 index 000000000000..b9fc5cdd5162 --- /dev/null +++ b/packages/service/test/unit/decorators/service-proxy.decorator.unit.ts @@ -0,0 +1,90 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {Context} from '@loopback/context'; +import {serviceProxy} from '../../../'; + +import {DataSourceConstructor, juggler} from '../../../'; + +interface GeoCode { + lat: number; + lng: number; +} + +interface GeoService { + geocode(street: string, city: string, zipcode: string): Promise; +} + +class MyController { + constructor(@serviceProxy('googleMap') public geoService: GeoService) {} +} + +describe('serviceProxy decorator', () => { + let ctx: Context; + let ds: juggler.DataSource; + + before(function() { + ds = new DataSourceConstructor({ + name: 'db', + + connector: 'loopback-connector-rest', + strictSSL: false, + debug: false, + defaults: { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + }, + operations: [ + { + template: { + method: 'GET', + url: 'http://maps.googleapis.com/maps/api/geocode/{format=json}', + query: { + address: '{street},{city},{zipcode}', + sensor: '{sensor=false}', + }, + options: { + strictSSL: true, + useQuerystring: true, + }, + responsePath: '$.results[0].geometry.location', + }, + functions: { + geocode: ['street', 'city', 'zipcode'], + }, + }, + ], + }); + + ctx = new Context(); + ctx.bind('datasources.googleMap').to(ds); + ctx.bind('controllers.MyController').toClass(MyController); + }); + + it('supports @serviceProxy(dataSourceName)', async () => { + const myController = await ctx.get( + 'controllers.MyController', + ); + expect(myController.geoService).to.be.a.Function(); + }); + + it('supports @serviceProxy(dataSource)', async () => { + class MyControllerWithService { + constructor(@serviceProxy(ds) public geoService: GeoService) {} + } + + ctx + .bind('controllers.MyControllerWithService') + .toClass(MyControllerWithService); + + const myController = await ctx.get( + 'controllers.MyControllerWithService', + ); + expect(myController.geoService).to.be.a.Function(); + }); +}); diff --git a/packages/service/test/unit/service-proxy/service-proxy.unit.ts b/packages/service/test/unit/service-proxy/service-proxy.unit.ts new file mode 100644 index 000000000000..22835e8f1c3c --- /dev/null +++ b/packages/service/test/unit/service-proxy/service-proxy.unit.ts @@ -0,0 +1,68 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; + +import {proxify, Invocable, Invoker} from '../../..'; + +// tslint:disable:no-any +describe('proxify', () => { + class MyService implements Invocable { + [property: string]: any; + name = 'MyService'; + + invoke(operationName: string, ...args: any[]) { + return operationName + ': ' + args; + } + + getName() { + return this.name; + } + } + + class YourService { + [property: string]: any; + + getName() { + return 'YourService'; + } + } + + class MyInvoker implements Invoker { + invoke(target: any, operationName: string, ...args: any[]) { + return operationName + ': ' + args; + } + } + + it('creates a proxy', () => { + const proxy = proxify(new MyService()); + let result = proxy.x(1); + expect(result).to.eql('x: 1'); + result = proxy.y('a', 1); + expect(result).to.eql('y: a,1'); + }); + + it('honors existing methods', () => { + const proxy = proxify(new MyService()); + let result = proxy.getName(); + expect(result).to.eql('MyService'); + }); + + it('creates a proxy from an invoker', () => { + const proxy = proxify({} as {[property: string]: any}, new MyInvoker()); + let result = proxy.x(1); + expect(result).to.eql('x: 1'); + result = proxy.y('a', 1); + expect(result).to.eql('y: a,1'); + }); + + it('creates a proxy from a regular object', () => { + const proxy = proxify(new YourService()); + let result = proxy.getName(); + expect(result).to.eql('YourService'); + + expect(proxy.x).to.be.undefined(); + }); +}); diff --git a/packages/service/test/unit/service-proxy/service-rest.unit.ts b/packages/service/test/unit/service-proxy/service-rest.unit.ts new file mode 100644 index 000000000000..f6d6f2b46468 --- /dev/null +++ b/packages/service/test/unit/service-proxy/service-rest.unit.ts @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/service +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import * as util from 'util'; + +import {DataSourceConstructor, juggler, getService} from '../../../'; + +describe('loopback-connector-rest', () => { + let ds: juggler.DataSource; + + before(function() { + ds = new DataSourceConstructor({ + name: 'db', + + connector: 'loopback-connector-rest', + strictSSL: false, + debug: false, + defaults: { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + }, + operations: [ + { + template: { + method: 'GET', + url: 'http://maps.googleapis.com/maps/api/geocode/{format=json}', + query: { + address: '{street},{city},{zipcode}', + sensor: '{sensor=false}', + }, + options: { + strictSSL: true, + useQuerystring: true, + }, + responsePath: '$.results[0].geometry.location', + }, + functions: { + geocode: ['street', 'city', 'zipcode'], + }, + }, + ], + }); + }); + + it('invokes geocode()', async () => { + const loc = { + street: '107 S B St', + city: 'San Mateo', + zipcode: '94401', + }; + + const geoService: {geocode: Function} = getService(ds); + + const geocode = util.promisify(geoService.geocode); + //tslint:disable:no-any + const result = await geocode(loc.street, loc.city, loc.zipcode); + + // { lat: 37.5669986, lng: -122.3237495 } + expect(result[0].lat).approximately(37.5669986, 0.5); + expect(result[0].lng).approximately(-122.3237495, 0.5); + }); +}); diff --git a/packages/service/tsconfig.build.json b/packages/service/tsconfig.build.json new file mode 100644 index 000000000000..3ffcd508d23e --- /dev/null +++ b/packages/service/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +}