Skip to content

Commit

Permalink
Improve server-side project structure (#365)
Browse files Browse the repository at this point in the history
  • Loading branch information
koistya authored Sep 4, 2019
1 parent 695ba8e commit d9d6060
Show file tree
Hide file tree
Showing 37 changed files with 482 additions and 284 deletions.
49 changes: 39 additions & 10 deletions .vscode/snippets/javascript.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
{
"Route": {
"prefix": "route",
"body": [
"import React from 'react';",
"import { graphql } from 'relay-runtime';",
"import Layout from '../common/Layout';",
"",
"export default [",
" {",
" path: '/${1:path}',",
" query: graphql`",
" query ${TM_DIRECTORY/.*[\\/](.*)$/$1/}${2:Page}Query {",
" ...Layout_data",
" ...${2:Page}_data",
" }",
" `,",
" components: () => [import(/* webpackChunkName: '${1:path}' */ './${2:Page}')],",
" render: ([${2:Page}], data, { config }) => ({",
" title: `${3:Title} • ${config.app.name}`,",
" component: (",
" <Layout data={data}>",
" <${2:Page} data={data} />",
" </Layout>",
" ),",
" chunks: ['${1:path}'],",
" }),",
" },",
"];",
""
],
"description": "Route"
},
"ReactComponent": {
"prefix": "reactComponent",
"body": [
Expand Down Expand Up @@ -76,16 +108,13 @@
" );",
"}",
"",
"export default createFragmentContainer(",
" ${1:Component},",
" {",
" data: graphql`",
" fragment ${1:Component}_data on Query {",
" id",
" }",
" `,",
" },",
");",
"export default createFragmentContainer(${1:Component}, {",
" data: graphql`",
" fragment ${1:Component}_data on Query {",
" id",
" }",
" `,",
"});",
""
],
"description": "React/Relay Fragment Container"
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,23 @@ Also, you need to be familiar with [HTML][html], [CSS][css], [JavaScript][js] ([
│ ├── mutations/ # GraphQL mutations to be used on the client
│ ├── news/ # News section (example)
│ ├── server/ # Server-side code (API, authentication, etc.)
│ │ ├── db/ # Database client
│ │ ├── mutations/ # GraphQL mutations
│ │ ├── queries/ # The top-level GraphQL query fields
│ │ ├── story/ # GraphQL types: Story, Comment etc.
│ │ ├── templates/ # HTML templates for server-side rendering
│ │ ├── user/ # GraphQL types: User, UserRole, UserIdentity etc.
│ │ ├── types/ # GraphQL types: User, UserRole, UserIdentity etc.
│ │ ├── api.js # GraphQL API middleware
│ │ ├── app.js # Express.js application
│ │ ├── config.js # Configuration settings to be passed to the client
│ │ ├── Context.js # GraphQL context wrapper
│ │ ├── createRelay.js # Relay factory method for Node.js environment
│ │ ├── context.js # GraphQL context wrapper
│ │ ├── db.js # PostgreSQL database client (Knex.js)
│ │ ├── relay.js # Relay factory method for Node.js environment
│ │ ├── index.js # Node.js app entry point
│ │ ├── login.js # Authentication middleware (e.g. /login/facebook)
│ │ ├── schema.js # GraphQL schema
│ │ └── ssr.js # Server-side rendering, e.g. ReactDOMServer.renderToString(<App />)
│ ├── user/ # User pages (login, account settings, user profile, etc)
│ ├── utils/ # Utility functions
│ ├── createRelay.js # Relay factory method for browser envrironment
│ ├── relay.js # Relay factory method for browser environment
│ ├── index.js # Client-side entry point, e.g. ReactDOM.render(<App />, container)
│ ├── router.js # Universal application router
│ ├── serviceWorker.js # Service worker helper methods
Expand Down
10 changes: 6 additions & 4 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ type LikeStoryPayload {
}

type Mutation {
# Creates or updates a story.
upsertStory(input: UpsertStoryInput!): UpsertStoryPayload
likeStory(input: LikeStoryInput!): LikeStoryPayload

# Updates a user.
updateUser(input: UpdateUserInput!): UpdateUserPayload

# Deletes a user.
deleteUser(input: DeleteUserInput!): DeleteUserPayload

# Creates or updates a story.
upsertStory(input: UpsertStoryInput!): UpsertStoryPayload

# Marks the story as "liked".
likeStory(input: LikeStoryInput!): LikeStoryPayload
}

# An object with an ID
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import qs from 'query-string';
import { createBrowserHistory } from 'history';

import App from './common/App';
import createRelay from './createRelay';
import * as serviceWorker from './serviceWorker';
import router from './router';
import { createRelay } from './relay';
import { setHistory } from './utils/scrolling';

const container = document.getElementById('root');
Expand Down
6 changes: 1 addition & 5 deletions src/news/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ export default [
}
`,
render: ([News], data, { config }) => ({
title:
`News • ${config.app.name}` +
(() => {
console.dir(data);
})(),
title: `News • ${config.app.name}`,
component: (
<Layout data={data}>
<News data={data} />
Expand Down
2 changes: 1 addition & 1 deletion src/createRelay.js → src/relay.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
import loading from './utils/loading';

export default function createRelay() {
export function createRelay() {
function fetchQuery(operation, variables, cacheConfig = {}) {
// Instead of making an actual HTTP request to the API, use
// hydrated data available during the initial page load.
Expand Down
2 changes: 1 addition & 1 deletion src/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { printSchema } from 'graphql';
import passport from './passport';
import schema from './schema';
import templates from './templates';
import Context from './Context';
import { Context } from './context';

const router = new Router();

Expand Down
6 changes: 2 additions & 4 deletions src/server/Context.js → src/server/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import DataLoader from 'dataloader';

import db from './db';
import Validator from './Validator';
import { Validator } from './validator';
import { mapTo, mapToMany, mapToValues } from './utils';
import { UnauthorizedError, ForbiddenError, ValidationError } from './errors';

class Context {
export class Context {
errors = [];

constructor(req) {
Expand Down Expand Up @@ -162,5 +162,3 @@ class Context {
.then(mapToValues(keys, x => x.id, x => x.given));
});
}

export default Context;
1 change: 0 additions & 1 deletion src/server/db/index.js → src/server/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,4 @@ const db = knex({
},
});

export { default as findUserByCredentials } from './findUserByCredentials';
export default db;
81 changes: 81 additions & 0 deletions src/server/mutations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# GraphQL Mutations

Please, prefer creating UPSERT mutations in instead of CREATE + UPDATE whenever possible, this reduces unnecessary code duplication. For example:

```js
import { mutationWithClientMutationId } from 'graphql-relay';
import { GraphQLID, GraphQLString } from 'graphql';

import db from '../db';
import { StoryType } from '../types';
import { fromGlobalId } from '../utils';

export const upsertStory = mutationWithClientMutationId({
name: 'UpsertStory',
description: 'Creates or updates a story.',

inputFields: {
id: { type: GraphQLID },
text: { type: GraphQLString },
},

outputFields: {
story: { type: StoryType },
},

async mutateAndGetPayload({ id, ...data }, ctx) {
ctx.ensureIsAuthorized();

let story;

if (id) {
// Updates an existing story by ID
[story] = await db
.table('stories')
.where({ id: fromGlobalId(id, 'Story') })
.update(data)
.returning('*');
} else {
// Otherwise, creates a new story
[story] = await db
.table('stories')
.insert(data)
.returning('*');
}

return { story };
},
});
```

Don't forget to check permissions using `ctx.ensureIsAuthorized()` helper method
from `src/server/context.js`. For example:

```js
const story = await db
.table('stories')
.where({ id })
.first();

ctx.ensureIsAuthorized(user => story.author_id === user.id);
```

Always validate user and sanitize user input! We use [`validator.js`](https://github.com/validatorjs/validator.js) + a custom helper function `ctx.validate(input)(...)` for that. For example:

```js
const data = await ctx.validate(input, id ? 'update' : 'create')(x =>
x
.field('title', { trim: true })
.isRequired()
.isLength({ min: 5, max: 80 })

.field('text', { alias: 'URL or text', trim: true })
.isRequired()
.isLength({ min: 10, max: 1000 }),
);

await db
.table('stories')
.where({ id })
.update(data);
```
40 changes: 0 additions & 40 deletions src/server/mutations/deleteUser.js

This file was deleted.

6 changes: 2 additions & 4 deletions src/server/mutations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@
* Copyright (c) 2015-present Kriasoft | MIT License
*/

export * from './upsertStory';
export * from './likeStory';
export * from './updateUser';
export * from './deleteUser';
export * from './user';
export * from './story';
53 changes: 0 additions & 53 deletions src/server/mutations/likeStory.js

This file was deleted.

Loading

0 comments on commit d9d6060

Please sign in to comment.