Skip to content

Commit 184730f

Browse files
committed
Implemented GraphQL-based API gateway
1 parent 911b4c6 commit 184730f

36 files changed

+918
-57
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ target
1212
.project
1313
.settings
1414
bin
15+
node_modules
16+
dist

build-and-test-all.sh

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,38 @@
11
#! /bin/bash -e
22

3+
KEEP_RUNNING=
4+
ASSEMBLE_ONLY=
5+
6+
while [ ! -z "$*" ] ; do
7+
case $1 in
8+
"--keep-running" )
9+
KEEP_RUNNING=yes
10+
;;
11+
"--assemble-only" )
12+
ASSEMBLE_ONLY=yes
13+
;;
14+
"--help" )
15+
echo ./build-and-test-all.sh --keep-running --assemble-only
16+
exit 0
17+
;;
18+
esac
19+
shift
20+
done
21+
22+
echo KEEP_RUNNING=$KEEP_RUNNING
23+
324
. ./set-env.sh
425

526
initializeDynamoDB() {
6-
echo preparing dynamodblocal table data
7-
cd dynamodblocal-init
8-
./create-dynamodb-tables.sh
9-
cd ..
10-
echo data is prepared
27+
./initialize-dynamodb.sh
1128
}
1229

30+
# TODO Temporarily
31+
32+
./build-contracts.sh
33+
34+
./gradlew testClasses
35+
1336
docker-compose down -v
1437
docker-compose up -d --build dynamodblocal mysql
1538

@@ -21,36 +44,49 @@ initializeDynamoDB
2144

2245
docker-compose up -d --build eventuate-local-cdc-service tram-cdc-service
2346

24-
# TODO Temporarily
2547

26-
./build-contracts.sh
48+
if [ -z "$ASSEMBLE_ONLY" ] ; then
2749

28-
./gradlew -x :ftgo-end-to-end-tests:test $* build
50+
./gradlew -x :ftgo-end-to-end-tests:test $* build
2951

30-
docker-compose build
52+
docker-compose build
3153

32-
./gradlew $* integrationTest
54+
./gradlew $* integrationTest
3355

3456

35-
# Component tests need to use the per-service database schema
57+
# Component tests need to use the per-service database schema
3658

37-
SPRING_DATASOURCE_URL=jdbc:mysql://${DOCKER_HOST_IP?}/ftgoorderservice ./gradlew :ftgo-order-service:cleanComponentTest :ftgo-order-service:componentTest
59+
SPRING_DATASOURCE_URL=jdbc:mysql://${DOCKER_HOST_IP?}/ftgoorderservice ./gradlew :ftgo-order-service:cleanComponentTest :ftgo-order-service:componentTest
3860

39-
# Reset the DB/messages
61+
# Reset the DB/messages
4062

41-
docker-compose down -v
42-
docker-compose up -d
63+
docker-compose down -v
64+
docker-compose up -d
65+
66+
67+
else
68+
69+
./gradlew $* assemble
70+
docker-compose up -d --build
71+
72+
fi
4373

4474
./wait-for-mysql.sh
4575

4676
echo mysql is started
4777

4878
initializeDynamoDB
4979

50-
date
80+
./wait-for-mysql.sh
81+
82+
echo mysql is started
5183

5284
./wait-for-services.sh
5385

5486
./gradlew :ftgo-end-to-end-tests:cleanTest :ftgo-end-to-end-tests:test
5587

56-
docker-compose down -v
88+
./run-graphql-api-gateway-tests.sh
89+
90+
if [ -z "$KEEP_RUNNING" ] ; then
91+
docker-compose down -v
92+
fi

docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ services:
180180
SPRING_SLEUTH_SAMPLER_PROBABILITY: 1
181181
SPRING_ZIPKIN_BASE_URL: http://zipkin:9411/
182182

183+
ftgo-api-gateway-graphql:
184+
build: ./ftgo-api-gateway-graphql
185+
ports:
186+
- "8088:3000"
187+
environment:
188+
ORDER_HISTORY_SERVICE_URL: http://ftgo-order-history-service:8080
189+
CONSUMER_SERVICE_URL: http://ftgo-consumer-service:8080
190+
RESTAURANT_SERVICE_URL: http://ftgo-restaurant-service:8080
191+
183192
zipkin:
184193
image: openzipkin/zipkin:2.5.0
185194
ports:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

