@rxdi/graphqj
Create easy Graphql server from json
, yml
, graphql
, js
or ts
files
Features
- Graphql Voyager available
- Helps with prototyping MVP
- In about a minute you have working graphql API
- Graphiql included in the pack for easy development
- Watch and rebuild GQL Schema dynamically without restarting server
- It provides HotRealod of Graphql Schema without rebuilding or restarting application.Everyting is happening on Runtime.
@rxdi/graphqj
What is - Tool for creating Graphql backend from Different source for testing purposes and Client MVP's
@rxdi/graphqj
What is not - A Production ready server (it is created only for MVP's)
For production ready server check @gapi/core
Installation
npm i -g @rxdi/graphqj
Configuration
Define gj.json
or execute gj init
,
gj init
by default creates basic
configuration with json
Available config templates:
gj init {advanced | es6 | typescript | jml}
Basic configuration
{
"$mode": "basic",
"$resolvers": {
"findUser": {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [515151, 412414],
"arrayOfStrings": ["515151", "412414"]
}
}
}
Advanced configuration
{
"$mode": "advanced",
"$types": {
"User": {
"name": "String",
"email": "String",
"phone": "number",
"arrayOfNumbers": "number[]",
"arrayOfStrings": "string[]"
}
},
"$args": {
"UserPayload": {
"userId":"String!",
"userId2":"String",
"userId3":"String!",
"userId4":"String",
}
},
"$resolvers": {
"findUser": {
"type": "User",
"args": {
"userId":"String!",
"userId":"String",
},
"resolve": {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [515151, 412414],
"arrayOfStrings": ["515151", "412414"]
}
},
"findUserWithPayloadRequired": {
"type": "User",
"args": {
"payload":"UserPayload!",
},
"resolve": {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [515151, 412414],
"arrayOfStrings": ["515151", "412414"]
}
},
}
}
Schema:
type Query {
findUser: User
status: StatusQueryType
}
type StatusQueryType {
status: String
}
type User {
name: String
email: String
phone: Int
arrayOfNumbers: [Int]
}
Query:
query {
findUser {
name
email
phone
arrayOfNumbers
arrayOfStrings
}
}
Result:
{
"data": {
"findUser": {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [
515151,
412414
],
"arrayOfStrings": [
"515151",
"412414"
]
}
}
}
Starting server
This command will look for gj.{json | js | ts | yml}
configuration inside working directory
gj
Changing port
Default port is 9000
gj --port 5000
Hot reload of Bundles (Beta)
gj --hot-reload
Build client side application inside Configuration file (Beta)
gj --client
schema.graphql
from JSON
Generating gj --generate
Spawn random PORT on every start
gj --random
Try experimental HOT Module reload when developing client side application
gj --client --hot-reload
Advanced configuration
Typescript
To be able to run config with typescript you need to install @gapi/cli
globally
This will transpile our typescript
file to javascript
and load it automatically
npm i -g @gapi/cli
Filename: gj.ts
export default {
$mode: 'advanced',
$types: {
user: {
name: 'String',
email: 'String',
phone: 'Number',
arrayOfNumbers: 'Number[]',
arrayOfStrings: 'String[]'
}
},
$resolvers: {
findUser: {
type: 'user',
args: {
userId: "String!",
userId2: "String",
},
resolve: async (root, payload: { userId: string; userId2?: string }) => ({
name: 'Kristiyan Tachev',
email: 'test@gmail.com',
phone: 4141423,
arrayOfNumbers: [515151, 412414],
arrayOfStrings: ['515151', '412414']
})
}
}
};
ES6
Filename: gj.js
export default {
$mode: 'advanced',
$types: {
user: {
name: 'String',
email: 'String',
phone: 'Number',
arrayOfNumbers: 'Number[]',
arrayOfStrings: 'String[]'
}
},
$resolvers: {
findUser: {
type: 'user',
args: {
userId: "String!",
userId2: "String",
},
resolve: async (root, payload: { userId: string; userId2?: string }) => ({
name: 'Kristiyan Tachev',
email: 'test@gmail.com',
phone: 4141423,
arrayOfNumbers: [515151, 412414],
arrayOfStrings: ['515151', '412414']
})
}
}
};
YML
Filename: gj.yml
$mode: advanced
$types:
User:
name: String
email: String
phone: Number
arrayOfNumbers: Number[]
arrayOfStrings: String[]
$args:
UserPayload:
userId: String!
userId2: String
userId3: String
userId4: String
$resolvers:
findUser:
type: User
args:
payload: UserPayload
resolve:
name: Kristiyan Tachev
email: test@gmail.com
phone: 414141
arrayOfNumbers:
- 515151
- 412414
arrayOfStrings:
- '515151'
- '412414'
findUser2:
type: User
args:
payload: UserPayload
resolve:
name: Kristiyan Tachev
email: test@gmail.com
phone: 414141
arrayOfNumbers:
- 515152
- 412414
arrayOfStrings:
- '515151'
- '412414'
Loading existing generated schema
Filename: gj.json
{
"$schema": "./schema.graphql"
}
Or
gj --schema ./schema.graphql
Graphql Voyager
Open http://localhost:9000/voyager
Aliases
graphqj
, gg
, gj
Exclude
Exclude .gj
folder inside your .gitignore
or .dockerignore
files
Folder .gj
is working directory when we store transpiled typescript
configuration file
📡
Experimental $mode: advanced
$directives: ./directives.ts
$externals:
- map: 🛰
file: ./interceptors.ts
- map: 🛡️
file: ./guards.ts
- map: 🕵️
file: ./modifiers.ts
- map: ⌛
file: ./helpers/moment.js
$types:
User:
name: String => {🕵️OnlyAdmin}
email: String => {🛰LoggerInterceptor}
phone: Number => {🛡️IsLogged}
arrayOfNumbers: Number[] => {🕵️OnlyAdmin}
arrayOfStrings: String[]
createdAt: String => {⌛fromNow}
$args:
UserPayload:
userId: String!
userId2: String
userId3: String
userId4: String
$resolvers:
findUser:
type: User
args:
payload: UserPayload
resolve:
name: Kristiyan Tachev
email: test@gmail.com
phone: 414141
arrayOfNumbers:
- 515151
- 412414
arrayOfStrings:
- '515151'
- '412414'
$views:
home:
query: findUser
props: User
output: UserPayload
html: |
<bla-component></bla-component>
{userId} {name} {email} {phone} {createdAt}
A rich framework for building applications and services with GraphQL and Apollo inspired by Angular
Moment helper
import moment from 'moment';
export function fromNow() {
return moment('20111031', 'YYYYMMDD').fromNow();
}
Chaining multiple $externals
is quite easy
email: String => {🛰LoggerInterceptor} => {🛡️IsLogged} => {🕵️OnlyAdmin}
Magics
With Syringe 💉
operator you can inject yml
, js
, ts
, json
, graphql
and html
files,
$views:
home:
query: findUser2
payload: UserPayload
html: 💉./my.html
You can compose anything inside gj.yml
$mode: advanced
$directives: ./directives.ts
$externals:
- map: 🛰
file: ./interceptors.ts
- map: 🛡️
file: ./guards.ts
- map: 🕵️
file: ./modifiers.ts
$types: 💉./types.yml
$args: 💉./args.yml
$resolvers: 💉./resolvers.yml
$views:
home:
query: findUser2
payload: UserPayload
html: 💉./test.html
Even defining Graphql
resolvers is simply easy
$mode: advanced
$resolvers:
findUser:
type: User
args:
payload: UserPayload
resolve: 💉./findUser.ts
Resolver
import { Observable } from 'rxjs';
import { IUserType } from '@api';
export async function findUser(root, payload, context, info):
| Promise<Observable<IUserType>>
| Promise<IUserType>
| Observable<IUserType>
| IUserType {
return {
name: 'dada',
email: 'dada',
phone: 13131,
arrayOfNumbers: [111, 222],
arrayOfStrings: ['dada', 'dada']
};
}
Guard
import { Observable } from 'rxjs';
export async function IsLogged(
chainable$: Observable<any>,
root,
payload,
context,
descriptor
) {
if (!context.user) {
throw new Error('Unauthorized');
}
}
Interceptor
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
export async function LoggerInterceptor(
chainable$: Observable<any>,
root,
payload,
context,
descriptor
) {
console.log('Before...');
const now = Date.now();
return chainable$.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`))
);
}
Modifier
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
export async function OnlyAdmin(
chainable$: Observable<any>,
root,
payload,
context,
descriptor
) {
return chainable$.pipe(map(() => null));
}
Directives
import {
DirectiveLocation,
GraphQLCustomDirective,
GraphQLNonNull,
GraphQLString
} from '@gapi/core';
export async function toUppercase() {
return new GraphQLCustomDirective({
name: 'toUpperCase',
description: 'change the case of a string to uppercase',
locations: [DirectiveLocation.FIELD],
resolve: async resolve => (await resolve()).toUpperCase()
});
}
export async function AddTextDirective() {
return new GraphQLCustomDirective({
name: 'AddTextDirective',
description: 'change the case of a string to uppercase',
locations: [DirectiveLocation.FIELD],
args: {
inside: {
type: new GraphQLNonNull(GraphQLString),
description: 'the times to duplicate the string'
},
outside: {
type: new GraphQLNonNull(GraphQLString),
description: 'the times to duplicate the string'
}
},
resolve: async (
resolve,
root,
args
) => args.inside + (await resolve()) + args.outside
});
}
Possible query
{
findUser {
name
email @toUpperCase @AddTextDirective(inside: "dada", outside: "dadada")
phone
arrayOfNumbers
arrayOfStrings
}
}
Omg YML
Defining javascript
function in yml
$omg: !!js/function >
function foobar() {
return 'Wow! JS-YAML Rocks!';
}
Defining JS function in resolver
$resolvers:
findUser:
type: User
args:
payload: UserPayload
resolve: !!js/function >
function foobar(root, payload, context, info) {
console.log('OMG')
return {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [515151, 412414],
"arrayOfStrings": ['515151', '412414']
}
}
Possible flows
seq:
# Ordered sequence of nodes
Block style: !!seq
- Mercury # Rotates - no light/dark sides.
- Venus # Deadliest. Aptly named.
- Earth # Mostly dirt.
- Mars # Seems empty.
- Jupiter # The king.
- Saturn # Pretty.
- Uranus # Where the sun hardly shines.
- Neptune # Boring. No rings.
- Pluto # You call this a planet?
Flow style: !!seq [ Mercury, Venus, Earth, Mars, # Rocks
Jupiter, Saturn, Uranus, Neptune, # Gas
Pluto ] # Overrated
Will create the following json object
{
"seq":{
"Block style":[
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
"Pluto"
],
"Flow style":[
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
"Pluto"
]
}
}
Dependencies can be injected also
Define inside $externals
following:
$externals:
- map: 🕵️
file: ./my-functions.js
Where ./my-functions.js
looks like this
export async function test() {
return {};
}
export async function test2() {
return {};
}
export async function test3() {
return {};
}
Then you can inject these functions and use them
findUser:
deps: [{ provide: 🕵️, map: 'myFunctions'}]
type: User
args:
payload: UserPayload
resolve: !!js/function >
function foobar(root, payload, context, info) {
console.log(this.myFunctions.test()) // {}
console.log(this.myFunctions.test2()) // {}
console.log(this.myFunctions.test3()) // {}
return {
"name": "Kristiyan Tachev",
"email": "test@gmail.com",
"phone": 414141,
"arrayOfNumbers": [515151, 412414],
"arrayOfStrings": ['515151', '412414']
}
}
findUser2: 💉./resolvers/findUser.resolver.yml
Possible view configuration
$mode: advanced
# $imports:
# - 💉./examples/mix/hamburger/server/hamburger.server.module.ts
# $components:
# - 💉./examples/mix/hamburger/client/hamburger.client.module.ts
$types:
User:
name: String
email: String
phone: Number
arrayOfNumbers: Number[]
arrayOfStrings: String[]
arrayOfStrings2: String[]
users: User[]
$args:
UserPayload:
name: String!
pesho: String
$resolvers:
findUser:
type: User
args:
userId: UserPayload
resolve: !!js/function >
function foobar(root, payload, context, info) {
return {
"name": "Кристиян Тачев",
"arrayOfStrings": ["dada", "dada"],
"email": "kristiqn.tachev@gmail.com",
"phone": 876667537
}
}
$views:
app:
components:
html: |
<style>
.spacer {
flex: 1 3 auto;
}
.container {
display: flex;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f3f3f3;
cursor: pointer;
}
li {
float: left;
}
li a {
display: block;
color: #666;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
li a:hover:not(.active) {
background-color: #ddd;
}
li a.active {
color: white;
background-color: #4caf50;
}
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #03a9f4;
color: white;
text-align: center;
}
</style>
<ul class="container" slot="header">
<li><a href="/">Home</a></li>
<li><a href="/gosho">Gosho</a></li>
<li><a href="/gosho444">Gosho444</a></li>
<li><a href="/dadada">1</a></li>
<li><a href="/dadada">2</a></li>
<li><a href="/dadada">3</a></li>
<li><a href="/dadada">4</a></li>
<li><a href="/dadada">5</a></li>
<span class="spacer"></span>
</ul>
<div class="footer" slot="footer">
<p>Footer</p>
</div>
home:
query: |
query findUser {
findUser {
name
email
phone
arrayOfStrings
}
}
output: UserPayload
policy: network-only
html: |
Welcome to Home component
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Phone: {phone}</p>
{arrayOfStrings}
<div style="background-color: red">
<hamburger-component type="3dx" active=true enableBackendStatistics=${true}></hamburger-component>
</div>
not-found:
html: |
Not found
gosho:
query: findUser
html: |
Welcome to Gosho
<p>Name: {name}</p>
<p>Email: {email}</p>
<p>Phone: {phone}</p>
dadada:
html: |
Welcome to Dadada