Skip to content
This repository was archived by the owner on Sep 2, 2025. It is now read-only.

Commit 0f8b0ab

Browse files
Thomas-gitdekelev
andauthored
joinEager Fixes + $not Operator all with tests (#114)
* Fix $joinEager Allow $mergeEager query * add test * fix bug Allow paginated query * add test * fix bug * Add $not operator * objectify modified * include $not in default operators * add test * Lint * Fix $select and $noSelect with mutating methods * were not honored with graph * $select prevented update to update a whole object (only selected parts were updated) * a consequence of this commit is that $eager is not need anymore to upsert data (partial fix #97) * $noSelect always return input data instead of an empty object * Fix #97 update with user query and upsert * Enforce user query in patch with upsert * Ensure user data and query are not overwritten * in upsert graph, id is added if needed but not modified and if data[id] and patch/update id mismatch it throw * in get request, id is added to query so that the query must match id and whole user query * Improve $select after upsert * now support '*' and 'table.*' in $select * Updated README file * new features * migrating * Update README.md Co-authored-by: Dekel Barzilay <[email protected]>
1 parent 3dd1496 commit 0f8b0ab

File tree

4 files changed

+438
-80
lines changed

4 files changed

+438
-80
lines changed

README.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ operators are:
135135
'$notILike',
136136
'$or',
137137
'$and',
138-
'$sort'
138+
'$sort',
139+
'$not'
139140
```
140141

141142
### Eager Queries
@@ -199,7 +200,9 @@ Note that all this eager related options are optional.
199200

200201
- **`$noSelect`** - skips SELECT queries in create, patch & remove requests. response data will be based on the input data.
201202

202-
- **`$null`** - filter based on if a column is NULL with REST support, e.g. `companies.find({ query: { ceo: { $null: false } } })`, `companies.find({ query: { ceo: { $null: 'false' } } })`
203+
- **`$null`** - filter based on if a column is NULL with REST support, e.g. `companies.find({ query: { ceo: { $null: false } } })`, `companies.find({ query: { ceo: { $null: 'false' } } })`
204+
205+
- **`$not`** - filter based on if a query is NOT true. It can be used with an object `$not: { name: { $in: ['craig', 'tim'] } }` or array `$not: [ { $id: 1 }, { $id: 2 } ]`
203206

204207
- **`$between`** - filter based on if a column value is between range of values
205208

@@ -335,12 +338,9 @@ app.service('companies').find({
335338
336339
Arbitrary relation graphs can be upserted (insert + update + delete) using the
337340
upsertGraph method. See
338-
[`examples`](https://vincit.github.io/objection.js/guide/query-examples.html#graph-upserts) for a better
339-
explanation.
340-
Runs on `update` and `patch` service methods when `id` is set.
341+
[`examples`](https://vincit.github.io/objection.js/guide/query-examples.html#graph-upserts) for a better explanation.
341342
342-
_The relation being upserted must also be present in `allowedEager` option and
343-
included in `$eager` query when using the `update` service method._
343+
Runs on `update` and `patch` service methods when `id` is set. When the `data` object also contains `id`, then both must be the same or an error is thrown.
344344
345345
#### Service Options
346346
@@ -385,11 +385,9 @@ be updated (if there are any changes at all).
385385
### Graph insert
386386
387387
Arbitrary relation graphs can be inserted using the insertGraph method. Provides
388-
the ability to relate the inserted object with its associations. Runs on the
389-
`.create(data, params)` service method.
388+
the ability to relate the inserted object with its associations.
390389
391-
_The relation being created must also be present in `allowedEager` option and
392-
included in `$eager` query._
390+
Runs on the `.create(data, params)` service method.
393391
394392
#### Service Options
395393
@@ -866,8 +864,23 @@ The following breaking changes have been introduced:
866864
- `namedEagerFilters` service option was removed. use Model's [`modifiers`](https://vincit.github.io/objection.js/recipes/modifiers.html#modifiers) instead
867865
- Model's `namedFilters` property was renamed to `modifiers`
868866
867+
## Migrating to `feathers-objection` v6
868+
869+
`feathers-objection` 6.0.0 comes with usability and security updates
870+
871+
- `$not` operator is now available. It can be used with an object `$not: { name: { $in: ["craig", "tim"] } }` or array `$not: [ { $id: 1 }, { $id: 2 } ]`
872+
- `$eager` is no longer needed with upsert operations
873+
874+
The following breaking changes have been introduced:
875+
876+
- Graph upsert now requires that `id` fields in the `data` object will match the `id` argument
877+
- `$noSelect` now always return the input data
878+
- `$select` is now honored with upsert methods
879+
- `patch` method now enforce `params.query` with upsert
880+
- NotFound error will be thrown when `get` & `update` methods are called with different values in `id` & `params.query.id`
881+
869882
## License
870883
871-
Copyright © 2019
884+
Copyright © 2020
872885
873886
Licensed under the [MIT license](LICENSE).

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 151 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const METHODS = {
1010
$ne: 'whereNot',
1111
$in: 'whereIn',
1212
$nin: 'whereNotIn',
13-
$null: 'whereNull'
13+
$null: 'whereNull',
14+
$not: 'whereNot'
1415
};
1516

1617
const OPERATORS = {
@@ -27,7 +28,8 @@ const OPERATORS = {
2728
ilike: '$ilike',
2829
notILike: '$notILike',
2930
or: '$or',
30-
and: '$and'
31+
and: '$and',
32+
whereNot: '$not'
3133
};
3234

3335
const OPERATORS_MAP = {
@@ -168,6 +170,17 @@ class Service extends AdapterService {
168170
Object.keys(params || {}).forEach(key => {
169171
let value = params[key];
170172

173+
if (key === '$not') {
174+
const self = this;
175+
if (Array.isArray(value)) { // Array = $and operator
176+
value = { $and: value };
177+
}
178+
return query.whereNot(function () {
179+
// continue with all queries inverted
180+
self.objectify(this, value, parentKey, methodKey, allowRefs);
181+
});
182+
}
183+
171184
if (utils.isPlainObject(value)) {
172185
return this.objectify(query, value, key, parentKey, allowRefs);
173186
}
@@ -293,18 +306,9 @@ class Service extends AdapterService {
293306
return schema ? query.withSchema(schema) : query;
294307
}
295308

296-
createQuery (params = {}) {
297-
const { filters, query } = this.filterQuery(params);
298-
const q = this._createQuery(params).skipUndefined();
299-
const eagerOptions = { ...this.eagerOptions, ...params.eagerOptions };
300-
301-
if (this.allowedEager) { q.allowGraph(this.allowedEager); }
302-
303-
if (params.mergeAllowEager) { q.allowGraph(params.mergeAllowEager); }
304-
305-
// $select uses a specific find syntax, so it has to come first.
306-
if (filters.$select) {
307-
const items = filters.$select.concat(`${this.Model.tableName}.${this.id}`);
309+
_selectQuery (q, $select) {
310+
if ($select && Array.isArray($select)) {
311+
const items = $select.concat(`${this.Model.tableName}.${this.id}`);
308312

309313
for (const [key, item] of Object.entries(items)) {
310314
const matches = item.match(/^ref\((.+)\)( as (.+))?$/);
@@ -315,6 +319,74 @@ class Service extends AdapterService {
315319

316320
q.select(...items);
317321
}
322+
return q;
323+
}
324+
325+
// Analyse $select and get an object map with fields -> alias
326+
_selectAliases ($select) {
327+
if (!Array.isArray($select)) {
328+
return {
329+
};
330+
}
331+
return $select.reduce((result, item) => {
332+
const matches = item.match(/^(?:ref\((\S+)\)|(\S+))(?: as (.+))?$/);
333+
if (matches) {
334+
const tableField = matches[1] || matches[2];
335+
const field = tableField.startsWith(`${this.Model.tableName}.`) ? tableField.substr(this.Model.tableName.length + 1) : tableField;
336+
const alias = matches[3] || field;
337+
result[field] = alias;
338+
} else {
339+
// Can't parse $select !
340+
throw new errors.BadRequest(`${item} is not a valid select statement`);
341+
}
342+
return result;
343+
}, {});
344+
}
345+
346+
_selectFields (params, originalData = {}) {
347+
return newObject => {
348+
if (params.query && params.query.$noSelect) {
349+
return originalData;
350+
}
351+
// Remove not selected fields
352+
if (params.query && params.query.$select && !params.query.$select.find(field => field === '*' || field === `${this.Model.tableName}.*`)) {
353+
const $fieldsOrAliases = this._selectAliases(params.query.$select);
354+
for (const key of Object.keys(newObject)) {
355+
if (!$fieldsOrAliases[key]) {
356+
delete newObject[key];
357+
} else if ($fieldsOrAliases[key] !== key) {
358+
// Aliased field
359+
newObject[$fieldsOrAliases[key]] = newObject[key];
360+
delete newObject[key];
361+
}
362+
}
363+
}
364+
return newObject;
365+
};
366+
}
367+
368+
_checkUpsertId (id, newObject) {
369+
const updateId = this.getIdsQuery(id, undefined, false);
370+
Object.keys(updateId).forEach(key => {
371+
if (!Object.prototype.hasOwnProperty.call(newObject, key)) {
372+
newObject[key] = updateId[key]; // id is missing in data, we had it
373+
} else if (newObject[key] !== updateId[key]) {
374+
throw new errors.BadRequest(`Id '${key}': values mismatch between data '${newObject[key]}' and request '${updateId[key]}'`);
375+
}
376+
});
377+
}
378+
379+
createQuery (params = {}) {
380+
const { filters, query } = this.filterQuery(params);
381+
const q = this._createQuery(params).skipUndefined();
382+
const eagerOptions = { ...this.eagerOptions, ...params.eagerOptions };
383+
384+
if (this.allowedEager) { q.allowGraph(this.allowedEager); }
385+
386+
if (params.mergeAllowEager) { q.allowGraph(params.mergeAllowEager); }
387+
388+
// $select uses a specific find syntax, so it has to come first.
389+
this._selectQuery(q, filters.$select);
318390

319391
// $eager for Objection eager queries
320392
if (query && query.$eager) {
@@ -323,7 +395,9 @@ class Service extends AdapterService {
323395
delete query.$eager;
324396
}
325397

326-
if (query && query.$joinEager) {
398+
const joinEager = query && query.$joinEager;
399+
400+
if (joinEager) {
327401
q.withGraphJoined(query.$joinEager, eagerOptions);
328402

329403
delete query.$joinEager;
@@ -338,7 +412,7 @@ class Service extends AdapterService {
338412
}
339413

340414
if (query && query.$mergeEager) {
341-
q[query.$joinEager ? 'withGraphJoined' : 'withGraphFetched'](query.$mergeEager, eagerOptions);
415+
q[joinEager ? 'withGraphJoined' : 'withGraphFetched'](query.$mergeEager, eagerOptions);
342416

343417
delete query.$mergeEager;
344418
}
@@ -440,6 +514,10 @@ class Service extends AdapterService {
440514
countQuery
441515
.joinRelated(query.$joinRelation)
442516
.countDistinct({ total: countColumns });
517+
} else if (query.$joinEager) {
518+
countQuery
519+
.joinRelated(query.$joinEager)
520+
.countDistinct({ total: countColumns });
443521
} else if (countColumns.length > 1) {
444522
countQuery.countDistinct({ total: countColumns });
445523
} else {
@@ -472,9 +550,11 @@ class Service extends AdapterService {
472550
}
473551

474552
_get (id, params) {
475-
const query = Object.assign({}, params.query, this.getIdsQuery(id));
553+
// merge user query with the 'id' to get
554+
const findQuery = Object.assign({}, { $and: [] }, params.query);
555+
findQuery.$and.push(this.getIdsQuery(id));
476556

477-
return this._find(Object.assign({}, params, { query }))
557+
return this._find(Object.assign({}, params, { query: findQuery }))
478558
.then(page => {
479559
const data = page.data || page;
480560

@@ -545,49 +625,56 @@ class Service extends AdapterService {
545625
* @param params
546626
*/
547627
_update (id, data, params) {
548-
// NOTE (EK): First fetch the old record so
549-
// that we can fill any existing keys that the
550-
// client isn't updating with null;
628+
// NOTE : First fetch the item to update to account for user query
551629
return this._get(id, params)
552-
.then(oldData => {
553-
let newObject = {};
630+
.then(() => {
631+
// NOTE: Next, fetch table metadata so
632+
// that we can fill any existing keys that the
633+
// client isn't updating with null;
634+
return this.Model.fetchTableMetadata()
635+
.then(meta => {
636+
let newObject = Object.assign({}, data);
637+
638+
const allowedUpsert = this.mergeRelations(this.allowedUpsert, params.mergeAllowUpsert);
639+
if (allowedUpsert) {
640+
// Ensure the object we fetched is the one we update
641+
this._checkUpsertId(id, newObject);
642+
}
554643

555-
for (const key of Object.keys(oldData)) {
556-
if (data[key] === undefined) {
557-
newObject[key] = null;
558-
} else {
559-
newObject[key] = data[key];
560-
}
561-
}
644+
for (const key of meta.columns) {
645+
if (newObject[key] === undefined) {
646+
newObject[key] = null;
647+
}
648+
}
562649

563-
const allowedUpsert = this.mergeRelations(this.allowedUpsert, params.mergeAllowUpsert);
564-
if (allowedUpsert) {
565-
return this._createQuery(params)
566-
.allowGraph(allowedUpsert)
567-
.upsertGraphAndFetch(newObject, this.upsertGraphOptions);
568-
}
650+
if (allowedUpsert) {
651+
return this._createQuery(params)
652+
.allowGraph(allowedUpsert)
653+
.upsertGraphAndFetch(newObject, this.upsertGraphOptions);
654+
}
569655

570-
// NOTE (EK): Delete id field so we don't update it
571-
if (Array.isArray(this.id)) {
572-
for (const idKey of this.id) {
573-
delete newObject[idKey];
574-
}
575-
} else {
576-
delete newObject[this.id];
577-
}
578-
return this._createQuery(params)
579-
.where(this.getIdsQuery(id))
580-
.update(newObject)
581-
.then(() => {
582-
// NOTE (EK): Restore the id field so we can return it to the client
656+
// NOTE (EK): Delete id field so we don't update it
583657
if (Array.isArray(this.id)) {
584-
newObject = Object.assign({}, newObject, this.getIdsQuery(id));
658+
for (const idKey of this.id) {
659+
delete newObject[idKey];
660+
}
585661
} else {
586-
newObject[this.id] = id;
662+
delete newObject[this.id];
587663
}
588-
589-
return newObject;
590-
});
664+
return this._createQuery(params)
665+
.where(this.getIdsQuery(id))
666+
.update(newObject)
667+
.then(() => { // BUG if nothing updated, throw a NotFound
668+
// NOTE (EK): Restore the id field so we can return it to the client
669+
if (Array.isArray(this.id)) {
670+
newObject = Object.assign({}, newObject, this.getIdsQuery(id));
671+
} else {
672+
newObject[this.id] = id;
673+
}
674+
return newObject;
675+
});
676+
})
677+
.then(this._selectFields(params, data));
591678
})
592679
.catch(errorHandler);
593680
}
@@ -603,11 +690,16 @@ class Service extends AdapterService {
603690

604691
const allowedUpsert = this.mergeRelations(this.allowedUpsert, params.mergeAllowUpsert);
605692
if (allowedUpsert && id !== null) {
606-
const dataCopy = Object.assign({}, data, this.getIdsQuery(id, null, false));
693+
const dataCopy = Object.assign({}, data);
694+
this._checkUpsertId(id, dataCopy);
607695

608-
return this._createQuery(params)
609-
.allowGraph(allowedUpsert)
610-
.upsertGraphAndFetch(dataCopy, this.upsertGraphOptions);
696+
// Get object first to ensure it satisfy user query
697+
return this._get(id, params).then(() => {
698+
return this._createQuery(params)
699+
.allowGraph(allowedUpsert)
700+
.upsertGraphAndFetch(dataCopy, this.upsertGraphOptions)
701+
.then(this._selectFields(params, data));
702+
});
611703
}
612704

613705
const dataCopy = Object.assign({}, data);
@@ -654,9 +746,8 @@ class Service extends AdapterService {
654746
findParams.query[key] = dataCopy[key];
655747
}
656748
}
657-
658749
return q.patch(dataCopy).then(() => {
659-
return params.query && params.query.$noSelect ? {} : this._find(findParams).then(page => {
750+
return params.query && params.query.$noSelect ? dataCopy : this._find(findParams).then(page => {
660751
const items = page.data || page;
661752

662753
if (id !== null) {

0 commit comments

Comments
 (0)