Skip to content

Commit

Permalink
ToJsonOptions.virtuals can now be an array of property names. closes #…
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Mar 15, 2019
1 parent 3456e9c commit 5530377
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 41 deletions.
10 changes: 9 additions & 1 deletion doc/includes/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6904,6 +6904,10 @@ const jsonObj = modelInstance.$toJson(opt);
const shallowObj = modelInstance.$toJson({shallow: true, virtuals: true});
```

```js
const onlySomeVirtuals = modelInstance.toJSON({virtuals: ['fullName']});
```

Exports this model as a JSON object.

##### Arguments
Expand Down Expand Up @@ -6931,6 +6935,10 @@ const jsonObj = modelInstance.toJSON(opt);
const shallowObj = modelInstance.toJSON({shallow: true, virtuals: true});
```

```js
const onlySomeVirtuals = modelInstance.toJSON({virtuals: ['fullName']});
```

Exports this model as a JSON object.

##### Arguments
Expand Down Expand Up @@ -8609,7 +8617,7 @@ shallow|boolean|If true, relations are ignored
Property|Type|Description
--------|----|-----------
shallow|boolean|If true, relations are ignored. Default is false.
virtuals|boolean|If false, virtual attributes are omitted from the output. Default is true.
virtuals|boolean|string[]|If false, virtual attributes are omitted from the output. Default is true. You can also pass an array of property names and only those virtual properties get picked. You can even pass in property/function names that are not included in the static `virtualAttributes` array.



Expand Down
8 changes: 8 additions & 0 deletions lib/model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,10 @@ class Model {
return cachedGet(this, '$$relationNames', getRelationNames);
}

static getVirtualAttributes() {
return cachedGet(this, '$$virtualAttributes', getVirtualAttributes);
}

static query(trx) {
return this.QueryBuilder.forClass(this).transacting(trx);
}
Expand Down Expand Up @@ -783,6 +787,10 @@ function getRelationNames(modelClass) {
return Object.keys(modelClass.getRelationMappings());
}

function getVirtualAttributes(modelClass) {
return modelClass.virtualAttributes;
}

