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

Support for nested links #479

Merged
34 changes: 34 additions & 0 deletions docs/linking_collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,40 @@ Posts.addLinks({
You created the link, and now you can use the query illustrated above.
We decided to choose `author` as a name for our link and `authorId` the field to store it in, but it's up to you to decide this.

## Nested links

Nested links are also supported:

```js
// file: /imports/db/posts/links.js
import Posts from '...';

Posts.addLinks({
'authorObject.authorId': {
type: 'one',
collection: Meteor.users,
field: 'authorObject.authorId',
},
})
```

In this example we're assuming that `authorObject` is a nested document inside `Posts` collection, and we want to link it to `Meteor.users`.

Nested arrays are also supported, e.g.:

```js
// file: /imports/db/posts/links.js
import Posts from '...';

Posts.addLinks({
'authorsArray.authorId': {
type: 'one',
collection: Meteor.users,
field: 'authorsArray.authorId',
},
})
```

## Inversed links

Because we linked `Posts` with `Meteor.users` it means that we can also get all `posts` of an user.
Expand Down
32 changes: 19 additions & 13 deletions lib/links/lib/createSearchFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,29 @@ export default function createSearchFilters(object, linker, metaFilters) {
}
}

function getIdQueryFieldStorage(object, fieldStorage, isMany = false) {
const [root, ...rest] = fieldStorage.split('.');
if (rest.length === 0) {
const ids = object[fieldStorage];
return _.isArray(ids) ? {$in: ids} : ids;
}

const nestedPath = rest.join('.');
const rootValue = object[root];
if (_.isArray(rootValue)) {
bhunjadi marked this conversation as resolved.
Show resolved Hide resolved
return {$in: _.uniq(_.union(...rootValue.map(item => dot.pick(nestedPath, item))))};
}
else if (_.isObject(rootValue)) {
return isMany ? {$in: dot.pick(nestedPath, rootValue) || []} : dot.pick(nestedPath, rootValue);
}
}

export function createOne(object, linker) {
return {
// Using {$in: []} as a workaround because foreignIdentityField which is not _id is not required to be set
// and {something: undefined} in query returns all the records.
// $in: [] ensures that nothing will be returned for this query
[linker.foreignIdentityField]: dot.pick(linker.linkStorageField, object) || {$in: []},
[linker.foreignIdentityField]: getIdQueryFieldStorage(object, linker.linkStorageField) || {$in: []},
};
}

Expand Down Expand Up @@ -69,19 +86,8 @@ export function createOneMetaVirtual(object, fieldStorage, metaFilters) {
}

export function createMany(object, linker) {
const [root, ...nested] = linker.linkStorageField.split('.');
if (nested.length > 0) {
const arr = object[root];
const ids = arr ? _.uniq(_.union(arr.map(obj => _.isObject(obj) ? dot.pick(nested.join('.'), obj) : []))) : [];
return {
[linker.foreignIdentityField]: {$in: ids}
};
}
const value = object[linker.linkStorageField];
return {
[linker.foreignIdentityField]: {
$in: _.isArray(value) ? value : (value ? [value] : []),
}
[linker.foreignIdentityField]: getIdQueryFieldStorage(object, linker.linkStorageField, true) || {$in: []},
};
}

Expand Down
20 changes: 18 additions & 2 deletions lib/query/hypernova/aggregateSearchFilters.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import sift from 'sift';
import dot from 'dot-object';

function getIdsFromObject(object, field) {
const parts = field.split('.');
if (parts.length === 1) {
return [dot.pick(field, object)];
}

const rootValue = object[parts[0]];
if (_.isArray(rootValue)) {
return rootValue.map(item => dot.pick(parts.slice(1).join('.'), item));
}
else if (_.isObject(rootValue)) {
return [dot.pick(parts.slice(1).join('.'), rootValue)];
}
return [];
}

function extractIdsFromArray(array, field) {
return (array || []).map(obj => _.isObject(obj) ? dot.pick(field, obj) : undefined).filter(v => !!v);
return _.flatten((array || []).map(obj => _.isObject(obj) ? getIdsFromObject(obj, field) : [])).filter(v => !!v);
}

/**
Expand Down Expand Up @@ -44,7 +60,7 @@ export default class AggregateFilters {
createOne() {
if (!this.isVirtual) {
return {
_id: {
[this.foreignIdentityField]: {
$in: _.uniq(extractIdsFromArray(this.parentObjects, this.linkStorageField))
}
};
Expand Down
Loading