ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

commit eb0da472d2b4bbc8b69dc2e1be41edc2384c738c
parent 0b745f24a10ba849623c752e4406aec0b5d10eb3
Author: Konstantin Tarkus <hello@tarkus.me>
Date:   Sat, 27 Feb 2016 16:39:03 +0300

Remove RESTful API endpoint in favor of GraphQL

Diffstat:
MREADME.md | 2+-
Mdocs/data-fetching.md | 2+-
Mdocs/getting-started.md | 5+++++
Mdocs/recipes/how-to-implement-routing.md | 10+++++-----
Dsrc/api/content.js | 60------------------------------------------------------------
Asrc/data/queries/content.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/data/schema.js | 1+
Asrc/data/types/ContentType.js | 26++++++++++++++++++++++++++
Msrc/routes.js | 6+++---
Msrc/server.js | 1-
10 files changed, 98 insertions(+), 71 deletions(-)

diff --git a/README.md b/README.md @@ -35,11 +35,11 @@ visit our sponsors: ├── /node_modules/ # 3rd-party libraries and utilities ├── /src/ # The source code of the application │ ├── /actions/ # Action creators that allow to trigger a dispatch to stores -│ ├── /api/ # REST API / Relay endpoints │ ├── /components/ # React components │ ├── /constants/ # Constants (action types etc.) │ ├── /content/ # Static content (plain HTML or Markdown, Jade, you name it) │ ├── /core/ # Core framework and utility functions +│ ├── /data/ # GraphQL server schema │ ├── /decorators/ # Higher-order React components │ ├── /public/ # Static files which are copied into the /build/public folder │ ├── /stores/ # Stores contain the application state and logic diff --git a/docs/data-fetching.md b/docs/data-fetching.md @@ -8,7 +8,7 @@ import fetch from '../core/fetch'; export const path = '/products'; export const action = async () => { - const response = await fetch('/api/products'); + const response = await fetch('/graphql?query={products{id,name}}'); const data = await response.json(); return <Layout><Products {...data} /></Layout>; }; diff --git a/docs/getting-started.md b/docs/getting-started.md @@ -37,6 +37,11 @@ This command will build the app from the source files (`/src`) into the output Node.js server (`node build/server.js`) and [Browsersync](https://browsersync.io/) with [HMR](https://webpack.github.io/docs/hot-module-replacement) on top of it. +> [http://localhost:3000/](http://localhost:3000/) — Node.js server (`build/sever.js`)<br> +> [http://localhost:3000/graphql](http://localhost:3000/graphql) — GraphQL server and IDE<br> +> [http://localhost:3001/](http://localhost:3001/) — BrowserSync proxy with HMR, React Hot Transform<br> +> [http://localhost:3002/](http://localhost:3002/) — BrowserSync control panel (UI) + Now you can open your web app in a browser, on mobile devices and start hacking. Whenever you modify any of the source files inside the `/src` folder, the module bundler ([Webpack](http://webpack.github.io/)) will recompile the diff --git a/docs/recipes/how-to-implement-routing.md b/docs/recipes/how-to-implement-routing.md @@ -60,12 +60,12 @@ import ErrorPage from './components/ErrorPage'; const routes = { '/': async () => { - const response = await fetch('/api/data/home'); + const response = await fetch('/graphql?query={content(path:"/"){title,html}}'); const data = await response.json(); return <Layout><HomePage {...data} /></Layout> }, '/about': async () => { - const response = await fetch('/api/data/about'); + const response = await fetch('/graphql?query={content(path:"/about"){title,html}}'); const data = await response.json(); return <Layout><AboutPage {...data} /></Layout>; } @@ -108,12 +108,12 @@ import ErrorPage from './components/ErrorPage'; const router = new Router(on => { on('/products', async () => { - const response = await fetch('/api/products'); + const response = await fetch('/graphql?query={products{id,name}}'); const data = await response.json(); return <Layout><ProductListing {...data} /></Layout> }); - on('/products/:id', async (req) => { - const response = await fetch(`/api/products/${req.params.id}`); + on('/products/:id', async ({ params }) => { + const response = await fetch('/graphql?query={product(id:"${params.id}"){name,summary}}'); const data = await response.json(); return <Layout><ProductInfo {...data} /></Layout>; }); diff --git a/src/api/content.js b/src/api/content.js @@ -1,60 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import fs from 'fs'; -import { join } from 'path'; -import { Router } from 'express'; -import Promise from 'bluebird'; -import jade from 'jade'; -import fm from 'front-matter'; - -// A folder with Jade/Markdown/HTML content pages -const CONTENT_DIR = join(__dirname, './content'); - -// Extract 'front matter' metadata and generate HTML -const parseJade = (path, jadeContent) => { - const fmContent = fm(jadeContent); - const htmlContent = jade.render(fmContent.body); - return Object.assign({ path, content: htmlContent }, fmContent.attributes); -}; - -const readFile = Promise.promisify(fs.readFile); -const fileExists = filename => new Promise(resolve => { - fs.exists(filename, resolve); -}); - -const router = new Router(); - -router.get('/', async (req, res, next) => { - try { - const path = req.query.path; - - if (!path || path === 'undefined') { - res.status(400).send({ error: 'The \'path\' query parameter cannot be empty.' }); - return; - } - - let fileName = join(CONTENT_DIR, `${path === '/' ? '/index' : path}.jade`); - if (!(await fileExists(fileName))) { - fileName = join(CONTENT_DIR, `${path}/index.jade`); - } - - if (!(await fileExists(fileName))) { - res.status(404).send({ error: `The page '${path}' is not found.` }); - } else { - const source = await readFile(fileName, { encoding: 'utf8' }); - const content = parseJade(path, source); - res.status(200).send(content); - } - } catch (err) { - next(err); - } -}); - -export default router; diff --git a/src/data/queries/content.js b/src/data/queries/content.js @@ -0,0 +1,56 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import fs from 'fs'; +import { join } from 'path'; +import Promise from 'bluebird'; +import jade from 'jade'; +import fm from 'front-matter'; + +import { + GraphQLString as StringType, + GraphQLNonNull as NonNull, +} from 'graphql'; + +import ContentType from '../types/ContentType'; + +// A folder with Jade/Markdown/HTML content pages +const CONTENT_DIR = join(__dirname, './content'); + +// Extract 'front matter' metadata and generate HTML +const parseJade = (path, jadeContent) => { + const fmContent = fm(jadeContent); + const htmlContent = jade.render(fmContent.body); + return Object.assign({ path, content: htmlContent }, fmContent.attributes); +}; + +const readFile = Promise.promisify(fs.readFile); +const fileExists = filename => new Promise(resolve => { + fs.exists(filename, resolve); +}); + +export default { + type: ContentType, + args: { + path: { type: new NonNull(StringType) }, + }, + async resolve({ request }, { path }) { + let fileName = join(CONTENT_DIR, `${path === '/' ? '/index' : path}.jade`); + if (!(await fileExists(fileName))) { + fileName = join(CONTENT_DIR, `${path}/index.jade`); + } + + if (!(await fileExists(fileName))) { + return null; + } + + const source = await readFile(fileName, { encoding: 'utf8' }); + return parseJade(path, source); + }, +}; diff --git a/src/data/schema.js b/src/data/schema.js @@ -17,6 +17,7 @@ const schema = new Schema({ name: 'Query', fields: { me: require('./queries/me').default, + content: require('./queries/content').default, }, }), }); diff --git a/src/data/types/ContentType.js b/src/data/types/ContentType.js @@ -0,0 +1,26 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import { + GraphQLObjectType as ObjectType, + GraphQLString as StringType, + GraphQLNonNull as NonNull, +} from 'graphql'; + +const ContentType = new ObjectType({ + name: 'Content', + fields: { + path: { type: new NonNull(StringType) }, + title: { type: new NonNull(StringType) }, + content: { type: new NonNull(StringType) }, + component: { type: new NonNull(StringType) }, + }, +}); + +export default ContentType; diff --git a/src/routes.js b/src/routes.js @@ -31,9 +31,9 @@ const router = new Router(on => { on('/register', async () => <RegisterPage />); on('*', async (state) => { - const response = await fetch(`/api/content?path=${state.path}`); - const content = await response.json(); - return response.ok && content && <ContentPage {...content} />; + const response = await fetch(`/graphql?query={content(path:"${state.path}"){path,title,content,component}}`); + const { data } = await response.json(); + return data && data.content && <ContentPage {...data.content} />; }); on('error', (state, error) => state.statusCode === 404 ? diff --git a/src/server.js b/src/server.js @@ -69,7 +69,6 @@ server.get('/login/facebook/return', // // Register API middleware // ----------------------------------------------------------------------------- -server.use('/api/content', require('./api/content').default); server.use('/graphql', expressGraphQL(req => ({ schema, graphiql: true,