Skip to content

Commit

Permalink
Merge pull request #123 from fortunejs/form-serializer
Browse files Browse the repository at this point in the history
Form serializer
  • Loading branch information
0x8890 committed Sep 1, 2015
2 parents 343ed51 + 9363598 commit 6efe017
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 134 deletions.
5 changes: 5 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changelog


##### 1.3.0 (2015-09-01)
- Polish: all serializer methods may return a Promise.
- Feature: Form serializer now accepts `application/x-www-form-urlencoded` or `multipart/form-data`.


##### 1.2.5 (2015-08-29)
- Polish: drop `node-fetch` as a dependency for testing, instead use `http` module directly.

Expand Down
30 changes: 18 additions & 12 deletions lib/dispatch/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ import * as updateHelpers from './update_helpers'
*/
export default function (context) {
const { adapter, serializer, recordTypes, transforms } = this
let records = serializer.parseCreate(context)

if (!records || !records.length)
throw new BadRequestError(
`There are no valid records in the request.`)
let records

const { type, meta } = context.request
const transform = transforms[type]
Expand All @@ -30,14 +26,24 @@ export default function (context) {
const updates = {}
let transaction

// Delete denormalized inverse fields.
for (let field in fields)
if (fields[field][keys.denormalizedInverse])
for (let record of records)
delete record[field]
return serializer.parseCreate(context)

.then(results => {
records = results

if (!records || !records.length)
throw new BadRequestError(
`There are no valid records in the request.`)

return (transform && transform.input ? Promise.all(records.map(record =>
transform.input(context, record))) : Promise.resolve(records))
// Delete denormalized inverse fields.
for (let field in fields)
if (fields[field][keys.denormalizedInverse])
for (let record of records)
delete record[field]

return (transform && transform.input ? Promise.all(records.map(record =>
transform.input(context, record))) : Promise.resolve(records))
})

.then(records => Promise.all(records.map(record => {
// Enforce the fields.
Expand Down
4 changes: 1 addition & 3 deletions lib/dispatch/end.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ export default function (context) {
if (records) args.push(records)
if (include) args.push(include)

context = serializer.showResponse(...args)

return context
return serializer.showResponse(...args)
})
}
17 changes: 8 additions & 9 deletions lib/dispatch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export { default as include } from './include'
export { default as end } from './end'


const defaultMethod = methods.find
const { find: defaultMethod } = methods


/*!
Expand All @@ -37,8 +37,7 @@ export default function dispatch (options, ...args) {

// Make sure that IDs are an array of unique, non-falsy values.
if (ids) context.request.ids =
[ ...new Set(Array.isArray(ids) ? ids : [ ids ]) ]
.filter(id => id)
[ ...new Set((Array.isArray(ids) ? ids : [ ids ]).filter(id => id)) ]

// If a type is unspecified, block the request.
if (type === null && method !== defaultMethod &&
Expand All @@ -65,15 +64,15 @@ export default function dispatch (options, ...args) {
return new OK(response)
})

.catch(error => {
context = showError(context, nativeErrors.has(error.constructor) ?
new Error(`An internal server error occurred.`) : error)
.catch(error => Promise.resolve(
showError(context, nativeErrors.has(error.constructor) ?
new Error(`An internal server error occurred.`) : error))

.then(context => Promise.resolve(processResponse(context, ...args)))

return Promise.resolve(processResponse(context, ...args))
.then(context => {
throw Object.assign(error, context.response)
})
})
}))
}


Expand Down
28 changes: 17 additions & 11 deletions lib/dispatch/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import deepEqualOptions from '../common/deep_equal_options'
*/
export default function (context) {
const { adapter, serializer, recordTypes, transforms } = this
const updates = serializer.parseUpdate(context)

// Keyed by update, valued by record.
const updateMap = new WeakMap()
Expand All @@ -36,19 +35,26 @@ export default function (context) {
const relatedUpdates = {}
const transformedUpdates = []
let transaction
let updates

validateUpdates(updates)
return serializer.parseUpdate(context)

// Delete denormalized inverse fields, can't be updated.
for (let field in fields)
if (fields[field][keys.denormalizedInverse])
for (let update of updates) {
if ('replace' in update) delete update.replace[field]
if ('push' in update) delete update.push[field]
if ('pull' in update) delete update.pull[field]
}
.then(results => {
updates = results

validateUpdates(updates)

return adapter.find(type, updates.map(update => update.id), null, meta)
// Delete denormalized inverse fields, can't be updated.
for (let field in fields)
if (fields[field][keys.denormalizedInverse])
for (let update of updates) {
if ('replace' in update) delete update.replace[field]
if ('push' in update) delete update.push[field]
if ('pull' in update) delete update.pull[field]
}

return adapter.find(type, updates.map(update => update.id), null, meta)
})

.then(records => Promise.all(records.map(record => {
const update = find(updates, update =>
Expand Down
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import defineArguments from './common/define_arguments'
// Static exports.
import memory from './adapter/adapters/memory'
import adHoc from './serializer/serializers/ad_hoc'
import formUrlEncoded from './serializer/serializers/form_urlencoded'
import { formUrlEncoded, formData } from './serializer/serializers/form'
import http from './net/http'
import websocket from './net/websocket'


const adapters = { memory }
const serializers = { adHoc, formUrlEncoded }
const serializers = { adHoc, formUrlEncoded, formData }
const net = { http, websocket }


Expand Down
8 changes: 4 additions & 4 deletions lib/serializer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default class Serializer {
* @param {Object} context
* @param {Object[]} [records]
* @param {Object} [include]
* @return {Object}
* @return {Promise|Object}
*/
showResponse (context) {
return context
Expand All @@ -100,7 +100,7 @@ export default class Serializer {
*
* @param {Object} context
* @param {Object} error should be an instance of Error
* @return {Object}
* @return {Promise|Object}
*/
showError (context) {
return context
Expand All @@ -113,7 +113,7 @@ export default class Serializer {
* It should not mutate the context object.
*
* @param {Object} context
* @return {Object[]}
* @return {Promise|Object[]}
*/
parseCreate () {
return []
Expand All @@ -126,7 +126,7 @@ export default class Serializer {
* It should not mutate the context object.
*
* @param {Object} context
* @return {Object[]}
* @return {Promise|Object[]}
*/
parseUpdate () {
return []
Expand Down
78 changes: 78 additions & 0 deletions lib/serializer/serializers/form/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Busboy from 'busboy'
import stream from 'stream'


const formUrlEncodedType = 'application/x-www-form-urlencoded'
const formDataType = 'multipart/form-data'


function inherit (Serializer) {
return class FormSerializer extends Serializer {

processRequest () {
throw new this.errors.UnsupportedError(`Form input only.`)
}

parseCreate (context) {
const { request: { meta, payload, type } } = context
const { keys, recordTypes, options, castValue } = this
const fields = recordTypes[type]
const busboy = new Busboy({ headers: meta })
const bufferStream = new stream.PassThrough()
const record = {}

return new Promise(resolve => {
busboy.on('file', (field, file, filename) => {
const fieldDefinition = fields[field] || {}
const fieldIsArray = fieldDefinition[keys.isArray]
const chunks = []

if (fieldIsArray && !(field in record)) record[field] = []

file.on('data', chunk => chunks.push(chunk))
file.on('end', () => {
const data = Buffer.concat(chunks)
data.filename = filename
if (fieldIsArray) {
record[field].push(data)
return
}
record[field] = data
})
})

busboy.on('field', (field, value) => {
const fieldDefinition = fields[field] || {}
const fieldType = fieldDefinition[keys.type]
const fieldIsArray = fieldDefinition[keys.isArray]

if (fieldIsArray) {
if (!(field in record)) record[field] = []
record[field].push(castValue(value, fieldType, options))
return
}

record[field] = castValue(value, fieldType, options)
})

busboy.on('finish', () => resolve([ record ]))

bufferStream.end(payload)
bufferStream.pipe(busboy)
})
}

parseUpdate () {
throw new this.errors.UnsupportedError(`Can not update records.`)
}

}
}


export const formUrlEncoded = Serializer => Object.assign(
inherit(Serializer), { id: formUrlEncodedType })


export const formData = Serializer => Object.assign(
inherit(Serializer), { id: formDataType })
39 changes: 0 additions & 39 deletions lib/serializer/serializers/form_urlencoded/index.js

This file was deleted.

18 changes: 9 additions & 9 deletions lib/serializer/singleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export default class SerializerSingleton extends Serializer {


const inputMethods = new Set([ 'parseCreate', 'parseUpdate' ])
const asynchronousMethods = new Set([ 'processRequest', 'processResponse' ])


// Assign the proxy methods on top of the base methods.
Expand Down Expand Up @@ -121,16 +120,17 @@ function proxyMethod (options, context, ...args) {
// Fail if no serializer was found.
else throw new NoopError(`The serializer for "${format}" is unrecognized.`)

const isAsynchronous = asynchronousMethods.has(method)

try {
const result = serializer[method](context, ...args)
return isAsynchronous ? Promise.resolve(result) : result
return Promise.resolve(serializer[method](context, ...args))
}
catch (error) {
if (isAsynchronous) return Promise.reject(error)
catch (e) {
let error = e

// Only in the special case of input methods, it may be more appropriate to
// throw a BadRequestError.
if (nativeErrors.has(error.constructor) && isInput)
throw new BadRequestError(`The request is malformed.`)
throw error
error = new BadRequestError(`The request is malformed.`)

return Promise.reject(error)
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fortune",
"description": "High-level I/O for web applications.",
"version": "1.2.5",
"version": "1.3.0",
"license": "MIT",
"author": {
"email": "[email protected]",
Expand Down Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"array-buffer": "^1.0.2",
"babel-runtime": "^5.8.20",
"busboy": "^0.2.9",
"chalk": "^1.1.1",
"clone": "^1.0.2",
"deep-equal": "^1.0.1",
Expand All @@ -49,6 +50,7 @@
"docchi": "^0.11.0",
"eslint": "^1.3.1",
"eslint-config-0x8890": "^1.0.0",
"form-data": "^0.2.0",
"highlight.js": "^8.7.0",
"html-minifier": "^0.7.2",
"http-server": "^0.8.0",
Expand Down
2 changes: 1 addition & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import './integration/methods/create'
import './integration/methods/update'
import './integration/methods/delete'
import './integration/serializers/ad_hoc'
import './integration/serializers/form_urlencoded'
import './integration/serializers/form'
import './integration/adapters/memory'
import './integration/websocket'
Loading

0 comments on commit 6efe017

Please sign in to comment.