Skip to content

Commit

Permalink
[#2] first orm model
Browse files Browse the repository at this point in the history
  • Loading branch information
b051 committed Mar 21, 2017
1 parent a959941 commit 4bceb47
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"presets": ["es2015", "stage-0"],
"plugins": [
"transform-runtime"
"transform-runtime",
"transform-flow-strip-types"
]
}
2 changes: 1 addition & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[include]

[libs]
api/decls/
decls/

[options]
module.system=haste
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
### get started

```bash
git init
npm init
```

### install babel (presets and plugins)

```bash
cnpm i -D babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-stage-0
cnpm i -D babel-plugin-transform-flow-strip-types babel-plugin-transform-runtime
```

your `.babelrc` should look like:

```json
{
"presets": ["es2015", "stage-0"],
"plugins": [
"transform-runtime",
"transform-flow-strip-types"
]
}
```

### plan to use koa2 for http service, sequelize for ORM, jwt for authenticate, and we do test-driven develop.

```bash
cnpm install koa@latest koa-body@next koa-router@next --save
cnpm install boom jsonwebtoken --save
cnpm install sequelize --save
cnpm install chai sqlite3 sequelize-fixtures superagent --save-dev
```

### setup mysql & sqlite3

```bash
mysql> create database ilabs;
mysql> grant all on ilabs.* to ''@'localhost';
```

_(setup fixtures.yml for test)_

### [#1] first router
### first orm model
29 changes: 29 additions & 0 deletions decls/sequelize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

declare type Parameters = {
[key: string]: string
}

declare type Filter = (key: string) => boolean

declare type Model = {
name: string,
attributes: {[key: string]: any},
options: {
setterMethods?: {[key: string]: any}
},
associations: {
[key: string]: any
},
readbodyFilter?: Filter,
findAll: Function,
create: Function,
bulkCreate: Function,
sequelize: {
transaction: Function,
Utils: { inflection: { [key: string]: Function }}
}
}

declare type Order = [string, 'ASC'|'DESC'][]

declare type Attributes = Model | string[]
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,26 @@
"author": "",
"license": "ISC",
"dependencies": {
"boom": "^4.2.0",
"jsonwebtoken": "^7.3.0",
"koa": "^2.2.0",
"koa-router": "^7.0.1"
"koa-body": "^2.0.0",
"koa-router": "^7.0.1",
"mysql": "^2.13.0",
"sequelize": "^3.30.2"
},
"devDependencies": {
"babel-core": "^6.24.0",
"babel-loader": "^6.4.1",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.0",
"babel-preset-stage-0": "^6.22.0",
"chai": "^3.5.0",
"sequelize-fixtures": "^0.5.6",
"sqlite3": "^3.1.8",
"supertest": "^3.0.0"
}
}
4 changes: 4 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// @flow

import Koa from 'koa'
import body from 'koa-body'
import person from './person/router'
import { errors } from './middleware'

export default () => {
const app = new Koa()
app.name = 'techtalk(20170322)'
app.use(body({ multipart: true }))
app.use(errors())
for (const router of [person]) {
app.use(router.routes()).use(router.allowedMethods())
}
Expand Down
32 changes: 32 additions & 0 deletions src/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @flow

import Sequelize from 'sequelize'
const env = process.env.NODE_ENV

let _instance: ?Sequelize = null

export default class Database {
static getInstance() {
if (!_instance) {
if (env === 'test') {
_instance = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false
})
console.log('db - use in memory sqlite3')
} else {
_instance = new Sequelize(process.env.MYSQL_DB, process.env.MYSQL_USERNAME, process.env.MYSQL_PASSWORD, {
host: process.env.MYSQL_HOST,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 10000
}
})
}
}
return _instance
}
}
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// @flow

import Database from './database'
import App from './app'

