Skip to content
This repository was archived by the owner on Apr 17, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ npm install --save @knorm/knorm @knorm/postgres

> @knorm/postgres has a peer dependency on [@knorm/knorm](https://www.npmjs.com/package/knorm)

## Usage
## Initialization

```js
const knorm = require('@knorm/knorm');
Expand All @@ -51,5 +51,89 @@ const orm = knorm({
| `connection` | object or string | if set, this option is passed directly to [pg](https://node-postgres.com/features/connecting#programmatic). However, connections can also be configured via [environment variables](https://www.postgresql.org/docs/current/static/libpq-envars.html) |
| `initClient` | (async) function | a function called when a new client is acquired from the pool. useful for configuring the connection e.g. setting session variables. it's called with the client as the only argument |
| `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.

## 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' } }
});
```
74 changes: 72 additions & 2 deletions lib/KnormPostgres.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ class KnormPostgres {
const { Field } = 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);
}

Expand Down Expand Up @@ -299,6 +302,10 @@ class KnormPostgres {
this.config.columnsToFields = columnsToFields;
}

patch(patch = true) {
return this.setOption('patch', patch);
}

quote(value) {
return `"${value}"`;
}
Expand Down Expand Up @@ -333,6 +340,69 @@ class KnormPostgres {
return super.prepareSql(sql, options);
}

// 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
// it ends up being a hard-to-debug sql syntax error
prepareUpdateBatch(batch) {
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
"author": "Joel Mukuthu <joelmukuthu@gmail.com>",
"license": "MIT",
"peerDependencies": {
"@knorm/knorm": "^1.3.0",
"@knorm/knorm": "^1.7.0",
"@knorm/relations": "^1.2.3"
},
"devDependencies": {
"@knorm/knorm": "^1.3.0",
"@knorm/knorm": "^1.7.0",
"@knorm/relations": "^1.2.3",
"coveralls": "^3.0.0",
"eslint": "^4.15.0",
Expand Down
Loading