diff --git a/.eslintrc.json b/.eslintrc.json index 5eba7fca..a6a9a6c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,8 +31,8 @@ "max-len": ["error", 80], "arrow-parens": 0, "comma-dangle": [2, "only-multiline"], - "prefer-reflect": 2, "global-require": 0, + "operator-assignment": 0, "class-methods-use-this": 0, "no-restricted-syntax": [ "error", diff --git a/bin/lux b/bin/lux index d1bc1ccb..e4859a46 100755 --- a/bin/lux +++ b/bin/lux @@ -35,9 +35,9 @@ function commandNotFound(cmd) { function setEnvVar(key, val, def) { if (val) { - Reflect.set(process.env, key, val); - } else if (!Reflect.has(process.env, key)) { - Reflect.set(process.env, key, def); + process.env[key] = val; + } else if (!process.env[key]) { + process.env[key] = def; } } diff --git a/src/constants.js b/src/constants.js index cf999131..0ff4b3b5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,3 +23,4 @@ export const LUX_CONSOLE = ENV.LUX_CONSOLE || false; export const PLATFORM = platform(); export const CIRCLECI = ENV.CIRCLECI; export const APPVEYOR = ENV.APPVEYOR; +export const IS_PRODUCTION = NODE_ENV === 'production'; diff --git a/src/interfaces.js b/src/interfaces.js index 44f7d8d8..0985329e 100644 --- a/src/interfaces.js +++ b/src/interfaces.js @@ -13,3 +13,23 @@ export interface Chain { value(): T; construct>(constructor: V): Chain; } + +export type ObjectMap = { + [key: K]: V; +}; + +export interface Thenable<+R> { + constructor(callback: ( + resolve: (result: Promise | R) => void, + reject: (error: any) => void + ) => mixed): void; + + then( + onFulfill?: (value: R) => Promise | U, + onReject?: (error: any) => Promise | U + ): Promise; + + catch( + onReject?: (error: any) => ?Promise | U + ): Promise; +} diff --git a/src/packages/application/index.js b/src/packages/application/index.js index 182f445f..cf52d738 100644 --- a/src/packages/application/index.js +++ b/src/packages/application/index.js @@ -88,7 +88,7 @@ class Application { * @type {Map} * @private */ - controllers: FreezeableMap; + controllers: FreezeableMap>; /** * A map containing each `Serializer` instance. diff --git a/src/packages/application/initialize.js b/src/packages/application/initialize.js index f54662ae..8b23d48c 100644 --- a/src/packages/application/initialize.js +++ b/src/packages/application/initialize.js @@ -48,7 +48,7 @@ export default async function initialize(app: T, { ); models.forEach(model => { - Reflect.defineProperty(model, 'serializer', { + Object.defineProperty(model, 'serializer', { value: closestChild(serializers, model.resourceName), writable: false, enumerable: false, @@ -67,7 +67,7 @@ export default async function initialize(app: T, { ); controllers.forEach(controller => { - Reflect.defineProperty(controller, 'controllers', { + Object.defineProperty(controller, 'controllers', { value: controllers, writable: true, enumerable: false, diff --git a/src/packages/application/interfaces.js b/src/packages/application/interfaces.js index d1d9feba..384d2ca5 100644 --- a/src/packages/application/interfaces.js +++ b/src/packages/application/interfaces.js @@ -10,7 +10,7 @@ export type Application$opts = Config & { database: Database$config; }; -export type Application$factoryOpts> = { +export type Application$factoryOpts | Serializer<*>> = { key: string; store: Database; parent: ?T; diff --git a/src/packages/application/utils/create-controller.js b/src/packages/application/utils/create-controller.js index 4e7a46e2..eea986a6 100644 --- a/src/packages/application/utils/create-controller.js +++ b/src/packages/application/utils/create-controller.js @@ -9,12 +9,12 @@ import type Controller from '../../controller'; import type Serializer from '../../serializer'; import type { Bundle$Namespace } from '../../loader'; // eslint-disable-line max-len, no-duplicate-imports -export default function createController( +export default function createController>( constructor: Class, opts: { key: string; store: Database; - parent: ?Controller; + parent: ?Controller<*>; serializers: Bundle$Namespace>; } ): T { @@ -36,11 +36,11 @@ export default function createController( serializer = closestAncestor(serializers, key); } - const instance: T = Reflect.construct(constructor, [{ + const instance: T = new constructor({ model, namespace, serializer - }]); + }); if (serializer) { if (!instance.filter.length) { @@ -64,7 +64,7 @@ export default function createController( ]; } - Reflect.defineProperty(instance, 'parent', { + Object.defineProperty(instance, 'parent', { value: parent, writable: false, enumerable: true, diff --git a/src/packages/application/utils/create-serializer.js b/src/packages/application/utils/create-serializer.js index d5d1e500..09f32722 100644 --- a/src/packages/application/utils/create-serializer.js +++ b/src/packages/application/utils/create-serializer.js @@ -23,13 +23,13 @@ export default function createSerializer>( parent = null; } - const instance: T = Reflect.construct(constructor, [{ + const instance: T = new constructor({ model, parent, namespace - }]); + }); - Reflect.defineProperty(instance, 'parent', { + Object.defineProperty(instance, 'parent', { value: parent, writable: false, enumerable: true, diff --git a/src/packages/cli/commands/dbcreate.js b/src/packages/cli/commands/dbcreate.js index 173ec9bc..7ba6d462 100644 --- a/src/packages/cli/commands/dbcreate.js +++ b/src/packages/cli/commands/dbcreate.js @@ -13,7 +13,7 @@ import { createLoader } from '../../loader'; */ export function dbcreate() { const load = createLoader(CWD); - const config = Reflect.get(load('config').database, NODE_ENV); + const config = load('config').database[NODE_ENV]; if (!config) { throw new DatabaseConfigMissingError(NODE_ENV); diff --git a/src/packages/cli/commands/dbdrop.js b/src/packages/cli/commands/dbdrop.js index 7b73e7c7..551793bb 100644 --- a/src/packages/cli/commands/dbdrop.js +++ b/src/packages/cli/commands/dbdrop.js @@ -13,7 +13,7 @@ import { createLoader } from '../../loader'; */ export function dbdrop() { const load = createLoader(CWD); - const config = Reflect.get(load('config').database, NODE_ENV); + const config = load('config').database[NODE_ENV]; if (!config) { throw new DatabaseConfigMissingError(NODE_ENV); diff --git a/src/packages/cli/commands/repl.js b/src/packages/cli/commands/repl.js index b82ea36b..2f593274 100644 --- a/src/packages/cli/commands/repl.js +++ b/src/packages/cli/commands/repl.js @@ -7,9 +7,8 @@ import type Application from '../../application'; export function repl(): Promise { return new Promise(async (resolve) => { - const app: Application = await Reflect.apply(require, null, [ - path.join(CWD, 'dist', 'boot') - ]); + // $FlowIgnore + const app: Application = require(path.join(CWD, 'dist', 'boot')); const instance = startRepl({ prompt: '> ' diff --git a/src/packages/cli/commands/serve.js b/src/packages/cli/commands/serve.js index 2f3016e0..29329f64 100644 --- a/src/packages/cli/commands/serve.js +++ b/src/packages/cli/commands/serve.js @@ -24,6 +24,7 @@ export async function serve({ const load = createLoader(CWD); const { logging } = load('config'); const logger = new Logger(logging); + let maxWorkers; if (hot) { const watcher = await watch(CWD); @@ -34,11 +35,15 @@ export async function serve({ }); } + if (!cluster) { + maxWorkers = 1; + } + createCluster({ logger, + maxWorkers, path: CWD, - port: PORT, - maxWorkers: cluster ? undefined : 1 + port: PORT }).once('ready', () => { logger.info(`Lux Server listening on port: ${cyan(`${PORT}`)}`); }); diff --git a/src/packages/cli/generator/utils/generator-for.js b/src/packages/cli/generator/utils/generator-for.js index 9826ff88..72d80e9d 100644 --- a/src/packages/cli/generator/utils/generator-for.js +++ b/src/packages/cli/generator/utils/generator-for.js @@ -5,7 +5,7 @@ import * as generators from './generate-type'; export default function generatorFor(type: string): Generator { const normalized = type.toLowerCase(); - const generator: void | Generator = Reflect.get(generators, normalized); + const generator: void | Generator = generators[normalized]; if (!generator) { throw new Error(`Could not find a generator for '${type}'.`); diff --git a/src/packages/cli/templates/model.js b/src/packages/cli/templates/model.js index 0d42540e..64fe3d6c 100644 --- a/src/packages/cli/templates/model.js +++ b/src/packages/cli/templates/model.js @@ -35,7 +35,7 @@ export default (name: string, attrs: Array) => { .pipe(str => camelize(str, true)) .value(); - const value = Reflect.get(types, key); + const value = types[key]; if (value) { const inverse = camelize(normalized, true); diff --git a/src/packages/compiler/utils/config.js b/src/packages/compiler/utils/config.js new file mode 100644 index 00000000..0c54cb57 --- /dev/null +++ b/src/packages/compiler/utils/config.js @@ -0,0 +1,32 @@ +// @flow +import merge from '../../../utils/merge'; + +const IGNORE_WARNING = /^treating '.+' as external dependency$/i; + +export function createRollupConfig(options: T): T { + return merge({ + acorn: { + sourceType: 'module', + ecmaVersion: 2017, + allowReserved: true, + preserveParens: true + }, + entry: '', + onwarn: warning => { + if (IGNORE_WARNING.test(warning)) { + return; + } + + process.stderr.write(`${warning}\n`); + }, + preferConst: true + }, options); +} + +export function createBundleConfig(options: T): T { + return merge({ + format: 'cjs', + sourceMap: true, + useStrict: false + }, options); +} diff --git a/src/packages/compiler/utils/create-manifest.js b/src/packages/compiler/utils/create-manifest.js index 439a49a4..52a086ee 100644 --- a/src/packages/compiler/utils/create-manifest.js +++ b/src/packages/compiler/utils/create-manifest.js @@ -106,7 +106,7 @@ export default async function createManifest( Array .from(assets) .map(([key, value]) => { - const write = Reflect.get(writer, key); + const write = writer[key]; if (write) { return write(value); diff --git a/src/packages/controller/index.js b/src/packages/controller/index.js index 13c81b87..fc0cc0f0 100644 --- a/src/packages/controller/index.js +++ b/src/packages/controller/index.js @@ -1,19 +1,43 @@ // @flow -import { Model } from '../database'; +// eslint-disable-next-line no-unused-vars +import { Model, Query } from '../database'; +import { line } from '../logger'; import { getDomain } from '../server'; import { freezeProps } from '../freezeable'; import type Serializer from '../serializer'; -import type { Query } from '../database'; // eslint-disable-line max-len, no-duplicate-imports -import type { Request, Response } from '../server'; // eslint-disable-line max-len, no-duplicate-imports +// eslint-disable-next-line no-duplicate-imports +import type { Request, Response } from '../server'; +import type { Thenable } from '../../interfaces'; import findOne from './utils/find-one'; import findMany from './utils/find-many'; import resolveRelationships from './utils/resolve-relationships'; -import type { - Controller$opts, - Controller$beforeAction, - Controller$afterAction -} from './interfaces'; + +/** + * @private + */ +export type BeforeAction = ( + request: Request, + response: Response +) => Promise; + +/** + * @private + */ +export type AfterAction = ( + request: Request, + response: Response, + responseData: any +) => Promise; + +/** + * @private + */ +export type Options = { + model?: ?Class; + namespace?: string; + serializer?: Serializer; +}; /** * ## Overview @@ -274,7 +298,7 @@ import type { * @class Controller * @public */ -class Controller { +class Controller { /** * An array of custom query parameter keys that are allowed to reach a * Controller instance from an incoming `HTTP` request. @@ -397,7 +421,7 @@ class Controller { * @default [] * @public */ - beforeAction: Array = []; + beforeAction: Array = []; /** * Functions to execute on each request handled by a `Controller` after the @@ -448,7 +472,7 @@ class Controller { * @default [] * @public */ - afterAction: Array = []; + afterAction: Array = []; /** * The default amount of items to include per each response of the index @@ -468,7 +492,7 @@ class Controller { * @type {Model} * @private */ - model: Class; + model: ?Class; /** * A reference to the root Controller for the namespace that a Controller @@ -478,7 +502,7 @@ class Controller { * @type {?Controller} * @private */ - parent: ?Controller; + parent: ?Controller<*>; /** * The namespace that a Controller instance is a member of. @@ -506,7 +530,7 @@ class Controller { * @type {Map} * @private */ - controllers: Map; + controllers: Map>; /** * A boolean value representing whether or not a Controller instance has a @@ -538,7 +562,7 @@ class Controller { */ hasSerializer: boolean; - constructor({ model, namespace, serializer }: Controller$opts) { + constructor({ model, namespace, serializer }: Options) { Object.assign(this, { model, namespace, @@ -573,8 +597,14 @@ class Controller { * @return {Promise} Resolves with an array of Model instances. * @public */ - index(req: Request): Query> { - return findMany(this.model, req); + index(request: Request): Thenable> { + const { model } = this; + + if (model) { + return findMany(model, request); + } + + return Query.resolve([]); } /** @@ -589,8 +619,14 @@ class Controller { * id url parameter. * @public */ - show(req: Request): Query { - return findOne(this.model, req); + show(request: Request): Thenable { + const { model } = this; + + if (model) { + return findOne(model, request); + } + + return Promise.resolve(null); } /** @@ -604,9 +640,15 @@ class Controller { * @return {Promise} Resolves with the newly created Model instance. * @public */ - async create(req: Request, res: Response): Promise { + async create(req: Request, res: Response): Promise { const { model } = this; + if (!model) { + throw new Error(line` + Controllers without a Model must override the built in "create" action. + `); + } + const { url: { pathname @@ -629,7 +671,7 @@ class Controller { `${getDomain(req) + pathname}/${record.getPrimaryKey()}` ); - Reflect.set(res, 'statusCode', 201); + res.status(201); return record.unwrap(); } @@ -646,32 +688,36 @@ class Controller { * Resolves with the number `204` if no changes occur. * @public */ - update(req: Request): Promise { + update(request: Request): Promise { const { model } = this; - return findOne(model, req) - .then(record => { - const { - params: { - data: { - attributes, - relationships + if (model) { + return findOne(model, request) + .then(record => { + const { + params: { + data: { + attributes, + relationships + } } + } = request; + + return record.update({ + ...attributes, + ...resolveRelationships(model, relationships) + }); + }) + .then(record => { + if (record.didPersist) { + return record.unwrap(); } - } = req; - return record.update({ - ...attributes, - ...resolveRelationships(model, relationships) + return 204; }); - }) - .then(record => { - if (record.didPersist) { - return record.unwrap(); - } + } - return 204; - }); + return Promise.resolve(null); } /** @@ -685,10 +731,16 @@ class Controller { * @return {Promise} Resolves with the number `204`. * @public */ - destroy(req: Request): Promise { - return findOne(this.model, req) - .then(record => record.destroy()) - .then(() => 204); + destroy(request: Request): Promise { + const { model } = this; + + if (model) { + return findOne(model, request) + .then(record => record.destroy()) + .then(() => 204); + } + + return Promise.resolve(null); } /** diff --git a/src/packages/controller/test/controller.test.js b/src/packages/controller/test/controller.test.js index 404f9fca..c2257703 100644 --- a/src/packages/controller/test/controller.test.js +++ b/src/packages/controller/test/controller.test.js @@ -7,8 +7,8 @@ import Controller from '../index'; import Serializer from '../../serializer'; import { Model } from '../../database'; -import setType from '../../../utils/set-type'; import { getTestApp } from '../../../../test/utils/get-test-app'; +import { createResponse } from '../../../../test/utils/mocks'; import type { Request, Response } from '../../server'; @@ -17,7 +17,7 @@ const HOST = 'localhost:4000'; describe('module "controller"', () => { describe('class Controller', () => { let Post: Class; - let subject: Controller; + let subject: Controller<*>; const attributes = [ 'id', @@ -58,7 +58,8 @@ describe('module "controller"', () => { }); describe('#index()', () => { - const createRequest = (params = {}): Request => setType(() => ({ + // $FlowIgnore + const createRequest = (params = {}): Request => ({ params, route: { controller: subject @@ -74,60 +75,67 @@ describe('module "controller"', () => { number: 1 } } - })); - - it('returns an array of records', async () => { - const request = createRequest(); - const result = await subject.index(request); + }); - expect(result).to.be.an('array').with.lengthOf(25); - result.forEach(item => assertRecord(item)); + it('returns an array of records', () => { + return subject + .index(createRequest()) + .then(result => { + expect(result).to.be.an('array').with.lengthOf(25); + result.forEach(item => assertRecord(item)); + }); }); - it('supports specifying page size', async () => { + it('supports specifying page size', () => { const request = createRequest({ page: { size: 10 } }); - const result = await subject.index(request); - - expect(result).to.be.an('array').with.lengthOf(10); - result.forEach(item => assertRecord(item)); + return subject + .index(request) + .then(result => { + expect(result).to.be.an('array').with.lengthOf(10); + result.forEach(item => assertRecord(item)); + }); }); - it('supports filter parameters', async () => { + it('supports filter parameters', () => { const request = createRequest({ filter: { isPublic: false } }); - const result = await subject.index(request); + return subject + .index(request) + .then(result => { + expect(result).to.be.an('array').with.length.above(0); - expect(result).to.be.an('array').with.length.above(0); - - result.forEach(item => { - assertRecord(item); - expect(Reflect.get(item, 'isPublic')).to.be.false; - }); + result.forEach(item => { + assertRecord(item); + expect(item).to.have.property('isPublic', false); + }); + }); }); - it('supports sparse field sets', async () => { + it('supports sparse field sets', () => { const request = createRequest({ fields: { posts: ['id', 'title'] } }); - const result = await subject.index(request); - - expect(result).to.be.an('array').with.lengthOf(25); - result.forEach(item => assertRecord(item, ['id', 'title'])); + return subject + .index(request) + .then(result => { + expect(result).to.be.an('array').with.lengthOf(25); + result.forEach(item => assertRecord(item, ['id', 'title'])); + }); }); - it('supports eager loading relationships', async () => { + it('supports eager loading relationships', () => { const request = createRequest({ include: ['user'], fields: { @@ -139,27 +147,30 @@ describe('module "controller"', () => { } }); - const result = await subject.index(request); - - expect(result).to.be.an('array').with.lengthOf(25); - - result.forEach(item => { - assertRecord(item, [ - ...attributes, - 'user' - ]); - - expect(item.rawColumnData.user).to.have.all.keys([ - 'id', - 'name', - 'email' - ]); + return subject + .index(request) + .then(result => { + expect(result).to.be.an('array').with.lengthOf(25); + + result.forEach(item => { + assertRecord(item, [ + ...attributes, + 'user' + ]); + + expect(item.rawColumnData.user).to.have.all.keys([ + 'id', + 'name', + 'email' + ]); + }); }); }); }); describe('#show()', () => { - const createRequest = (params = {}): Request => setType(() => ({ + // $FlowIgnore + const createRequest = (params = {}): Request => ({ params, route: { controller: subject @@ -169,7 +180,7 @@ describe('module "controller"', () => { posts: attributes } } - })); + }); it('returns a single record', async () => { const request = createRequest({ id: 1 }); @@ -204,7 +215,7 @@ describe('module "controller"', () => { assertRecord(result, ['id', 'title']); }); - it('supports eager loading relationships', async () => { + it('supports eager loading relationships', () => { const request = createRequest({ id: 1, include: ['user'], @@ -217,29 +228,32 @@ describe('module "controller"', () => { } }); - const result = await subject.show(request); - - expect(result).to.be.ok; - - if (result) { - assertRecord(result, [ - ...attributes, - 'user' - ]); - - expect(result.rawColumnData.user).to.have.all.keys([ - 'id', - 'name', - 'email' - ]); - } + return subject + .show(request) + .then(result => { + expect(result).to.be.ok; + + if (result) { + assertRecord(result, [ + ...attributes, + 'user' + ]); + + expect(result.rawColumnData.user).to.have.all.keys([ + 'id', + 'name', + 'email' + ]); + } + }); }); }); describe('#create()', () => { let result: Model; - const createRequest = (params = {}): Request => setType(() => ({ + // $FlowIgnore + const createRequest = (params = {}): Request => ({ params, url: { pathname: '/posts' @@ -259,20 +273,7 @@ describe('module "controller"', () => { users: ['id'] } } - })); - - const createResponse = (): Response => setType(() => ({ - headers: new Map(), - statusCode: 200, - - setHeader(key: string, value: string): void { - this.headers.set(key, value); - }, - - getHeader(key: string): string | void { - return this.headers.get(key); - } - })); + }); afterEach(async () => { await result.destroy(); @@ -314,9 +315,9 @@ describe('module "controller"', () => { 'updatedAt' ]); - const user = await Reflect.get(result, 'user'); - const title = Reflect.get(result, 'title'); - const isPublic = Reflect.get(result, 'isPublic'); + const user = await result.user; + const title = result.title; + const isPublic = result.isPublic; expect(user.id).to.equal(1); expect(title).to.equal('#create() Test'); @@ -354,7 +355,7 @@ describe('module "controller"', () => { result = await subject.create(request, response); - const id = Reflect.get(result, 'id'); + const id = result.getPrimaryKey(); const location = response.getHeader('Location'); expect(location).to.equal(`http://${HOST}/posts/${id}`); @@ -362,10 +363,11 @@ describe('module "controller"', () => { }); describe('#update()', () => { - let User; - let record; + let User: Class; + let record: Model; - const createRequest = (params = {}): Request => setType(() => ({ + // $FlowIgnore + const createRequest = (params = {}): Request => ({ params, route: { controller: subject @@ -375,7 +377,7 @@ describe('module "controller"', () => { posts: attributes } } - })); + }); beforeEach(async () => { const { models } = await getTestApp(); @@ -403,7 +405,10 @@ describe('module "controller"', () => { it('returns a record if attribute(s) change', async () => { let item = record; - const id = Reflect.get(item, 'id'); + // $FlowIgnore + let isPublic = item.isPublic; + // $FlowIgnore + const id = item.id; expect(item).to.have.property('isPublic', false); @@ -423,8 +428,10 @@ describe('module "controller"', () => { it('returns a record if relationships(s) change', async () => { let item = record; - let user = await Reflect.get(item, 'user'); - let comments = await Reflect.get(item, 'comments'); + // $FlowIgnore + let user = await item.user; + // $FlowIgnore + let comments = await item.comments; const id = item.getPrimaryKey(); expect(user).to.be.null; @@ -559,7 +566,9 @@ describe('module "controller"', () => { describe('#destroy()', () => { let record: Model; - const createRequest = (params = {}): Request => setType(() => ({ + + // $FlowIgnore + const createRequest = (params = {}): Request => ({ params, route: { controller: subject @@ -569,7 +578,7 @@ describe('module "controller"', () => { posts: attributes } } - })); + }); before(async () => { record = await Post.create({ @@ -578,7 +587,7 @@ describe('module "controller"', () => { }); it('returns the number `204` if the record is destroyed', async () => { - const id = Reflect.get(record, 'id'); + const id = record.getPrimaryKey(); const result = await subject.destroy(createRequest({ id })); expect(result).to.equal(204); diff --git a/src/packages/controller/test/params-to-query.test.js b/src/packages/controller/test/params-to-query.test.js index 7daf9d56..8f7d5d5b 100644 --- a/src/packages/controller/test/params-to-query.test.js +++ b/src/packages/controller/test/params-to-query.test.js @@ -5,32 +5,30 @@ import { describe, it, before } from 'mocha'; import type { Model } from '../../database'; import type { Request$params } from '../../server'; import merge from '../../../utils/merge'; -import setType from '../../../utils/set-type'; import paramsToQuery from '../utils/params-to-query'; import { getTestApp } from '../../../../test/utils/get-test-app'; describe('module "controller"', () => { describe('util paramsToQuery()', () => { let Post: Class; - const createParams = (obj: Object): Request$params => setType(() => { - return merge({ - sort: 'createdAt', - filter: {}, - fields: { - posts: [ - 'body', - 'title', - 'createdAt', - 'updatedAt' - ] - } - }, obj); - }); + const createParams = (obj: Object): Request$params => merge({ + sort: 'createdAt', + filter: {}, + fields: { + posts: [ + 'body', + 'title', + 'createdAt', + 'updatedAt' + ] + } + }, obj); before(async () => { const { models } = await getTestApp(); - Post = setType(() => models.get('post')); + // $FlowIgnore + Post = models.get('post'); }); it('returns the correct params object', () => { diff --git a/src/packages/controller/utils/find-many.js b/src/packages/controller/utils/find-many.js index 9e06da2f..7c2a1dd0 100644 --- a/src/packages/controller/utils/find-many.js +++ b/src/packages/controller/utils/find-many.js @@ -1,7 +1,9 @@ // @flow import merge from '../../../utils/merge'; -import type { Model, Query } from '../../database'; +// eslint-disable-next-line no-unused-vars +import type { Model } from '../../database'; import type { Request } from '../../server'; +import type { Thenable } from '../../../interfaces'; import paramsToQuery from './params-to-query'; @@ -10,9 +12,9 @@ import paramsToQuery from './params-to-query'; */ export default function findMany( model: Class, - req: Request -): Query> { - const params = merge(req.defaultParams, req.params); + request: Request +): Thenable> { + const params = merge(request.defaultParams, request.params); const { sort, page, diff --git a/src/packages/controller/utils/params-to-query.js b/src/packages/controller/utils/params-to-query.js index 38fb8412..fa41408e 100644 --- a/src/packages/controller/utils/params-to-query.js +++ b/src/packages/controller/utils/params-to-query.js @@ -21,7 +21,7 @@ export default function paramsToQuery(model: Class, { let query = { id, filter, - select: [model.primaryKey, ...Reflect.get(fields, model.resourceName)] + select: [model.primaryKey, ...fields[model.resourceName]] }; if (page) { diff --git a/src/packages/controller/utils/resolve-relationships.js b/src/packages/controller/utils/resolve-relationships.js index 255cfe01..d54efa69 100644 --- a/src/packages/controller/utils/resolve-relationships.js +++ b/src/packages/controller/utils/resolve-relationships.js @@ -11,23 +11,26 @@ export default function resolveRelationships( relationships: Object = {} ): Object { return entries(relationships).reduce((obj, [key, value]) => { - let { data = null } = value || {}; + const result = obj; - if (data) { + if (value && value.data) { const opts = model.relationshipFor(key); if (opts) { - if (Array.isArray(data)) { - data = data.map(item => Reflect.construct(opts.model, [item])); + /* eslint-disable new-cap */ + if (Array.isArray(value.data)) { + result[key] = value.data.map(item => new opts.model(item)); } else { - data = Reflect.construct(opts.model, [data]); + result[key] = new opts.model(value.data); } + /* eslint-enable new-cap */ + + return result; } } - return { - ...obj, - [key]: data - }; + result[key] = null; + + return result; }, {}); } diff --git a/src/packages/database/initialize.js b/src/packages/database/initialize.js index b4a63ebd..8581f8fe 100644 --- a/src/packages/database/initialize.js +++ b/src/packages/database/initialize.js @@ -20,7 +20,7 @@ export default async function initialize( const { path, models, logger, checkMigrations } = opts; let { config } = opts; - config = Reflect.get(config, NODE_ENV); + config = config[NODE_ENV]; if (!config) { throw new ConfigMissingError(NODE_ENV); @@ -29,7 +29,7 @@ export default async function initialize( const { debug = (NODE_ENV === 'development') }: { - debug: boolean + debug?: boolean } = config; Object.defineProperties(instance, { diff --git a/src/packages/database/model/index.js b/src/packages/database/model/index.js index d07a8017..039c6232 100644 --- a/src/packages/database/model/index.js +++ b/src/packages/database/model/index.js @@ -683,7 +683,7 @@ class Model { Object.assign(this, props); if (initialize) { - Reflect.defineProperty(this, 'initialized', { + Object.defineProperty(this, 'initialized', { value: true, writable: false, enumerable: false, @@ -1146,7 +1146,8 @@ class Model { * @private */ getPrimaryKey(): number { - return Reflect.get(this, this.constructor.primaryKey); + // $FlowIgnore + return this[this.constructor.primaryKey]; } /** @@ -1165,7 +1166,7 @@ class Model { ): Promise> { const run = async (trx: Object) => { const { hooks, logger, primaryKey } = this; - const instance = Reflect.construct(this, [props, false]); + const instance = new this(props, false); let statements = []; const associations = Object @@ -1194,10 +1195,11 @@ class Model { const runner = createRunner(logger, statements); const [[primaryKeyValue]] = await runner(await create(instance, trx)); - Reflect.set(instance, primaryKey, primaryKeyValue); - Reflect.set(instance.rawColumnData, primaryKey, primaryKeyValue); + // $FlowIgnore + instance[primaryKey] = primaryKeyValue; + instance.rawColumnData[primaryKey] = primaryKeyValue; - Reflect.defineProperty(instance, 'initialized', { + Object.defineProperty(instance, 'initialized', { value: true, writable: false, enumerable: false, @@ -1380,7 +1382,7 @@ class Model { * @public */ static hasScope(name: string): boolean { - return Boolean(Reflect.get(this.scopes, name)); + return Boolean(this.scopes[name]); } /** @@ -1418,14 +1420,14 @@ class Model { const getTableName = compose(pluralize, underscore); const tableName = getTableName(this.name); - Reflect.defineProperty(this, 'tableName', { + Object.defineProperty(this, 'tableName', { value: tableName, writable: false, enumerable: true, configurable: false }); - Reflect.defineProperty(this.prototype, 'tableName', { + Object.defineProperty(this.prototype, 'tableName', { value: tableName, writable: false, enumerable: false, @@ -1449,7 +1451,7 @@ class Model { * @private */ static columnFor(key: string): void | Object { - return Reflect.get(this.attributes, key); + return this.attributes[key]; } /** @@ -1463,7 +1465,11 @@ class Model { static columnNameFor(key: string): void | string { const column = this.columnFor(key); - return column ? column.columnName : undefined; + if (column) { + return column.columnName; + } + + return undefined; } /** @@ -1475,7 +1481,7 @@ class Model { * @private */ static relationshipFor(key: string): void | Relationship$opts { - return Reflect.get(this.relationships, key); + return this.relationships[key]; } } diff --git a/src/packages/database/model/initialize-class.js b/src/packages/database/model/initialize-class.js index 42973f97..33382e7f 100644 --- a/src/packages/database/model/initialize-class.js +++ b/src/packages/database/model/initialize-class.js @@ -57,6 +57,8 @@ function initializeProps(prototype, attributes, relationships) { function initializeHooks({ model, hooks, logger }) { return Object.freeze( entries(hooks).reduce((obj, [key, value]) => { + const result = obj; + if (!VALID_HOOKS.has(key)) { logger.warn(line` Invalid hook '${key}' will not be added to Model '${model.name}'. @@ -65,15 +67,14 @@ function initializeHooks({ model, hooks, logger }) { }. `); - return obj; + return result; } - return { - ...obj, - [key]: async (instance, transaction) => { - await Reflect.apply(value, model, [instance, transaction]); - } - }; + result[key] = (instance, transaction) => Promise.resolve( + value.call(model, instance, transaction) + ); + + return result; }, {}) ); } diff --git a/src/packages/database/model/utils/get-columns.js b/src/packages/database/model/utils/get-columns.js index f6bdc3e4..692f708b 100644 --- a/src/packages/database/model/utils/get-columns.js +++ b/src/packages/database/model/utils/get-columns.js @@ -7,14 +7,22 @@ import type Model from '../index'; * @private */ export default function getColumns(record: Model, only?: Array) { - let { constructor: { attributes: columns } } = record; + let { + constructor: { + attributes: columns + } + } = record; if (only) { columns = pick(columns, ...only); } - return entries(columns).reduce((obj, [key, { columnName }]) => ({ - ...obj, - [columnName]: Reflect.get(record, key) - }), {}); + return entries(columns).reduce((obj, [key, { columnName }]) => { + const result = obj; + + // $FlowIgnore + result[columnName] = record[key]; + + return result; + }, {}); } diff --git a/src/packages/database/model/utils/persistence.js b/src/packages/database/model/utils/persistence.js index 439bbad3..be032238 100644 --- a/src/packages/database/model/utils/persistence.js +++ b/src/packages/database/model/utils/persistence.js @@ -11,24 +11,25 @@ import getColumns from './get-columns'; * @private */ export function create(record: Model, trx: Object): Array { + const target = record; const timestamp = new Date(); - Object.assign(record, { + Object.assign(target, { createdAt: timestamp, updatedAt: timestamp }); - Object.assign(record.rawColumnData, { + Object.assign(target.rawColumnData, { createdAt: timestamp, updatedAt: timestamp }); return [ - record.constructor + target.constructor .table() .transacting(trx) - .returning(record.constructor.primaryKey) - .insert(omit(getColumns(record), record.constructor.primaryKey)) + .returning(target.constructor.primaryKey) + .insert(omit(getColumns(target), target.constructor.primaryKey)) ]; } @@ -36,16 +37,18 @@ export function create(record: Model, trx: Object): Array { * @private */ export function update(record: Model, trx: Object): Array { - Reflect.set(record, 'updatedAt', new Date()); + const target = record; + + target.updatedAt = new Date(); return [ - record.constructor + target.constructor .table() .transacting(trx) - .where(record.constructor.primaryKey, record.getPrimaryKey()) + .where(target.constructor.primaryKey, target.getPrimaryKey()) .update(getColumns( - record, - Array.from(record.dirtyAttributes.keys()) + target, + Array.from(target.dirtyAttributes.keys()) )) ]; } diff --git a/src/packages/database/model/utils/validate.js b/src/packages/database/model/utils/validate.js index 38a4ae01..a235552a 100644 --- a/src/packages/database/model/utils/validate.js +++ b/src/packages/database/model/utils/validate.js @@ -11,7 +11,7 @@ export default function validate(instance: Model): true { .map(([key, value]) => ({ key, value, - validator: Reflect.get(instance.constructor.validates, key) + validator: instance.constructor.validates[key] })) .filter(({ validator }) => validator) .map(props => new Validation(props)) diff --git a/src/packages/database/query/index.js b/src/packages/database/query/index.js index 2851468a..590d9d6f 100644 --- a/src/packages/database/query/index.js +++ b/src/packages/database/query/index.js @@ -446,16 +446,12 @@ class Query<+T: any> extends Promise { relationships } = src; - const dest = Reflect.construct(this, [model]); - - Object.assign(dest, { + return Object.assign(new this(model), { snapshots, collection, shouldCount, relationships }); - - return dest; } } diff --git a/src/packages/database/query/runner/index.js b/src/packages/database/query/runner/index.js index 10e5e620..515b0c87 100644 --- a/src/packages/database/query/runner/index.js +++ b/src/packages/database/query/runner/index.js @@ -49,13 +49,13 @@ export function createRunner(target: Query<*>, opts: { name = 'select'; } - const method = Reflect.get(query, name); + const method = query[name]; if (!Array.isArray(params)) { params = [params]; } - return Reflect.apply(method, query, params); + return method.apply(query, params); }, model.table()); if (model.store.debug) { diff --git a/src/packages/database/query/runner/utils/build-results.js b/src/packages/database/query/runner/utils/build-results.js index 6db66fd1..4e251779 100644 --- a/src/packages/database/query/runner/utils/build-results.js +++ b/src/packages/database/query/runner/utils/build-results.js @@ -1,6 +1,7 @@ // @flow import { camelize, singularize } from 'inflection'; +// eslint-disable-next-line no-unused-vars import Model from '../../../model'; import entries from '../../../../../utils/entries'; import underscore from '../../../../../utils/underscore'; @@ -27,110 +28,103 @@ export default async function buildResults({ } if (Object.keys(relationships).length) { - related = entries(relationships) - .reduce((obj, entry) => { - const [name, relationship] = entry; - let foreignKey = camelize(relationship.foreignKey, true); - - if (relationship.through) { - const query = relationship.model.select(...relationship.attrs); - - const baseKey = `${relationship.through.tableName}.` + - `${singularize(underscore(name))}_id`; - - foreignKey = `${relationship.through.tableName}.` + - `${relationship.foreignKey}`; - - query.snapshots.push( - ['select', [ - `${baseKey} as ${camelize(baseKey.split('.').pop(), true)}`, - `${foreignKey} as ${camelize(foreignKey.split('.').pop(), true)}` - ]], - - ['innerJoin', [ - relationship.through.tableName, - `${relationship.model.tableName}.` + - `${relationship.model.primaryKey}`, - '=', - baseKey - ]], - - ['whereIn', [ - foreignKey, - results.map(({ id }) => id) - ]] - ); - - return { - ...obj, - [name]: query - }; - } + related = entries(relationships).reduce((obj, entry) => { + const result = obj; + const [name, relationship] = entry; + let foreignKey = camelize(relationship.foreignKey, true); + + if (relationship.through) { + const query = relationship.model.select(...relationship.attrs); + + const baseKey = `${relationship.through.tableName}.` + + `${singularize(underscore(name))}_id`; + + foreignKey = `${relationship.through.tableName}.` + + `${relationship.foreignKey}`; + + query.snapshots.push( + ['select', [ + `${baseKey} as ${camelize(baseKey.split('.').pop(), true)}`, + `${foreignKey} as ${camelize(foreignKey.split('.').pop(), true)}` + ]], + + ['innerJoin', [ + relationship.through.tableName, + `${relationship.model.tableName}.` + + `${relationship.model.primaryKey}`, + '=', + baseKey + ]], + + ['whereIn', [ + foreignKey, + results.map(({ id }) => id) + ]] + ); + + result[name] = query; + + return result; + } + + result[name] = relationship.model + .select(...relationship.attrs) + .where({ + [foreignKey]: results.map(({ id }) => id) + }); - return { - ...obj, - [name]: relationship.model - .select(...relationship.attrs) - .where({ - [foreignKey]: results.map(({ id }) => id) - }) - }; - }, {}); + return result; + }, {}); related = await promiseHash(related); } return results.map(record => { if (related) { - entries(related) - .forEach(([name, relatedResults]: [string, Array]) => { - const relationship = model.relationshipFor(name); + const target = record; - if (relationship) { - let { foreignKey } = relationship; + entries(related).forEach(([name, relatedResults]) => { + const relationship = model.relationshipFor(name); - foreignKey = camelize(foreignKey, true); + if (relationship) { + let { foreignKey } = relationship; - Reflect.set(record, name, relatedResults.filter(({ - rawColumnData - }) => { - const fk = Reflect.get(rawColumnData, foreignKey); - const pk = Reflect.get(record, model.primaryKey); - - return fk === pk; - })); - } - }); + foreignKey = camelize(foreignKey, true); + target[name] = relatedResults.filter(({ rawColumnData }) => ( + rawColumnData[foreignKey] === record[model.primaryKey] + )); + } + }); } - const instance = Reflect.construct(model, [ - entries(record) - .reduce((r, entry) => { - let [key, value] = entry; - - if (!value && pkPattern.test(key)) { - return r; - } else if (key.indexOf('.') >= 0) { - const [a, b] = key.split('.'); - let parent: ?Object = r[a]; - - if (!parent) { - parent = {}; - } - - key = a; - value = { - ...parent, - [b]: value - }; + // eslint-disable-next-line new-cap + const instance = new model( + entries(record).reduce((obj, entry) => { + const result = obj; + let [key, value] = entry; + + if (!value && pkPattern.test(key)) { + return result; + } else if (key.indexOf('.') >= 0) { + const [a, b] = key.split('.'); + let parent: ?Object = result[a]; + + if (!parent) { + parent = {}; } - return { - ...r, - [key]: value + key = a; + value = { + ...parent, + [b]: value }; - }, {}) - ]); + } + + result[key] = value; + + return result; + }, {}) + ); instance.currentChangeSet.persist(); diff --git a/src/packages/database/query/runner/utils/get-find-param.js b/src/packages/database/query/runner/utils/get-find-param.js index 8b912fe5..97644b64 100644 --- a/src/packages/database/query/runner/utils/get-find-param.js +++ b/src/packages/database/query/runner/utils/get-find-param.js @@ -17,7 +17,7 @@ export default function getFindParam({ const [, params] = snapshot; if (params && isObject(params)) { - return Reflect.get(params, `${tableName}.${primaryKey}`); + return params[`${tableName}.${primaryKey}`]; } } } diff --git a/src/packages/database/query/utils/scopes-for.js b/src/packages/database/query/utils/scopes-for.js index bb1bef88..755424b9 100644 --- a/src/packages/database/query/utils/scopes-for.js +++ b/src/packages/database/query/utils/scopes-for.js @@ -1,17 +1,20 @@ // @flow +import type { ObjectMap } from '../../../../interfaces'; import type Query from '../index'; -export default function scopesFor(target: Query): { - [key: string]: () => Query -} { - return Object.keys(target.model.scopes).reduce((scopes, name) => ({ - ...scopes, - [name]: { +export default function scopesFor( + target: Query +): ObjectMap) => Query> { + return Object.keys(target.model.scopes).reduce((scopes, name) => { + const result = scopes; + + result[name] = { get() { // eslint-disable-next-line func-names const scope = function (...args: Array) { - const fn = Reflect.get(target.model, name); - const { snapshots } = Reflect.apply(fn, target.model, args); + // $FlowIgnore + const fn = target.model[name]; + const { snapshots } = fn.apply(target.model, args); Object.assign(target, { snapshots: [ @@ -26,7 +29,7 @@ export default function scopesFor(target: Query): { return target; }; - Reflect.defineProperty(scope, 'name', { + Object.defineProperty(scope, 'name', { value: name, writable: false, enumerable: false, @@ -35,6 +38,8 @@ export default function scopesFor(target: Query): { return scope; } - } - }), {}); + }; + + return result; + }, {}); } diff --git a/src/packages/database/relationship/utils/getters.js b/src/packages/database/relationship/utils/getters.js index ccf1b03f..8a1a3a4b 100644 --- a/src/packages/database/relationship/utils/getters.js +++ b/src/packages/database/relationship/utils/getters.js @@ -27,7 +27,7 @@ async function getHasManyThrough(owner: Model, { if (records.length) { value = await model.where({ [model.primaryKey]: records - .map(record => Reflect.get(record, foreignKey)) + .map(record => record[foreignKey]) .filter(Boolean) }); } @@ -66,7 +66,8 @@ export function getBelongsTo(owner: Model, { model, foreignKey }: Relationship$opts) { - const foreignValue = Reflect.get(owner, foreignKey); + // $FlowIgnore + const foreignValue = owner[foreignKey]; return foreignValue ? model.find(foreignValue) : Promise.resolve(null); } diff --git a/src/packages/database/relationship/utils/inverse-setters.js b/src/packages/database/relationship/utils/inverse-setters.js index 0bd3f953..85f60cfa 100644 --- a/src/packages/database/relationship/utils/inverse-setters.js +++ b/src/packages/database/relationship/utils/inverse-setters.js @@ -2,6 +2,15 @@ import type { Model } from '../../index'; import type { Relationship$opts } from '../index'; +type Setter$opts = { + type: $PropertyType; + model: $PropertyType; + inverse: $PropertyType; + through?: $PropertyType; + foreignKey: $PropertyType; + inverseModel: Class; +}; + /** * @private */ @@ -9,11 +18,16 @@ export function setHasManyInverse(owner: Model, value: Array, { inverse, foreignKey, inverseModel -}: Relationship$opts & { - inverseModel: Class; -}) { - const primaryKey = Reflect.get(owner, owner.constructor.primaryKey); - const { type: inverseType } = inverseModel.relationshipFor(inverse); +}: Setter$opts) { + // $FlowIgnore + const primaryKey = owner[owner.constructor.primaryKey]; + const relationship = inverseModel.relationshipFor(inverse); + + if (!relationship) { + return; + } + + const { type: inverseType } = relationship; for (const record of value) { let { currentChangeSet: changeSet } = record; @@ -26,7 +40,8 @@ export function setHasManyInverse(owner: Model, value: Array, { changeSet.set(inverse, owner); if (inverseType === 'belongsTo') { - Reflect.set(record, foreignKey, primaryKey); + // $FlowIgnore + record[foreignKey] = primaryKey; } } } @@ -39,11 +54,16 @@ export function setHasOneInverse(owner: Model, value?: ?Model, { inverse, foreignKey, inverseModel -}: Relationship$opts & { - inverseModel: Class; -}) { +}: Setter$opts) { if (value) { - const { type: inverseType } = inverseModel.relationshipFor(inverse); + const nextValue = value; + const relationship = inverseModel.relationshipFor(inverse); + + if (!relationship) { + return; + } + + const { type: inverseType } = relationship; let inverseValue = value.currentChangeSet.get(inverse); if (inverseType === 'hasMany') { @@ -58,14 +78,15 @@ export function setHasOneInverse(owner: Model, value?: ?Model, { inverseValue = owner; if (inverseType === 'belongsTo') { - Reflect.set(value, foreignKey, inverseValue.getPrimaryKey()); + // $FlowIgnore + nextValue[foreignKey] = inverseValue.getPrimaryKey(); } } - let { currentChangeSet: changeSet } = value; + let { currentChangeSet: changeSet } = nextValue; if (changeSet.isPersisted) { - changeSet = changeSet.applyTo(value); + changeSet = changeSet.applyTo(nextValue); } changeSet.set(inverse, inverseValue || null); diff --git a/src/packages/database/relationship/utils/setters.js b/src/packages/database/relationship/utils/setters.js index 1c415940..2200c91d 100644 --- a/src/packages/database/relationship/utils/setters.js +++ b/src/packages/database/relationship/utils/setters.js @@ -60,7 +60,7 @@ export function setHasOne(owner: Model, key: string, value?: ?Model, { let valueToSet = value; if (value && typeof value === 'object' && !model.isInstance(value)) { - valueToSet = Reflect.construct(model, [valueToSet]); + valueToSet = new model(value); // eslint-disable-line new-cap } let { currentChangeSet: changeSet } = owner; @@ -99,16 +99,15 @@ export function setBelongsTo(owner: Model, key: string, value?: ?Model, { inverse, foreignKey }: Relationship$opts) { - setHasOne(owner, key, value, { + const target = owner; + + setHasOne(target, key, value, { type, model, inverse, foreignKey }); - if (value) { - Reflect.set(owner, foreignKey, Reflect.get(value, model.primaryKey)); - } else { - Reflect.set(owner, foreignKey, null); - } + // $FlowIgnore + target[foreignKey] = value ? value[model.primaryKey] : null; } diff --git a/src/packages/database/relationship/utils/unassociate.js b/src/packages/database/relationship/utils/unassociate.js index ec0231dc..3dd76086 100644 --- a/src/packages/database/relationship/utils/unassociate.js +++ b/src/packages/database/relationship/utils/unassociate.js @@ -5,11 +5,14 @@ import type { Model } from '../../index'; // eslint-disable-line no-unused-vars * @private */ function unassociateOne(value: T, foreignKey: string): T { - if (value) { - Reflect.set(value, foreignKey, null); + const target = value; + + if (target) { + // $FlowIgnore + target[foreignKey] = null; } - return value; + return target; } /** diff --git a/src/packages/database/relationship/utils/update-relationship.js b/src/packages/database/relationship/utils/update-relationship.js index 396c0509..2eca6c15 100644 --- a/src/packages/database/relationship/utils/update-relationship.js +++ b/src/packages/database/relationship/utils/update-relationship.js @@ -110,21 +110,23 @@ function updateBelongsTo({ trx }: Params): Array { if (value instanceof opts.model) { + const target = record; const inverseOpts = opts.model.relationshipFor(opts.inverse); const foreignKeyValue = value.getPrimaryKey(); - Reflect.set(record, opts.foreignKey, foreignKeyValue); + // $FlowIgnore + target[opts.foreignKey] = foreignKeyValue; if (inverseOpts && inverseOpts.type === 'hasOne') { return [ - record.constructor + target.constructor .table() .transacting(trx) .update(opts.foreignKey, null) .where(opts.foreignKey, foreignKeyValue) .whereNot( - `${record.constructor.tableName}.${record.constructor.primaryKey}`, - record.getPrimaryKey() + `${target.constructor.tableName}.${target.constructor.primaryKey}`, + target.getPrimaryKey() ) ]; } diff --git a/src/packages/database/test/database.test.js b/src/packages/database/test/database.test.js index 6a3b8da8..5dea3e63 100644 --- a/src/packages/database/test/database.test.js +++ b/src/packages/database/test/database.test.js @@ -5,9 +5,13 @@ import { it, before, describe } from 'mocha'; import Database from '../index'; import { getTestApp } from '../../../../test/utils/get-test-app'; -const DATABASE_DRIVER: string = Reflect.get(process.env, 'DATABASE_DRIVER'); -const DATABASE_USERNAME: string = Reflect.get(process.env, 'DATABASE_USERNAME'); -const DATABASE_PASSWORD: string = Reflect.get(process.env, 'DATABASE_PASSWORD'); + +// $FlowIgnore +const DATABASE_DRIVER: string = process.env.DATABASE_DRIVER; +// $FlowIgnore +const DATABASE_USERNAME: string = process.env.DATABASE_USERNAME; +// $FlowIgnore +const DATABASE_PASSWORD: string = process.env.DATABASE_PASSWORD; const DEFAULT_CONFIG = { development: { diff --git a/src/packages/database/test/model.test.js b/src/packages/database/test/model.test.js index 1be19a8b..36b98872 100644 --- a/src/packages/database/test/model.test.js +++ b/src/packages/database/test/model.test.js @@ -7,7 +7,6 @@ import Model from '../model'; import Query, { RecordNotFoundError } from '../query'; import { ValidationError } from '../validation'; -import setType from '../../../utils/set-type'; import { getTestApp } from '../../../../test/utils/get-test-app'; describe('module "database/model"', () => { @@ -133,7 +132,7 @@ describe('module "database/model"', () => { ]); Object.keys(Subject.attributes).forEach(key => { - const value = Reflect.get(Subject.attributes, key); + const value = Subject.attributes[key]; expect(value).to.have.all.keys([ 'type', @@ -162,7 +161,7 @@ describe('module "database/model"', () => { it('adds attribute accessors on the `prototype`', () => { Object.keys(Subject.attributes).forEach(key => { - const desc = Reflect.getOwnPropertyDescriptor(Subject.prototype, key); + const desc = Object.getOwnPropertyDescriptor(Subject.prototype, key); expect(desc).to.have.property('get').and.be.a('function'); expect(desc).to.have.property('set').and.be.a('function'); @@ -181,7 +180,7 @@ describe('module "database/model"', () => { ]); Object.keys(Subject.hasMany).forEach(key => { - const value = Reflect.get(Subject.hasMany, key); + const value = Subject.hasMany[key]; expect(value).to.be.an('object'); expect(value).to.have.property('type').and.equal('hasMany'); @@ -199,7 +198,7 @@ describe('module "database/model"', () => { expect(Subject.belongsTo).to.have.all.keys(['user']); Object.keys(Subject.belongsTo).forEach(key => { - const value = Reflect.get(Subject.belongsTo, key); + const value = Subject.belongsTo[key]; expect(value).to.be.an('object'); expect(value).to.have.property('type').and.equal('belongsTo'); @@ -221,7 +220,7 @@ describe('module "database/model"', () => { ]); Object.keys(Subject.relationships).forEach(key => { - const value = Reflect.get(Subject.relationships, key); + const value = Subject.relationships[key]; expect(value).to.have.property('type'); @@ -255,7 +254,7 @@ describe('module "database/model"', () => { it('adds relationship accessors to the `prototype`', () => { Object.keys(Subject.relationships).forEach(key => { - const desc = Reflect.getOwnPropertyDescriptor(Subject.prototype, key); + const desc = Object.getOwnPropertyDescriptor(Subject.prototype, key); expect(desc).to.have.property('get').and.be.a('function'); expect(desc).to.have.property('set').and.be.a('function'); @@ -284,7 +283,8 @@ describe('module "database/model"', () => { ]); Object.keys(Subject.scopes).forEach(key => { - const value = Reflect.get(Subject, key); + // $FlowIgnore + const value = Subject[key]; expect(value).to.be.a('function'); }); @@ -837,7 +837,8 @@ describe('module "database/model"', () => { const assertSaveHook = async (instance: Model, hookSpy) => { hookSpy.reset(); - Reflect.set(instance, 'isPublic', true); + // $FlowIgnore + instance.isPublic = true; await instance.save(); expect(hookSpy.calledWith(instance)).to.be.true; diff --git a/src/packages/database/test/query.test.js b/src/packages/database/test/query.test.js index bdcbf41f..aeb8088d 100644 --- a/src/packages/database/test/query.test.js +++ b/src/packages/database/test/query.test.js @@ -5,7 +5,6 @@ import { it, describe, before, beforeEach } from 'mocha'; import Query from '../query'; import Model from '../model'; -import setType from '../../../utils/set-type'; import { getTestApp } from '../../../../test/utils/get-test-app'; describe('module "database/query"', () => { diff --git a/src/packages/database/test/relationship.test.js b/src/packages/database/test/relationship.test.js index d7c08031..39d68b1f 100644 --- a/src/packages/database/test/relationship.test.js +++ b/src/packages/database/test/relationship.test.js @@ -189,11 +189,10 @@ describe('module "database/relationship"', () => { let subjectId; const setup = async () => { - subject = await Post.create({ - title: '#set() test' - }); + subject = await Post + .create({ title: '#set() test' }) + .then(post => post.unwrap()); - subject = subject.unwrap(); subjectId = subject.getPrimaryKey(); }; @@ -212,11 +211,9 @@ describe('module "database/relationship"', () => { beforeEach(async () => { await setup(); - image = await Image.create({ - url: 'http://postlight.com' - }); - - image = image.unwrap(); + image = await Image + .create({ url: 'http://postlight.com' }) + .then(img => img.unwrap()); instances.add(image); set(subject, 'image', image); @@ -226,7 +223,8 @@ describe('module "database/relationship"', () => { it('can add a record to the relationship', async () => { expect(image).to.have.property('postId', subjectId); - expect(await Reflect.get(image, 'post')).be.an.instanceof(Post); + // $FlowIgnore + expect(await image.post).be.an.instanceof(Post); }); }); @@ -252,14 +250,16 @@ describe('module "database/relationship"', () => { it('can add a record to the relationship', async () => { expect(subject).to.have.property('userId', user.getPrimaryKey()); - expect(await Reflect.get(subject, 'user')).to.be.an.instanceof(User); + // $FlowIgnore + expect(await subject.user).to.be.an.instanceof(User); }); it('can remove a record from the relationship', async () => { set(subject, 'user', null); expect(subject).to.have.property('userId', null); - expect(await Reflect.get(subject, 'user')).to.be.null; + // $FlowIgnore + expect(await subject.user).to.be.null; }); }); diff --git a/src/packages/freezeable/utils/freeze.js b/src/packages/freezeable/utils/freeze.js index b27b6779..35828899 100644 --- a/src/packages/freezeable/utils/freeze.js +++ b/src/packages/freezeable/utils/freeze.js @@ -41,15 +41,21 @@ export function freezeProps( makePublic: boolean, ...props: Array ): T { - Object.defineProperties(target, props.reduce((obj, key) => ({ - ...obj, - [key]: { - value: Reflect.get(target, key), + const properties = props.reduce((obj, key) => { + const result = obj; + + result[key] = { + // $FlowIgnore + value: target[key], writable: false, enumerable: makePublic, configurable: false, - } - }), {})); + }; + + return result; + }, {}); + + Object.defineProperties(target, properties); return target; } @@ -62,8 +68,10 @@ export function deepFreezeProps( makePublic: boolean, ...props: Array ): T { - Object.defineProperties(target, props.reduce((obj, key) => { - let value = Reflect.get(target, key); + const properties = props.reduce((obj, key) => { + const result = obj; + // $FlowIgnore + let value = target[key]; if (Array.isArray(value)) { value = freezeArray(value); @@ -71,16 +79,17 @@ export function deepFreezeProps( value = freezeValue(value); } - return { - ...obj, - [key]: { - value, - writable: false, - enumerable: makePublic, - configurable: false, - } + result[key] = { + value, + writable: false, + enumerable: makePublic, + configurable: false, }; - }, {})); + + return result; + }, {}); + + Object.defineProperties(target, properties); return target; } diff --git a/src/packages/fs/test/fs.test.js b/src/packages/fs/test/fs.test.js index 323c23e9..9f1abba1 100644 --- a/src/packages/fs/test/fs.test.js +++ b/src/packages/fs/test/fs.test.js @@ -39,7 +39,7 @@ describe('module "fs"', () => { // unwrap spies of node fs methods spies = spiedMethods.reduce((memo, methodName) => { memo[methodName].restore(); - Reflect.deleteProperty(memo, methodName); + delete memo[methodName]; return memo; }, spies); }); @@ -62,7 +62,7 @@ describe('module "fs"', () => { it('delegates to node fs#mkdir', async () => { const dirPath = join(tmpDirPath, 'test-mkdir'); await fs.mkdir(dirPath); - expect(spies['mkdir'].calledWith(dirPath)).to.be.true; + expect(spies.mkdir.calledWith(dirPath)).to.be.true; }); it('returns a promise', () => { const dirPath = join(tmpDirPath, 'test-mkdir'); @@ -73,7 +73,7 @@ describe('module "fs"', () => { describe('#rmdir()', () => { it('delegates to node fs#rmdir', async () => { await fs.rmdir(tmpDirPath); - expect(spies['rmdir'].calledWith(tmpDirPath)).to.be.true; + expect(spies.rmdir.calledWith(tmpDirPath)).to.be.true; }); it('returns a promise', returnsPromiseSpec('rmdir', tmpDirPath)); }); @@ -81,7 +81,7 @@ describe('module "fs"', () => { describe('#readdir()', () => { it('delegates to node fs#readdir', async () => { await fs.readdir(tmpDirPath); - expect(spies['readdir'].calledWith(tmpDirPath)).to.be.true; + expect(spies.readdir.calledWith(tmpDirPath)).to.be.true; }); it('returns a promise', returnsPromiseSpec('readdir', tmpDirPath)); }); @@ -96,7 +96,7 @@ describe('module "fs"', () => { it('delegates to node fs#readFile', async () => { await fs.readFile(tmpFilePath); - expect(spies['readFile'].calledWith(tmpFilePath)).to.be.true; + expect(spies.readFile.calledWith(tmpFilePath)).to.be.true; }); it('returns a promise', returnsPromiseSpec('readFile', tmpFilePath)); }); @@ -111,7 +111,7 @@ describe('module "fs"', () => { it('delegates to node fs#writeFile', async () => { await fs.writeFile(tmpFilePath, 'test data'); - expect(spies['writeFile'].calledWith(tmpFilePath)).to.be.true; + expect(spies.writeFile.calledWith(tmpFilePath)).to.be.true; }); it('returns a promise', returnsPromiseSpec('writeFile', tmpFilePath)); }); @@ -126,7 +126,7 @@ describe('module "fs"', () => { it('delegates to node fs#appendFile', async () => { await fs.appendFile(tmpFilePath, 'test data'); - expect(spies['appendFile'].calledWith(tmpFilePath)); + expect(spies.appendFile.calledWith(tmpFilePath)); }); it('returns a promise', returnsPromiseSpec('appendFile', tmpFilePath)); }); @@ -141,7 +141,7 @@ describe('module "fs"', () => { it('delegates to node fs#stat', async () => { await fs.stat(tmpFilePath); - expect(spies['stat'].calledWith(tmpFilePath)); + expect(spies.stat.calledWith(tmpFilePath)); }); it('returns a promise', returnsPromiseSpec('stat', tmpFilePath)); }); @@ -156,7 +156,7 @@ describe('module "fs"', () => { it('delegates to node fs#unlink', async () => { await fs.unlink(tmpFilePath); - expect(spies['unlink'].calledWith(tmpFilePath)); + expect(spies.unlink.calledWith(tmpFilePath)); }); it('returns a promise', returnsPromiseSpec('unlink', tmpFilePath)); }); @@ -187,7 +187,7 @@ function returnsPromiseSpec( ...args: Array ): (done?: () => void) => void | Promise { return function () { - const res = Reflect.apply(fs[method], fs, args); + const res = fs[method].apply(fs, args); expect(res).to.be.an.instanceOf(Promise); }; } diff --git a/src/packages/jsonapi/interfaces.js b/src/packages/jsonapi/interfaces.js index c607c29e..1d8badf3 100644 --- a/src/packages/jsonapi/interfaces.js +++ b/src/packages/jsonapi/interfaces.js @@ -6,7 +6,7 @@ type JSONAPI$value = | ?JSONAPI$BaseObject | Array; -interface JSONAPI$BaseObject { +type JSONAPI$BaseObject = { [key: void | string]: JSONAPI$value; meta?: JSONAPI$BaseObject; } @@ -75,7 +75,6 @@ export interface JSONAPI$ErrorObject { } export interface JSONAPI$Document { - // $FlowIgnore data?: Array | JSONAPI$ResourceObject; links?: JSONAPI$DocumentLinks; errors?: Array; diff --git a/src/packages/loader/builder/test/create-parent-builder.test.js b/src/packages/loader/builder/test/create-parent-builder.test.js index 530f6919..8937a5ee 100644 --- a/src/packages/loader/builder/test/create-parent-builder.test.js +++ b/src/packages/loader/builder/test/create-parent-builder.test.js @@ -18,22 +18,21 @@ describe('module "loader/builder"', () => { class ApiV1ApplicationController extends Controller {} beforeEach(() => { - subject = createParentBuilder((key, target, parent) => { + subject = createParentBuilder((key, Target, parent) => { const namespace = posix.dirname(key).replace('.', ''); - // $FlowIgnore const serializer = new Serializer({ namespace, model: null, parent: null }); - return Reflect.construct(target, [{ + return new Target({ parent, namespace, serializer, model: null - }]); + }); }); }); diff --git a/src/packages/loader/test/loader.test.js b/src/packages/loader/test/loader.test.js index 9a132c68..49a1cc90 100644 --- a/src/packages/loader/test/loader.test.js +++ b/src/packages/loader/test/loader.test.js @@ -43,7 +43,7 @@ describe('module "loader"', () => { result.forEach(value => { expect( - Reflect.getPrototypeOf(value).name.endsWith('Controller') + Object.getPrototypeOf(value).name.endsWith('Controller') ).to.be.true; }); }); @@ -64,7 +64,7 @@ describe('module "loader"', () => { expect(result).to.be.an.instanceof(FreezeableMap); result.forEach(value => { - expect(Reflect.getPrototypeOf(value).name).to.equal('Model'); + expect(Object.getPrototypeOf(value).name).to.equal('Model'); }); }); @@ -83,7 +83,7 @@ describe('module "loader"', () => { result.forEach(value => { expect( - Reflect.getPrototypeOf(value).name.endsWith('Serializer') + Object.getPrototypeOf(value).name.endsWith('Serializer') ).to.be.true; }); }); diff --git a/src/packages/loader/utils/bundle-for.js b/src/packages/loader/utils/bundle-for.js index 7e14d0d3..30d67ea5 100644 --- a/src/packages/loader/utils/bundle-for.js +++ b/src/packages/loader/utils/bundle-for.js @@ -17,25 +17,24 @@ const SUFFIX_PATTERN = /^.+(Controller|Down|Serializer|Up)/; */ function normalize(manifest: Object) { return entries(manifest).reduce((obj, [key, value]) => { + const result = obj; + if (SUFFIX_PATTERN.test(key)) { const suffix = key.replace(SUFFIX_PATTERN, '$1'); const stripSuffix = source => source.replace(suffix, ''); switch (suffix) { case 'Controller': - obj.controllers.set(formatKey(key, stripSuffix), value); + result.controllers.set(formatKey(key, stripSuffix), value); break; case 'Serializer': - obj.serializers.set(formatKey(key, stripSuffix), value); + result.serializers.set(formatKey(key, stripSuffix), value); break; case 'Up': case 'Down': - obj.migrations.set( - formatKey(key), - Reflect.construct(Migration, [value]) - ); + result.migrations.set(formatKey(key), new Migration(value)); break; default: @@ -46,32 +45,30 @@ function normalize(manifest: Object) { case 'Application': case 'routes': case 'seed': - Reflect.set(obj, formatKey(key), value); + result[formatKey(key)] = value; break; case 'config': - Reflect.set(obj, 'config', { - ...merge(createDefaultConfig(), { - ...obj.config, - ...value - }) + result.config = merge(createDefaultConfig(), { + ...result.config, + ...value }); break; case 'database': - Reflect.set(obj, 'config', { - ...obj.config, + result.config = { + ...result.config, database: value - }); + }; break; default: - obj.models.set(formatKey(key), value); + result.models.set(formatKey(key), value); break; } } - return obj; + return result; }, { config: {}, controllers: new FreezeableMap(), @@ -85,9 +82,8 @@ function normalize(manifest: Object) { * @private */ export default function bundleFor(path: string): FreezeableMap { - const manifest: Object = Reflect.apply(require, null, [ - joinPath(path, 'dist', 'bundle') - ]); + // $FlowIgnore + const manifest: Object = require(joinPath(path, 'dist', 'bundle')); return chain(manifest) .pipe(normalize) diff --git a/src/packages/logger/index.js b/src/packages/logger/index.js index 3fb10d6e..5a3d2aa0 100644 --- a/src/packages/logger/index.js +++ b/src/packages/logger/index.js @@ -240,7 +240,7 @@ class Logger { const levelNum = LEVELS.get(level) || 0; LEVELS.forEach((val, key: Logger$level) => { - Reflect.defineProperty(this, key.toLowerCase(), { + Object.defineProperty(this, key.toLowerCase(), { writable: false, enumerable: false, configurable: false, diff --git a/src/packages/logger/request-logger/templates.js b/src/packages/logger/request-logger/templates.js index 831f2943..11e25117 100644 --- a/src/packages/logger/request-logger/templates.js +++ b/src/packages/logger/request-logger/templates.js @@ -41,8 +41,8 @@ export const debugTemplate = ({ }: RequestLogger$templateData) => `\ ${line` Processed ${cyan(`${method}`)} "${path}" from ${remoteAddress} - with ${Reflect.apply(colorStr, null, [`${statusCode}`])} - ${Reflect.apply(colorStr, null, [`${statusMessage}`])} by ${ + with ${colorStr(String(statusCode))} + ${colorStr(String(statusMessage))} by ${ route ? `${yellow(route.controller.constructor.name)}#${blue(route.action)}` : null @@ -91,9 +91,9 @@ export const infoTemplate = ({ Processed ${cyan(`${method}`)} "${path}" ${magenta('Params')} ${ JSON.stringify(params)} from ${remoteAddress } in ${(endTime - startTime).toString()} ms with ${ - Reflect.apply(colorStr, null, [`${statusCode}`]) + colorStr(String(statusCode)) } ${ - Reflect.apply(colorStr, null, [`${statusMessage}`]) + colorStr(String(statusMessage)) } by ${ route ? `${yellow(route.controller.constructor.name)}#${blue(route.action)}` diff --git a/src/packages/logger/request-logger/utils/log-text.js b/src/packages/logger/request-logger/utils/log-text.js index e4718242..65d72dea 100644 --- a/src/packages/logger/request-logger/utils/log-text.js +++ b/src/packages/logger/request-logger/utils/log-text.js @@ -50,7 +50,7 @@ export default function logText(logger: Logger, { statusColor = 'red'; } - let colorStr = Reflect.get(chalk, statusColor); + let colorStr = chalk[statusColor]; if (typeof colorStr === 'undefined') { colorStr = (str: string) => str; diff --git a/src/packages/logger/test/logger.test.js b/src/packages/logger/test/logger.test.js index 83337364..4741b6ba 100644 --- a/src/packages/logger/test/logger.test.js +++ b/src/packages/logger/test/logger.test.js @@ -106,7 +106,7 @@ function hookWrite (cb) { const cbWrapper = (...args) => { if (isLoggerData(...args)) { - Reflect.apply(cb, null, args); + cb(...args); } }; diff --git a/src/packages/luxify/index.js b/src/packages/luxify/index.js index b874f42e..dbe0ef03 100644 --- a/src/packages/luxify/index.js +++ b/src/packages/luxify/index.js @@ -21,7 +21,7 @@ export default function luxify( ): Action { const result = function (req, res) { // eslint-disable-line func-names return new Promise((resolve, reject) => { - Reflect.apply(middleware, null, [ + middleware( req, createResponseProxy(res, resolve), (err) => { @@ -31,11 +31,11 @@ export default function luxify( resolve(); } } - ]); + ); }); }; - Reflect.defineProperty(result, 'name', { + Object.defineProperty(result, 'name', { value: middleware.name }); diff --git a/src/packages/luxify/test/luxify.test.js b/src/packages/luxify/test/luxify.test.js index 0d0484d3..6912bd80 100644 --- a/src/packages/luxify/test/luxify.test.js +++ b/src/packages/luxify/test/luxify.test.js @@ -2,21 +2,21 @@ import { expect } from 'chai'; import { it, describe } from 'mocha'; -import luxify from '../index'; - import K from '../../../utils/k'; -import setType from '../../../utils/set-type'; +import luxify from '../index'; +import type { Request, Response } from '../../server'; describe('module "luxify"', () => { describe('#luxify()', () => { - const [request, response] = setType(() => [ - {}, - { - getHeader: K, - setHeader: K, - removeHeader: K - } - ]); + // $FlowIgnore + const request: Request = {}; + + // $FlowIgnore + const response: Response = { + getHeader: K, + setHeader: K, + removeHeader: K + }; it('promisifies a callback based middleware function', () => { const subject = luxify((req, res, next) => { @@ -38,7 +38,8 @@ describe('module "luxify"', () => { it('resolves when Response#send is called', () => { const subject = luxify((req, res) => { - Reflect.apply(Reflect.get(res, 'send'), res, ['Hello world!']); + // $FlowIgnore + res.send('Hello world!'); }); return subject(request, response).then(data => { @@ -48,9 +49,10 @@ describe('module "luxify"', () => { it('resolves when Response#json is called', () => { const subject = luxify((req, res) => { - Reflect.apply(Reflect.get(res, 'json'), res, [{ + // $FlowIgnore + res.json({ data: 'Hello world!' - }]); + }); }); return subject(request, response).then(data => { diff --git a/src/packages/luxify/utils/create-response-proxy.js b/src/packages/luxify/utils/create-response-proxy.js index f9bc01e2..99abd25a 100644 --- a/src/packages/luxify/utils/create-response-proxy.js +++ b/src/packages/luxify/utils/create-response-proxy.js @@ -20,7 +20,7 @@ export default function createResponseProxy( return resolve; default: - return Reflect.get(target, key); + return target[key]; } } }); diff --git a/src/packages/pm/cluster/index.js b/src/packages/pm/cluster/index.js index 4a12e741..3f215a9a 100644 --- a/src/packages/pm/cluster/index.js +++ b/src/packages/pm/cluster/index.js @@ -9,6 +9,7 @@ import { red, green } from 'chalk'; import { NODE_ENV } from '../../../constants'; import { line } from '../../logger'; +import { freezeProps } from '../../freezeable'; import omit from '../../../utils/omit'; import range from '../../../utils/range'; import { composeAsync } from '../../../utils/compose'; @@ -31,45 +32,30 @@ class Cluster extends EventEmitter { maxWorkers: number; constructor({ path, port, logger, maxWorkers }: Cluster$opts) { + let numCPUs = os.cpus().length; + super(); - Object.defineProperties(this, { - path: { - value: path, - writable: false, - enumerable: true, - configurable: false - }, - - port: { - value: port, - writable: false, - enumerable: true, - configurable: false - }, - - logger: { - value: logger, - writable: false, - enumerable: true, - configurable: false - }, - - workers: { - value: new Set(), - writable: false, - enumerable: true, - configurable: false - }, - - maxWorkers: { - value: maxWorkers || os.cpus().length, - writable: false, - enumerable: true, - configurable: false - } + if (numCPUs > 2 && numCPUs % 2 === 0) { + numCPUs /= 2; + } + + Object.assign(this, { + path, + port, + logger, + workers: new Set(), + maxWorkers: maxWorkers || numCPUs }); + freezeProps(this, true, + 'path', + 'port', + 'logger', + 'workers', + 'maxWorkers' + ); + cluster.setupMaster({ exec: joinPath(path, 'dist', 'boot.js') }); diff --git a/src/packages/router/definitions/context/index.js b/src/packages/router/definitions/context/index.js index 75e04008..fd3a0ce4 100644 --- a/src/packages/router/definitions/context/index.js +++ b/src/packages/router/definitions/context/index.js @@ -29,15 +29,11 @@ export function contextFor(build: Router$DefinitionBuilder<*>) { ...context, member(builder: () => void) { - const childCtx = createDefinitionGroup('member', namespace); - - Reflect.apply(builder, childCtx, []); + builder.call(createDefinitionGroup('member', namespace)); }, collection(builder: () => void) { - const childCtx = createDefinitionGroup('collection', namespace); - - Reflect.apply(builder, childCtx, []); + builder.call(createDefinitionGroup('collection', namespace)); } }; } else { diff --git a/src/packages/router/definitions/context/utils/create-definition-group.js b/src/packages/router/definitions/context/utils/create-definition-group.js index 9c953329..117e2222 100644 --- a/src/packages/router/definitions/context/utils/create-definition-group.js +++ b/src/packages/router/definitions/context/utils/create-definition-group.js @@ -1,22 +1,55 @@ // @flow -import { REQUEST_METHODS } from '../../../../server'; import type { Route$type, Router$Namespace } from '../../../index'; // eslint-disable-line max-len, no-unused-vars +import type { Router$Definition } from '../../interfaces'; import createDefinition from './create-definition'; +type Router$DefinitionGroup = { + get: Router$Definition; + head: Router$Definition; + post: Router$Definition; + patch: Router$Definition; + delete: Router$Definition; + options: Router$Definition; +}; + /** * @private */ export default function createDefinitionGroup( type: Route$type, namespace: T -) { - return REQUEST_METHODS.reduce((methods, method) => ({ - ...methods, - [method.toLowerCase()]: createDefinition({ +): Router$DefinitionGroup { + return { + get: createDefinition({ + type, + namespace, + method: 'GET' + }), + head: createDefinition({ + type, + namespace, + method: 'HEAD' + }), + post: createDefinition({ + type, + namespace, + method: 'POST' + }), + patch: createDefinition({ + type, + namespace, + method: 'PATCH' + }), + delete: createDefinition({ + type, + namespace, + method: 'DELETE' + }), + options: createDefinition({ type, - method, - namespace + namespace, + method: 'OPTIONS' }) - }), {}); + }; } diff --git a/src/packages/router/definitions/context/utils/create-definition.js b/src/packages/router/definitions/context/utils/create-definition.js index d8d24613..e1b52494 100644 --- a/src/packages/router/definitions/context/utils/create-definition.js +++ b/src/packages/router/definitions/context/utils/create-definition.js @@ -3,6 +3,7 @@ import { Route } from '../../../index'; import { normalizeName, normalizePath } from '../../../namespace'; import type { Request$method } from '../../../../server'; import type { Router$Namespace, Route$type } from '../../../index'; // eslint-disable-line max-len, no-duplicate-imports +import type { Router$Definition } from '../../interfaces'; /** * @private @@ -11,7 +12,7 @@ export default function createDefinition({ type, method, namespace }: { type: Route$type; method: Request$method; namespace: Router$Namespace; -}) { +}): Router$Definition { return function define(name: string, action?: string = normalizeName(name)) { const normalized = normalizeName(name); const { controller } = namespace; diff --git a/src/packages/router/definitions/index.js b/src/packages/router/definitions/index.js index 64316f43..0d924a80 100644 --- a/src/packages/router/definitions/index.js +++ b/src/packages/router/definitions/index.js @@ -40,7 +40,7 @@ export function build(builder?: () => void, namespace: T) { } if (builder) { - Reflect.apply(builder, context, []); + builder.call(context); } return namespace; diff --git a/src/packages/router/definitions/interfaces.js b/src/packages/router/definitions/interfaces.js index 332bd6d2..a4012d65 100644 --- a/src/packages/router/definitions/interfaces.js +++ b/src/packages/router/definitions/interfaces.js @@ -1,6 +1,7 @@ // @flow import type { Router$Namespace, Resource$opts } from '../index'; +export type Router$Definition = (name: string, action: string) => void; export type Router$DefinitionBuilder = ( builder?: () => void, namespace: T diff --git a/src/packages/router/index.js b/src/packages/router/index.js index 201ed68d..ccdec014 100644 --- a/src/packages/router/index.js +++ b/src/packages/router/index.js @@ -25,7 +25,7 @@ class Router extends FreezeableMap { super(); define(this, definitions); - Reflect.defineProperty(this, 'replacer', { + Object.defineProperty(this, 'replacer', { value: createReplacer(controllers), writable: false, enumerable: false, @@ -35,14 +35,16 @@ class Router extends FreezeableMap { this.freeze(); } - match({ method, url }: Request): void | Route { + match(req: Request): void | Route { const params = []; + const { url, method } = req; + const staticPath = url.pathname.replace(this.replacer, (str, g1, g2) => { params.push(g2); return `${g1}/:dynamic`; }); - Reflect.set(url, 'params', params); + url.params = params; return this.get(`${method}:${staticPath}`); } diff --git a/src/packages/router/interfaces.js b/src/packages/router/interfaces.js index e9713193..9255ddc2 100644 --- a/src/packages/router/interfaces.js +++ b/src/packages/router/interfaces.js @@ -4,8 +4,8 @@ import type Controller from '../controller'; import type { FreezeableSet } from '../freezeable'; export type Router$opts = { - controller: Controller; - controllers: Map; + controller: Controller<*>; + controllers: Map>; routes(): void; }; @@ -19,6 +19,6 @@ export interface Router$Namespace extends FreezeableSet { path: string; isRoot: boolean; namespace: Router$Namespace; - controller: Controller; - controllers: Map; + controller: Controller<*>; + controllers: Map>; } diff --git a/src/packages/router/namespace/index.js b/src/packages/router/namespace/index.js index 1121b3b3..5bb5c421 100644 --- a/src/packages/router/namespace/index.js +++ b/src/packages/router/namespace/index.js @@ -19,9 +19,9 @@ class Namespace extends FreezeableSet { namespace: Router$Namespace; - controller: Controller; + controller: Controller<*>; - controllers: Map; + controllers: Map>; constructor({ name, diff --git a/src/packages/router/namespace/interfaces.js b/src/packages/router/namespace/interfaces.js index f5b23069..b58da2a2 100644 --- a/src/packages/router/namespace/interfaces.js +++ b/src/packages/router/namespace/interfaces.js @@ -6,6 +6,6 @@ export type Namespace$opts = { name: string; path: string; namespace?: Router$Namespace; - controller: Controller; - controllers: Map; + controller: Controller<*>; + controllers: Map>; }; diff --git a/src/packages/router/resource/index.js b/src/packages/router/resource/index.js index faca5afc..d9c942a3 100644 --- a/src/packages/router/resource/index.js +++ b/src/packages/router/resource/index.js @@ -15,7 +15,7 @@ class Resource extends Namespace { constructor({ only, ...opts }: Resource$opts) { super(opts); - Reflect.defineProperty(this, 'only', { + Object.defineProperty(this, 'only', { value: new FreezeableSet(normalizeOnly(only)), writable: false, enumerable: false, diff --git a/src/packages/router/route/action/constants.js b/src/packages/router/route/action/constants.js deleted file mode 100644 index 77d0abb2..00000000 --- a/src/packages/router/route/action/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export const FINAL_HANDLER = '__FINAL_HANDLER__'; diff --git a/src/packages/router/route/action/enhancers/resource.js b/src/packages/router/route/action/enhancers/resource.js index f4f1603c..8ab9683d 100644 --- a/src/packages/router/route/action/enhancers/resource.js +++ b/src/packages/router/route/action/enhancers/resource.js @@ -9,72 +9,71 @@ import type { Action } from '../interfaces'; */ export default function resource(action: Action): Action { // eslint-disable-next-line func-names - const resourceAction = async function (req, res) { - const { route: { action: actionName } } = req; - const result = action(req, res); - let links = {}; - let data; - let total; + const resourceAction = function (req, res) { + const isIndex = req.route.action === 'index'; - if (actionName === 'index' && result instanceof Query) { - [data, total] = await Promise.all([ - result, - Query.from(result).count() - ]); - } else { - data = await result; + const init = [ + action(req, res), + Promise.resolve(0) + ]; + + if (isIndex && init[0] instanceof Query) { + init[1] = Query.from(init[0]).count(); } - if (Array.isArray(data) || (data && data.isModelInstance)) { - const domain = getDomain(req); + return Promise + .all(init) + .then(([data, total]) => { + if (Array.isArray(data) || (data && data.isModelInstance)) { + const domain = getDomain(req); + let links = {}; - const { - params, - url: { - path, - pathname - }, - route: { - controller: { - namespace, - serializer, - defaultPerPage - } - } - } = req; + const { + params, + url: { + path, + pathname + }, + route: { + controller: { + namespace, + serializer, + defaultPerPage + } + } + } = req; - const include = params.include || []; + if (isIndex) { + links = createPageLinks({ + params, + domain, + pathname, + defaultPerPage, + total: total || 0 + }); + } else if (namespace) { + links = { + self: domain.replace(`/${namespace}`, '') + path + }; + } else { + links = { + self: domain + path + }; + } - if (actionName === 'index') { - links = createPageLinks({ - params, - domain, - pathname, - defaultPerPage, - total: total || 0 - }); - } else if (actionName !== 'index' && namespace) { - links = { - self: domain.replace(`/${namespace}`, '') + path - }; - } else if (actionName !== 'index' && !namespace) { - links = { - self: domain + path - }; - } + return serializer.format({ + data, + links, + domain, + include: params.include || [] + }); + } - return serializer.format({ - data, - links, - domain, - include + return data; }); - } - - return data; }; - Reflect.defineProperty(resourceAction, 'name', { + Object.defineProperty(resourceAction, 'name', { value: action.name }); diff --git a/src/packages/router/route/action/enhancers/track-perf.js b/src/packages/router/route/action/enhancers/track-perf.js index 7b944888..f72f90df 100644 --- a/src/packages/router/route/action/enhancers/track-perf.js +++ b/src/packages/router/route/action/enhancers/track-perf.js @@ -1,6 +1,4 @@ // @flow -import { FINAL_HANDLER } from '../constants'; -import getActionName from '../utils/get-action-name'; import getControllerName from '../utils/get-controller-name'; import type { Action } from '../interfaces'; @@ -9,31 +7,29 @@ import type { Action } from '../interfaces'; */ export default function trackPerf>(action: U): Action { // eslint-disable-next-line func-names - const trackedAction = async function (...args: Array) { - const [req, res] = args; + const trackedAction = function (req, res, data) { const start = Date.now(); - const result = await action(...args); - let { name } = action; - let type = 'middleware'; - if (name === FINAL_HANDLER) { - type = 'action'; - name = getActionName(req); - } else if (!name) { - name = 'anonymous'; - } + return action(req, res, data).then(result => { + let { name } = action; + const type = 'middleware'; - res.stats.push({ - type, - name, - duration: Date.now() - start, - controller: getControllerName(req) - }); + if (!name) { + name = 'anonymous'; + } + + res.stats.push({ + type, + name, + duration: Date.now() - start, + controller: getControllerName(req) + }); - return result; + return result; + }); }; - Reflect.defineProperty(trackedAction, 'name', { + Object.defineProperty(trackedAction, 'name', { value: action.name }); diff --git a/src/packages/router/route/action/index.js b/src/packages/router/route/action/index.js index c6ec4f73..458438dd 100644 --- a/src/packages/router/route/action/index.js +++ b/src/packages/router/route/action/index.js @@ -1,4 +1,6 @@ // @flow +import { IS_PRODUCTION } from '../../../../constants'; +import insert from '../../../../utils/insert'; import type Controller from '../../../controller'; import resource from './enhancers/resource'; @@ -11,25 +13,26 @@ import type { Action } from './interfaces'; export function createAction( type: string, action: Action, - controller: Controller + controller: Controller<*> ): Array> { - let fn = action.bind(controller); + let controllerAction = action.bind(controller); if (type !== 'custom' && controller.hasModel && controller.hasSerializer) { - fn = resource(fn); + controllerAction = resource(controllerAction); } - return [ + let handlers = [ ...controller.beforeAction, - // eslint-disable-next-line no-underscore-dangle - function __FINAL_HANDLER__(req, res) { - return fn(req, res); - }, - ...controller.afterAction, - ].map(trackPerf); + controllerAction, + ...controller.afterAction + ]; + + if (!IS_PRODUCTION) { + handlers = handlers.map(trackPerf); + } + + return insert(new Array(handlers.length), handlers); } -export { FINAL_HANDLER } from './constants'; export { default as createPageLinks } from './utils/create-page-links'; - export type { Action } from './interfaces'; diff --git a/src/packages/router/route/index.js b/src/packages/router/route/index.js index 88189eda..66e08b08 100644 --- a/src/packages/router/route/index.js +++ b/src/packages/router/route/index.js @@ -1,9 +1,10 @@ // @flow import { FreezeableSet, freezeProps, deepFreezeProps } from '../../freezeable'; +import { tryCatchSync } from '../../../utils/try-catch'; import type Controller from '../../controller'; import type { Request, Response, Request$method } from '../../server'; -import { FINAL_HANDLER, createAction } from './action'; +import { createAction } from './action'; import { paramsFor, defaultParamsFor, validateResourceId } from './params'; import getStaticPath from './utils/get-static-path'; import getDynamicSegments from './utils/get-dynamic-segments'; @@ -25,10 +26,14 @@ class Route extends FreezeableSet> { method: Request$method; - controller: Controller; + handlers: Array>; + + controller: Controller<*>; staticPath: string; + actionIndex: number; + defaultParams: Object; dynamicSegments: Array; @@ -40,74 +45,72 @@ class Route extends FreezeableSet> { method, controller }: Route$opts) { - const dynamicSegments = getDynamicSegments(path); + // $FlowIgnore + const handler = controller[action]; + + if (typeof handler !== 'function') { + const { + constructor: { + name: controllerName + } + } = controller; - if (action && controller) { - const handler = Reflect.get(controller, action); - - if (typeof handler === 'function') { - const params = paramsFor({ - type, - method, - controller, - dynamicSegments - }); - - const staticPath = getStaticPath(path, dynamicSegments); - - const defaultParams = defaultParamsFor({ - type, - controller - }); - - super(createAction(type, handler, controller)); - - Object.assign(this, { - type, - path, - params, - action, - method, - controller, - staticPath, - defaultParams, - dynamicSegments - }); - - freezeProps(this, true, - 'type', - 'path' - ); - - freezeProps(this, false, - 'action', - 'params', - 'method', - 'controller', - 'staticPath' - ); - - deepFreezeProps(this, false, - 'defaultParams', - 'dynamicSegments' - ); - } else { - const { - constructor: { - name: controllerName - } - } = controller; - - throw new TypeError( - `Handler for ${controllerName}#${action} is not a function.` - ); - } - } else { throw new TypeError( - 'Arguements `controller` and `action` must not be undefined' + `Handler for ${controllerName}#${action} is not a function.` ); } + const dynamicSegments = getDynamicSegments(path); + const staticPath = getStaticPath(path, dynamicSegments); + + const params = paramsFor({ + type, + method, + controller, + dynamicSegments + }); + + const defaultParams = defaultParamsFor({ + type, + controller + }); + + const handlers = createAction(type, handler, controller); + + super(handlers); + + Object.assign(this, { + type, + path, + params, + action, + method, + handlers, + controller, + staticPath, + defaultParams, + dynamicSegments, + actionIndex: controller.beforeAction.length + }); + + freezeProps(this, true, + 'type', + 'path' + ); + + freezeProps(this, false, + 'action', + 'params', + 'method', + 'controller', + 'staticPath' + ); + + deepFreezeProps(this, false, + 'defaultParams', + 'dynamicSegments' + ); + this.freeze(); } @@ -126,44 +129,64 @@ class Route extends FreezeableSet> { }, {}); } - async execHandlers(req: Request, res: Response): Promise { + execHandlers(req: Request, res: Response): Promise { + let result = Promise.resolve(); let calledFinal = false; - let data; - - for (const handler of this) { - // eslint-disable-next-line no-await-in-loop - data = await handler(req, res, data); + let shouldContinue = true; + const { handlers, actionIndex } = this; - if (handler.name === FINAL_HANDLER) { - calledFinal = true; + const step = (prev, nextIdx) => prev.then(data => { + if (!calledFinal && typeof data !== 'undefined') { + shouldContinue = false; + return prev; } - if (!calledFinal && typeof data !== 'undefined') { + return handlers[nextIdx](req, res, data); + }); + + for (let i = 0; i < handlers.length; i = i + 1) { + if (!shouldContinue) { break; } + + result = step(result, i); + + if (i === actionIndex) { + calledFinal = true; + } } - return data; + return result; } - async visit(req: Request, res: Response): Promise { - const { defaultParams } = this; - let params = { - ...req.params, - ...this.parseParams(req.url.params) - }; + visit(req: Request, res: Response): Promise { + let error; - if (req.method !== 'OPTIONS') { - params = this.params.validate(params); - } + tryCatchSync(() => { + const { defaultParams } = this; + let params = { + ...req.params, + ...this.parseParams(req.url.params) + }; - Object.assign(req, { - params, - defaultParams + if (req.method !== 'OPTIONS') { + params = this.params.validate(params); + } + + Object.assign(req, { + params, + defaultParams + }); + + if (this.type === 'member' && req.method === 'PATCH') { + validateResourceId(req); + } + }, err => { + error = err; }); - if (this.type === 'member' && req.method === 'PATCH') { - validateResourceId(req); + if (error) { + return Promise.reject(error); } return this.execHandlers(req, res); diff --git a/src/packages/router/route/interfaces.js b/src/packages/router/route/interfaces.js index 5d48193e..a1d79dbd 100644 --- a/src/packages/router/route/interfaces.js +++ b/src/packages/router/route/interfaces.js @@ -12,5 +12,5 @@ export type Route$opts = { path: string; action: string; method: Request$method; - controller: Controller; + controller: Controller<*>; }; diff --git a/src/packages/router/route/params/index.js b/src/packages/router/route/params/index.js index f384ba3a..9914046e 100644 --- a/src/packages/router/route/params/index.js +++ b/src/packages/router/route/params/index.js @@ -69,7 +69,7 @@ export function defaultParamsFor({ controller }: { type: string; - controller: Controller + controller: Controller<*> }): Object { const { hasModel } = controller; diff --git a/src/packages/router/route/params/interfaces.js b/src/packages/router/route/params/interfaces.js index 49e5832e..7d3aaabd 100644 --- a/src/packages/router/route/params/interfaces.js +++ b/src/packages/router/route/params/interfaces.js @@ -7,7 +7,7 @@ import type { Lux$Collection } from '../../../../interfaces'; export type Params$opts = { type: Route$type; method: Request$method; - controller: Controller; + controller: Controller<*>; dynamicSegments: Array; }; diff --git a/src/packages/router/route/params/parameter-group/index.js b/src/packages/router/route/params/parameter-group/index.js index 3fa04ed2..1f971ad0 100644 --- a/src/packages/router/route/params/parameter-group/index.js +++ b/src/packages/router/route/params/parameter-group/index.js @@ -56,7 +56,7 @@ class ParameterGroup extends FreezeableMap { const match = this.get(key); if (match) { - Reflect.set(validated, key, match.validate(value)); + validated[key] = match.validate(value); } else if (!match && !sanitize) { throw new InvalidParameterError(`${path}${key}`); } diff --git a/src/packages/router/route/params/parameter-group/utils/has-required-params.js b/src/packages/router/route/params/parameter-group/utils/has-required-params.js index 38084c07..f315d5ed 100644 --- a/src/packages/router/route/params/parameter-group/utils/has-required-params.js +++ b/src/packages/router/route/params/parameter-group/utils/has-required-params.js @@ -10,7 +10,7 @@ export default function hasRequiredParams( params: Object ): boolean { for (const [key, { path, required }] of group) { - if (required && !Reflect.has(params, key)) { + if (required && !(key in params)) { throw new ParameterRequiredError(path); } } diff --git a/src/packages/router/route/params/test/params.test.js b/src/packages/router/route/params/test/params.test.js index 5b0ede3a..c28743c2 100644 --- a/src/packages/router/route/params/test/params.test.js +++ b/src/packages/router/route/params/test/params.test.js @@ -4,7 +4,6 @@ import { it, describe, before } from 'mocha'; import { paramsFor, defaultParamsFor } from '../index'; -import setType from '../../../../../utils/set-type'; import { getTestApp } from '../../../../../../test/utils/get-test-app'; import type Controller from '../../../../controller'; @@ -16,9 +15,8 @@ describe('module "router/route/params"', () => { before(async () => { const { controllers } = await getTestApp(); - getController = (name: string): Controller => setType(() => { - return controllers.get(name); - }); + // $FlowIgnore + getController = (name: string): Controller => controllers.get(name); }); describe('with model-less controller', () => { @@ -45,9 +43,8 @@ describe('module "router/route/params"', () => { before(async () => { const { controllers } = await getTestApp(); - getController = (name: string): Controller => setType(() => { - return controllers.get(name); - }); + // $FlowIgnore + getController = (name: string): Controller => controllers.get(name); }); describe('with collection route', () => { diff --git a/src/packages/router/route/params/utils/get-data-params.js b/src/packages/router/route/params/utils/get-data-params.js index 797a41bc..2a7bf69a 100644 --- a/src/packages/router/route/params/utils/get-data-params.js +++ b/src/packages/router/route/params/utils/get-data-params.js @@ -6,15 +6,20 @@ import { typeForColumn } from '../../../../database'; import type Controller from '../../../../controller'; import type { ParameterLike } from '../interfaces'; +type ParamTuple = [string, ParameterLike]; + /** * @private */ -function getIDParam({ model }: Controller): [string, ParameterLike] { - const primaryKeyColumn = model.columnFor(model.primaryKey); +function getIDParam({ model }: Controller<*>): ParamTuple { let primaryKeyType = 'number'; - if (primaryKeyColumn) { - primaryKeyType = typeForColumn(primaryKeyColumn); + if (model) { + const primaryKeyColumn = model.columnFor(model.primaryKey); + + if (primaryKeyColumn) { + primaryKeyType = typeForColumn(primaryKeyColumn); + } } return ['id', new Parameter({ @@ -27,13 +32,19 @@ function getIDParam({ model }: Controller): [string, ParameterLike] { /** * @private */ -function getTypeParam({ - model -}: Controller): [string, ParameterLike] { +function getTypeParam(controller: Controller<*>): ParamTuple { + if (controller.model) { + return ['type', new Parameter({ + type: 'string', + path: 'data.type', + values: [controller.model.resourceName], + required: true + })]; + } + return ['type', new Parameter({ type: 'string', path: 'data.type', - values: [model.resourceName], required: true })]; } @@ -44,22 +55,33 @@ function getTypeParam({ function getAttributesParam({ model, params -}: Controller): [string, ParameterLike] { +}: Controller<*>): ParamTuple { return ['attributes', new ParameterGroup(params.reduce((group, param) => { - const col = model.columnFor(param); + const path = `data.attributes.${param}`; - if (col) { - const type = typeForColumn(col); - const path = `data.attributes.${param}`; - const required = !col.nullable && isNull(col.defaultValue); + if (model) { + const col = model.columnFor(param); - return [ - ...group, - [param, new Parameter({ type, path, required })] - ]; + if (col) { + const type = typeForColumn(col); + const required = !col.nullable && isNull(col.defaultValue); + + return [ + ...group, + [param, new Parameter({ type, path, required })] + ]; + } + + return group; } - return group; + return [ + ...group, + [param, new Parameter({ + path, + required: false + })] + ]; }, []), { path: 'data.attributes', sanitize: true @@ -69,68 +91,76 @@ function getAttributesParam({ /** * @private */ -function getRelationshipsParam({ - model, - params -}: Controller): [string, ParameterLike] { - return ['relationships', new ParameterGroup(params.reduce((group, param) => { - const path = `data.relationships.${param}`; - const opts = model.relationshipFor(param); - - if (!opts) { - return group; - } - - if (opts.type === 'hasMany') { - return [ - ...group, - - [param, new ParameterGroup([ - ['data', new Parameter({ - type: 'array', - path: `${path}.data`, - required: true - })] - ], { - path - })] - ]; - } - - const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey); - let primaryKeyType = 'number'; - - if (primaryKeyColumn) { - primaryKeyType = typeForColumn(primaryKeyColumn); - } +function getRelationshipsParam(controller: Controller<*>): ParamTuple { + if (controller.model) { + const { model, params } = controller; return [ - ...group, - - [param, new ParameterGroup([ - ['data', new ParameterGroup([ - ['id', new Parameter({ - type: primaryKeyType, - path: `${path}.data.id`, - required: true - })], - - ['type', new Parameter({ - type: 'string', - path: `${path}.data.type`, - values: [opts.model.resourceName], - required: true + 'relationships', + new ParameterGroup(params.reduce((group, param) => { + const path = `data.relationships.${param}`; + const opts = model.relationshipFor(param); + + if (!opts) { + return group; + } + + if (opts.type === 'hasMany') { + return [ + ...group, + + [param, new ParameterGroup([ + ['data', new Parameter({ + type: 'array', + path: `${path}.data`, + required: true + })] + ], { + path + })] + ]; + } + + const primaryKeyColumn = opts.model.columnFor(opts.model.primaryKey); + let primaryKeyType = 'number'; + + if (primaryKeyColumn) { + primaryKeyType = typeForColumn(primaryKeyColumn); + } + + return [ + ...group, + + [param, new ParameterGroup([ + ['data', new ParameterGroup([ + ['id', new Parameter({ + type: primaryKeyType, + path: `${path}.data.id`, + required: true + })], + + ['type', new Parameter({ + type: 'string', + path: `${path}.data.type`, + values: [opts.model.resourceName], + required: true + })] + ], { + type: 'array', + path: `${path}.data`, + required: true + })] + ], { + path })] - ], { - type: 'array', - path: `${path}.data`, - required: true - })] - ], { - path - })] + ]; + }, []), { + path: 'data.relationships' + }) ]; - }, []), { + } + + return ['relationships', new ParameterGroup([], { path: 'data.relationships' })]; } @@ -139,9 +169,9 @@ function getRelationshipsParam({ * @private */ export default function getDataParams( - controller: Controller, + controller: Controller<*>, includeID: boolean -): [string, ParameterLike] { +): ParamTuple { let params = [getTypeParam(controller)]; if (controller.hasModel) { diff --git a/src/packages/router/route/params/utils/get-default-collection-params.js b/src/packages/router/route/params/utils/get-default-collection-params.js index b6fa6ab4..376013b0 100644 --- a/src/packages/router/route/params/utils/get-default-collection-params.js +++ b/src/packages/router/route/params/utils/get-default-collection-params.js @@ -4,36 +4,52 @@ import type Controller from '../../../../controller'; /** * @private */ -export default function getDefaultCollectionParams({ - model, - defaultPerPage, - serializer: { - hasOne, - hasMany, - attributes +function getDefaultCollectionParams(controller: Controller<*>): Object { + if (controller.model) { + const { + model, + defaultPerPage, + serializer: { + hasOne, + hasMany, + attributes + } + } = controller; + + return { + sort: 'createdAt', + filter: {}, + fields: { + [model.resourceName]: attributes, + ...[...hasOne, ...hasMany].reduce((include, key) => { + const opts = model.relationshipFor(key); + + if (!opts) { + return include; + } + + return { + ...include, + [opts.model.resourceName]: [opts.model.primaryKey] + }; + }, {}) + }, + page: { + size: defaultPerPage, + number: 1 + } + }; } -}: Controller): Object { + return { sort: 'createdAt', filter: {}, - fields: { - [model.resourceName]: attributes, - ...[...hasOne, ...hasMany].reduce((include, key) => { - const opts = model.relationshipFor(key); - - if (!opts) { - return include; - } - - return { - ...include, - [opts.model.resourceName]: [opts.model.primaryKey] - }; - }, {}) - }, + fields: {}, page: { - size: defaultPerPage, + size: 25, number: 1 } }; } + +export default getDefaultCollectionParams; diff --git a/src/packages/router/route/params/utils/get-default-member-params.js b/src/packages/router/route/params/utils/get-default-member-params.js index af493d4a..46f381d7 100644 --- a/src/packages/router/route/params/utils/get-default-member-params.js +++ b/src/packages/router/route/params/utils/get-default-member-params.js @@ -4,29 +4,39 @@ import type Controller from '../../../../controller'; /** * @private */ -export default function getDefaultMemberParams({ - model, - serializer: { - hasOne, - hasMany, - attributes - } -}: Controller): Object { - return { - fields: { - [model.resourceName]: attributes, - ...[...hasOne, ...hasMany].reduce((include, key) => { - const opts = model.relationshipFor(key); +function getDefaultMemberParams(controller: Controller<*>): Object { + if (controller.model) { + const { + model, + serializer: { + hasOne, + hasMany, + attributes + } + } = controller; - if (!opts) { - return include; - } + return { + fields: { + [model.resourceName]: attributes, + ...[...hasOne, ...hasMany].reduce((include, key) => { + const opts = model.relationshipFor(key); - return { - ...include, - [opts.model.resourceName]: [opts.model.primaryKey] - }; - }, {}) - } + if (!opts) { + return include; + } + + return { + ...include, + [opts.model.resourceName]: [opts.model.primaryKey] + }; + }, {}) + } + }; + } + + return { + fields: {} }; } + +export default getDefaultMemberParams; diff --git a/src/packages/router/route/params/utils/get-query-params.js b/src/packages/router/route/params/utils/get-query-params.js index 5f97bbb1..be0d8020 100644 --- a/src/packages/router/route/params/utils/get-query-params.js +++ b/src/packages/router/route/params/utils/get-query-params.js @@ -21,7 +21,7 @@ function getPageParam(): [string, ParameterLike] { */ function getSortParam({ sort -}: Controller): [string, ParameterLike] { +}: Controller<*>): [string, ParameterLike] { return ['sort', new Parameter({ path: 'sort', type: 'string', @@ -38,7 +38,7 @@ function getSortParam({ */ function getFilterParam({ filter -}: Controller): [string, ParameterLike] { +}: Controller<*>): [string, ParameterLike] { return ['filter', new ParameterGroup(filter.map(param => [ param, new Parameter({ @@ -52,48 +52,56 @@ function getFilterParam({ /** * @private */ -function getFieldsParam({ - model, - serializer: { - hasOne, - hasMany, - attributes - } -}: Controller): [string, ParameterLike] { - const relationships = [...hasOne, ...hasMany]; - - return ['fields', new ParameterGroup([ - [model.resourceName, new Parameter({ - path: `fields.${model.resourceName}`, - type: 'array', - values: attributes, - sanitize: true - })], - ...relationships.reduce((result, relationship) => { - const opts = model.relationshipFor(relationship); - - if (opts) { - return [ - ...result, - - [opts.model.resourceName, new Parameter({ - path: `fields.${opts.model.resourceName}`, - type: 'array', - sanitize: true, - - values: [ - opts.model.primaryKey, - ...opts.model.serializer.attributes - ] - })] - ]; +function getFieldsParam(controller: Controller<*>): [string, ParameterLike] { + if (controller.model) { + const { + model, + serializer: { + hasOne, + hasMany, + attributes } + } = controller; + + const relationships = [...hasOne, ...hasMany]; + + return ['fields', new ParameterGroup([ + [model.resourceName, new Parameter({ + path: `fields.${model.resourceName}`, + type: 'array', + values: attributes, + sanitize: true + })], + ...relationships.reduce((result, relationship) => { + const opts = model.relationshipFor(relationship); + + if (opts) { + return [ + ...result, + + [opts.model.resourceName, new Parameter({ + path: `fields.${opts.model.resourceName}`, + type: 'array', + sanitize: true, + + values: [ + opts.model.primaryKey, + ...opts.model.serializer.attributes + ] + })] + ]; + } + + return result; + }, []) + ], { + path: 'fields', + sanitize: true + })]; + } - return result; - }, []) - ], { - path: 'fields', - sanitize: true + return ['fields', new ParameterGroup([], { + path: 'fields' })]; } @@ -105,7 +113,7 @@ function getIncludeParam({ hasOne, hasMany } -}: Controller): [string, ParameterLike] { +}: Controller<*>): [string, ParameterLike] { const relationships = [...hasOne, ...hasMany]; return ['include', new Parameter({ @@ -120,7 +128,7 @@ function getIncludeParam({ */ export function getCustomParams({ query -}: Controller): Array<[string, ParameterLike]> { +}: Controller<*>): Array<[string, ParameterLike]> { return query.map(param => [param, new Parameter({ path: param })]); @@ -130,7 +138,7 @@ export function getCustomParams({ * @private */ export function getMemberQueryParams( - controller: Controller + controller: Controller<*> ): Array<[string, ParameterLike]> { if (controller.hasModel) { return [ @@ -147,7 +155,7 @@ export function getMemberQueryParams( * @private */ export function getCollectionQueryParams( - controller: Controller + controller: Controller<*> ): Array<[string, ParameterLike]> { if (controller.hasModel) { return [ diff --git a/src/packages/router/route/test/route.test.js b/src/packages/router/route/test/route.test.js index 2b8169d4..b2bae1a5 100644 --- a/src/packages/router/route/test/route.test.js +++ b/src/packages/router/route/test/route.test.js @@ -16,13 +16,17 @@ import Route from '../index'; describe('module "router/route"', () => { describe('class Route', () => { describe('#constructor()', () => { - let controller: Controller; + let controller: Controller<*>; before(async () => { const { controllers } = await getTestApp(); + const postsController = controllers.get('posts'); - // $FlowIgnore - controller = controllers.get('posts'); + if (!postsController) { + throw new Error('Could not find controller "posts".'); + } + + controller = postsController; }); it('creates an instance of route', () => { @@ -327,7 +331,7 @@ describe('module "router/route"', () => { }); describe('#visit', () => { - let controller: Controller; + let controller: Controller<*>; before(async () => { const { controllers } = await getTestApp(); diff --git a/src/packages/router/test/namespace.test.js b/src/packages/router/test/namespace.test.js index fbcd91bc..7e4bf8ef 100644 --- a/src/packages/router/test/namespace.test.js +++ b/src/packages/router/test/namespace.test.js @@ -3,17 +3,14 @@ import { expect } from 'chai'; import { it, describe, before, beforeEach } from 'mocha'; import Namespace from '../namespace'; - -import setType from '../../../utils/set-type'; import { getTestApp } from '../../../../test/utils/get-test-app'; - import type Controller from '../../controller'; describe('module "router/namespace"', () => { describe('class Namespace', () => { describe('#constructor()', () => { let root; - let controller: Controller; + let controller: Controller<*>; let controllers; let createRootNamespace; const expectNamspaceToBeValid = (subject: Namespace, name, path) => { @@ -27,20 +24,25 @@ describe('module "router/namespace"', () => { before(async () => { const app = await getTestApp(); + const rootController = app.controllers.get('application'); + const namespaceController = controllers.get('admin/application'); - controllers = app.controllers; + if (!rootController) { + throw new Error('Could not find controller "application".'); + } - controller = setType(() => { - return controllers.get('admin/application'); - }); + if (!namespaceController) { + throw new Error('Could not find controller "admin/application".'); + } + + controllers = app.controllers; + controller = namespaceController; createRootNamespace = (): Namespace => new Namespace({ controllers, path: '/', name: 'root', - controller: setType(() => { - return app.controllers.get('application'); - }) + controller: rootController }); }); diff --git a/src/packages/router/test/router.test.js b/src/packages/router/test/router.test.js index 53ba59ec..31f806e0 100644 --- a/src/packages/router/test/router.test.js +++ b/src/packages/router/test/router.test.js @@ -14,7 +14,7 @@ const CONTROLLER_MISSING_MESSAGE = /Could not resolve controller by name '.+'/; describe('module "router"', () => { describe('class Router', () => { - let controller: Controller; + let controller: Controller<*>; let controllers; before(async () => { diff --git a/src/packages/router/utils/create-replacer.js b/src/packages/router/utils/create-replacer.js index 4640d584..818f2a7f 100644 --- a/src/packages/router/utils/create-replacer.js +++ b/src/packages/router/utils/create-replacer.js @@ -5,7 +5,7 @@ import type Controller from '../../controller'; * @private */ export default function createReplacer( - controllers: Map + controllers: Map> ): RegExp { const names = Array .from(controllers) diff --git a/src/packages/serializer/index.js b/src/packages/serializer/index.js index 245f44eb..4c44e3c1 100644 --- a/src/packages/serializer/index.js +++ b/src/packages/serializer/index.js @@ -5,9 +5,8 @@ import { VERSION } from '../jsonapi'; import { freezeProps } from '../freezeable'; import uniq from '../../utils/uniq'; import underscore from '../../utils/underscore'; -import promiseHash from '../../utils/promise-hash'; import { dasherizeKeys } from '../../utils/transform-keys'; -import type { Model } from '../database'; // eslint-disable-line no-unused-vars +import { Model } from '../database'; // eslint-disable-line no-unused-vars import type { // eslint-disable-line no-duplicate-imports JSONAPI$Document, JSONAPI$DocumentLinks, @@ -15,7 +14,11 @@ import type { // eslint-disable-line no-duplicate-imports JSONAPI$RelationshipObject } from '../jsonapi'; -import type { Serializer$opts } from './interfaces'; +export type Options = { + model?: ?Class; + parent: ?Serializer<*>; + namespace: string; +}; /** * ## Overview @@ -402,7 +405,7 @@ class Serializer { * @type {Model} * @private */ - model: Class; + model: ?Class; /** * A reference to the root Serializer for the namespace that a Serializer @@ -423,7 +426,7 @@ class Serializer { */ namespace: string; - constructor({ model, parent, namespace }: Serializer$opts) { + constructor({ model, parent, namespace }: Options) { Object.assign(this, { model, parent, @@ -467,7 +470,7 @@ class Serializer { * * @private */ - async format({ + format({ data, links, domain, @@ -477,24 +480,22 @@ class Serializer { links: JSONAPI$DocumentLinks; domain: string; include: Array; - }): Promise { + }): JSONAPI$Document { let serialized = {}; const included: Array = []; if (Array.isArray(data)) { serialized = { - data: await Promise.all( - data.map(item => this.formatOne({ - item, - domain, - include, - included - })) - ) + data: data.map(item => this.formatOne({ + item, + domain, + include, + included + })) }; } else { serialized = { - data: await this.formatOne({ + data: this.formatOne({ domain, include, included, @@ -514,7 +515,6 @@ class Serializer { return { ...serialized, links, - jsonapi: { version: VERSION } @@ -553,12 +553,11 @@ class Serializer { * relationships should be formatted and included in the returned * [JSON API](http://jsonapi.org) resource object. * - * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) resource - * object. + * @return {Object} A [JSON API](http://jsonapi.org) resource object. * * @private */ - async formatOne({ + formatOne({ item, links, domain, @@ -572,17 +571,22 @@ class Serializer { include: Array; included: Array; formatRelationships?: boolean - }): Promise { + }): JSONAPI$ResourceObject { const { resourceName: type } = item; const id = String(item.getPrimaryKey()); - let relationships = {}; - const attributes = dasherizeKeys( - item.getAttributes( - ...Object - .keys(item.rawColumnData) - .filter(key => this.attributes.includes(key)) - ) + Array + .from(item.currentChangeSet) + .reduce((obj, [key, value]) => { + if (this.attributes.includes(key)) { + return { + ...obj, + [key]: value + }; + } + + return obj; + }, {}) ); const serialized: JSONAPI$ResourceObject = { @@ -591,46 +595,42 @@ class Serializer { attributes }; - if (formatRelationships) { - relationships = await promiseHash( - [...this.hasOne, ...this.hasMany].reduce((hash, name) => ({ - ...hash, - - [dasherize(underscore(name))]: (async () => { - const related = await Reflect.get(item, name); - - if (Array.isArray(related)) { - return { - data: await Promise.all( - related.map(async (relatedItem) => { - const { - data: relatedData - } = await this.formatRelationship({ - domain, - included, - item: relatedItem, - include: include.includes(name) - }); - - return relatedData; - }) - ) - }; - } else if (related && related.id) { - return this.formatRelationship({ - domain, - included, - item: related, - include: include.includes(name) - }); - } + let relationships = {}; - return { - data: null + if (formatRelationships) { + relationships = this.hasOne + .concat(this.hasMany) + .reduce((obj, name) => { + const related = item.currentChangeSet.get(name); + let data = { + data: null + }; + + if (Array.isArray(related)) { + data = { + data: related.map(relatedItem => ( + this.formatRelationship({ + domain, + included, + item: relatedItem, + include: include.includes(name) + }).data + )) }; - })() - }), {}) - ); + } else if (related && related.id) { + data = this.formatRelationship({ + domain, + included, + item: related, + include: include.includes(name) + }); + } + + return { + ...obj, + [dasherize(underscore(name))]: data + }; + }, {}); } if (Object.keys(relationships).length) { @@ -677,12 +677,11 @@ class Serializer { * http://jsonapi.org) resource objects that will be added to the top level * included array of a [JSON API](http://jsonapi.org) document object. * - * @return {Promise} Resolves with a [JSON API](http://jsonapi.org) - * relationship object. + * @return {Object} A [JSON API](http://jsonapi.org) relationship object. * * @private */ - async formatRelationship({ + formatRelationship({ item, domain, include, @@ -692,7 +691,7 @@ class Serializer { domain: string; include: boolean; included: Array; - }): Promise { + }): JSONAPI$RelationshipObject { const { namespace } = this; const { resourceName: type, constructor: { serializer } } = item; const id = String(item.getPrimaryKey()); @@ -710,7 +709,7 @@ class Serializer { if (include) { included.push( - await serializer.formatOne({ + serializer.formatOne({ item, domain, include: [], diff --git a/src/packages/serializer/test/serializer.test.js b/src/packages/serializer/test/serializer.test.js index acd9417a..549692ae 100644 --- a/src/packages/serializer/test/serializer.test.js +++ b/src/packages/serializer/test/serializer.test.js @@ -91,11 +91,14 @@ describe('module "serializer"', () => { } = {}, transaction) => { let include = []; const run = async trx => { - const post = await Post.transacting(trx).create({ - body: faker.lorem.paragraphs(), - title: faker.lorem.sentence(), - isPublic: faker.random.boolean() - }); + const post = await Post + .transacting(trx) + .create({ + body: faker.lorem.paragraphs(), + title: faker.lorem.sentence(), + isPublic: faker.random.boolean() + }) + .then(record => record.unwrap()); const postId = post.getPrimaryKey(); @@ -110,7 +113,8 @@ describe('module "serializer"', () => { instances.add(user); include = [...include, 'user']; - Reflect.set(post, 'user', user); + // $FlowIgnore + post.user = user; } if (includeImage) { @@ -232,10 +236,14 @@ describe('module "serializer"', () => { image, comments ] = await Promise.all([ - Reflect.get(post, 'user'), - Reflect.get(post, 'tags'), - Reflect.get(post, 'image'), - Reflect.get(post, 'comments') + // $FlowIgnore + post.user, + // $FlowIgnore + post.tags, + // $FlowIgnore + post.image, + // $FlowIgnore + post.comments ]); const postId = post.getPrimaryKey(); @@ -329,7 +337,7 @@ describe('module "serializer"', () => { it('works with a single instance of `Model`', async () => { const post = await createPost(); - const result = await subject.format({ + const result = subject.format({ data: post, domain: DOMAIN, include: [], @@ -369,7 +377,7 @@ describe('module "serializer"', () => { .map(post => post.getPrimaryKey()) .map(String); - const result = await subject.format({ + const result = subject.format({ data: posts, domain: DOMAIN, include: [], @@ -403,7 +411,7 @@ describe('module "serializer"', () => { subject = createSerializer('admin'); const post = await createPost(); - const result = await subject.format({ + const result = subject.format({ data: post, domain: DOMAIN, include: [], @@ -437,7 +445,7 @@ describe('module "serializer"', () => { includeComments: true }); - const result = await subject.format({ + const result = subject.format({ data: post, domain: DOMAIN, include: [], @@ -465,8 +473,9 @@ describe('module "serializer"', () => { it('supports including a has-one relationship', async () => { const post = await createPost(); - const image = await Reflect.get(post, 'image'); - const result = await subject.format({ + // $FlowIgnore + const image = await post.image; + const result = subject.format({ data: post, domain: DOMAIN, include: ['image'], @@ -496,8 +505,9 @@ describe('module "serializer"', () => { it('supports including belongs-to relationships', async () => { const post = await createPost(); - const user = await Reflect.get(post, 'user'); - const result = await subject.format({ + // $FlowIgnore + const user = await post.user; + const result = subject.format({ data: post, domain: DOMAIN, include: ['user'], @@ -528,8 +538,9 @@ describe('module "serializer"', () => { it('supports including a one-to-many relationship', async () => { const post = await createPost(); - const comments = await Reflect.get(post, 'comments'); - const result = await subject.format({ + // $FlowIgnore + const comments = await post.comments; + const result = subject.format({ data: post, domain: DOMAIN, include: ['comments'], @@ -567,8 +578,9 @@ describe('module "serializer"', () => { it('supports including a many-to-many relationship', async () => { const post = await createPost(); - const tags = await Reflect.get(post, 'tags'); - const result = await subject.format({ + // $FlowIgnore + const tags = await post.tags; + const result = subject.format({ data: post, domain: DOMAIN, include: ['tags'], diff --git a/src/packages/server/constants.js b/src/packages/server/constants.js index a1e8d988..8a7c6f73 100644 --- a/src/packages/server/constants.js +++ b/src/packages/server/constants.js @@ -1,6 +1,5 @@ // @flow -export const HAS_BODY = /^(POST|PATCH)$/; - +export const HAS_BODY = /^(?:POST|PATCH)$/; export const STATUS_CODES = new Map([ [100, 'Continue'], [101, 'Switching Protocols'], diff --git a/src/packages/server/index.js b/src/packages/server/index.js index 7b1b2d06..5a4d933e 100644 --- a/src/packages/server/index.js +++ b/src/packages/server/index.js @@ -3,6 +3,7 @@ import { createServer } from 'http'; import type { Writable } from 'stream'; import type { IncomingMessage, Server as HTTPServer } from 'http'; // eslint-disable-line max-len, no-duplicate-imports +import { freezeProps } from '../freezeable'; import { tryCatchSync } from '../../utils/try-catch'; import type Logger from '../logger'; import type Router from '../router'; @@ -30,36 +31,22 @@ class Server { instance: HTTPServer; - constructor({ logger, router, cors }: Server$opts) { - Object.defineProperties(this, { - router: { - value: router, - writable: false, - enumerable: false, - configurable: false - }, - - logger: { - value: logger, - writable: false, - enumerable: false, - configurable: false - }, - - cors: { - value: cors, - writable: false, - enumerable: false, - configurable: false - }, - - instance: { - value: createServer(this.receiveRequest), - writable: false, - enumerable: false, - configurable: false - } + constructor({ logger, router, cors }: Server$opts): this { + Object.assign(this, { + cors, + router, + logger, + instance: createServer(this.receiveRequest) }); + + freezeProps(this, false, + 'cors', + 'router', + 'logger', + 'instance' + ); + + return this; } listen(port: number): void { @@ -67,22 +54,21 @@ class Server { } initializeRequest(req: IncomingMessage, res: Writable): [Request, Response] { - const { logger, router, cors } = this; - - req.setEncoding('utf8'); - const response = createResponse(res, { - logger + logger: this.logger }); - setCORSHeaders(response, cors); + setCORSHeaders(response, this.cors); const request = createRequest(req, { - logger, - router + logger: this.logger, + router: this.router }); - return [request, response]; + return [ + request, + response + ]; } validateRequest({ method, headers }: Request): true { @@ -96,11 +82,10 @@ class Server { } receiveRequest = (req: IncomingMessage, res: Writable): void => { - const { logger } = this; const [request, response] = this.initializeRequest(req, res); const respond = createResponder(request, response); - logger.request(request, response, { + this.logger.request(request, response, { startTime: Date.now() }); @@ -109,21 +94,19 @@ class Server { if (isValid) { parseRequest(request) .then(params => { - const { route } = request; - Object.assign(request, { params }); - if (route) { - return route.visit(request, response); + if (request.route) { + return request.route.visit(request, response); } return undefined; }) .then(respond) .catch(err => { - logger.error(err); + this.logger.error(err); respond(err); }); } diff --git a/src/packages/server/request/interfaces.js b/src/packages/server/request/interfaces.js index 85b7190d..99ec0189 100644 --- a/src/packages/server/request/interfaces.js +++ b/src/packages/server/request/interfaces.js @@ -63,7 +63,7 @@ declare export class Request extends stream$Readable { defaultParams: Request$params; route: Route; action: string; - controller: Controller; + controller: Controller<*>; url: Request$url; connection: { diff --git a/src/packages/server/request/parser/index.js b/src/packages/server/request/parser/index.js index 95c3032c..bb23def3 100644 --- a/src/packages/server/request/parser/index.js +++ b/src/packages/server/request/parser/index.js @@ -11,10 +11,9 @@ export function parseRequest(req: Request): Promise { switch (req.method) { case 'POST': case 'PATCH': - return parseWrite(req).then(params => ({ - ...parseRead(req), - ...params - })); + return parseWrite(req).then(params => ( + Object.assign(params, parseRead(req)) + )); default: return Promise.resolve(parseRead(req)); diff --git a/src/packages/server/request/parser/utils/format.js b/src/packages/server/request/parser/utils/format.js index 3cfc811b..012694b8 100644 --- a/src/packages/server/request/parser/utils/format.js +++ b/src/packages/server/request/parser/utils/format.js @@ -78,10 +78,12 @@ export function formatSort(sort: string): string { * @private */ export function formatFields(fields: Object): Object { - return entries(fields).reduce((result, [key, value]) => ({ - ...result, - [key]: makeArray(value) - }), {}); + return entries(fields).reduce((obj, [key, value]) => { + const result = obj; + + result[key] = makeArray(value); + return result; + }, {}); } /** @@ -96,30 +98,29 @@ export function formatInclude(include: string | Array): Array { */ export default function format(params: Object, method: Request$method): Object { const result = entries(params).reduce((obj, param) => { + const data = obj; const [, value] = param; let [key] = param; + let formatted; key = key.replace(BRACKETS, ''); switch (typeof value) { case 'object': - return { - ...obj, - [key]: isNull(value) ? null : formatObject(value, method, format) - }; + formatted = isNull(value) ? null : formatObject(value, method, format); + break; case 'string': - return { - ...obj, - [key]: formatString(value, key === 'id' ? 'GET' : method) - }; + formatted = formatString(value, key === 'id' ? 'GET' : method); + break; default: - return { - ...obj, - [key]: value - }; + formatted = value; + break; } + + data[key] = formatted; + return data; }, {}); return camelizeKeys(result, true); diff --git a/src/packages/server/request/parser/utils/parse-nested-object.js b/src/packages/server/request/parser/utils/parse-nested-object.js index 8346cce0..8bdba7f8 100644 --- a/src/packages/server/request/parser/utils/parse-nested-object.js +++ b/src/packages/server/request/parser/utils/parse-nested-object.js @@ -7,23 +7,23 @@ const DELIMITER = /^(.+)\[(.+)]$/g; * @private */ export default function parseNestedObject(source: Object): Object { - return entries(source).reduce((result, [key, value]) => { + return entries(source).reduce((obj, [key, value]) => { + const result = obj; + if (DELIMITER.test(key)) { const parentKey = key.replace(DELIMITER, '$1'); - const parentValue = Reflect.get(result, parentKey); + let parentValue = result[parentKey]; + + if (!parentValue) { + parentValue = {}; + result[parentKey] = parentValue; + } - return { - ...result, - [parentKey]: { - ...(parentValue || {}), - [key.replace(DELIMITER, '$2')]: value - } - }; + parentValue[key.replace(DELIMITER, '$2')] = value; + } else { + result[key] = value; } - return { - ...result, - [key]: value - }; + return result; }, {}); } diff --git a/src/packages/server/request/parser/utils/parse-write.js b/src/packages/server/request/parser/utils/parse-write.js index 42c76308..ba8770e4 100644 --- a/src/packages/server/request/parser/utils/parse-write.js +++ b/src/packages/server/request/parser/utils/parse-write.js @@ -5,37 +5,56 @@ import type { Request } from '../../interfaces'; import format from './format'; +function getLength(req: Request): number { + const contentLength = req.headers.get('content-length'); + + if (contentLength) { + const length = Number.parseInt(contentLength, 10); + + if (Number.isFinite(length)) { + return length; + } + } + + return 0; +} + /** * @private */ export default function parseWrite(req: Request): Promise { return new Promise((resolve, reject) => { - let body = ''; - const cleanUp = () => { - req.removeAllListeners('end'); - req.removeAllListeners('data'); - req.removeAllListeners('error'); - }; + const body = Buffer.allocUnsafe(getLength(req)); + let offset = 0; - req.on('data', data => { - body += data.toString(); - }); + const handleData = data => { + data.copy(body, offset); + offset = offset + data.length; + }; - req.once('end', () => { - const parsed = tryCatchSync(() => JSON.parse(body)); + const handleEnd = () => { + const parsed = tryCatchSync(() => JSON.parse(body.toString())); - cleanUp(); + // eslint-disable-next-line no-use-before-define + req.removeListener('error', handleError); + req.removeListener('data', handleData); + req.removeListener('end', handleEnd); if (parsed) { resolve(format(parsed, req.method)); } else { reject(new MalformedRequestError()); } - }); + }; + + const handleError = () => { + req.removeListener('error', handleError); + req.removeListener('data', handleData); + req.removeListener('end', handleEnd); + }; - req.once('error', err => { - cleanUp(); - reject(err); - }); + req.on('data', handleData); + req.once('end', handleEnd); + req.once('error', handleError); }); } diff --git a/src/packages/server/responder/index.js b/src/packages/server/responder/index.js index c03bc8e5..8a6ed6d6 100644 --- a/src/packages/server/responder/index.js +++ b/src/packages/server/responder/index.js @@ -9,17 +9,17 @@ import hasContentType from './utils/has-content-type'; * @private */ export function createResponder(req: Request, res: Response) { - return function respond(data?: ?mixed) { - const normalized = normalize(data); + return function respond(input: any) { + const { data, statusCode } = normalize(input); - if (normalized.statusCode) { - Reflect.set(res, 'statusCode', normalized.statusCode); + if (statusCode) { + res.status(statusCode); } - if (res.statusCode !== 204 && !hasContentType(res)) { + if (statusCode !== 204 && !hasContentType(res)) { res.setHeader('Content-Type', MIME_TYPE); } - res.end(normalized.data); + res.end(data); }; } diff --git a/src/packages/server/responder/utils/has-content-type.js b/src/packages/server/responder/utils/has-content-type.js index 044f7cc0..45e25186 100644 --- a/src/packages/server/responder/utils/has-content-type.js +++ b/src/packages/server/responder/utils/has-content-type.js @@ -1,12 +1,6 @@ // @flow import type { Response } from '../../index'; -export default function hasContentType(res: Response) { - let contentType = res.getHeader('Content-Type'); - - if (!contentType) { - contentType = res.getHeader('content-type'); - } - - return Boolean(contentType); +export default function hasContentType(res: Response): boolean { + return Boolean(res.getHeader('content-type')); } diff --git a/src/packages/server/response/index.js b/src/packages/server/response/index.js index 057e9fc7..ac0d233a 100644 --- a/src/packages/server/response/index.js +++ b/src/packages/server/response/index.js @@ -13,7 +13,14 @@ import type { Response, Response$opts } from './interfaces'; * @private */ export function createResponse(res: any, opts: Response$opts): Response { - return Object.assign(res, opts, { - stats: [] + const response = res; + + return Object.assign(response, opts, { + stats: [], + + status(value: number): Response { + response.statusCode = value; + return response; + } }); } diff --git a/src/packages/server/response/interfaces.js b/src/packages/server/response/interfaces.js index 36eb3109..a073a170 100644 --- a/src/packages/server/response/interfaces.js +++ b/src/packages/server/response/interfaces.js @@ -12,7 +12,7 @@ export type Response$opts = { logger: Logger }; -declare export class Response extends stream$Writable { +export interface Response extends stream$Writable { [key: string]: void | ?mixed; stats: Array; @@ -20,6 +20,7 @@ declare export class Response extends stream$Writable { statusCode: number; statusMessage: string; + status(value: number): Response; getHeader(name: string): void | string; setHeader(name: string, value: string): void; removeHeader(name: string): void; diff --git a/src/packages/server/utils/create-server-error.js b/src/packages/server/utils/create-server-error.js index dc5c96c4..b757c068 100644 --- a/src/packages/server/utils/create-server-error.js +++ b/src/packages/server/utils/create-server-error.js @@ -1,5 +1,4 @@ // @flow -import setType from '../../../utils/set-type'; import type { Server$Error } from '../interfaces'; /** @@ -9,20 +8,19 @@ export default function createServerError( Target: Class, statusCode: number ): Class & Class { - return setType(() => { - const ServerError = class extends Target { - statusCode: number; + const ServerError = class extends Target { + statusCode: number; - constructor(...args: Array) { - super(...args); - this.statusCode = statusCode; - } - }; + constructor(...args: Array) { + super(...args); + this.statusCode = statusCode; + } + }; - Reflect.defineProperty(ServerError, 'name', { - value: Target.name - }); - - return ServerError; + Object.defineProperty(ServerError, 'name', { + value: Target.name }); + + // $FlowIgnore + return ServerError; } diff --git a/src/utils/chain.js b/src/utils/chain.js index ed344ab7..774bb387 100644 --- a/src/utils/chain.js +++ b/src/utils/chain.js @@ -15,7 +15,8 @@ export default function chain(source: T): Chain { }, construct>(constructor: V): Chain { - return chain(Reflect.construct(constructor, [source])); + // $FlowIgnore + return chain(new constructor(source)); } }; } diff --git a/src/utils/compact.js b/src/utils/compact.js index 7dfcd364..efbad15e 100644 --- a/src/utils/compact.js +++ b/src/utils/compact.js @@ -1,23 +1,24 @@ // @flow import isNull from './is-null'; import entries from './entries'; -import setType from './set-type'; import isUndefined from './is-undefined'; /** * @private */ export default function compact>(source: T): T { - return setType(() => { - if (Array.isArray(source)) { - return source.filter(value => !isNull(value) && !isUndefined(value)); - } + if (Array.isArray(source)) { + return source.filter(value => !isNull(value) && !isUndefined(value)); + } - return entries(source) - .filter(([, value]) => !isNull(value) && !isUndefined(value)) - .reduce((result, [key, value]) => ({ - ...result, - [key]: value - }), {}); - }); + // $FlowIgnore + return entries(source) + .filter(([, value]) => !isNull(value) && !isUndefined(value)) + .reduce((obj, [key, value]) => { + const result = obj; + + result[key] = value; + + return result; + }, {}); } diff --git a/src/utils/entries.js b/src/utils/entries.js index c62854ed..0c29c605 100644 --- a/src/utils/entries.js +++ b/src/utils/entries.js @@ -9,11 +9,18 @@ export default function entries(source: Object): Array<[string, any]> { return Object.entries(source); } - return Object.keys(source).reduce((result, key) => { - const value = Reflect.get(source, key); + const keys = Object.keys(source); + const result = new Array(keys.length); - result.push([key, value]); + return keys.reduce((prev, key, idx) => { + const next = prev; + const entry = new Array(2); - return result; - }, []); + entry[0] = key; + entry[1] = source[key]; + // $FlowIgnore + next[idx] = entry; + + return next; + }, result); } diff --git a/src/utils/has-own-property.js b/src/utils/has-own-property.js index 6444e678..7e229c42 100644 --- a/src/utils/has-own-property.js +++ b/src/utils/has-own-property.js @@ -1,8 +1,4 @@ // @flow export default function hasOwnProperty(target: Object, key: string): boolean { - return Reflect.apply( - Object.prototype.hasOwnProperty, - target, - [key] - ); + return Object.prototype.hasOwnProperty.call(target, key); } diff --git a/src/utils/map-to-object.js b/src/utils/map-to-object.js index 3e6ef431..ca609e6d 100644 --- a/src/utils/map-to-object.js +++ b/src/utils/map-to-object.js @@ -1,11 +1,14 @@ // @flow -export default function mapToObject( - source: Map -): { [key: string]: T } { +import type { ObjectMap } from '../interfaces'; + +export default function mapToObject(source: Map): ObjectMap { return Array .from(source) - .reduce((obj, [key, value]) => ({ - ...obj, - [String(key)]: value - }), {}); + .reduce((obj, [key, value]) => { + const result = obj; + + result[String(key)] = value; + + return result; + }, {}); } diff --git a/src/utils/merge.js b/src/utils/merge.js index 03efde8f..e2b8aa13 100644 --- a/src/utils/merge.js +++ b/src/utils/merge.js @@ -1,33 +1,27 @@ // @flow import entries from './entries'; -import setType from './set-type'; import isObject from './is-object'; - -function hasOwnProperty(target: Object, key: string): boolean { - return Reflect.apply(Object.prototype.hasOwnProperty, target, [key]); -} +import hasOwnProperty from './has-own-property'; /** * @private */ export default function merge(dest: T, source: U): T & U { - return setType(() => entries(source).reduce((result, [key, value]) => { + // $FlowIgnore + return entries(source).reduce((obj, [key, value]) => { + const result = obj; + let mergeValue = value; + if (hasOwnProperty(result, key) && isObject(value)) { - const currentValue = Reflect.get(result, key); + const currentValue = result[key]; if (isObject(currentValue)) { - return { - ...result, - [key]: merge(currentValue, value) - }; + mergeValue = merge(currentValue, value); } } - return { - ...result, - [key]: value - }; - }, { - ...dest - })); + result[key] = mergeValue; + + return result; + }, { ...dest }); } diff --git a/src/utils/omit.js b/src/utils/omit.js index b4ef6070..68a6b2ba 100644 --- a/src/utils/omit.js +++ b/src/utils/omit.js @@ -1,15 +1,18 @@ // @flow import entries from './entries'; -import setType from './set-type'; /** * @private */ export default function omit(src: T, ...omitted: Array): T { - return setType(() => entries(src) + // $FlowIgnore + return entries(src) .filter(([key]) => omitted.indexOf(key) < 0) - .reduce((result, [key, value]: [string, mixed]) => ({ - ...result, - [key]: value - }), {})); + .reduce((obj, [key, value]) => { + const result = obj; + + result[key] = value; + + return result; + }, {}); } diff --git a/src/utils/pick.js b/src/utils/pick.js index 580d80ec..d5a192ff 100644 --- a/src/utils/pick.js +++ b/src/utils/pick.js @@ -1,15 +1,18 @@ // @flow -import setType from './set-type'; /** * @private */ export default function pick(src: T, ...keys: Array): T { - return setType(() => keys - .map((key): [string, mixed] => [key, Reflect.get(src, key)]) + // $FlowIgnore + return keys + .map((key): [string, mixed] => [key, src[key]]) .filter(([, value]) => typeof value !== 'undefined') - .reduce((result, [key, value]) => ({ - ...result, - [key]: value - }), {})); + .reduce((obj, [key, value]) => { + const result = obj; + + result[key] = value; + + return result; + }, {}); } diff --git a/src/utils/promise-hash.js b/src/utils/promise-hash.js index 07aced4b..4c157777 100644 --- a/src/utils/promise-hash.js +++ b/src/utils/promise-hash.js @@ -5,24 +5,24 @@ import entries from './entries'; * @private */ export default function promiseHash(promises: Object): Promise { - if (Object.keys(promises).length) { + const iter = entries(promises); + + if (iter.length) { return Promise.all( - entries(promises) - .map(([key, promise]: [string, Promise]) => ( - new Promise((resolve, reject) => { - if (promise && typeof promise.then === 'function') { - promise - .then((value) => resolve({ [key]: value })) - .catch(reject); - } else { - resolve({ [key]: promise }); - } - }) - )) - ).then((objects) => objects.reduce((hash, object) => ({ - ...hash, - ...object - }), {})); + iter.map(([key, promise]: [string, Promise]) => ( + new Promise((resolve, reject) => { + if (promise && typeof promise.then === 'function') { + promise + .then((value) => resolve({ [key]: value })) + .catch(reject); + } else { + resolve({ [key]: promise }); + } + }) + )) + ).then(objects => ( + Object.assign({}, ...objects) + )); } return Promise.resolve({}); diff --git a/src/utils/proxy.js b/src/utils/proxy.js index 462aa3d3..3a075d53 100644 --- a/src/utils/proxy.js +++ b/src/utils/proxy.js @@ -13,7 +13,7 @@ export function trapGet(traps: Object): Proxy$get { } if (hasOwnProperty(traps, key)) { - const value = Reflect.get(traps, key); + const value = traps[key]; if (typeof value === 'function') { return value.bind(receiver, target); @@ -22,6 +22,7 @@ export function trapGet(traps: Object): Proxy$get { return value; } - return Reflect.get(target, key); + // $FlowIgnore + return target[key]; }; } diff --git a/src/utils/set-type.js b/src/utils/set-type.js deleted file mode 100644 index e67a6f5b..00000000 --- a/src/utils/set-type.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow - -/** - * Use this util as a brute force way of tricking flow into understanding intent - * to extend or combine a type in a polymorphic function. - * - * In essence, this function allows you to declare your types for a high order - * function that wraps the inner logic of this function without flow throwing - * any type errors. This allows you to properly set the return value of the - * high order function to whatever you like so consumers of the high order - * function can still benifit from type inference and safety as long as the - * return value type declaration is 100% accurate. - * - * WARNING: - * This function should rarely be used as it requires a good understanding of - * the flow type system to ensure that the function this util wraps is still - * type safe. - * - * @private - */ -export default function setType(fn: () => any): any { - return fn(); -} diff --git a/src/utils/test/k.test.js b/src/utils/test/k.test.js index a4e18955..8d14f7bd 100644 --- a/src/utils/test/k.test.js +++ b/src/utils/test/k.test.js @@ -8,8 +8,8 @@ describe('util K()', () => { it('always returns the context it is called with', () => { const obj = {}; - expect(Reflect.apply(K, 1, [])).to.equal(1); - expect(Reflect.apply(K, obj, [])).to.equal(obj); - expect(Reflect.apply(K, 'Test', [])).to.equal('Test'); + expect(K.call(1)).to.equal(1); + expect(K.call(obj)).to.equal(obj); + expect(K.call('Test')).to.equal('Test'); }); }); diff --git a/src/utils/test/set-type.test.js b/src/utils/test/set-type.test.js deleted file mode 100644 index c58cac83..00000000 --- a/src/utils/test/set-type.test.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import { expect } from 'chai'; -import { it, describe } from 'mocha'; - -import setType from '../set-type'; - -describe('util setType()', () => { - it('returns the function call of the first and only argument', () => { - expect(setType(() => 'Test')).to.equal('Test'); - }); -}); diff --git a/src/utils/transform-keys.js b/src/utils/transform-keys.js index dc72961b..c33e31b1 100644 --- a/src/utils/transform-keys.js +++ b/src/utils/transform-keys.js @@ -2,7 +2,6 @@ import { camelize, dasherize } from 'inflection'; import entries from './entries'; -import setType from './set-type'; import underscore from './underscore'; /** @@ -13,33 +12,32 @@ export function transformKeys>( transformer: (key: string) => string, deep: boolean = false ): T { - return setType(() => { - if (Array.isArray(source)) { - return source.slice(0); - } else if (source && typeof source === 'object') { - return entries(source).reduce((result, [key, value]) => { - const recurse = deep - && value - && typeof value === 'object' - && !Array.isArray(value) - && !(value instanceof Date); + if (Array.isArray(source)) { + return source.slice(0); + } else if (source && typeof source === 'object') { + // $FlowIgnore + return entries(source).reduce((obj, [key, value]) => { + const result = obj; + const recurse = ( + deep + && value + && typeof value === 'object' + && !Array.isArray(value) + && !(value instanceof Date) + ); - if (recurse) { - return { - ...result, - [transformer(key)]: transformKeys(value, transformer, true) - }; - } + if (recurse) { + result[transformer(key)] = transformKeys(value, transformer, true); + } else { + result[transformer(key)] = value; + } - return { - ...result, - [transformer(key)]: value - }; - }, {}); - } + return result; + }, {}); + } - return {}; - }); + // $FlowIgnore + return {}; } /** diff --git a/test/test-app/config/environments/production.js b/test/test-app/config/environments/production.js index 319760b4..597cce59 100644 --- a/test/test-app/config/environments/production.js +++ b/test/test-app/config/environments/production.js @@ -2,8 +2,7 @@ export default { logging: { level: 'INFO', format: 'json', - enabled: true, - + enabled: false, filter: { params: [] } diff --git a/test/utils/mocks.js b/test/utils/mocks.js index 34db73aa..8b773f29 100644 --- a/test/utils/mocks.js +++ b/test/utils/mocks.js @@ -23,6 +23,11 @@ export const createResponse = (): Response => ({ statusCode: 200, statusMessage: 'OK', + status(value: number): Response { + this.statusCode = value; + return this; + }, + getHeader(key: string) { return headersFor(this).get(key); },