function getTraverseArgs(filterConstructor, models, traverser) {
filterConstructor = filterConstructor || null;

Expand Down
84 changes: 53 additions & 31 deletions lib/model/modelToJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,31 @@ const { isInternalProp } = require('../utils/internalPropUtils');
const { mergeQueryProps } = require('./modelQueryProps');
const { isObject, cloneDeep, isFunction } = require('../utils/objectUtils');

function toJson(model, opt) {
function toJson(model, optIn) {
const modelClass = model.constructor;

if (!isObject(opt)) {
opt = {};
}

// We don't take a copy of `opt` here which can cause some problems in some
// super rare cases since we modify an object passed from the outside. I can't
// think of a realistic scenario where this would actually have some unwanted
// effects. We don't take a copy for performance reasons.
opt.omit = null;
opt.pick = null;
opt.omitFromJson = model.$omitFromJson() || null;

if (opt.virtuals === undefined) {
opt.virtuals = true;
}

if (opt.shallow) {
opt.omit = modelClass.getRelationNames();
}
const opt = {
virtuals: getVirtuals(optIn, modelClass),
shallow: isShallow(optIn),
omit: getOmit(optIn, modelClass),
pick: null,
omitFromJson: model.$omitFromJson() || null
};

const json = toExternalJsonImpl(model, opt);
let json = toExternalJsonImpl(model, opt);
json = model.$formatJson(json);

return model.$formatJson(json);
return json;
}

function toDatabaseJson(model, builder) {
const modelClass = model.constructor;
const jsonSchema = modelClass.getJsonSchema();

const opt = {
virtuals: false,
shallow: true,
omit: modelClass.getRelationNames(),
pick: (jsonSchema && modelClass.pickJsonSchemaProperties && jsonSchema.properties) || null,
pick: getPick(modelClass),
omitFromJson: model.$omitFromDatabaseJson() || null
};

Expand All @@ -50,10 +38,34 @@ function toDatabaseJson(model, builder) {
return mergeQueryProps(model, json, opt.omitFromJson, builder);
}

function getVirtuals(opt, modelClass) {
if (!opt) {
return modelClass.getVirtualAttributes();
} else if (Array.isArray(opt.virtuals)) {
return opt.virtuals;
} else if (opt.virtuals) {
return modelClass.getVirtualAttributes();
} else {
return null;
}
}

function isShallow(opt) {
return !!opt && !!opt.shallow;
}

function getOmit(opt, modelClass) {
return isShallow(opt) ? modelClass.getRelationNames() : null;
}

function getPick(modelClass) {
const jsonSchema = modelClass.getJsonSchema();
return (jsonSchema && modelClass.pickJsonSchemaProperties && jsonSchema.properties) || null;
}

function toExternalJsonImpl(model, opt) {
const json = {};
const keys = Object.keys(model);
const vAttr = model.constructor.virtualAttributes;

for (let i = 0, l = keys.length; i < l; ++i) {
const key = keys[i];
Expand All @@ -62,8 +74,8 @@ function toExternalJsonImpl(model, opt) {
assignJsonValue(json, key, value, opt);
}

if (vAttr && opt.virtuals === true) {
assignVirtualAttributes(json, model, vAttr, opt);
if (opt.virtuals !== null) {
assignVirtualAttributes(json, model, opt.virtuals, opt);
}

return json;
Expand All @@ -87,12 +99,11 @@ function assignJsonValue(json, key, value, opt) {
const type = typeof value;

if (
(opt.omit === null || !opt.omit.includes(key)) &&
(opt.pick === null || key in opt.pick) &&
(opt.omitFromJson === null || opt.omitFromJson.indexOf(key) === -1) &&
type !== 'function' &&
type !== 'undefined' &&
!isInternalProp(key)
!isInternalProp(key) &&
!shouldOmit(opt, key) &&
shouldPick(opt, key)
) {
if (isObject(value)) {
json[key] = toJsonObject(value, opt);
Expand All @@ -102,6 +113,17 @@ function assignJsonValue(json, key, value, opt) {
}
}

function shouldOmit(opt, key) {
return (
(opt.omit !== null && opt.omit.includes(key)) ||
(opt.omitFromJson !== null && opt.omitFromJson.includes(key))
);
}

function shouldPick(opt, key) {
return opt.pick === null || key in opt.pick;
}

function assignVirtualAttributes(json, model, vAttr, opt) {
for (let i = 0, l = vAttr.length; i < l; ++i) {
const key = vAttr[i];
Expand Down
3 changes: 2 additions & 1 deletion lib/model/modelUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const staticHiddenProps = [
'$$columnNameMappers',
'$$tableMetadata',
'$$readOnlyAttributes',
'$$idRelationProperty'
'$$idRelationProperty',
'$$virtualAttributes'
];

function defineNonEnumerableProperty(obj, prop, value) {
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,45 @@ describe('Model', () => {
});
});

it('should pick a set of virtuals if array is passed to in `virtuals` option', () => {
class Model1 extends Model {
get foo() {
return this.a + this.b;
}

get bar() {
return this.a * this.b;
}

static get virtualAttributes() {
return ['foo'];
}
}

expect(
Model1.fromJson({
a: 100,
b: 10,
rel1: Model1.fromJson({ a: 101, b: 11 }),
rel2: [Model1.fromJson({ a: 102, b: 12 }), Model1.fromJson({ a: 103, b: 13 })]
}).$toJson({ virtuals: ['foo', 'bar'] })
).to.eql({
a: 100,
b: 10,
foo: 110,
bar: 1000,

rel1: {
a: 101,
b: 11,
foo: 112,
bar: 1111
},

rel2: [{ a: 102, b: 12, foo: 114, bar: 1224 }, { a: 103, b: 13, foo: 116, bar: 1339 }]
});
});

it('should include methods', () => {
class Model1 extends Model {
foo() {
Expand Down
33 changes: 25 additions & 8 deletions typings/objection/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ declare namespace Objection {
}

export interface ToJsonOptions extends CloneOptions {
virtuals?: boolean;
virtuals?: boolean | Array<string>;
}

export class NotFoundError extends Error {
Expand Down Expand Up @@ -379,7 +379,10 @@ declare namespace Objection {
}

interface Filters<QM extends Model> {
[filterName: string]: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void;
[filterName: string]: (
this: QueryBuilder<QM, QM[]>,
queryBuilder: QueryBuilder<QM, QM[]>
) => void;
}

interface Properties {
Expand Down Expand Up @@ -1145,7 +1148,9 @@ declare namespace Objection {

interface Table<QM extends Model, RM, RV> {
(tableName: TableName): QueryBuilder<QM, RM, RV>;
(callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void): QueryBuilder<QM, RM, RV>;
(
callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void
): QueryBuilder<QM, RM, RV>;
}

interface Distinct<QM extends Model, RM, RV> extends ColumnNamesMethod<QM, RM, RV> {}
Expand Down Expand Up @@ -1191,7 +1196,9 @@ declare namespace Objection {
}

interface Where<QM extends Model, RM, RV> extends WhereRaw<QM, RM, RV> {
(callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void): QueryBuilder<QM, RM, RV>;
(
callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void
): QueryBuilder<QM, RM, RV>;
(object: object): QueryBuilder<QM, RM, RV>;
(
column: keyof QM | ColumnRef,
Expand All @@ -1210,7 +1217,9 @@ declare namespace Objection {

interface FindOne<QM extends Model> {
(condition: boolean): QueryBuilderYieldingOneOrNone<QM>;
(callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void): QueryBuilderYieldingOneOrNone<QM>;
(
callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void
): QueryBuilderYieldingOneOrNone<QM>;
(object: object): QueryBuilderYieldingOneOrNone<QM>;
(sql: string, ...bindings: any[]): QueryBuilderYieldingOneOrNone<QM>;
(sql: string, bindings: any): QueryBuilderYieldingOneOrNone<QM>;
Expand Down Expand Up @@ -1271,9 +1280,17 @@ declare namespace Objection {
}

interface Union<QM extends Model> {
(callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void, wrap?: boolean): QueryBuilder<QM, QM[]>;
(callbacks: ((this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void)[], wrap?: boolean): QueryBuilder<QM, QM[]>;
(...callbacks: ((this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void)[]): QueryBuilder<QM, QM[]>;
(
callback: (this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void,
wrap?: boolean
): QueryBuilder<QM, QM[]>;
(
callbacks: ((this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void)[],
wrap?: boolean
): QueryBuilder<QM, QM[]>;
(
...callbacks: ((this: QueryBuilder<QM, QM[]>, queryBuilder: QueryBuilder<QM, QM[]>) => void)[]
): QueryBuilder<QM, QM[]>;
}

// commons
Expand Down

0 comments on commit 5530377

Please sign in to comment.