Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typings revised for passing result type down the query builder chain #744

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f63b74c
revised typings to pass result type along query builder chain
jtlapp Jan 21, 2018
df26e3e
Combined returning() into top QueryBuilder
jtlapp Jan 22, 2018
93fe100
prettied up result-pass-thru typings
jtlapp Jan 22, 2018
7ee4228
made non-throwIfNotFound() versions of tests not having them
jtlapp Jan 22, 2018
d6d818c
separate Selection class from SelectOperation.js
koskimas Jan 22, 2018
0b90f6a
rename Selection.selects --> includes
koskimas Jan 22, 2018
29c3481
add mention about private API in API.md
koskimas Jan 22, 2018
a8f0d15
added first()-order typings tests and returning() tests
jtlapp Jan 22, 2018
a15cf85
fixed runBefore() and runAfter() types, added more tests
jtlapp Jan 22, 2018
fd461a2
confirms equiv of query() and () so don't have to dup tests
jtlapp Jan 22, 2018
332e4bb
prettied runBefore/runAfter and query/ tests
jtlapp Jan 22, 2018
ce7689d
moved returning() and runAfter() to QueryBuilderBase
jtlapp Jan 22, 2018
c16aaae
move db metadata querying from RelationJoinBuilder to Model
koskimas Jan 22, 2018
43ec07b
fixes #747
koskimas Jan 22, 2018
b57d1bf
Fix parsing relationships in case source data is already Model instance
kibertoad Jan 22, 2018
5451ff4
v1.0.0-rc.5
koskimas Jan 22, 2018
a8b28e6
generalized runBefore/runAfter builder arg result type
jtlapp Jan 23, 2018
3f09ca5
QBs default to Model[], but relatedQuery not working
jtlapp Jan 23, 2018
f838f14
removed explicit type-paraming result to Model[]
jtlapp Jan 23, 2018
f305d88
fixes #750
koskimas Jan 23, 2018
961e6b4
fixes #745
koskimas Jan 23, 2018
2108337
add error handling recipe
koskimas Jan 23, 2018
b58a6bb
better error handling recipe
koskimas Jan 23, 2018
5a84d81
removed inferred $relatedQuery() typing and associated examples
jtlapp Jan 23, 2018
055b025
new typings header provided by @mceachen
jtlapp Jan 23, 2018
f519c45
Merge remote-tracking branch 'joe/pass-thru-typings' into pr_744_try_2
mceachen Jan 23, 2018
f3ee8f9
Merge remote-tracking branch 'joe/model-array-default' into pr_744_try_2
mceachen Jan 23, 2018
85f0d87
add filter example
mceachen Jan 23, 2018
aa69d9d
add keyof types to where clause to add IDE help when writing conditio…
mceachen Jan 23, 2018
65ba2f5
add $relatedQuery workaround
mceachen Jan 23, 2018
449ba84
add note and more examples
mceachen Jan 23, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions doc/includes/API.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# API reference