ftgo-api-gateway-graphql/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM node:9.11.2-alpine
2+
COPY package.json .
3+
RUN npm install
4+
COPY tsconfig.json .
5+
ADD src ./src
6+
RUN npm run build
7+
CMD npm run start
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
globals: {
3+
'ts-jest': {
4+
tsConfigFile: 'tsconfig.json'
5+
}
6+
},
7+
moduleFileExtensions: [
8+
'ts',
9+
'js'
10+
],
11+
transform: {
12+
'^.+\\.(ts|tsx)$': './node_modules/ts-jest/preprocessor.js'
13+
},
14+
testMatch: [
15+
'**/tests/**/*.test.(ts|js)'
16+
],
17+
testEnvironment: 'node'
18+
};

ftgo-api-gateway-graphql/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"private": true,
3+
"main": "./src/index.js",
4+
"scripts": {
5+
"start": "node ./dist/index.js",
6+
"build": "tsc",
7+
"dev": "tsc --watch & nodemon dist/server.js",
8+
"unit-test": "tsc && jest --config jest.config.js tests/unit",
9+
"end-to-end-test": "tsc && jest --config jest.config.js tests/end-to-end",
10+
"lint": "eslint src --ext ts",
11+
"tsc": "tsc",
12+
"clean": "rm -fr dist"
13+
},
14+
"dependencies": {
15+
"apollo-cache-inmemory": "^1.2.1",
16+
"apollo-client": "^2.3.1",
17+
"apollo-engine": "^1.0.1",
18+
"apollo-link-http": "^1.5.4",
19+
"apollo-server-express": "1.3.2",
20+
"body-parser": "^1.17.1",
21+
"dataloader": "^1.4.0",
22+
"express": "^4.15.2",
23+
"graphql-tools": "2.20.2",
24+
"node-fetch": "^2.0.0"
25+
},
26+
"devDependencies": {
27+
"@types/mocha": "^5.2.0",
28+
"@types/node": "^10.1.4",
29+
"apollo-boost": "^0.1.6",
30+
"apollo-link-persisted-queries": "^0.2.0",
31+
"apollo-link-schema": "^1.1.0",
32+
"graphql": "^0.13.2",
33+
"graphql-tag": "^2.9.2",
34+
"jest": "^23.0.1",
35+
"ts-jest": "^22.4.6",
36+
"tsc": "^1.20150623.0",
37+
"typescript": "^2.8.4"
38+
}
39+
}

