stradivario-gapi
v0.4.2
Published
![Build Status](http://gitlab.youvolio.com/gapi/gapi/badges/master/build.svg)
Downloads
4
Readme
@gapi
@StrongTyped @GraphQL @API @Hapi @Apollo
Inspired by Angular intended to provide complex applications with minimum effort.
For questions/issues you can write ticket here
(Video) Video starting tutorial with some explanation here
(Video) Easy-starter-in-2-mins-installation-with-cli here
(Video) Advanced-starter-in-2-mins-installation-with-cli here
(Video) Start gapi a graphql server with Workers advanced(DOCKER) here
(Video) Start gapi a graphql server with Workers in 2 minutes(DOCKER) here
Integrated external modules:
@Gapi-Typescript-Sequelize
Installation and basic examples:
To install this library, run:
$ npm install Stradivario/gapi --save
Consuming gapi
First we need to install ts-node and nodemon globally
npm install -g nodemon ts-node
Next install gapi globally using npm
npm install gapi-cli -g
Next create project using CLI or read above how to bootstrap your custom application
With CLI
To skip the following steps creating project and bootstraping from scratch you can type the following command:
It may take 20 seconds because it will install project dependencies.
Basic project
gapi new my-project
Advanced Project
gapi new my-project --advanced
Enter inside my-project and type:
npm start
Open browser to
http://localhost:9000/graphiql
Testing
To start developing with testing GAPI uses JEST and gapi-cli is preconfigurated for your needs! :)
To run single test type:
gapi test
Testing watch mode
Note: You need to start server before running tests
Note: Everytime you make change to server it will restart server and execute tests
Note: To add more tests just create e2e.spec.ts or unit.spec.ts somewhere inside the application
Start the application
gapi start
Execute test with --watch argument
gapi test --watch
You will end up with something like this
Custom logic before testing ( for example creating MOCK users to database before testing)
Create file test.ts inside root/src/test.ts with this content
Everytime you run test with --before argument it will set environment variable BEFORE_HOOK
if (process.env.BEFORE_HOOK) {
// do something here
}
Then execute tests with --before
gapi test --before
This command will start root/src/test.ts file and will wait for process.exit(0) so you can customize your before logic check this link for reference
Docker
Following commands will start RabbitMQ, PostgreSQL, API, NGINX as a services and you need DOCKER installed on your system!
More information you can find inside project basic or advanced Documentation
API will be served on https://localhost:80 and https://localhost:80/subscriptions
Your custom certificates can be added here "root/nginx/certs/cert.key" "root/nginx/certs/cert.pem"
To build project with Docker type:
gapi app build
To start project with Docker type:
gapi app start
To stop project type:
gapi app stop
Workers
All workers will be mapped as Proxy and will be reverted to https://localhost:80 and https://localhost:80/subscriptions
So you don't have to worry about if some of your workers stopped responding
Todo: Create monitoring for all workers and main API
To start workers type:
gapi workers start
To stop workers type:
gapi workers stop
Creating application from scratch and custom bootstrapping Without CLI
Create folder structure like this root/src/app
Create AppModule like the example above
Inject UserModule into imports inside AppModule
Folder root/src/app/app.module.ts
import { GapiModule, GapiServerModule, ConfigService } from 'gapi';
import { UserModule } from './user/user.module';
@GapiModule({
imports: [
UserModule
],
services: [
ConfigService.forRoot({
APP_CONFIG: {
port: 9000
}
})
]
})
export class AppModule {}
Create module UserModule in where we will Inject our created controllers
Folder root/src/app/user/user.module.ts
import { GapiModule } from 'gapi';
import { UserQueriesController } from './user.queries.controller';
import { UserMutationsController } from './user.mutations.controller';
import { UserService, AnotherService } from './user/services/user.service';
@GapiModule({
controllers: [
UserQueriesController,
UserMutationsController
],
services: [
UserService,
AnotherService
]
})
export class UserModule {}
Define UserType schema
Folder root/src/user/type/user.type.ts
You can customize every resolver from schema and you can create nested schemas with @GapiObjectType decorator
User Schema
Note that you can modify response result via @Resolve('key for modifier defined inside constructor')
Root is the value of previews resolver so for example root.id = '1';
When you return some value from @Resolve decorator root.id will be replaced with returned value so it will be 5 in the example
If you remove @Resolve decorator it will be passed value returned from the first root resolver
import { GraphQLObjectType, GraphQLString, GraphQLInt, GapiObjectType, Type, Resolve, GraphQLScalarType } from "gapi";
import { UserSettingsObjectType } from './user-settings.type';
@GapiObjectType()
export class UserType {
id: number | GraphQLScalarType = GraphQLInt;
settings: string | UserSettings = UserSettingsObjectType;
@Resolve('id')
getId?(root, payload, context) {
return 5;
}
}
export const UserObjectType = new UserType();
UserSettings Schema
import { GraphQLObjectType, GraphQLString, GraphQLInt, GapiObjectType, Type, Resolve, GraphQLScalarType } from "gapi";
@GapiObjectType()
export class UserSettings {
@Injector(AnotherService) private anotherService?: AnotherService;
readonly username: string | GraphQLScalarType = GraphQLString;
readonly firstname: string | GraphQLScalarType = GraphQLString;
@Resolve('username')
async getUsername?(root, payload, context) {
return await this.anotherService.trimFirstLetterAsync(root.username);
}
@Resolve('firstname')
getFirstname?(root, payload, context) {
return 'firstname-changed';
}
}
export const UserSettingsObjectType = new UserSettings();
UserMessage Schema for Subscriptions
import { GapiObjectType, GraphQLScalarType, GraphQLString } from 'gapi';
@GapiObjectType()
export class UserMessage {
readonly message: number | GraphQLScalarType = GraphQLString;
}
export const UserMessageType = new UserMessage();
Query
Folder root/src/user/query.controller.ts
import { Query, GraphQLNonNull, Scope, Type, GraphQLObjectType, Mutation, GapiController, Service, GraphQLInt, Injector } from "gapi";
import { UserService } from './services/user.service';
import { UserObjectType } from './services/type/user.type';
import { UserMessageType } from './user.subscription.controller';
@GapiController()
export class UserQueriesController {
@Injector(UserService) userService: UserService;
@Scope('ADMIN')
@Type(UserObjectType)
@Query({
id: {
type: new GraphQLNonNull(GraphQLInt)
}
})
findUser(root, { id }, context) {
return this.userService.findUser(id);
}
}
Mutation
Folder root/src/user/mutation.controller.ts
import { Query, GraphQLNonNull, Scope, Type, GraphQLObjectType, Mutation, GapiController, Service, GraphQLInt, Container, Injector, GapiPubSubService, GraphQLString } from "gapi";
import { UserService } from './services/user.service';
import { UserObjectType, UserType } from './types/user.type';
import { UserMessageType, UserMessage } from "./types/user-message.type";
@GapiController()
export class UserMutationsController {
@Injector(UserService) private userService: UserService;
@Injector(GapiPubSubService) private pubsub: GapiPubSubService;
@Scope('ADMIN')
@Type(UserObjectType)
@Mutation({
id: {
type: new GraphQLNonNull(GraphQLInt)
}
})
deleteUser(root, { id }, context): UserType {
return this.userService.deleteUser(id);
}
@Scope('ADMIN')
@Type(UserObjectType)
@Mutation({
id: {
type: new GraphQLNonNull(GraphQLInt)
}
})
updateUser(root, { id }, context): UserType {
return this.userService.updateUser(id);
}
@Scope('ADMIN')
@Type(UserObjectType)
@Mutation({
id: {
type: new GraphQLNonNull(GraphQLInt)
}
})
addUser(root, { id }, context): UserType {
return this.userService.addUser(id);
}
@Scope('ADMIN')
@Type(UserMessageType)
@Mutation({
message: {
type: new GraphQLNonNull(GraphQLString)
},
signal: {
type: new GraphQLNonNull(GraphQLString)
},
})
publishSignal(root, { message, signal }, context): UserMessage {
this.pubsub.publish(signal, `Signal Published message: ${message} by ${context.email}`);
return {message};
}
}
Subscription
Folder root/src/user/user.subscription.controller.ts
import {
GapiObjectType, GraphQLScalarType, GraphQLString, GapiController,
GapiPubSubService, Type, Injector, Subscribe, Subscription, withFilter, Scope, GraphQLInt, GraphQLNonNull
} from 'gapi';
import { UserService } from './services/user.service';
import { UserMessageType, UserMessage } from './types/user-message.type';
@GapiController()
export class UserSubscriptionsController {
@Injector(UserService) private userService: UserService;
@Injector(GapiPubSubService) private static pubsub: GapiPubSubService;
@Scope('USER')
@Type(UserMessageType)
@Subscribe(() => UserSubscriptionsController.pubsub.asyncIterator('CREATE_SIGNAL_BASIC'))
@Subscription()
subscribeToUserMessagesBasic(message): UserMessage {
return { message };
}
@Scope('ADMIN')
@Type(UserMessageType)
@Subscribe(
withFilter(
() => UserSubscriptionsController.pubsub.asyncIterator('CREATE_SIGNAL_WITH_FILTER'),
(payload, {id}, context) => {
console.log('Subscribed User: ', id, JSON.stringify(context));
return id !== context.id;
}
)
)
@Subscription({
id: {
type: new GraphQLNonNull(GraphQLInt)
}
})
subscribeToUserMessagesWithFilter(message): UserMessage {
return { message };
}
}
Example subscription query Basic
subscription {
subscribeToUserMessagesBasic(id:1) {
message
}
}
Example subscription query Basic
subscription {
subscribeToUserMessagesWithFilter(id:1) {
message
}
}
Create Service with @Service decorator somewhere
Folder root/src/user/services/user.service.ts
import { Service } from "gapi";
@Service()
class AnotherService {
trimFirstLetter(username: string) {
return username.charAt(1);
}
trimFirstLetterAsync(username): Promise<string> {
return Promise.resolve(this.trimFirstLetter(username));
}
}
@Service()
export class UserService {
constructor(
private anotherService: AnotherService
) {}
findUser(id: number) {
return { id: 1 };
}
addUser(id: number) {
const username = this.anotherService.trimFirstLetter('username');
return { id: 1, username };
}
deleteUser(id: number) {
return { id: 1 };
}
updateUser(id) {
return { id: 1 };
}
subscribeToUserUpdates() {
return { id: 1 };
}
}
Finally Bootstrap your application
Folder root/src/main.ts
import { AppModule } from './app/app.module';
import { Bootstrap } from 'gapi';
Bootstrap(AppModule);
Start your application using following command inside root folder of the repo
Important the script will search main.ts inside root/src/main.ts where we bootstrap our module bellow
gapi start
Basic authentication
Create Core Module
Folder root/src/app/core/core.module.ts
import { GapiModule, ConfigService } from 'gapi';
import { AuthPrivateService } from './services/auth/auth.service';
import { readFileSync } from 'fs';
@GapiModule({
services: [
ConfigService.forRoot({
APP_CONFIG: {
port: 9000,
cert: readFileSync('./cert.key'),
graphiqlToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImtyaXN0aXFuLnRhY2hldkBnbWFpbC5jb20iLCJpZCI6MSwic2NvcGUiOlsiQURNSU4iXSwiaWF0IjoxNTIwMjkxMzkyfQ.9hpIDPkSiGvjTmUEyg_R_izW-ra2RzzLbe3Uh3IFsZg'
},
}),
AuthPrivateService
]
})
export class CoreModule {}
Create PrivateAuthService @Service() this is complete Subscriptions Query Mutation Authentication via single method "validateToken()"
Above there are example methods from GapiAuth module which are provided on air for encrypting and decrypting user password
Folder root/src/app/core/services/auth/auth.service
import { Service, ConnectionHookService, AuthService, Injector, Container, TokenData } from "gapi";
import * as Boom from 'boom';
interface UserInfo extends TokenData {
scope: ['ADMIN', 'USER']
type: 'ADMIN' | 'USER';
}
@Service()
export class AuthPrivateService {
@Injector(AuthService) private authService: AuthService
@Injector(ConnectionHookService) private connectionHookService: ConnectionHookService
constructor() {
this.connectionHookService.modifyHooks.onSubConnection = this.onSubConnection.bind(this);
this.authService.modifyFunctions.validateToken = this.validateToken.bind(this);
}
onSubConnection(connectionParams): UserInfo {
if (connectionParams.token) {
return this.validateToken(connectionParams.token, 'Subscription');
} else {
throw Boom.unauthorized();
}
}
validateToken(token: string, requestType: 'Query' | 'Subscription' = 'Query'): UserInfo {
const user = <UserInfo>this.verifyToken(token);
user.type = user.scope[0];
console.log(`${requestType} from: ${JSON.stringify(user)}`)
if (user) {
return user;
} else {
throw Boom.unauthorized();
}
}
verifyToken(token: string): TokenData {
return this.authService.verifyToken(token);
}
signJWTtoken(): string {
const jwtToken = this.authService.sign({
email: '',
id: 1,
scope: ['ADMIN', 'USER']
});
return jwtToken;
}
decryptPassword(password: string): string {
return this.authService.decrypt(password)
}
encryptPassword(password: string): string {
return this.authService.encrypt(password)
}
}
Final import CoreModule inside AppModule
import { GapiModule, GapiServerModule } from 'gapi';
import { UserModule } from './user/user.module';
import { UserService } from './user/services/user.service';
import { CoreModule } from './core/core.module';
@GapiModule({
imports: [
UserModule,
CoreModule
]
})
export class AppModule { }
Complex schema object with nested schemas of same type
import { GraphQLObjectType, GraphQLString, GraphQLInt, GapiObjectType, Type, Resolve, GraphQLList, GraphQLBoolean } from "gapi";
import { GraphQLScalarType } from "graphql";
@GapiObjectType()
export class UserSettingsType {
readonly id: number | GraphQLScalarType = GraphQLInt;
readonly color: string | GraphQLScalarType = GraphQLString;
readonly language: string | GraphQLScalarType = GraphQLString;
readonly sidebar: boolean | GraphQLScalarType = GraphQLBoolean;
}
export const UserSettingsObjectType = new UserSettingsType();
@GapiObjectType()
export class UserWalletSettingsType {
readonly type: string | GraphQLScalarType = GraphQLString;
readonly private: string | GraphQLScalarType = GraphQLString;
readonly security: string | GraphQLScalarType = GraphQLString;
readonly nested: UserSettingsType = UserSettingsObjectType;
readonly nested2: UserSettingsType = UserSettingsObjectType;
readonly nested3: UserSettingsType = UserSettingsObjectType;
// If you want you can change every value where you want with @Resolve decorator
@Resolve('type')
changeType(root, args, context) {
return root.type + ' new-type';
}
// 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 :)
@Resolve('nested3')
Example3(root, args, context) {
// CHANGE value of object type when returning
// UserSettingsType {
// "id": 1,
// "color": "black",
// "language": "en-US",
// "sidebar": true
// }
// root.nested3.id
// root.nested3.color
// root.nested3.language
// root.nested3.sidebar
return root.nested3;
}
}
export const UserWalletSettingsObjectType = new UserWalletSettingsType();
@GapiObjectType()
export class UserWalletType {
readonly id: number | GraphQLScalarType = GraphQLInt;
readonly address: string | GraphQLScalarType = GraphQLString;
readonly settings: string | UserWalletSettingsType = UserWalletSettingsObjectType;
}
export const UserWalletObjectType = new UserWalletType();
@GapiObjectType()
export class UserType {
readonly id: number | GraphQLScalarType = GraphQLInt;
readonly email: string | GraphQLScalarType = GraphQLString;
readonly firstname: string | GraphQLScalarType = GraphQLString;
readonly lastname: string | GraphQLScalarType = GraphQLString;
readonly settings: string | UserSettingsType = UserSettingsObjectType;
readonly wallets: UserWalletType = new GraphQLList(UserWalletObjectType);
}
export const UserObjectType = new UserType();
When you create such a query from graphiql dev tools
query {
findUser(id:1) {
id
email
firstname
lastname
settings {
id
color
language
sidebar
}
wallets {
id
address
settings {
type
private
security
nested {
id
color
language
sidebar
}
nested2 {
id
color
language
sidebar
}
nested3 {
id
color
language
sidebar
}
}
}
}
}
This query respond to chema above
findUser(id: number): UserType {
return {
id: 1,
email: "[email protected]",
firstname: "Kristiyan",
lastname: "Tachev",
settings: {
id: 1,
color: 'black',
language: 'en-US',
sidebar: true
},
wallets: [{
id: 1, address: 'dadadada', settings: {
type: "ethereum",
private: false,
security: "TWO-STEP",
nested: {
id: 1,
color: 'black',
language: 'en-US',
sidebar: true
},
nested2: {
id: 1,
color: 'black',
language: 'en-US',
sidebar: true
},
nested3: {
id: 1,
color: 'black',
language: 'en-US',
sidebar: true
},
nested4: {
id: 1,
color: 'black',
language: 'en-US',
sidebar: true
},
}
}]
};
}
The return result from graphql QL will be
{
"data": {
"findUser": {
"id": 1,
"email": "[email protected]",
"firstname": "Kristiyan",
"lastname": "Tachev",
"settings": {
"id": 1,
"color": "black",
"language": "en-US",
"sidebar": true
},
"wallets": [
{
"id": 1,
"address": "dadadada",
"settings": {
"type": "ethereum new-type",
"private": "false",
"security": "TWO-STEP",
"nested": {
"id": 1,
"color": "black",
"language": "en-US",
"sidebar": true
},
"nested2": {
"id": 1,
"color": "black",
"language": "en-US",
"sidebar": true
},
"nested3": {
"id": 1,
"color": "black",
"language": "en-US",
"sidebar": true
}
}
}
]
}
}
}
TODO: Better documentation...
Enjoy ! :)