__NOTE__: Everything not mentioned in the API documentation is considered private implementation
and shouldn't be relied upon. Private implemementation can change without any notice even between
patch versions. Public API described here follows [semantic versioning](https://semver.org/).




Expand Down Expand Up @@ -5668,11 +5672,6 @@ Shortcut for `Person.knex().fn`



#### formatter

Shortcut for `Person.knex().client.formatter()`



#### knexQuery

Expand Down Expand Up @@ -6086,6 +6085,70 @@ Object.<string, [`Relation`](#relation)>|Object whose keys are relation na



#### columnNameToPropertyName

```js
const propertyName = Person.columnNameToPropertyName(columnName);
```

> For example, if you have defined `columnNameMappers = snakeCaseMappers()` for your model:

```js
const propName = Person.columnNameToPropertyName('foo_bar');
console.log(propName); // --> 'fooBar'
```

Runs the property through possible `columnNameMappers` and `$parseDatabaseJson` hooks to apply
any possible conversion for the column name.

##### Arguments

Argument|Type|Description
--------|----|-------------------
columnName|string|A column name

##### Return value

Type|Description
----|-----------------------------
string|The property name





#### propertyNameToColumnName

```js
const columnName = Person.propertyNameToColumnName(propertyName);
```

> For example, if you have defined `columnNameMappers = snakeCaseMappers()` for your model:

```js
const columnName = Person.propertyNameToColumnName('fooBar');
console.log(columnName); // --> 'foo_bar'
```

Runs the property through possible `columnNameMappers` and `$formatDatabaseJson` hooks to apply
any possible conversion for the property name.

##### Arguments

Argument|Type|Description
--------|----|-------------------
propertyName|string|A property name

##### Return value

Type|Description
----|-----------------------------
string|The column name






### Instance methods

Expand Down
20 changes: 11 additions & 9 deletions doc/includes/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@

* The static [`relatedQuery`](#relatedquery) method.
* New reflection methods:
[`isFind`](http://vincit.github.io/objection.js/#isfind),
[`isInsert`](http://vincit.github.io/objection.js/#isinsert),
[`isUpdate`](http://vincit.github.io/objection.js/#isupdate),
[`isDelete`](http://vincit.github.io/objection.js/#isdelete),
[`isRelate`](http://vincit.github.io/objection.js/#isrelate),
[`isUnrelate`](http://vincit.github.io/objection.js/#isunrelate),
[`hasWheres`](http://vincit.github.io/objection.js/#haswheres),
[`hasSelects`](http://vincit.github.io/objection.js/#hasselects),
[`hasEager`](http://vincit.github.io/objection.js/#haseager).
[`isFind`](#isfind),
[`isInsert`](#isinsert),
[`isUpdate`](#isupdate),
[`isDelete`](#isdelete),
[`isRelate`](#isrelate),
[`isUnrelate`](#isunrelate),
[`hasWheres`](#haswheres),
[`hasSelects`](#hasselects),
[`hasEager`](#haseager),
[`columnNameToPropertyName`](#columnnametopropertyname),
[`propertyNameToColumnName`](#propertynametocolumnname).

### Breaking changes

Expand Down
141 changes: 141 additions & 0 deletions doc/includes/RECIPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,147 @@ select "Tweet".*, (select count(*) from "Like" where "Like"."tweetId" = "Tweet".
Naturally you can add as many subquery selects as you like. For example you could also get the count of retweets
in the same query. [`relatedQuery`](#relatedquery) method works with all relations and not just `HasManyRelation`.

## Error handling

> An example error handler function that handles all possible errors. This example uses the [`objection-db-errors`](https://github.com/Vincit/objection-db-errors) library.

```js
const {
ValidationError,
NotFoundError
} = require('objection');

const {
DbError,
ConstraintViolationError,
UniqueViolationError,
NotNullViolationError,
ForeignKeyViolationError,
CheckViolationError,
DataError
} = require('objection-db-errors');

function errorHandler(err, res) {
if (err instanceof ValidationError) {
switch (err.type) {
case 'ModelValidation':
res.status(400).send({
message: err.message,
type: 'ModelValidation',
data: err.data
});
break;
case 'RelationExpression':
res.status(400).send({
message: err.message,
type: 'InvalidRelationExpression',
data: {}
});
break;
case 'UnallowedRelation':
res.status(400).send({
message: err.message,
type: 'UnallowedRelation',
data: {}
});
break;
case 'InvalidGraph':
res.status(400).send({
message: err.message,
type: 'InvalidGraph',
data: {}
});
break;
default:
res.status(400).send({
message: err.message,
type: 'UnknownValidationError',
data: {}
});
break;
}
} else if (err instanceof NotFoundError) {
res.status(404).send({
message: err.message,
type: 'NotFound',
data: {}
});
} else if (err instanceof UniqueViolationError) {
res.status(409).send({
message: err.message,
type: 'UniqueViolation',
data: {
columns: err.columns,
table: err.table,
constraint: err.constraint
}
});
} else if (err instanceof NotNullViolationError) {
res.status(400).send({
message: err.message,
type: 'NotNullViolation',
data: {
column: err.column,
table: err.table,
}
});
} else if (err instanceof ForeignKeyViolationError) {
res.status(409).send({
message: err.message,
type: 'ForeignKeyViolation',
data: {
table: err.table,
constraint: err.constraint
}
});
} else if (err instanceof CheckViolationError) {
res.status(400).send({
message: err.message,
type: 'CheckViolation',
data: {
table: err.table,
constraint: err.constraint
}
});
} else if (err instanceof DataError) {
res.status(400).send({
message: err.message,
type: 'InvalidData',
data: {}
});
} else if (err instanceof DbError) {
res.status(500).send({
message: err.message,
type: 'UnknownDatabaseError',
data: {}
});
} else {
res.status(500).send({
message: err.message,
type: 'UnknownError',
data: {}
});
}
}
```

Objection throws four kinds of errors:

1. [`ValidationError`](#validationerror) when an input that could come from the outside world is invalid. These inputs
include model instances and POJO's, eager expressions object graphs etc. `ValidationError` has a `type` property
that can be used to distinguish the different error types.

2. [`NotFoundError`](#notfounderror) when [`throwIfNotFound`](#throwifnotfound) was called for a query and no
results were found.

3. Database errors (unique violation error etc.) are thrown by the database client libraries and the error types depend on the
library. You can use the [`objection-db-errors`](https://github.com/Vincit/objection-db-errors) plugin to handle these.

4. A basic javascript `Error` when a programming or logic error is detected. In these cases there is nothing the users
can do and the only correct way to handle the error is to send a 500 response to the user and to fix the program.

See the example error handler that handles each error type.

## Indexing PostgreSQL JSONB columns

Good reading on the subject:
Expand Down
15 changes: 15 additions & 0 deletions lib/model/DbMetadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class DbMetadata {
constructor(columnInfo) {
this.columns = Object.keys(columnInfo);
}

static fetch({ modelClass, parentBuilder = null, knex = null } = {}) {
return modelClass
.query(knex)
.childQueryOf(parentBuilder)
.columnInfo()
.then(columnInfo => new DbMetadata(columnInfo));
}
}

module.exports = DbMetadata;
25 changes: 25 additions & 0 deletions lib/model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
propertyNameToColumnName
} = require('./modelColPropMap');

const DbMetadata = require('./DbMetadata');
const AjvValidator = require('./AjvValidator');
const QueryBuilder = require('../queryBuilder/QueryBuilder');
const NotFoundError = require('./NotFoundError');
Expand Down Expand Up @@ -523,6 +524,30 @@ class Model {
return modelClass.query().findOperationFactory(builder => relation.subQuery(builder));
}

static dbMetadata({ parentBuilder = null, knex = null } = {}) {
// Memoize metadata but only for this class. The hasOwnProperty check
// will fail for subclasses and the value gets recreated.
if (!this.hasOwnProperty('$$dbMetadata')) {
defineNonEnumerableProperty(this, '$$dbMetadata', Object.create(null));
}

// The table isn't necessarily same as `this.getTableName()` for example if
// a view is queried instead.
const table = (parentBuilder && parentBuilder.tableNameFor(this)) || this.getTableName();

if (this.$$dbMetadata[table]) {
return Promise.resolve(this.$$dbMetadata[table]);
} else {
const promise = DbMetadata.fetch({ modelClass: this, parentBuilder, knex }).then(metadata => {
this.$$dbMetadata[table] = metadata;
return metadata;
});

this.$$dbMetadata[table] = promise;
return promise;
}
}

static knex() {
if (arguments.length) {
defineNonEnumerableProperty(this, '$$knex', arguments[0]);
Expand Down
5 changes: 3 additions & 2 deletions lib/model/modelSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,9 @@ function setDatabaseJson(model, json) {

function setFast(model, obj) {
if (obj) {
// Don't try to set read-only virtual properties. They can easily get here through `fromJson`
// when parsing an object that was previously serialized from a model instance.
// Don't try to set read-only virtual properties. They can easily get here
// through `fromJson` when parsing an object that was previously serialized
// from a model instance.
const readOnlyVirtuals = model.constructor.getReadOnlyVirtualAttributes();
const keys = Object.keys(obj);

Expand Down
3 changes: 2 additions & 1 deletion lib/model/modelUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const staticHiddenProps = [
'$$relations',
'$$relationArray',
'$$jsonAttributes',
'$$columnNameMappers'
'$$columnNameMappers',
'$$dbMetadata'
];

function defineNonEnumerableProperty(obj, prop, value) {
Expand Down
24 changes: 15 additions & 9 deletions lib/model/modelValidate.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
const clone = require('./modelClone').clone;
const { defineNonEnumerableProperty } = require('./modelUtils');

function validate(model, json, options) {
function validate(model, json, options = {}) {
json = json || model;
options = options || {};
const inputJson = json;
const validatingModelInstance = inputJson && inputJson.$isObjectionModel;

if (json && json.$isObjectionModel) {
if (options.skipValidation) {
return json;
}

if (validatingModelInstance) {
// Strip away relations and other internal stuff.
json = clone(json, true, true);
// We can mutate `json` now that we took a copy of it.
options.mutable = true;
}

if (options.skipValidation) {
return json;
options = Object.assign({}, options, { mutable: true });
}

const ModelClass = model.constructor;
Expand All @@ -29,7 +30,12 @@ function validate(model, json, options) {
json = validator.validate(args);
validator.afterValidate(args);

return json;
if (validatingModelInstance) {
// If we cloned `json`, we need to copy the possible default values.
return inputJson.$set(json);
} else {
return json;
}
}

module.exports = {
Expand Down
Loading