ftgo-api-gateway-graphql/queries.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
3+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "query { consumer(consumerId:1) { id, firstName, lastName} } " }'
4+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "query { consumer(consumerId:1) { id, firstName, lastName orders { orderId restaurant {id name }} } } " }'
5+
6+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "{ orders(consumerId:1) { orderId, consumer { id, firstName, lastName} } }" }'
7+
8+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "{ orders(consumerId:1) { orderId, restaurant { name } } }" }'
9+
10+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "{ consumer(consumerId:1) { id, firstName, lastName, orders { orderId } } } " }'
11+
12+
13+
# doesn't like spaces in query!
14+
15+
curl -v -g 'http://localhost:3000/graphql?query={orders(consumerId:1){orderId,restaurant{id}}}'
16+
17+
18+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{ "query": "{ orders(consumerId:1) { orderId, restaurant { name } } }" }'
19+
20+
21+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{
22+
23+
"query": "mutation ($fn : String, $ln : String) { createConsumer(c: {firstName: $fn, lastName: $ln}) { id, orders { orderId } } } ",
24+
"variables" : { "fn": "Chris", "ln" : "Richardson" }
25+
26+
}'
27+
28+
curl http://localhost:3000/graphql -X POST -H "Content-Type: application/json" --data '{
29+
30+
"query": "{ __schema { types { name } } }"
31+
}'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const fetch = require("node-fetch");
2+
const DataLoader = require('dataloader');
3+
4+
5+
class ConsumerServiceProxy {
6+
7+
constructor(options) {
8+
console.log("ConsumerServiceProxy constructor", options, DataLoader);
9+
this.dataLoader = new DataLoader(keys => this.batchFindConsumers(keys));
10+
this.consumerService = `${options.baseUrl}/consumers`;
11+
}
12+
13+
findConsumer(consumerId) {
14+
console.log("findConsumer")
15+
return this.dataLoader.load(consumerId);
16+
}
17+
18+
findConsumerInternal(consumerId) {
19+
console.log("findConsumerInternal")
20+
return fetch(`${this.consumerService}/${consumerId}`)
21+
.then(response => {
22+
console.log("response=", response.status);
23+
if (response.status == 200) {
24+
return response.json().then(body => {
25+
console.log("response=", body);
26+
return Object.assign({
27+
id: consumerId,
28+
firstName: body.name.firstName,
29+
lastName: body.name.lastName
30+
}, body);
31+
})
32+
} else
33+
return Promise.reject(new Error("cannot found consumer for id" + consumerId))
34+
});
35+
}
36+
37+
createConsumer(firstName, lastName) {
38+
console.log("createConsumer")
39+
return fetch(this.consumerService, {
40+
method: 'POST',
41+
body: JSON.stringify({name: {firstName, lastName}}),
42+
headers: {"Content-Type": "application/json"}
43+
})
44+
.then(r => r.json())
45+
.then(({consumerId}) => {
46+
console.log("consumerId=", consumerId);
47+
return {id: consumerId, orders: []};
48+
})
49+
}
50+
51+
batchFindConsumers(keys) {
52+
console.log("keys=", keys);
53+
return Promise.all(keys.map(k => this.findConsumerInternal(k)));
54+
}
55+
56+
}
57+
58+
module.exports = {ConsumerServiceProxy};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const fetch = require("node-fetch");
2+
3+
class OrderServiceProxy {
4+
5+
constructor(options) {
6+
this.orderService = `${options.baseUrl}/orders`;
7+
}
8+
9+
findOrders(consumerId) {
10+
return fetch(`${this.orderService}?consumerId=${consumerId}`)
11+
.then(res => res.json())
12+
.then(data => {
13+
// Sometimes, there are no upcoming events
14+
console.log("orders=", data);
15+
const x = data.orders.map((order) => Object.assign({consumerId}, order));
16+
console.log("orders=", x);
17+
return x;
18+
});
19+
}
20+
21+
}
22+
23+
module.exports = {OrderServiceProxy};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const fetch = require("node-fetch");
2+
const DataLoader = require('dataloader');
3+
4+
class RestaurantServiceProxy {
5+
constructor(options) {
6+
this.restaurantServiceUrl = `${options.baseUrl}/restaurants`;
7+
this.dataLoader = new DataLoader(restaurantIds => this.batchFindRestaurants(restaurantIds));
8+
console.log("this.restaurantServiceUrl", this.restaurantServiceUrl);
9+
}
10+
11+
findRestaurant(restaurantId) {
12+
return this.dataLoader.load(restaurantId);
13+
}
14+
15+
findRestaurantInternal(restaurantId) {
16+
return fetch(`${this.restaurantServiceUrl}/${restaurantId}`)
17+
.then(response => {
18+
console.log("response=", response.status);
19+
if (response.status === 200) {
20+
return response.json().then(body => {
21+
console.log("response=", body);
22+
return Object.assign({id: restaurantId, name: body.name.name}, body);
23+
})
24+
} else
25+
return Promise.reject(new Error("cannot found restaurant for id" + restaurantId))
26+
});
27+
}
28+
29+
batchFindRestaurants(restaurantIds) {
30+
console.log("restaurantIds=", restaurantIds);
31+
return Promise.all(restaurantIds.map(k => this.findRestaurantInternal(k)));
32+
}
33+
34+
}
35+
36+
module.exports = {RestaurantServiceProxy};

ftgo-api-gateway-graphql/src/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
const {app} = require("./server");
3+
4+
const PORT = 3000;
5+
6+
app.listen(PORT);
7+
8+
console.log("Server listing on port: ", PORT);

0 commit comments

Comments
 (0)