async function start() {
const app = App()
await Database.getInstance().sync({ logging: console.log })
const port = process.env.PORT || 3000
app.listen(port)
console.log(`${app.name}-${app.env}: app.listen(${port})`)
Expand Down
32 changes: 32 additions & 0 deletions src/middleware/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//@flow

import Boom from 'boom'

export default () => {
return async (ctx: any, next: Function) => {
try {
await next()
} catch(error) {
let boom
if (error.errors) {
const err = error.errors[0]
if (err.type == 'unique violation') {
boom = Boom.conflict(err.message, err)
} else if (err.type == 'Validation error') {
boom = Boom.expectationFailed(err.message, err)
} else {
boom = Boom.badData(err.message, err)
}
} else if (error.name == 'UnauthorizedError') {
boom = Boom.unauthorized(error.message.trim())
} else {
boom = Boom.wrap(error)
}
if (boom.output.statusCode == 500) {
console.trace(error)
}
ctx.status = boom.output.statusCode
ctx.body = boom.output.payload
}
}
}
3 changes: 3 additions & 0 deletions src/middleware/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//@flow

export {default as errors} from './errors'
76 changes: 76 additions & 0 deletions src/person/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//@flow

import Sequelize from 'sequelize'
import crypto from 'crypto'
import jwt from 'jsonwebtoken'
import Boom from 'boom'
import Database from '../database'

const Person = Database.getInstance().define('person', {
first_name: {
type: Sequelize.STRING,
allowNull: false
},
last_name: {
type: Sequelize.STRING,
allowNull: false
},
email: {
type: Sequelize.STRING,
validate: {
isEmail: true,
notEmpty: true
},
allowNull: false,
unique: true
},
salt: {
type: Sequelize.STRING(64),
allowNull: false
},
hash: {
type: Sequelize.STRING(64),
allowNull: false
}
},
{
underscored: true,
defaultScope: {
attributes: { exclude: ['salt', 'hash'] }
},
setterMethods: {
password: function(password: string) {
if (!password || password.length < 6) {
throw Boom.expectationFailed('password should have at least 6 characters')
}
const buf = crypto.randomBytes(32)
const salt = buf.toString('hex')
this.setDataValue('salt', salt)
this.setDataValue('hash', Person.hash(password, salt))
}
},
classMethods: {
hash(password: string, salt: string): string {
return crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha512').toString('hex')
}
},

instanceMethods: {
toSafeJSON() {
const json = this.toJSON()
delete json.salt
delete json.hash
return json
},
matchPassword(password: string): boolean {
return this.hash === Person.hash(password, this.salt)
},
generateToken(): string {
return jwt.sign({
id: this.id
}, process.env.AUTH_SECRET)
}
}
})

export default Person
16 changes: 16 additions & 0 deletions src/person/router.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

import Router from 'koa-router'
import Boom from 'boom'
import Person from './model'

const router = new Router({
prefix: '/people'
Expand All @@ -10,4 +12,18 @@ router.get('/', async ctx => {
ctx.body = { message }
})

router.post('/login', async ctx => {
const { email, password } = ctx.request.body
const person = await Person.unscoped().findOne({ where: { email } })
if (person) {
if (person.matchPassword(password)) {
const token = person.generateToken()
ctx.body = {
token,
person: person.toSafeJSON()
}
}
}
})

export default router
7 changes: 7 additions & 0 deletions test/db-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import fixtures from 'sequelize-fixtures'
import Database from '../src/database'

beforeEach('db - fill with fixtures', async () => {
await Database.getInstance().sync({ force: true })
await fixtures.loadFile(`${__dirname}/fixtures.yml`, Database.getInstance().models, { log: () => {} })
})
17 changes: 17 additions & 0 deletions test/fixtures.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
fixtures:
- model: person
data:
id: 1
email: shengning@gmail.com
first_name: ning
last_name: sheng
salt: 99c751ab86303aefa9c2accd1118e90c4142ed629a600649ddcab8e693a971df
hash: edba21231351b18635c7469269cb35177ece5dcdefb759c73a5b1fbfcf08477d
- model: person
data:
id: 2
email: shengning+1@gmail.com
first_name: ning
last_name: sheng
salt: 99c751ab86303aefa9c2accd1118e90c4142ed629a600649ddcab8e693a971df
hash: edba21231351b18635c7469269cb35177ece5dcdefb759c73a5b1fbfcf08477d
16 changes: 16 additions & 0 deletions test/person/person.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,20 @@ describe("router - people", () => {
expect(res.body.message).to.equal('hello')
})

describe('/login', () => {

it('should login', async () => {
let res = await app.post('/people/login').send({
email: 'shengning@gmail.com',
password: 'password'
})
expect(res.status).to.equal(200)
expect(res.body).to.have.keys('person', 'token')
expect(res.body.person.id).to.equal(1)
expect(res.body.person.password).to.not.exist
expect(res.body.person.salt).to.not.exist
expect(res.body.person.hash).to.not.exist
})

})
})

0 comments on commit 4bceb47

Please sign in to comment.