@knorm/postgres
Advanced tools
Comparing version 1.1.2 to 1.2.0
@@ -61,5 +61,8 @@ const { Knorm } = require('@knorm/knorm'); | ||
knorm.Field = knorm.Model.Field = class PostgresField extends Field { | ||
// TODO: auto-cast decimal fields from string on fetch | ||
// TODO: v2: auto-cast decimal fields from string on fetch | ||
cast(value, modelInstance, options) { | ||
if (this.type !== 'json' && this.type !== 'jsonb') { | ||
if ( | ||
(this.type !== 'json' && this.type !== 'jsonb') || | ||
value instanceof knorm.Query.prototype.sql | ||
) { | ||
return super.cast(value, modelInstance, options); | ||
@@ -303,2 +306,6 @@ } | ||
patch(patch = true) { | ||
return this.setOption('patch', patch); | ||
} | ||
quote(value) { | ||
@@ -338,2 +345,65 @@ return `"${value}"`; | ||
// TODO: strict mode: warn/throw if a patch field is invalid | ||
// TODO: strict mode: warn/throw if a patch field is not a json(b) field | ||
isPatchedField(field) { | ||
field = this.config.fields[field]; | ||
if (field.type !== 'json' && field.type !== 'jsonb') { | ||
return false; | ||
} | ||
const { patch } = this.options; | ||
if (patch === true) { | ||
return true; | ||
} | ||
const fields = Array.isArray(patch) ? patch : [patch]; | ||
return fields.includes(field.name); | ||
} | ||
getCastFields(fields, { forInsert, forUpdate }) { | ||
if (forInsert || (forUpdate && !this.options.patch)) { | ||
return fields; | ||
} | ||
return fields.filter(field => !this.isPatchedField(field)); | ||
} | ||
getRowValue({ field, column, value }, { forInsert, forUpdate }) { | ||
if ( | ||
forInsert || | ||
value instanceof this.sql || | ||
(forUpdate && !this.options.patch) || | ||
!this.isPatchedField(field) | ||
) { | ||
return value; | ||
} | ||
if (Array.isArray(value) || typeof value !== 'object') { | ||
throw new this.constructor.QueryError( | ||
`${ | ||
this.model.name | ||
}: cannot patch field \`${field}\` (JSON patching is only supported for objects)` | ||
); | ||
} | ||
const isJson = this.config.fields[field].type === 'json'; | ||
let patch = isJson ? `${column}::jsonb` : column; | ||
Object.entries(value).forEach(([key, value]) => { | ||
patch = `jsonb_set(${patch}, '{${key}}', '${JSON.stringify(value)}')`; | ||
}); | ||
if (isJson) { | ||
patch = `${patch}::json`; | ||
} | ||
return this.sql(patch); | ||
} | ||
// TODO: support using column names in the raw sql for multi-updates | ||
// TODO: document the "v" alias used in the multi-update query or allow it | ||
// to be configurable | ||
// TODO: v2: refactor prepareUpdateBatch => getUpdateBatch | ||
// TODO: throw if the update is empty (after excluding notUpdated). otherwise | ||
@@ -340,0 +410,0 @@ // it ends up being a hard-to-debug sql syntax error |
{ | ||
"name": "@knorm/postgres", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "Postgres plugin for knorm", | ||
@@ -19,17 +19,17 @@ "main": "index.js", | ||
"peerDependencies": { | ||
"@knorm/knorm": "^1.3.0", | ||
"@knorm/relations": "^1.1.3" | ||
"@knorm/knorm": "^1.7.0", | ||
"@knorm/relations": "^1.2.3" | ||
}, | ||
"devDependencies": { | ||
"@knorm/knorm": "^1.3.0", | ||
"@knorm/relations": "^1.1.3", | ||
"@knorm/knorm": "^1.7.0", | ||
"@knorm/relations": "^1.2.3", | ||
"coveralls": "^3.0.0", | ||
"eslint": "^4.15.0", | ||
"eslint-config-ganintegrity": "^2.0.2", | ||
"knex": "^0.14.6", | ||
"knex": "^0.15.0", | ||
"mocha": "^5.1.1", | ||
"npm-run-all": "^4.1.2", | ||
"nyc": "^11.4.1", | ||
"nyc": "^12.0.1", | ||
"prettier": "^1.10.2", | ||
"sinon": "^5.0.7", | ||
"sinon": "^6.0.0", | ||
"unexpected": "^10.37.7", | ||
@@ -36,0 +36,0 @@ "unexpected-knex": "^1.2.1", |
@@ -31,3 +31,3 @@ # @knorm/postgres | ||
## Usage | ||
## Initialization | ||
@@ -55,3 +55,87 @@ ```js | ||
| `restoreClient` | (async) function | a function called before a client is released back into the pool. useful for restoring a client e.g. unsetting session variables. it's called with the client as the only argument | | ||
NOTE that all options are optional. | ||
NOTE that all options are optional. | ||
## Usage | ||
### JSON patching | ||
When updating `json` and `jsonb` fields, you may wish to only update part of the | ||
JSON data instead of the whole object. You can partially update json fields via | ||
the `patch` option: | ||
* set the option value to `true` to patch all the json and jsonb fields in the | ||
update data | ||
* set the option value to a string field-name to patch a single field in the | ||
update data | ||
* set the option value to an array of field-names to patch a multiple fields | ||
in the update data | ||
For example: | ||
```js | ||
class User extends Model {} | ||
User.fields = { | ||
id: { type: 'integer' }, | ||
jsonb: { type: 'jsonb' }, | ||
json: { type: 'json' } | ||
}; | ||
const data = { jsonb: { foo: 'bar' }, json: { foo: 'bar' } }; | ||
// to update whole object without patching: | ||
User.update(data); | ||
// to patch all fields in the update: | ||
User.update(data, { patch: true }); | ||
// to patch a single field: | ||
User.update(data, { patch: 'json' }); | ||
// to patch multiple fields: | ||
User.update(data, { patch: ['json', 'jsonb'] }); | ||
``` | ||
Note that only basic json-patching is supported: only the first level of patching | ||
is supported. For instance, nested objects or array values cannot be patched since | ||
@knorm/postgres cannot figure out if the intention is patch the object/array or | ||
to replace it entirely: | ||
```js | ||
// assuming the data is currently: | ||
new User({ | ||
jsonb: { top: { foo: 'foo' } }, | ||
json: { top: { foo: 'foo' } } | ||
}); | ||
// is the intention here to add a new `bar` key to the `top` object or to replace | ||
// the `top` key with the value `{ bar: 'bar' }`? | ||
User.query | ||
.patch(['json', 'jsonb']) | ||
.update({ | ||
jsonb: { top: { bar: 'bar' } }, | ||
json: { top: { bar: 'bar' } }, | ||
}); | ||
``` | ||
For complex patching, use | ||
[jsonb_set](https://www.postgresql.org/docs/9.5/static/functions-json.html) | ||
directly in a raw-sql update: | ||
```js | ||
// to add a nested `bar` key/value: | ||
User.query | ||
.patch(['json', 'jsonb']) | ||
.update({ | ||
jsonb: User.query.sql(`jsonb_set("jsonb", '{top,bar}', '"bar"')`), | ||
// for json field-types, you have to cast to jsonb and then cast the result | ||
// back to json | ||
json: User.query.sql(`jsonb_set("json"::jsonb, '{top,bar}', '"bar"')::json`) | ||
}); | ||
// result: | ||
new User({ | ||
jsonb: { top: { foo: 'foo', bar: 'bar' } }, | ||
json: { top: { foo: 'foo', bar: 'bar' } } | ||
}); | ||
``` |
Sorry, the diff of this file is too big to display
90409
2375
140