diff --git a/CHANGELOG.md b/CHANGELOG.md index 0736bf30..0e073b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Lux Changelog +### v1.2.3 (July 20, 2018) + +* [[`56225dac71`](https://github.com/postlight/lux/commit/56225dac71)] - **fix**: falsy IDs breaking relationships (#730) (Nick Schot) +* [[`667febd98f`](https://github.com/postlight/lux/commit/667febd98f)] - **release**: v1.2.2 🔧 (#722) (Zachary Golba) + +### v1.2.2 (Aug 28, 2017) + +* [[`6c22bdf071`](https://github.com/postlight/lux/commit/6c22bdf071)] - **fix**: do not validate param existence for patch requests (#721) (Zachary Golba) +* [[`ea2b8f9926`](https://github.com/postlight/lux/commit/ea2b8f9926)] - **release**: v1.2.1 🔧 (#716) (Zachary Golba) + +### v1.2.1 (July 11, 2017) + +* [[`6b3547f436`](https://github.com/postlight/lux/commit/6b3547f436)] - **fix**: filtering with an empty array as a value causes an error (#715) (Nick Schot) +* [[`0f406b5908`](https://github.com/postlight/lux/commit/0f406b5908)] - **fix**: detailed error messages leak into production responses (#713) (Zachary Golba) +* [[`2327c13d1a`](https://github.com/postlight/lux/commit/2327c13d1a)] - **docs**: additional documentation around cli generators (#711) (Kyle MacDonald) +* [[`1691f9ad50`](https://github.com/postlight/lux/commit/1691f9ad50)] - **release**: 1.2.0 ✨ (#705) (Zachary Golba) + ### v1.2.0 (May 16, 2017) ##### Commits diff --git a/bin/lux b/bin/lux index d1bc1ccb..ce076d57 100755 --- a/bin/lux +++ b/bin/lux @@ -148,7 +148,7 @@ cli cli .command('g [attrs...]') .alias('generate') - .description('Example: lux generate model user') + .description('Example: lux generate model user name:string email:string admin:boolean') .action((type, name, attrs) => { exec('generate', { type, name, attrs }) .then(exit) diff --git a/examples/social-network/package.json b/examples/social-network/package.json index 60c693b8..6b711111 100644 --- a/examples/social-network/package.json +++ b/examples/social-network/package.json @@ -14,7 +14,7 @@ "babel-preset-lux": "2.0.2", "bcryptjs": "2.4.3", "knex": "0.13.0", - "lux-framework": "1.2.0", + "lux-framework": "1.2.3", "sqlite3": "3.1.8" }, "devDependencies": { diff --git a/examples/todo/package.json b/examples/todo/package.json index a6af67f0..bc000a24 100644 --- a/examples/todo/package.json +++ b/examples/todo/package.json @@ -13,7 +13,7 @@ "babel-core": "6.24.1", "babel-preset-lux": "2.0.2", "knex": "0.13.0", - "lux-framework": "1.2.0", + "lux-framework": "1.2.3", "sqlite3": "3.1.8" }, "devDependencies": { diff --git a/guide/getting-started.md b/guide/getting-started.md index ffd870f6..12950c04 100644 --- a/guide/getting-started.md +++ b/guide/getting-started.md @@ -12,6 +12,22 @@ Use the new command to create your first project. lux new ``` +### Generators + +Lux allows you to use the CLI to generate boilerplate for the following types: + +- `model` +- `controller` +- `serializer` +- `middleware` +- `migration` +- `resource` +- `util` + +```bash +lux generate [attrs...] +``` + ### Running To run your application use the serve command. diff --git a/package.json b/package.json index b932d2ac..70b996a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lux-framework", - "version": "1.2.0", + "version": "1.2.3", "description": "Build scalable, Node.js-powered REST APIs with almost no code.", "repository": "github:postlight/lux", "keywords": [ diff --git a/src/packages/database/model/index.js b/src/packages/database/model/index.js index 546951d9..6f4b5922 100644 --- a/src/packages/database/model/index.js +++ b/src/packages/database/model/index.js @@ -1166,20 +1166,6 @@ class Model { const run = async (trx: Object) => { const { hooks, logger, primaryKey } = this; const instance = Reflect.construct(this, [props, false]); - let statements = []; - - const associations = Object - .keys(props) - .filter(key => ( - Boolean(this.relationshipFor(key)) - )); - - if (associations.length) { - statements = associations.reduce((arr, key) => [ - ...arr, - ...updateRelationship(instance, key, trx) - ], []); - } await runHooks(instance, trx, hooks.beforeValidation); @@ -1191,12 +1177,28 @@ class Model { hooks.beforeSave ); - const runner = createRunner(logger, statements); + const runner = createRunner(logger, []); const [[primaryKeyValue]] = await runner(await create(instance, trx)); Reflect.set(instance, primaryKey, primaryKeyValue); Reflect.set(instance.rawColumnData, primaryKey, primaryKeyValue); + let statements = []; + const associations = Object + .keys(props) + .filter(key => ( + Boolean(this.relationshipFor(key)) + )); + + if (associations.length) { + statements = associations.reduce((arr, key) => [ + ...arr, + ...updateRelationship(instance, key, trx) + ], []); + } + + await Promise.all(statements); + Reflect.defineProperty(instance, 'initialized', { value: true, writable: false, diff --git a/src/packages/database/query/index.js b/src/packages/database/query/index.js index be860e2e..7e672746 100644 --- a/src/packages/database/query/index.js +++ b/src/packages/database/query/index.js @@ -195,17 +195,17 @@ class Query<+T: any> extends Promise { } if (Array.isArray(value)) { - if (value.length > 1) { - this.snapshots.push([ - not ? 'whereNotIn' : 'whereIn', - [key, value] - ]); - } else { + if (value.length === 1) { return { ...obj, [key]: value[0] }; } + + this.snapshots.push([ + not ? 'whereNotIn' : 'whereIn', + [key, value] + ]); } else if (value === null) { this.snapshots.push([ not ? 'whereNotNull' : 'whereNull', diff --git a/src/packages/database/query/runner/utils/build-results.js b/src/packages/database/query/runner/utils/build-results.js index 6db66fd1..ae65915f 100644 --- a/src/packages/database/query/runner/utils/build-results.js +++ b/src/packages/database/query/runner/utils/build-results.js @@ -108,7 +108,7 @@ export default async function buildResults({ .reduce((r, entry) => { let [key, value] = entry; - if (!value && pkPattern.test(key)) { + if (value == null && pkPattern.test(key)) { return r; } else if (key.indexOf('.') >= 0) { const [a, b] = key.split('.'); diff --git a/src/packages/router/route/params/index.js b/src/packages/router/route/params/index.js index f384ba3a..e439dde5 100644 --- a/src/packages/router/route/params/index.js +++ b/src/packages/router/route/params/index.js @@ -33,7 +33,7 @@ export function paramsFor({ if (method === 'POST' || method === 'PATCH') { params = [ ...params, - getDataParams(controller, true) + getDataParams(controller, method, true) ]; } } else if (type === 'collection') { @@ -45,7 +45,7 @@ export function paramsFor({ if (method === 'POST' || method === 'PATCH') { params = [ ...params, - getDataParams(controller, false) + getDataParams(controller, method, false) ]; } } else if (type === 'custom') { 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..77d737da 100644 --- a/src/packages/router/route/params/utils/get-data-params.js +++ b/src/packages/router/route/params/utils/get-data-params.js @@ -41,17 +41,18 @@ function getTypeParam({ /** * @private */ -function getAttributesParam({ - model, - params -}: Controller): [string, ParameterLike] { +function getAttributesParam( + { model, params }: Controller, + method: 'PATCH' | 'POST', +): [string, ParameterLike] { return ['attributes', new ParameterGroup(params.reduce((group, param) => { const col = model.columnFor(param); if (col) { const type = typeForColumn(col); const path = `data.attributes.${param}`; - const required = !col.nullable && isNull(col.defaultValue); + const required = + method !== 'PATCH' && !col.nullable && isNull(col.defaultValue); return [ ...group, @@ -140,13 +141,14 @@ function getRelationshipsParam({ */ export default function getDataParams( controller: Controller, + method: 'PATCH' | 'POST', includeID: boolean ): [string, ParameterLike] { let params = [getTypeParam(controller)]; if (controller.hasModel) { params = [ - getAttributesParam(controller), + getAttributesParam(controller, method), getRelationshipsParam(controller), ...params ]; diff --git a/src/packages/serializer/index.js b/src/packages/serializer/index.js index 245f44eb..096c566c 100644 --- a/src/packages/serializer/index.js +++ b/src/packages/serializer/index.js @@ -616,7 +616,7 @@ class Serializer { }) ) }; - } else if (related && related.id) { + } else if (related && related.id != null) { return this.formatRelationship({ domain, included, diff --git a/src/packages/server/responder/test/responder.test.js b/src/packages/server/responder/test/responder.test.js index 006b91db..e5fcfc60 100644 --- a/src/packages/server/responder/test/responder.test.js +++ b/src/packages/server/responder/test/responder.test.js @@ -9,6 +9,7 @@ import { createRequest } from '../../request'; import { createResponse } from '../../response'; import { createResponder } from '../index'; +import setEnv from '../../../../../test/utils/set-env'; import { getTestApp } from '../../../../../test/utils/get-test-app'; const DOMAIN = 'http://localhost:4100'; @@ -198,6 +199,14 @@ describe('module "server/responder"', () => { }); describe('- responding with an error', () => { + beforeEach(() => { + setEnv('development'); + }); + + afterEach(() => { + setEnv('test'); + }); + it('works with vanilla errors', async () => { const result = await test((req, res) => { const respond = createResponder(req, res); diff --git a/src/packages/server/responder/utils/data-for.js b/src/packages/server/responder/utils/data-for.js index ba3af684..9c6303f8 100644 --- a/src/packages/server/responder/utils/data-for.js +++ b/src/packages/server/responder/utils/data-for.js @@ -1,6 +1,7 @@ // @flow import { VERSION } from '../../../jsonapi'; import { STATUS_CODES } from '../../constants'; +import * as env from '../../../../utils/env'; import type { JSONAPI$Document, JSONAPI$ErrorObject } from '../../../jsonapi'; // eslint-disable-line max-len, no-duplicate-imports /** @@ -23,7 +24,7 @@ export default function dataFor( errData.title = title; } - if (err) { + if (err && env.isDevelopment()) { errData.detail = err.message; } diff --git a/src/utils/env.js b/src/utils/env.js new file mode 100644 index 00000000..dc559dad --- /dev/null +++ b/src/utils/env.js @@ -0,0 +1,7 @@ +/* @flow */ + +const isEnv = value => () => process.env.NODE_ENV === value; + +export const isDevelopment: () => boolean = isEnv('development'); +export const isProduction: () => boolean = isEnv('production'); +export const isTest: () => boolean = isEnv('test'); diff --git a/src/utils/test/env.test.js b/src/utils/test/env.test.js new file mode 100644 index 00000000..f74909e2 --- /dev/null +++ b/src/utils/test/env.test.js @@ -0,0 +1,25 @@ +/* @flow */ + +import { expect } from 'chai'; +import { afterEach, test } from 'mocha'; + +import * as env from '../env'; +import setEnv from '../../../test/utils/set-env'; + +afterEach(() => { + setEnv('test'); +}); + +test('isDevelopment()', () => { + setEnv('development'); + expect(env.isDevelopment()).to.be.true; +}); + +test('isProduction()', () => { + setEnv('production'); + expect(env.isProduction()).to.be.true; +}); + +test('isTest()', () => { + expect(env.isTest()).to.be.true; +}); diff --git a/test/utils/set-env.js b/test/utils/set-env.js new file mode 100644 index 00000000..47337c1b --- /dev/null +++ b/test/utils/set-env.js @@ -0,0 +1,9 @@ +/* @flow */ + +type Environment = 'development' + | 'production' + | 'test'; + +export default function setEnv(value: Environment): void { + global.process.env.NODE_ENV = value; +}