Skip to content

Instantly share code, notes, and snippets.

@trongthanh
Created October 19, 2016 06:26
Show Gist options
  • Save trongthanh/f291f81002976cface6103c89e84aeff to your computer and use it in GitHub Desktop.
Save trongthanh/f291f81002976cface6103c89e84aeff to your computer and use it in GitHub Desktop.
Intro to GraphQL
/**
* Client-side GraphQL data fetch example with HTML5 Fetch API
*
* @param {String} url URL to the GraphQL endpoint
* @param {String} query The GraphQL query
* @return {Promise} the request resolving promise object
*/
function fetchData(url = 'http://localhost:3000/graphql', query = '{ hello }') { //eslint-disable-line no-unused-vars
let requiredHeaders = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
});
let req = new Request(url, {
method: 'POST',
headers: requiredHeaders,
body: JSON.stringify({ query }), // TODO: add 'variables' to the body
});
return fetch(req).then(response => {
console.log('response.status:', response.status);
if (response.ok) {
return response.json(); // result will be JSON object
} else {
return response.json(); // Even response is 4**, we still receive json object describe error
}
}).catch(err => {
console.log('Errors:', err);
return err;
});
}
{
"name": "graphql-demo",
"version": "1.0.0",
"description": "Hellow World",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Thanh Tran <[email protected]> (http://int3ractive.com)",
"license": "ISC",
"dependencies": {
"@risingstack/graffiti": "^3.2.0",
"@risingstack/graffiti-mongoose": "^5.3.0",
"body-parser": "^1.15.2",
"express": "^4.14.0",
"express-graphql": "^0.5.4",
"graphql": "^0.7.1",
"mongoose": "^4.6.4",
"react": "^15.3.2",
"react-dom": "^15.3.2"
},
"devDependencies": {
"webpack": "^1.13.2"
}
}
/**
* Step 1: Start simple GraphQL logic running inside NodeJS
*/
var graphql = require('graphql').graphql;
var buildSchema = require('graphql').buildSchema;
// Construct a schema, using GraphQL schema language
/* eslint no-multi-str:0 */
var schema = buildSchema('\
type Query {\
hello: String\
}\
');
// The root provides a resolver function for each API endpoint
var root = {
hello: function() {
return 'Hello world!';
},
};
// Run the GraphQL query '{ hello }' and print out the response
graphql(schema, '{ hello }', root).then(function(response) {
console.log(response);
});
/**
* Step 10: Nested objects
*
* Note #1: Mongoose queries are not real Promise, should run exec() at the end of the query
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
mongoose.Promise = global.Promise; //use native Promise for mongoose as per http://mongoosejs.com/docs/promises.html
mongoose.connect('mongodb://localhost/graphql');
const Post = mongoose.model('post', {
id: mongoose.Schema.Types.ObjectId,
timestamp: Date,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'author' }, // --> reference to Author collection
});
const Author = mongoose.model('author', {
id: mongoose.Schema.Types.ObjectId,
firstName: String,
lastName: String,
email: String,
});
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Post {
id: ID!,
content: String!,
timestamp: String,
author: Author,
}
type Author {
_id: ID!,
firstName: String!,
lastName: String,
email: String,
}
type Query {
post(id: ID!): Post,
posts: [Post],
author(id: ID!): Author,
authors: [Author],
}
input AuthorInput {
firstName: String!,
lastName: String,
email: String,
}
input PostInput {
author: ID!,
content: String!,
}
type Mutation {
createPost(input: PostInput): Post,
createAuthor(input: AuthorInput): Author,
}
`);
// The root provides a resolver function for each API endpoint
const root = {
author({id}) {
return Author.findOne({_id: id}).exec();
},
authors() {
return Author.find({}).exec();
},
createAuthor({input}) {
let author = new Author(input);
return author.save().exec();
},
post({id}) {
return Post.findOne({id: id}).populate('author').exec();
},
posts() {
return Post.find({}).populate('author').exec();
},
createPost({input}) {
let newPost = new Post(input);
// we can do some field remapping if needed:
// new Post({ by: input.author, body: input.content })
console.log('newPost', newPost);
// map id prop to built-in mongodb _id
newPost.id = newPost._id;
newPost.timestamp = Date.now();
return newPost.save().then((post) => {
// the returned post is raw object, we'll wrap it in Post model again
// and do the population for author
return new Post(post).populate('author').execPopulate();
});
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
#1. Create authors
mutation {
createAuthor(input: {
id: "hl",
firstName: "Hiếu",
lastName: "Lê",
email: "[email protected]"
}) {
id
}
}
mutation {
createAuthor(input: {
id: "ld",
firstName: "Long",
lastName: "Đỗ",
email: "[email protected]"
}) {
id
}
}
#2. Create post
mutation {
createPost(input: {
author: "58049c54f754033ed8150c0e",
content:"Tháng tư là lời nói dối của anh"
}) {
id,
author {
firstName
}
}
}
#3. Get author
{
author(id: "hl") {
firstName,
lastName,
email
}
}
#4. Get authors
{
authors {
firstName,
lastName,
email
}
}
#5. Get posts
{
posts {
author {
id,
firstName,
lastName
},
content
}
}
*/
/*const db = {
posts: [{
id: '58049c54f754033ed8150c0e',
author: 'Hiếu',
content: 'As I went on, still gaining velocity, the palpitation of night and day merged into one continuous greyness; the sky took on a wonderful deepness of blue, a splendid luminous color like that of early twilight; the jerking sun became a streak of fire, a brilliant arch, in space; the moon a fainter fluctuating band; and I could see nothing of the stars, save now and then a brighter circle flickering in the blue.',
timestamp: 1476548728731,
}, {
id: '2',
author: 'Long',
content: 'A peep at some distant orb has power to raise and purify our thoughts like a strain of sacred music, or a noble picture, or a passage from the grander poets. It always does one good.',
timestamp: 1476462317277,
}, {
id: '3',
author: 'Đạt',
content: 'Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car seemed to float in the middle of an immense dark sphere, whose upper half was strewn with silver.',
timestamp: 1473956762837,
}]
};
"authors": [
{
"_id": "58049c54f754033ed8150c0e",
"firstName": "Hiếu"
},
{
"_id": "58049c8df754033ed8150c0f",
"firstName": "Long"
}
]
*/
const express = require('express');
const { json } = require('body-parser');
const graffiti = require('@risingstack/graffiti');
const { getSchema } = require('@risingstack/graffiti-mongoose');
const mongoose = require('mongoose');
mongoose.Promise = global.Promise; //use native Promise for mongoose as per http://mongoosejs.com/docs/promises.html
mongoose.connect('mongodb://localhost/graphql');
const Post = mongoose.model('Post', {
id: mongoose.Schema.Types.ObjectId,
timestamp: Date,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, // --> reference to Author collection
});
const Author = mongoose.model('Author', {
firstName: String,
lastName: String,
email: String,
// TODO: fix this circular dependencies
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post' }]
});
const app = express();
// parse body as json
app.use(json());
app.use(graffiti.express({
schema: getSchema([Post, Author]),
context: {}, // custom context
graphiql: true
}));
app.listen(3000);
console.log('Server started and listen at http://localhost:3000');
/*
Sample queries
{
anh_hieu: author(id: "58049c54f754033ed8150c0e") {
firstName,
lastName,
email,
posts {
edges() {
node {
content,
timestamp,
}
}
}
}
}
// get all post
{
posts {
_id,
content
author {
firstName,
lastName
}
}
}
*/
/**
* Step 2: Add express and express-graphql to allows previewing with GraphiQL
* and test fetching from from browser
*
* NOTE #1: Use ES6 const & let
* NOTE #2: Use ES6 template string
* NOTE #3: Use ES6 object method literal
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const buildSchema = require('graphql').buildSchema;
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Query {
hello: String
}
`);
// The root provides a resolver function for each API endpoint
const root = {
hello() {
return 'Hello world!';
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
```
{
hello
}
```
Browser's Fetch API demo
```
let requiredHeaders = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json',
});
let req = new Request(url, {
method: 'POST',
headers: requiredHeaders,
body: JSON.stringify({ query })
});
fetch(req).then(response => {
console.log('response.status:', response.status);
if (response.ok) {
return response.json(); // result will be JSON object
} else {
return null; // OR null
}
}).catch(err => {
console.log('Errors:', err);
return err;
});
```
*/
/**
* Step 3: Implement query parameters, passing arguments to resolver
*
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const buildSchema = require('graphql').buildSchema;
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Query {
hello: String,
echo(name: String!): String
}
`);
// The root provides a resolver function for each API endpoint
const root = {
hello() {
return 'Hello world!';
},
echo(args) {
let name = args.name;
return `Hello ${name.toUpperCase()}. Greetings from GraphQL Server. Ahihi.`;
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
```
{
echo(name: 'Thanh')
}
```
Browser's Fetch API demo
```
// First declare the function fetchData() as in fetch-data.js
fatchData('http://localhost:3000/graphql', `{
echo(name: "Hiếu")
}`).then(json => {
console.log('Response:', JSON.stringify(json));
});
```
*/
/**
* Step 4: Adding placeholders to query, leaving query as literal string
*
* Note #1: Only changed the destructuring assignment here
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Query {
hello: String,
echo(name: String!): String
}
`);
// The root provides a resolver function for each API endpoint
const root = {
hello() {
return 'Hello world!';
},
echo({ name }) {
return `Hello ${name.toUpperCase()}. Greetings from GraphQL Server. Ahihi`;
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
```
query Echo(name: String) {
echo(name: $name)
}
```
Browser's Fetch API demo
```
// First declare the function fetchData() as in fetch-data.js
fatchData('http://localhost:3000/graphql', `
query Echo($name: String!) {
echo(name: $name)
}`, {
name: 'Hiếu'
})
.then(json => {
console.log('Response:', JSON.stringify(json));
});
```
*/
/**
* Step 5: Introduce GraphQL scala types: String, Int, Float, Boolean, ID
* List indicated with []
*
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Query {
hello: String!,
echo(name: String!): String!,
random: Float!,
randomRange(fromInt: Int!, toInt: Int!): Int!,
areYouGay: Boolean!,
names: [String]!
}
`);
// The root provides a resolver function for each API endpoint
const root = {
hello() {
return 'Hello world!';
},
echo({ name }) {
return `Hello ${name.toUpperCase()}. Greetings from GraphQL Server. Ahihi`;
},
random() {
return Math.random();
},
randomRange({fromInt, toInt}) {
return fromInt + Math.floor(Math.random() * (toInt - fromInt + 1));
},
areYouGay() {
return Math.random() < 0.5;
},
names() {
return ['Hiếu', 'Quân', 'Khoa', 'Đạt'];
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
```
{
areYouGay,
random,
randomRange(fromInt: 1, toInt: 6),
names,
}
```
*/
/**
* Step 6: Introduce custom types
*
* Note #1: Only scalar field at custom types
* Note #2: Custom type can be implemented as ES6 class / custom object in ES5
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Post {
author: String!,
content: String!,
timestamp: Float,
}
type Query {
getPosts(limit: Int): [Post]
}
`);
const db = {
posts: [{
author: 'Hiếu',
content: 'As I went on, still gaining velocity, the palpitation of night and day merged into one continuous greyness; the sky took on a wonderful deepness of blue, a splendid luminous color like that of early twilight; the jerking sun became a streak of fire, a brilliant arch, in space; the moon a fainter fluctuating band; and I could see nothing of the stars, save now and then a brighter circle flickering in the blue.',
timestamp: 1476548728731,
}, {
author: 'Long',
content: 'A peep at some distant orb has power to raise and purify our thoughts like a strain of sacred music, or a noble picture, or a passage from the grander poets. It always does one good.',
timestamp: 1476462317277,
}, {
author: 'Đạt',
content: 'Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car seemed to float in the middle of an immense dark sphere, whose upper half was strewn with silver.',
timestamp: 1473956762837,
}, {
author: 'Khoa',
content: 'As the minuteness of the parts formed a great hindrance to my speed, I resolved, contrary to my first intention, to make the being of a gigantic stature; that is to say, about eight feet in height, and proportionably large. After having formed this determination, and having spent some months in successfully collecting and arranging my materials, I began.',
timestamp: 1476549446134,
}]
};
// The root provides a resolver function for each API endpoint
const root = {
getPosts({ limit }) {
if (limit != null) {
return db.posts.slice(0, limit);
} else {
return db.posts;
}
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
```
{
getPosts(limit: 3) {
author,
content
}
}
```
*/
/**
* Step 7: Mutation and inputs
*
* NOTE #1: Although we can use query to pass stored data,
* we should use Mutation for clarification
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
input PostInput {
author: String!,
content: String!,
}
type Post {
id: ID!,
author: String!,
content: String!,
timestamp: Float,
}
type Query {
getPosts(limit: Int): [Post]
}
type Mutation {
createPost(input: PostInput): Post
}
`);
class Post {
constructor({content, author}) {
this.id = db.posts.length;
this.content = content;
this.author = author;
this.timestamp = Date.now();
}
}
const db = {
posts: [{
id: '1',
author: 'Hiếu',
content: 'As I went on, still gaining velocity, the palpitation of night and day merged into one continuous greyness; the sky took on a wonderful deepness of blue, a splendid luminous color like that of early twilight; the jerking sun became a streak of fire, a brilliant arch, in space; the moon a fainter fluctuating band; and I could see nothing of the stars, save now and then a brighter circle flickering in the blue.',
timestamp: 1476548728731,
}, {
id: '2',
author: 'Long',
content: 'A peep at some distant orb has power to raise and purify our thoughts like a strain of sacred music, or a noble picture, or a passage from the grander poets. It always does one good.',
timestamp: 1476462317277,
}, {
id: '3',
author: 'Đạt',
content: 'Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car seemed to float in the middle of an immense dark sphere, whose upper half was strewn with silver.',
timestamp: 1473956762837,
}]
};
// The root provides a resolver function for each API endpoint
const root = {
// #1
createPost({input}) {
let newPost = new Post(input);
db.posts.push(newPost);
return newPost;
},
// #2
getPosts({ limit }) {
return new Promise((resolve/*, reject*/) => {
let result;
if (limit != null) {
result = db.posts.slice(0, limit);
} else {
result = db.posts;
}
setTimeout(() => {
resolve(result);
}, 200);
});
},
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
#1. Without variables
```
mutation {
createPost(input: {
author: "Thanh",
content: "Hắn vừa đi vừa chửi. Bao giờ cũng thế, cứ rượu xong là hắn chửi. Bắt đầu hắn chửi trời. Có hề gì? Trời có của riêng nhà nào? Rồi hắn chửi đời. Thế cũng chẳng sao: đời là tất cả nhưng chẳng là ai. Tức mình, hắn chửi ngay tất cả làng Vũ Đại. Nhưng cả làng Vũ Đại ai cũng nhủ: “Chắc nó trừ mình ra!”."
}) {
id,
author
}
}
```
#2. With variables
mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
id,
author
}
}
"variables": {
"input": {
"author": "Huy",
"content": "Hello Graph QL. Ahihi"
}
}
*/
/**
* Step 8: Async resolver; express middleware
*
* #1: 1.1 Request param; 1.2 Middleware
* #2: Async resolver with Promise
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
type Post {
id: ID!,
author: String!,
content: String!,
timestamp: Float,
}
type Query {
lang: String!,
getPosts(limit: Int): [Post],
}
`);
function languageMiddleware(req, res, next) {
var lang = req.query.lang;
if (!lang) {
console.log('No lang defined. Using default lang');
req.query.lang = 'vi';
}
next();
}
const db = {
posts: [{
id: '1',
author: 'Hiếu',
content: 'As I went on, still gaining velocity, the palpitation of night and day merged into one continuous greyness; the sky took on a wonderful deepness of blue, a splendid luminous color like that of early twilight; the jerking sun became a streak of fire, a brilliant arch, in space; the moon a fainter fluctuating band; and I could see nothing of the stars, save now and then a brighter circle flickering in the blue.',
timestamp: 1476548728731,
}, {
id: '2',
author: 'Long',
content: 'A peep at some distant orb has power to raise and purify our thoughts like a strain of sacred music, or a noble picture, or a passage from the grander poets. It always does one good.',
timestamp: 1476462317277,
}, {
id: '3',
author: 'Đạt',
content: 'Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car seemed to float in the middle of an immense dark sphere, whose upper half was strewn with silver.',
timestamp: 1473956762837,
}]
};
// fake async function
function asyncGetPost(limit) {
if (limit != null) {
return db.posts.slice(0, limit);
} else {
return db.posts;
}
}
// The root provides a resolver function for each API endpoint
const root = {
// #1: The request param in resolver
lang(args, request) {
return request.query.lang;
},
// #2: Async demo
getPosts({ limit }) {
//*
return asyncGetPost(limit);
/*/
return new Promise((resolve) => {
setTimeout(() => {
resolve(asyncGetPost(limit));
}, 200);
});
//*/
},
};
let app = express();
app.use(languageMiddleware);
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
#1. middleware request
```
{
lang
}
```
#2. async resolver
```
{
getPosts {
id,
author
}
}
```
*/
/**
* Step 9: Async resolver and MongoDB operations
*
* Note #1: Compare Mongoose's Date type with GraphQL's (must use String)
*/
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { buildSchema } = require('graphql');
const mongoose = require('mongoose');
mongoose.Promise = global.Promise; //use native Promise for mongoose as per http://mongoosejs.com/docs/promises.html
mongoose.connect('mongodb://localhost/graphql');
const Post = mongoose.model('Post', {
id: mongoose.Schema.Types.ObjectId,
timestamp: Date,
content: String,
author: String,
});
// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
input PostInput {
author: String!,
content: String!,
}
type Post {
id: ID!,
author: String!,
content: String!,
timestamp: String,
}
type Query {
getPosts: [Post]
}
type Mutation {
createPost(input: PostInput): Post
}
`);
// The root provides a resolver function for each API endpoint
const root = {
getPosts() {
return Post.find({});
},
createPost({input}) {
let newPost = new Post(input);
// we can do some field remapping if needed:
// new Post({ by: input.author, body: input.content })
// map id prop to built-in mongodb _id
newPost.id = newPost._id;
newPost.timestamp = Date.now();
return newPost.save();
}
};
let app = express();
app.use('/graphql', graphqlHTTP({
schema,
rootValue: root,
graphiql: true, // enable graphiql's query previewer
}));
app.listen(3000);
console.log('Running a GraphQL API server at localhost:3000/graphql');
/*
Query to try in GraphiQL:
#1. Create new post
```
mutation {
createPost(input: {
author: "Thanh",
content: "Hắn vừa đi vừa chửi. Bao giờ cũng thế, cứ rượu xong là hắn chửi. Bắt đầu hắn chửi trời. Có hề gì? Trời có của riêng nhà nào? Rồi hắn chửi đời. Thế cũng chẳng sao: đời là tất cả nhưng chẳng là ai. Tức mình, hắn chửi ngay tất cả làng Vũ Đại. Nhưng cả làng Vũ Đại ai cũng nhủ: “Chắc nó trừ mình ra!”."
}) {
id,
author
}
}
```
#2. Get posts
```
{
getPosts {
id,
author,
}
}
```
*/
/*const db = {
posts: [{
id: '1',
author: 'Hiếu',
content: 'As I went on, still gaining velocity, the palpitation of night and day merged into one continuous greyness; the sky took on a wonderful deepness of blue, a splendid luminous color like that of early twilight; the jerking sun became a streak of fire, a brilliant arch, in space; the moon a fainter fluctuating band; and I could see nothing of the stars, save now and then a brighter circle flickering in the blue.',
timestamp: 1476548728731,
}, {
id: '2',
author: 'Long',
content: 'A peep at some distant orb has power to raise and purify our thoughts like a strain of sacred music, or a noble picture, or a passage from the grander poets. It always does one good.',
timestamp: 1476462317277,
}, {
id: '3',
author: 'Đạt',
content: 'Apparently we had reached a great height in the atmosphere, for the sky was a dead black, and the stars had ceased to twinkle. By the same illusion which lifts the horizon of the sea to the level of the spectator on a hillside, the sable cloud beneath was dished out, and the car seemed to float in the middle of an immense dark sphere, whose upper half was strewn with silver.',
timestamp: 1473956762837,
}]
};*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment