Gathering detailed insights and metrics for @gapi/main
Gathering detailed insights and metrics for @gapi/main
Gathering detailed insights and metrics for @gapi/main
Gathering detailed insights and metrics for @gapi/main
npm install @gapi/main
Typescript
Module System
Node Version
NPM Version
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
Really easy GraphQL API framework build on top of NodeJS inspired by @Angular
Build with @rxdi - reactive Extension Dependency Injection Container working inside Browser and Node
Created to provide complex backend scalable applications with minimum effort.
For questions/issues you can write ticket here
Starting project for less than 1 minute via Gapi-CLI
Heroku Ready!
Basic starter project has Heroku Easy Deploy Button!
Amazon ServerLess Ready!
Part of the frameworks and techniques used without which Gapi would not exist :love_letter:
IoC Containers are DI frameworks that can work outside of the programming language. In some you can configure which implementations to use in metadata files (e.g. XML) which are less invasive. With some you can do IoC that would normally be impossible like inject an implementation at pointcuts.
Pointcuts - aspect-oriented computer programming
Advanced Starter Example and production build
Video Tutorials
1npm install @gapi/core
1import { CoreModule } from '@gapi/core'; 2import { Controller, Module, BootstrapFramework } from '@rxdi/core'; 3import { GapiObjectType, Query, Type } from '@rxdi/graphql'; 4import { GraphQLScalarType, GraphQLInt, GraphQLNonNull } from 'graphql'; 5 6@GapiObjectType() 7export class UserType { 8 readonly id: number | GraphQLScalarType = GraphQLInt; 9} 10 11@Controller() 12export class UserQueriesController { 13 14 @Type(UserType) 15 @Query({ 16 id: { 17 type: new GraphQLNonNull(GraphQLInt) 18 } 19 }) 20 findUser(root, { id }, context): UserType { 21 return {id: id}; 22 } 23 24} 25 26@Module({ 27 controllers: [UserQueriesController] 28}) 29export class AppModule { } 30 31 32BootstrapFramework(AppModule, [CoreModule]).subscribe()
You need to create tsconfig.json
with following content
1{ 2 "compilerOptions": { 3 "emitDecoratorMetadata": true, 4 "experimentalDecorators": true 5 } 6}
Execute
1ts-node index.ts
Or execute the following command to set these options on runtime:
1export TS_NODE_COMPILER_OPTIONS='{"experimentalDecorators": true, "emitDecoratorMetadata": true}' && 2ts-node index.ts --compilerOptions $TS_NODE_COMPILER_OPTIONS
To add configuration click here
1npm install -g nodemon ts-node
1npm install @gapi/cli -g
It may take 20 seconds because it will install project dependencies.
1gapi new my-project
1gapi new my-project --advanced
1gapi new my-project --microservices
1gapi new my-project --serverless
1gapi new my-project --serverless-sequelize
Enter inside my-project and type:
1npm start
1http://localhost:9000/graphiql
1gapi test
1gapi start
1gapi test --watch
1 if (process.env.BEFORE_HOOK) { 2 // do something here 3 }
1gapi test --before
1import { Container } from '@rxdi/core'; 2import { AuthPrivateService } from './auth.service'; 3 4const authService: AuthPrivateService = Container.get(AuthPrivateService); 5 6describe('Auth Service', () => { 7 it('unit: signJWTtoken => token : Should sucessfully sign jwt', async done => { 8 const token = authService.signJWTtoken({ 9 email: 'dada@abv.bg', 10 id: 1, 11 scope: ['ADMIN'] 12 }); 13 expect(token).toBeTruthy(); 14 const verifyedToken = authService.verifyToken(token); 15 expect(verifyedToken.email).toBe('dada@abv.bg'); 16 expect(verifyedToken.id).toBe(1); 17 expect(verifyedToken.scope[0]).toBe('ADMIN'); 18 done(); 19 }); 20}); 21
Filepath: root/src/app/user/user-queries.controller.e2e.spec.ts
1import { Container } from '@rxdi/core'; 2import { IQuery } from '../core/api-introspection/index'; 3import { TestUtil } from '../core/test-util/testing.service'; 4import { map } from 'rxjs/operators'; 5 6const testUtil: TestUtil = Container.get(TestUtil); 7 8describe('User Queries Controller', () => { 9 it('e2e: queries => (findUser) : Should sucessfully find user', async done => { 10 testUtil.sendRequest<IQuery>({ 11 query: ` 12 query findUser($id: Int!) { 13 findUser(id: $id) { 14 id 15 settings { 16 username 17 firstname 18 } 19 } 20 } 21 `, 22 variables: { 23 id: 1 24 } 25 }) 26 .pipe( 27 map(res => { 28 expect(res.success).toBeTruthy(); 29 return res.data.findUser; 30 }) 31 ) 32 .subscribe(async res => { 33 expect(res.id).toBe(1); 34 expect(res.settings.username).toBe('o'); 35 expect(res.settings.firstname).toBe('pesho'); 36 done(); 37 }, err => { 38 expect(err).toBe(null); 39 done(); 40 }); 41 }); 42}); 43
Filepath: root/src/app/core/test-util/testing.service.ts
1 disableAuthorization() { 2 this.tester = tester({ url: process.env.ENDPOINT_TESTING, contentType: 'application/json' }); 3 } 4 5 enableAuthorization() { 6 this.tester = tester({ url: process.env.ENDPOINT_TESTING, contentType: 'application/json', authorization: process.env.TOKEN_TESTING}); 7 } 8 9 sendRequest<T>(query: SendRequestQueryType): Observable<Response<T>> { 10 if (query.signiture) { 11 this.tester = tester({ 12 url: process.env.ENDPOINT_TESTING, 13 contentType: 'application/json', 14 authorization: query.signiture.token 15 }); 16 } 17 return from(this.tester(JSON.stringify(query))); 18 }
1import { Effect } from '@rxdi/core'; 2import { OfType } from '@rxdi/graphql'; 3import { EffectTypes } from '../core/api-introspection/EffectTypes'; 4 5@Effect() 6export class UserEffects { 7 8 @OfType<EffectTypes>(EffectTypes.login) 9 findUser(result, payload, context) { 10 console.log(result, payload, context); 11 } 12 13}
How it works ?
When the application starts the whole schema is collected via Decorators applied inside GapiControllers.Now when we have our schema collected and bootstraping is done next step is to attach all BehaviorSubject Observables to particular resolver and from that step we got Type based string literal enums a.k.a Gapi Effects.They look like that:
1 2function strEnum<T extends string>(o: Array<T>): {[K in T]: K} { 3 return o.reduce((res, key) => { 4 res[key] = key; 5 return res; 6 }, Object.create(null)); 7} 8export const EffectTypes = strEnum(['myevent', 9'login', 10'subscribeToUserMessagesBasic', 11'subscribeToUserMessagesWithFilter', 12'destroyUser', 13'updateUser', 14'addUser', 15'publishSignal']); 16export type EffectTypes = keyof typeof EffectTypes; 17
Import GapiEffect inside Module
1 2import { Module } from '@rxdi/core'; 3import { UserQueriesController } from './user-queries.controller'; 4import { UserSubscriptionsController } from './user-subscriptions.controller'; 5import { UserMutationsController } from './user-mutations.controller'; 6import { UserService } from './services/user.service'; 7import { AnotherService } from './services/another.service'; 8import { UserEffects } from './user.effects'; 9 10@Module({ 11 controllers: [ 12 UserQueriesController, 13 UserSubscriptionsController, 14 UserMutationsController 15 ], 16 services: [ 17 UserService, 18 AnotherService 19 ], 20 effects: [ 21 UserEffects 22 ] 23}) 24export class UserModule {}
If you want to create custom Effect for particular resolver you need to use @EffectName('myevent') Decorator takes String This decorator will override default event which is Name of the Method in this example will be findUser
1 @Type(UserType) 2 @EffectName('myevent') 3 @Query({ 4 id: { 5 type: new GraphQLNonNull(GraphQLInt) 6 } 7 }) 8 findUser(root, { id }, context) { 9 return { 10 id: 1 11 }; 12 }
If you click Save app will automatically reload and you will have such a typing inside autogenerated EffectTypes from api-introspection Then you can lock it with new Typo generated "myevent"
1 @Type(UserType) 2 @EffectName(EffectTypes.myevent) 3 @Query({ 4 id: { 5 type: new GraphQLNonNull(GraphQLInt) 6 } 7 }) 8 findUser(root, { id }, context) { 9 return { 10 id: 1 11 }; 12 }
Purpose of Effects is to create logic after particular resolver is triggered Successfully without thrown error for example:
How to register plugin to the system.
Difference between Plugin and Service is that system will trigger register method inside constructor if exist,
else it will resolve OnInit and constructor properties.
That way you can register your own plugins to the system after everything is bootstrapped.
If you decide for example to return Promise on register
and resolve it inside setTimeout(() =>resolve(data), 1000) the app will wait till the promise is resolved.
1 2import { Module, Plugin, Service, PluginInterface } from '@rxdi/core'; 3import { HAPI_SERVER } from '@rxdi/hapi'; 4import { Server } from 'hapi'; 5 6@Service() 7export class TestService { 8 testMethod() { 9 return 'Hello world'; 10 } 11} 12 13@Plugin() 14export class MyHapiPlugin implements PluginInterface { 15 name = 'MyPlugin'; 16 version = '1.0.0'; 17 18 constructor( 19 private testService: TestService, 20 @Inject(HAPI_SERVER) private server: Server 21 ) {} 22 23 async register() { 24 this.server.route({ 25 method: 'GET', 26 path: '/test', 27 handler: this.handler.bind(this) 28 }); 29 } 30 31 async handler(request, h) { 32 return this.testService.helloWorld(); 33 } 34 35} 36 37@Module({ 38 plugins: [MyHapiPlugin], 39 services: [TestService] 40}) 41export class AppModule {} 42
1import { Module, ModuleWithServices, InjectionToken } from '@rxdi/core'; 2import { of } from 'rxjs'; 3 4@Service() 5export class MODULE_DI_CONFIG { 6 text: string = 'Hello world'; 7} 8 9const MY_MODULE_CONFIG = new InjectionToken<MODULE_DI_CONFIG>('my-module-config'); 10 11@Module({ 12 imports: [] 13}) 14export class YourModule { 15 public static forRoot(): ModuleWithServices { 16 return { 17 module: YourModule, 18 services: [ 19 { provide: MY_MODULE_CONFIG, useValue: { text: 'Hello world' } }, 20 { provide: MY_MODULE_CONFIG, useClass: MODULE_DI_CONFIG }, 21 { 22 provide: MY_MODULE_CONFIG, 23 useFactory: () => ({text: 'Hello world'}) 24 }, 25 { 26 provide: MY_MODULE_CONFIG, 27 lazy: true, // Will be evaluated and resolved if false will remain Promise 28 useFactory: async () => await Promise.resolve({text: 'Hello world'}) 29 }, 30 { 31 provide: MY_MODULE_CONFIG, 32 lazy: true, // Will be evaluated and resolved if false will remain Observable 33 useFactory: () => of({text: 'Hello world'}) 34 }, 35 { 36 // this example will download external module from link and save it inside node modules 37 // then will load it inside createUniqueHash token or MY_MODULE_CONFIG. 38 provide: MY_MODULE_CONFIG, 39 useDynamic: { 40 fileName: 'createUniqueHash', 41 namespace: '@helpers', 42 extension: 'js', 43 typings: '', 44 outputFolder: '/node_modules/', 45 link: 'https://ipfs.infura.io/ipfs/QmdQtC3drfQ6M6GFpDdrhYRKoky8BycKzWbTkc4NEzGLug' 46 } 47 } 48 ] 49 } 50 } 51}
(#amazon-serverless)
Inside main.ts
1import { AppModule } from './app/app.module'; 2import { BootstrapFramework, Container } from '@rxdi/core'; 3import { FrameworkImports } from './framework-imports'; 4import { format } from 'url'; 5import { HAPI_SERVER } from '@rxdi/hapi'; 6import { Server } from 'hapi'; 7 8const App = BootstrapFramework(AuthMicroserviceModule, [FrameworkImports], { init: true }).toPromise(); 9 10export const handler = async (event, context, callback) => { 11 const app = await App; 12 const url = format({ 13 pathname: event.path, 14 query: event.queryStringParameters 15 }); 16 const options = { 17 method: event.httpMethod, 18 url, 19 payload: event.body, 20 headers: event.headers, 21 validate: false 22 }; 23 let res = { 24 statusCode: 502, 25 result: null 26 }; 27 try { 28 res = await Container.get<Server>(HAPI_SERVER).inject(options); 29 } catch (e) { 30 console.error(JSON.stringify(e)); 31 } 32 const headers = Object.assign({ 33 'Access-Control-Allow-Origin': '*', 34 'Access-Control-Allow-Methods': 'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT' 35 }); 36 return { 37 statusCode: res.statusCode, 38 body: res.result, 39 headers 40 }; 41};
If we want to use AWS Lambdas Offline we need to set RandomPort to true inside HapiConfig because HAPI will generate random PORT everytime and as far as i know lambas are independent server which will be started everytime when someone execute that particular function.So when running offline it will not say that port 9000 is opened with another server.
1import { AppModule } from './app/app.module'; 2import { BootstrapFramework } from '@rxdi/core'; 3import { CoreModule } from '@gapi/core'; 4 5const GapiCoreModule = CoreModule.forRoot({ 6 server: { 7 randomPort: true, 8 hapi: { 9 port: 9000 10 } 11 }, 12 graphql: { 13 path: '/graphql', 14 openBrowser: false, 15 writeEffects: false, 16 graphiQlPath: '/graphiql', 17 graphiqlOptions: { 18 endpointURL: '/graphql', 19 subscriptionsEndpoint: `ws://localhost:9000/subscriptions`, 20 websocketConnectionParams: { 21 token: process.env.GRAPHIQL_TOKEN 22 } 23 }, 24 graphqlOptions: { 25 schema: null 26 } 27 }, 28}); 29BootstrapFramework(AppModule, [GapiCoreModule], { 30 init: false, 31 initOptions: { 32 effects: true, 33 plugins: true, 34 services: true, 35 controllers: true 36 }, 37 logger: { 38 logging: true, 39 date: true, 40 exitHandler: true, 41 fileService: true, 42 hashes: true 43 } 44}) 45.subscribe( 46 () => console.log('Started!'), 47 (e) => console.error(e) 48);
Lambdas cannot use Typescript so we need to compile our application to es6 as commonjs module
1tsc
Create serverless.yml
1service: gapi-serverless 2provider: 3 name: aws 4 runtime: nodejs8.10 5 stage: development 6 profile: default 7 region: us-east-2 8 9functions: 10 root: 11 handler: src/main.handler 12 events: 13 - http: 14 path: "/{proxy+}" 15 method: any 16 cors: true 17 integration: lambda-proxy 18 19plugins: 20 - serverless-offline 21
Then we can run
1serverless deploy
Later you can create PROXY server and map all existing Lambdas as a single GRAPHQL Schema
1import { Module } from '@rxdi/core'; 2import { MicroserviceModule } from '@gapi/microservices'; 3import { CoreModule } from './core/core.module'; 4 5@Module({ 6 imports: [ 7 CoreModule, 8 MicroserviceModule.forRoot([ 9 {name: 'microservice1', link: 'https://hkzdqnc1i2.execute-api.us-east-2.amazonaws.com/development/graphql'} 10 ]), 11 ] 12}) 13export class AppModule {} 14
(#gapi-configuration)
Since 1.0.0 version @gapi micrated to @rxdi there is a little changes which are with Configuration @gapi now uses @rxdi/core infrastructure so it is a Framework created on top of it This @gapi/core module is wrapper for 3 modules from @rxdi infrastructure: @rxdi/graphql, @rxdi/graphql-pubsub, @rxdi/hapi When you install @gapi/core automatically will be installed these 3 modules and inside of @gapi/core module there is a Default configuration for CoreModule which this module exports { CoreModule } example:
1const DEFAULT_CONFIG = { 2 server: { 3 hapi: { 4 port: 9000 5 }, 6 }, 7 graphql: { 8 path: '/graphql', 9 openBrowser: true, 10 writeEffects: true, 11 graphiql: true, 12 graphiQlPlayground: false, 13 graphiQlPath: '/graphiql', 14 watcherPort: '', 15 graphiqlOptions: { 16 endpointURL: '/graphql', 17 subscriptionsEndpoint: `${ 18 process.env.GRAPHIQL_WS_SSH ? 'wss' : 'ws' 19 }://${process.env.GRAPHIQL_WS_PATH || 'localhost'}${ 20 process.env.DEPLOY_PLATFORM === 'heroku' 21 ? '' 22 : `:${process.env.API_PORT || 23 process.env.PORT}` 24 }/subscriptions`, 25 websocketConnectionParams: { 26 token: process.env.GRAPHIQL_TOKEN 27 } 28 }, 29 graphqlOptions: { 30 schema: null 31 } 32 }, 33}; 34...
Full example can be checked here
Based on that change if we want to not use default configuration instead of this
1BootstrapFramework(AppModule, [CoreModule]).subscribe()
We need to add more complex configuration and call it framework-imports where our configuration will live This module will be imported before AppModule will bootstrap.
1import { CoreModule, Module } from '@gapi/core'; 2import { AuthService } from './app/core/services/auth/auth.service'; 3import { AuthModule } from '@gapi/auth'; 4import { readFileSync } from 'fs'; 5 6@Module({ 7 imports: [ 8 AuthModule.forRoot({ 9 algorithm: 'HS256', 10 cert: readFileSync('./cert.key'), 11 cyper: { 12 algorithm: 'aes256', 13 iv: 'Jkyt1H3FA8JK9L3B', 14 privateKey: '8zTVzr3p53VC12jHV54rIYu2545x47lA' 15 } 16 }), 17 CoreModule.forRoot({ 18 server: { 19 hapi: { 20 port: process.env.API_PORT || process.env.PORT || 9000 21 } 22 }, 23 pubsub: { 24 authentication: AuthService 25 }, 26 graphql: { 27 path: process.env.GRAPHQL_PATH, 28 openBrowser: process.env.OPEN_BROWSER === 'true' ? true : false, 29 watcherPort: 8967, 30 writeEffects: process.env.WRITE_EFFECTS === 'true' ? true : false, 31 graphiql: process.env.GRAPHIQL === 'true' ? true : false, 32 graphiQlPlayground: process.env.ENABLE_GRAPHIQL_PLAYGROUND === 'true' ? true : false, 33 graphiQlPath: process.env.GRAPHIQL_PATH, 34 authentication: AuthService, 35 graphiqlOptions: { 36 endpointURL: process.env.GRAPHQL_PATH, 37 passHeader: `'Authorization':'${process.env.GRAPHIQL_TOKEN}'`, 38 subscriptionsEndpoint: `${process.env.GRAPHIQL_WS_SSH ? 'wss' : 'ws'}://${process.env.GRAPHIQL_WS_PATH || 'localhost'}${process.env.DEPLOY_PLATFORM === 'heroku' 39 ? '' 40 : `:${process.env.API_PORT || 41 process.env.PORT}`}/subscriptions`, 42 websocketConnectionParams: { 43 token: process.env.GRAPHIQL_TOKEN 44 } 45 }, 46 graphqlOptions: { 47 schema: null 48 } 49 }, 50 }), 51 52 ] 53}) 54export class FrameworkImports {} 55
Now you can import FrameworkImports
1import { BootstrapFramework } from '@rxdi/core'; 2import { FrameworkImports } from './framework-imports'; 3import { AppModule } from './app/app.module'; 4 5BootstrapFramework(AppModule, [FrameworkImports]).subscribe()
1gapi app build
1gapi app start
1gapi app stop
1gapi workers start
1gapi workers stop
1import { Module } from '@rxdi/core'; 2import { UserModule } from './user/user.module'; 3import { CoreModule } from './core/core.module'; 4 5@Module({ 6 imports: [ 7 UserModule, 8 CoreModule 9 ] 10}) 11export class AppModule { } 12
1 2import { Module } from '@rxdi/core'; 3import { UserQueriesController } from './user-queries.controller'; 4import { UserSubscriptionsController } from './user-subscriptions.controller'; 5import { UserMutationsController } from './user-mutations.controller'; 6import { UserService } from './services/user.service'; 7import { AnotherService } from './services/another.service'; 8import { UserEffect } from './user.effect'; 9 10@Module({ 11 controllers: [ 12 UserQueriesController, 13 UserSubscriptionsController, 14 UserMutationsController 15 ], 16 services: [ 17 UserService, 18 AnotherService 19 ], 20 effects: [ 21 UserEffect 22 ] 23}) 24export class UserModule {}
1import { GapiObjectType, Resolve, InjectType } from '@rxdi/graphql'; 2import { GraphQLScalarType, GraphQLInt, GraphQLString } from 'graphql'; 3import { UserSettings } from './user.settings'; 4 5@GapiObjectType() 6export class UserType { 7 readonly id: number | GraphQLScalarType = GraphQLInt; 8 readonly email: string | GraphQLScalarType = GraphQLString; 9 readonly type: string | GraphQLScalarType = GraphQLString; 10 readonly password: string | GraphQLScalarType = GraphQLString; 11 readonly name: string | GraphQLScalarType = GraphQLString; 12 readonly settings: UserSettings = InjectType(UserSettings); 13 14 @Resolve('id') 15 getId?(root, payload, context) { 16 return 1; 17 } 18}
1import { Injector } from '@rxdi/core'; 2import { GapiObjectType, Resolve } from '@rxdi/graphql'; 3import { AnotherService } from '../services/another.service'; 4import { GraphQLScalarType, GraphQLBoolean } from 'graphql'; 5 6@GapiObjectType() 7export class UserSettings { 8 9 @Injector(AnotherService) private anotherService?: AnotherService; 10 11 readonly sidebar: boolean | GraphQLScalarType = GraphQLBoolean; 12 13 @Resolve('sidebar') 14 async getSidebar?(root, payload, context) { 15 return await this.anotherService.returnTrueAsync(); 16 } 17 18}
1import { GapiObjectType } from '@rxdi/graphql'; 2import { GraphQLScalarType, GraphQLString } from 'graphql'; 3 4@GapiObjectType() 5export class UserMessage { 6 readonly message: string | GraphQLScalarType = GraphQLString; 7}
1import { GapiObjectType, InjectType } from '@rxdi/graphql'; 2import { UserType } from './user.type'; 3import { GraphQLScalarType, GraphQLString } from 'graphql'; 4 5@GapiObjectType() 6export class UserTokenType { 7 readonly token: string | GraphQLScalarType = GraphQLString; 8 readonly user: UserType = InjectType(UserType); 9}
1import { Controller } from '@rxdi/core'; 2import { Type, Query, Public } from '@rxdi/graphql'; 3import { GraphQLNonNull, GraphQLInt, GraphQLString } from 'graphql'; 4import { UserService } from './services/user.service'; 5import { UserType } from './types/user.type'; 6import { UserTokenType } from './types/user-login.type'; 7import { AuthPrivateService } from '../core/services/auth/auth.service'; 8import { IUserType, IUserTokenType } from '../core/api-introspection/index'; 9 10@Controller() 11export class UserQueriesController { 12 13 constructor( 14 private userService: UserService, 15 private authService: AuthPrivateService 16 ) { } 17 18 @Type(UserType) 19 @Public() 20 @Query({ 21 id: { 22 type: new GraphQLNonNull(GraphQLInt) 23 } 24 }) 25 findUser(root, { id }, context): IUserType { 26 return this.userService.findUser(id); 27 } 28 29 @Type(UserTokenType) 30 @Public() 31 @Query({ 32 email: { 33 type: new GraphQLNonNull(GraphQLString) 34 }, 35 password: { 36 type: new GraphQLNonNull(GraphQLString) 37 } 38 }) 39 login(root, { email, password }, context) { 40 let credential: IUserTokenType; 41 42 // Find user from database 43 const user = <IUserType>{ 44 id: 1, 45 email: email, 46 type: 'ADMIN', 47 settings: { 48 sidebar: true 49 }, 50 password: this.authService.encryptPassword(password), 51 name: 'Test Testov' 52 }; 53 54 if (this.authService.decryptPassword(user.password) === password) { 55 credential = { 56 user: user, 57 token: this.authService.signJWTtoken({ 58 email: user.email, 59 id: user.id, 60 scope: [user.type] 61 }) 62 }; 63 } else { 64 throw new Error('missing-username-or-password'); 65 } 66 return credential; 67 } 68 69} 70
1import { Controller } from '@rxdi/core'; 2import { UserService } from './services/user.service'; 3import { UserType } from './types/user.type'; 4import { UserMessage } from './types/user-message.type'; 5import { IUserType } from '../core/api-introspection'; 6import { PubSubService } from '@rxdi/graphql-pubsub'; 7import { Scope, Type, Mutation, Public } from '@rxdi/graphql'; 8import { GraphQLNonNull, GraphQLString, GraphQLInt } from 'graphql'; 9 10@Controller() 11export class UserMutationsController { 12 13 constructor( 14 private userService: UserService, 15 private pubsub: PubSubService 16 ) {} 17 18 @Scope('ADMIN') 19 @Type(UserMessage) 20 @Public() 21 @Mutation({ 22 message: { 23 type: new GraphQLNonNull(GraphQLString) 24 }, 25 signal: { 26 type: new GraphQLNonNull(GraphQLString) 27 }, 28 }) 29 publishSignal(root, { message, signal }, context): UserMessage { 30 console.log(`${signal} Signal Published message: ${message} by ${context.email}`); 31 this.pubsub.publish(signal, `${signal} Signal Published message: ${message} by ${context.email}`); 32 return {message}; 33 } 34 35 @Scope('ADMIN') 36 @Type(UserType) 37 @Mutation({ 38 id: { 39 type: new GraphQLNonNull(GraphQLInt) 40 } 41 }) 42 deleteUser(root, { id }, context): IUserType { 43 return this.userService.deleteUser(id); 44 } 45 46 @Scope('ADMIN') 47 @Type(UserType) 48 @Mutation({ 49 id: { 50 type: new GraphQLNonNull(GraphQLInt) 51 } 52 }) 53 updateUser(root, { id }, context): IUserType { 54 return this.userService.updateUser(id); 55 } 56 57 @Scope('ADMIN') 58 @Type(UserType) 59 @Mutation({ 60 id: { 61 type: new GraphQLNonNull(GraphQLInt) 62 } 63 }) 64 addUser(root, { id }, context): IUserType { 65 return this.userService.addUser(id); 66 } 67 68} 69 70
1 2import { Controller } from '@rxdi/core'; 3import { Type, Scope, Public } from '@rxdi/graphql'; 4import { withFilter } from 'graphql-subscriptions'; 5import { GraphQLNonNull, GraphQLInt } from 'graphql'; 6import { PubSubService, Subscribe, Subscription } from '@rxdi/graphql-pubsub'; 7import { UserMessage } from './types/user-message.type'; 8 9@Controller() 10export class UserSubscriptionsController { 11 12 constructor( 13 private pubsub: PubSubService 14 ) {} 15 16 @Scope('ADMIN') 17 @Type(UserMessage) 18 @Public() 19 @Subscribe((self: UserSubscriptionsController) => self.pubsub.asyncIterator('CREATE_SIGNAL_BASIC')) 20 @Subscription() 21 subscribeToUserMessagesBasic(message): UserMessage { 22 return { message }; 23 } 24 25 @Scope('ADMIN') 26 @Type(UserMessage) 27 @Subscribe( 28 withFilter( 29 (self: UserSubscriptionsController) => self.pubsub.asyncIterator('CREATE_SIGNAL_WITH_FILTER'), 30 (payload, {id}, context) => { 31 console.log('Subscribed User: ', id, JSON.stringify(context)); 32 return true; 33 } 34 ) 35 ) 36 @Subscription({ 37 id: { 38 type: new GraphQLNonNull(GraphQLInt) 39 } 40 }) 41 subscribeToUserMessagesWithFilter(message): UserMessage { 42 return { message }; 43 } 44 45} 46 47
1subscription { 2 subscribeToUserMessagesBasic { 3 message 4 } 5}
1subscription { 2 subscribeToUserMessagesWithFilter(id:1) { 3 message 4 } 5}
1 2import { Service } from '@rxdi/core'; 3import { IUserType } from '../../core/api-introspection'; 4 5@Service() 6export class AnotherService { 7 trimFirstLetter(username: string): string { 8 return username.charAt(1); 9 } 10 11 trimFirstLetterAsync(username): Promise<string> { 12 return Promise.resolve(this.trimFirstLetter(username)); 13 } 14 15 returnTrueAsync() { 16 return Promise.resolve(true); 17 } 18} 19 20@Service() 21export class UserService { 22 constructor( 23 ) { } 24 25 findUser(id: number): IUserType { 26 return { 27 id: 1, 28 email: 'test@gmail.com', 29 type: 'ADMIN', 30 password: '123456', 31 name: 'Pesho', 32 settings: { 33 sidebar: true 34 } 35 }; 36 } 37 38 addUser(id: number): IUserType { 39 return { 40 id: 1, 41 email: 'test@gmail.com', 42 type: 'ADMIN', 43 password: '123456', 44 name: 'Pesho', 45 settings: { 46 sidebar: true 47 } 48 }; 49 } 50 51 deleteUser(id: number): IUserType { 52 return { 53 id: 1, 54 email: 'test@gmail.com', 55 type: 'ADMIN', 56 password: '123456', 57 name: 'Pesho', 58 settings: { 59 sidebar: true 60 } 61 }; 62 } 63 64 updateUser(id): IUserType { 65 return { 66 id: 1, 67 email: 'test@gmail.com', 68 type: 'ADMIN', 69 password: '123456', 70 name: 'Pesho', 71 settings: { 72 sidebar: true 73 } 74 }; 75 } 76 77}
1import { AppModule } from './app/app.module'; 2import { BootstrapFramework } from '@rxdi/core'; 3import { CoreModule } from '@gapi/core'; 4 5BootstrapFramework(AppModule, [CoreModule], { 6 init: false, 7 initOptions: { 8 effects: true, 9 plugins: true, 10 services: true, 11 controllers: true 12 }, 13 logger: { 14 logging: true, 15 date: true, 16 exitHandler: true, 17 fileService: true, 18 hashes: true 19 } 20}) 21.subscribe( 22 () => console.log('Started!'), 23 (e) => console.error(e) 24);
gapi start
1 2import { Module } from '@rxdi/core'; 3import { AuthPrivateService } from './services/auth/auth.service'; 4 5@Module({ 6 services: [ 7 AuthPrivateService 8 ] 9}) 10export class CoreModule {}
1 2import { Service } from '@rxdi/core'; 3import * as Boom from 'boom'; 4 5export interface UserInfo { 6 scope: ['ADMIN', 'USER']; 7 type: 'ADMIN' | 'USER'; 8 iat: number; 9} 10 11@Service() 12export class AuthPrivateService { 13 14 constructor( 15 // private authService: AuthService, 16 // private connectionHookService: ConnectionHookService 17 ) { 18 // this.connectionHookService.modifyHooks.onSubConnection = this.onSubConnection.bind(this); 19 // this.authService.modifyFunctions.validateToken = this.validateToken.bind(this); 20 } 21 22 onSubConnection(connectionParams): UserInfo { 23 if (connectionParams.token) { 24 return this.validateToken(connectionParams.token, 'Subscription'); 25 } else { 26 throw Boom.unauthorized(); 27 } 28 } 29 30 validateToken(token: string, requestType: 'Query' | 'Subscription' = 'Query'): UserInfo { 31 const user = <UserInfo>this.verifyToken(token); 32 user.type = user.scope[0]; 33 console.log(`${requestType} from: ${JSON.stringify(user)}`); 34 if (user) { 35 return user; 36 } else { 37 throw Boom.unauthorized(); 38 } 39 } 40 41 verifyToken(token: string): any { 42 // return this.authService.verifyToken(token); 43 return token; 44 } 45 46 signJWTtoken(tokenData: any): any { 47 // return this.authService.sign(tokenData); 48 return tokenData; 49 } 50 51 issueJWTToken(tokenData: any) { 52 // const jwtToken = this.authService.sign({ 53 // email: '', 54 // id: 1, 55 // scope: ['ADMIN', 'USER'] 56 // }); 57 // return jwtToken; 58 } 59 60 decryptPassword(password: string): any { 61 // return this.authService.decrypt(password); 62 return password; 63 } 64 65 encryptPassword(password: string): any { 66 // return this.authService.encrypt(password); 67 return password; 68 } 69 70} 71
1import { Module } from '@rxdi/core'; 2import { UserModule } from './user/user.module'; 3import { UserService } from './user/services/user.service'; 4import { CoreModule } from './core/core.module'; 5 6@Module({ 7 imports: [ 8 UserModule, 9 CoreModule 10 ] 11}) 12export class AppModule { }
1 2import { Module, InjectionToken } from '@rxdi/core'; 3import { UserModule } from './user/user.module'; 4import { CoreModule } from './core/core.module'; 5 6class UserId { 7 id: number; 8} 9 10export const UserIdToken = new InjectionToken<UserId>('UserId'); 11 12@Module({ 13 imports: [ 14 UserModule, 15 CoreModule 16 ], 17 services: [ 18 { 19 provide: 'UserId', 20 useValue: {id: 1} 21 }, 22 { 23 provide: UserIdToken, 24 useFactory: () => { 25 return {id: 1}; 26 } 27 }, 28 { 29 provide: UserIdToken, 30 useClass: UserId 31 } 32 ] 33}) 34export class AppModule { }
1@Controller() 2export class UserQueriesController { 3 constructor( 4 @Inject('UserId') private userId: {id: number}, // Value injection 5 @Inject(UserIdToken) private userId: UserId, // Token injection 6 @Inject(UserIdToken) private userId: UserId, // Class injection 7 ) { 8 console.log(this.userId.id); 9 // Will print 1 10 } 11}
You can put also Observable as a value
1import { Module } from '@rxdi/core'; 2import { UserModule } from './user/user.module'; 3import { CoreModule } from './core/core.module'; 4import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 5 6@Module({ 7 imports: [ 8 UserModule, 9 CoreModule 10 ], 11 services: [ 12 { 13 provide: 'Observable', 14 useValue: new BehaviorSubject(1) 15 } 16 ] 17}) 18export class AppModule { }
Then inject this service somewhere in your application
1import { Controller } from '@rxdi/core'; 2import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3import { PubSubService } from '@rxdi/graphql-pubsub'; 4 5@Controller() 6export class UserSubscriptionsController { 7 8 constructor( 9 @Inject('Observable') private observable: BehaviorSubject<number>, 10 private pubsub: PubSubService 11 ) { 12 this.observable.subscribe(() => this.pubsub.publish('CREATE_SIGNAL_BASIC', `Signal Published message: ${this.observable.getValue()}`)); 13 } 14}
Then you can put it inside another service and emit values
1import { Service, Inject } from '@rxdi/core'; 2import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 3import { TimerObservable } from 'rxjs/observable/TimerObservable'; 4 5@Service() 6export class UserService { 7 constructor( 8 @Inject('Observable') private observable: BehaviorSubject<number> 9 ) { 10 TimerObservable.create(0, 1000).subscribe((t) => this.observable.next(t)) 11 }
Cross reference circular dependency
1import { UserService } from './user.service'; 2 3@Service() 4export class AnotherService { 5 @Inject(() => UserService) private userService: UserService
1import { AnotherService } from './another.service'; 2 3@Service() 4export class UserService { 5 @Inject(() => AnotherService) private anotherService: AnotherService
You can see the subscription when you subscribe to basic chanel inside GraphiQL dev panel
1subscription { 2 subscribeToUserMessagesBasic { 3 message 4 } 5}
The result will be
1{ 2 "subscribeToUserMessagesBasic": { 3 "message": "Signal Published message: 495" 4 } 5}
1import { GapiObjectType, InjectType, Resolve, Type } from '@rxdi/graphql'; 2import { GraphQLGapiObjectType, GraphQLString, GraphQLInt, GraphQLList, GraphQLBoolean, GraphQLScalarType } from "graphql"; 3 4@GapiObjectType() 5export class UserSettingsType { 6 readonly id: number | GraphQLScalarType = GraphQLInt; 7 readonly color: string | GraphQLScalarType = GraphQLString; 8 readonly language: string | GraphQLScalarType = GraphQLString; 9 readonly sidebar: boolean | GraphQLScalarType = GraphQLBoolean; 10} 11 12@GapiObjectType() 13export class UserWalletSettingsType { 14 readonly type: string | GraphQLScalarType = GraphQLString; 15 readonly private: string | GraphQLScalarType = GraphQLString; 16 readonly security: string | GraphQLScalarType = GraphQLString; 17 readonly nested: UserSettingsType = InjectType(UserSettingsType); 18 readonly nested2: UserSettingsType = InjectType(UserSettingsType); 19 readonly nested3: UserSettingsType = InjectType(UserSettingsType); 20 21 // If you want you can change every value where you want with @Resolve decorator 22 @Resolve('type') 23 changeType(root, args, context) { 24 return root.type + ' new-type'; 25 } 26 27 // NOTE: you can name function methods as you wish can be Example3 for example important part is to define 'nested3' as a key to map method :) 28 @Resolve('nested3') 29 Example3(root, args, context) { 30 // CHANGE value of object type when returning 31 // UserSettingsType { 32 // "id": 1, 33 // "color": "black", 34 // "language": "en-US", 35 // "sidebar": true 36 // } 37 // root.nested3.id 38 // root.nested3.color 39 // root.nested3.language 40 // root.nested3.sidebar 41 return root.nested3; 42 } 43} 44 45 46@GapiObjectType() 47export class UserWalletType { 48 readonly id: number | GraphQLScalarType = GraphQLInt; 49 readonly address: string | GraphQLScalarType = GraphQLString; 50 readonly settings: string | UserWalletSettingsType = InjectType(UserWalletSettingsType); 51} 52 53 54@GapiObjectType() 55export class UserType { 56 readonly id: number | GraphQLScalarType = GraphQLInt; 57 readonly email: string | GraphQLScalarType = GraphQLString; 58 readonly firstname: string | GraphQLScalarType = GraphQLString; 59 readonly lastname: string | GraphQLScalarType = GraphQLString; 60 readonly settings: string | UserSettingsType = InjectType(UserWalletSettingsType); 61 readonly wallets: UserWalletType = new GraphQLList(InjectType(UserWalletType)); 62} 63
1query { 2 findUser(id:1) { 3 id 4 email 5 firstname 6 lastname 7 settings { 8 id 9 color 10 language 11 sidebar 12 } 13 wallets { 14 id 15 address 16 settings { 17 type 18 private 19 security 20 nested { 21 id 22 color 23 language 24 sidebar 25 } 26 nested2 { 27 id 28 color 29 language 30 sidebar 31 } 32 nested3 { 33 id 34 color 35 language 36 sidebar 37 } 38 } 39 } 40 } 41} 42
1 2 findUser(id: number): UserType { 3 return { 4 id: 1, 5 email: "kristiqn.tachev@gmail.com", 6 firstname: "Kristiyan", 7 lastname: "Tachev", 8 settings: { 9 id: 1, 10 color: 'black', 11 language: 'en-US', 12 sidebar: true 13 }, 14 wallets: [{ 15 id: 1, address: 'dadadada', settings: { 16 type: "ethereum", 17 private: false, 18 security: "TWO-STEP", 19 nested: { 20 id: 1, 21 color: 'black', 22 language: 'en-US', 23 sidebar: true 24 }, 25 nested2: { 26 id: 1, 27 color: 'black', 28 language: 'en-US', 29 sidebar: true 30 }, 31 nested3: { 32 id: 1, 33 color: 'black', 34 language: 'en-US', 35 sidebar: true 36 }, 37 nested4: { 38 id: 1, 39 color: 'black', 40 language: 'en-US', 41 sidebar: true 42 }, 43 44 } 45 }] 46 }; 47 }
1{ 2 "data": { 3 "findUser": { 4 "id": 1, 5 "email": "kristiqn.tachev@gmail.com", 6 "firstname": "Kristiyan", 7 "lastname": "Tachev", 8 "settings": { 9 "id": 1, 10 "color": "black", 11 "language": "en-US", 12 "sidebar": true 13 }, 14 "wallets": [ 15 { 16 "id": 1, 17 "address": "dadadada", 18 "settings": { 19 "type": "ethereum new-type", 20 "private": "false", 21 "security": "TWO-STEP", 22 "nested": { 23 "id": 1, 24 "color": "black", 25 "language": "en-US", 26 "sidebar": true 27 }, 28 "nested2": { 29 "id": 1, 30 "color": "black", 31 "language": "en-US", 32 "sidebar": true 33 }, 34 "nested3": { 35 "id": 1, 36 "color": "black", 37 "language": "en-US", 38 "sidebar": true 39 } 40 } 41 } 42 ] 43 } 44 } 45}
All Gapi Decorators
@Query - Define Query object added above method inside @Controller
@Mutation - Define Mutation object added above method inside @Controller
@Subscription - Define Subscription object added above method inside @Controller
@Subscribe - It will be used with @Subscription Decorator and it takes PubSubIterator function @Subscribe(() => UserSubscriptionsController.pubsub.asyncIterator('CREATE_SIGNAL_BASIC')) can be used also withFilter
1 @Subscribe( 2 withFilter( 3 (self: UserSubscriptionsController) => self.pubsub.asyncIterator('CREATE_SIGNAL_WITH_FILTER'), 4 (payload, {id}, context) => { 5 console.log('User trying to subscribe: ', id, JSON.stringify(context)); 6 // if user passes your expression it will be subscribed to this subscription 7 return id !== context.id; 8 } 9 ) 10 ) 11 @Subscription() 12 subscribeToUserMessagesWithFilter(message): UserMessage { 13 return { message }; 14 }
@Public - Works with (@Query, @Mutation, Subscription) adds property "public = true" will make this particular resolver Public without authentication
@Scope - Can take arguments like what kind of User can use this Resolver @scope('ADMIN', 'USER', 'SALES')
@Type - Works with (@Query, @Mutation, Subscription) passing ObjectType class here for example UserType this is internally new GraphQLObjectType
@GapiObjectType - Internally is using new GraphQLObjectType() adding name of the class as a {name: constructor.name} can take {name: 'YourCustomName'} as argument also the same Object type can be used for generating new GraphQLInputObjectType when passing {input: true} used for Arguments
@Resolve - This is used internally inside GapiObjectType and it is related with modifying return result from GraphQL like in the following example
1@GapiObjectType() 2export class UserType { 3 id: number | GraphQLScalarType = GraphQLInt; 4 5 @Resolve('id') 6 getId?(root, payload, context) { 7 return 5; 8 } 9} 10
Important part is that getId? method needs to be OPTIONAL because it will be part of the Interface defined by the class UserType so everywhere if you use UserType it will ask you to add getId as a function but we just want to modify the return result from Schema with Resolve method decorator.
@Controller - It will define Controllers inside Gapi Application you can have as many as you wish controllers just when you are ready import them inside @Module({controllers: ...CONTROLLERS})
@Guard - this decorator will add Guards to particular resolvers can be used this way:
1import { Controller } from '@rxdi/core'; 2import { Type, Query, Guard } from '@rxdi/graphql'; 3import { UserService } from './services/user.service'; 4import { UserListType } from './types/user-list.type'; 5import { AdminOnly } from '../core/guards/admin-only.guard'; 6 7@Controller() 8export class UserQueriesController { 9 10 constructor( 11 private userService: UserService 12 ) { } 13 14 @Type(UserListType) 15 @Guard(AdminOnly) 16 @Query() 17 listUsers() { 18 return this.userService.findAll(); 19 } 20 21} 22 23
Or you can apply them globaly to controller like this
1 2import { Query, GraphQLControllerOptions, InjectType } from '@rxdi/graphql'; 3import { UserService } from './services/user.service'; 4import { UserListType } from './types/user-list.type'; 5import { AdminOnly } from '../core/guards/admin-only.guard'; 6 7@Controller<GraphQLControllerOptions>({ 8 guards: [AdminOnly], 9 type: InjectType(UserListType) 10}) 11export class UserQueriesController { 12 13 constructor( 14 private userService: UserService 15 ) { } 16 17 @Query() 18 listUsers() { 19 return this.userService.findAll(); 20 } 21} 22
The basic guard example:
1import { Service, CanActivateResolver, GenericGapiResolversType } from '@gapi/core'; 2import { ENUMS } from '../enums'; 3import { UserType } from '../../user/types/user.type'; 4 5@Service() 6export class AdminOnly implements CanActivateResolver { 7 canActivate( 8 context: UserType, 9 payload, 10 descriptor: GenericGapiResolversType 11 ) { 12 return context.type === 'ADMIN'; 13 } 14} 15
Valid use cases are:
1@Service() 2export class AdminOnly implements CanActivateResolver { 3 canActivate( 4 context: UserType, 5 payload, 6 descriptor: GenericGapiResolversType 7 ) { 8 return Observable.create(o => o.next(true)); 9 10 return new Promise((r) => r(true)); 11 12 return Observable.create(o => o.next(false)); 13 14 return new Promise((r) => r(false)); 15 if (context.type !== 'ADMIN') { 16 throw new Error('error'); 17 } 18 } 19} 20
If you try to mix @Guard decorator and apply globaly guards it will merge them so you will have both of the guards.
@Interceptor - This abstraction can be used to modify or just log result before you process it to the client can be used like this:
Logging interceptor
1import { InterceptResolver, Service, GenericGapiResolversType } from '@gapi/core'; 2import { Observable } from 'rxjs'; 3import { tap } from 'rxjs/operators'; 4import { UserType } from '../../user/types/user.type'; 5 6@Service() 7export class LoggerInterceptor implements InterceptResolver { 8 intercept( 9 chainable$: Observable<any>, 10 context: UserType, 11 payload, 12 descriptor: GenericGapiResolversType 13 ) { 14 console.log('Before...'); 15 const now = Date.now(); 16 return chainable$.pipe( 17 tap(() => console.log(`After... ${Date.now() - now}ms`)), 18 ); 19 } 20}
Then you can attach it inside Query, Mutation, Subscription decorators
1 @Interceptor(LoggerInterceptor) 2 @Query({ 3 id: { 4 type: new GraphQLNonNull(GraphQLString) 5 } 6 }) 7 findUser(root, { id }): Promise<User> { 8 return this.userService.findUserById(id); 9 } 10
You can also modify result returned
Modify interceptor
1import { InterceptResolver, Service, GenericGapiResolversType } from '@gapi/core'; 2import { Observable } from 'rxjs'; 3import { tap } from 'rxjs/operators'; 4import { UserType } from '../../user/types/user.type'; 5 6@Service() 7export class ModifyInterceptor implements InterceptResolver { 8 intercept( 9 chainable$: Observable<any>, 10 context: UserType, 11 payload, 12 descriptor: GenericGapiResolversType 13 ) { 14 console.log('Before...'); 15 const now = Date.now(); 16 return chainable$.pipe( 17 map((res) => { 18 console.log(`After... ${Date.now() - now}ms`); 19 return res; 20 }), 21 ); 22 } 23}
@Module - This is the starting point of the application when we bootstrap our module inside root/src/main.ts.Also can be used to create another Modules which can be imported inside AppModule {imports: ...IMPORTS}
@Service - Passed above class, this Decorator will insert metadata related with this class and all Dependencies Injected inside constructor() so when application starts you will get a Singleton of many Services if they are not Factory Services(Will explain how to create Factory in next release).So you can use single instance of a Service injected everywhere inside your application.
@Inject - It will inject Service with specific NAME for example when using InjectionToken('my-name') you can do something like
1constructor( 2 @Inject('my-name') private name: string; 3) {}
@Injector - Use this very carefully! It will Inject Services before application is fully loaded used to load Instance of a class before the real load of application it is used only inside GapiObjectType because Types are the first thing that will be loaded inside Gapi Application so we need our Services on Processing Decorators which is when the application loads.If you can use Dependency Injection internally provided.
@InjectType - This is BETA decorator for now is used without Decorator sign @ example:
1@GapiObjectType() 2export class UserType { 3 readonly id: number | GraphQLScalarType = GraphQLInt; 4 readonly settings: string | UserSettings = InjectType(UserSettingsType); 5}
In future releases it will be used as follows:
1@GapiObjectType() 2export class UserType { 3 readonly id: number | GraphQLScalarType = GraphQLInt; 4 @InjectType(UserSettingsType) readonly settings: string; 5}
TODO: Better documentation...
Enjoy ! :)
No vulnerabilities found.
No security vulnerabilities found.