├── .editorconfig ├── .github ├── contributing.md ├── issue_template.md └── pull_request_template.md ├── .gitignore ├── .npmignore ├── .nycrc.yml ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── index.js ├── shallow-populate.js └── utils.js ├── mocha.opts ├── package-lock.json ├── package.json └── test ├── shallow-populate.general.test.js └── shallow-populate.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. To make this easier to manage we currently use [Zenhub](https://www.zenhub.com/) for issue triage and visibility. They have a free browser plugin you can install so that you can see what is in flight at any time, but of course you also always see current issues in Github. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### ES6 compilation 38 | 39 | Feathers uses [Babel](https://babeljs.io/) to leverage the latest developments of the JavaScript language. All code and samples are currently written in ES2015. To transpile the code in this repository run 40 | 41 | > npm run compile 42 | 43 | __Note:__ `npm test` will run the compilation automatically before the tests. 44 | 45 | ### Tests 46 | 47 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 48 | 49 | ### Documentation 50 | 51 | Feathers documentation is contained in Markdown files in the [feathers-docs](https://github.com/feathersjs/feathers-docs) repository. To change the documentation submit a pull request to that repo, referencing any other PR if applicable, and the docs will be updated with the next release. 52 | 53 | ## External Modules 54 | 55 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [showcase](https://docs.feathersjs.com/why/showcase.html). You also might want to check out the [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) that can be used to scaffold plugins to be Feathers compliant from the start. 56 | 57 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 58 | 59 | ## Contributor Code of Conduct 60 | 61 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 62 | 63 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 64 | 65 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 66 | 67 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 72 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | Tell us what should happen 11 | 12 | ### Actual behavior 13 | Tell us what happens instead 14 | 15 | ### System configuration 16 | 17 | Tell us about the applicable parts of your setup. 18 | 19 | **Module versions** (especially the part that's not working): 20 | 21 | **NodeJS version**: 22 | 23 | **Operating System**: 24 | 25 | **Browser Version**: 26 | 27 | **React Native Version**: 28 | 29 | **Module Loader**: -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | (If you have not already please refer to the contributing guideline as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#pull-requests)) 5 | 6 | - [ ] Tell us about the problem your pull request is solving. 7 | - [ ] Are there any open issues that are related to this? 8 | - [ ] Is this PR dependent on PRs in other repos? 9 | 10 | If so, please mention them to keep the conversations linked together. 11 | 12 | ### Other Information 13 | 14 | If there's anything else that's important and relevant to your pull 15 | request, mention that information here. This could include 16 | benchmarks, or other information. 17 | 18 | Your PR will be reviewed by a core team member and they will work with you to get your changes merged in a timely manner. If merged your PR will automatically be added to the changelog in the next release. 19 | 20 | If your changes involve documentation updates please mention that and link the appropriate PR in [feathers-docs](https://github.com/feathersjs/feathers-docs). 21 | 22 | Thanks for contributing to Feathers! :heart: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | .nyc_output 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | dist/ 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | .github/ 11 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./lib/ 4 | include-all-sources: true 5 | reporting: 6 | print: summary 7 | reports: 8 | - html 9 | - text 10 | - lcov 11 | watermarks: 12 | statements: [50, 80] 13 | lines: [50, 80] 14 | functions: [50, 80] 15 | branches: [50, 80] 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - '12' 5 | addons: 6 | code_climate: 7 | repo_token: 'your repo token' 8 | notifications: 9 | email: false 10 | before_script: 11 | - npm install -g codeclimate-test-reporter 12 | after_script: 13 | - codeclimate-test-reporter < coverage/lcov.info 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Mocha Tests", 8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 9 | "args": [ 10 | "-u", 11 | "bdd", 12 | "--timeout", 13 | "999999", 14 | "--colors", 15 | "${workspaceRoot}/test" 16 | ], 17 | "internalConsoleOptions": "openOnSessionStart" 18 | }, 19 | { 20 | "type": "node", 21 | "request": "launch", 22 | "name": "Launch Program", 23 | "program": "${workspaceRoot}/lib/" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.2.6](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.6) (2018-01-10) 4 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.5...v0.2.6) 5 | 6 | ## [v0.2.5](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.5) (2017-11-30) 7 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.4...v0.2.5) 8 | 9 | ## [v0.2.4](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.4) (2017-11-21) 10 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.3...v0.2.4) 11 | 12 | ## [v0.2.3](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.3) (2017-10-11) 13 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.2...v0.2.3) 14 | 15 | ## [v0.2.2](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.2) (2017-10-11) 16 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.1...v0.2.2) 17 | 18 | ## [v0.2.1](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.1) (2017-10-10) 19 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.2.0...v0.2.1) 20 | 21 | **Closed issues:** 22 | 23 | - Optional asArray for include [\#2](https://github.com/Mattchewone/feathers-shallow-populate/issues/2) 24 | 25 | ## [v0.2.0](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.2.0) (2017-10-10) 26 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.6...v0.2.0) 27 | 28 | ## [v0.1.6](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.6) (2017-10-09) 29 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.5...v0.1.6) 30 | 31 | **Closed issues:** 32 | 33 | - Populate getByDot items [\#3](https://github.com/Mattchewone/feathers-shallow-populate/issues/3) 34 | 35 | ## [v0.1.5](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.5) (2017-10-02) 36 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.4...v0.1.5) 37 | 38 | ## [v0.1.4](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.4) (2017-10-02) 39 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.3...v0.1.4) 40 | 41 | ## [v0.1.3](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.3) (2017-10-02) 42 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.2...v0.1.3) 43 | 44 | ## [v0.1.2](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.2) (2017-10-02) 45 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.1...v0.1.2) 46 | 47 | ## [v0.1.1](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.1) (2017-10-02) 48 | [Full Changelog](https://github.com/Mattchewone/feathers-shallow-populate/compare/v0.1.0...v0.1.1) 49 | 50 | ## [v0.1.0](https://github.com/Mattchewone/feathers-shallow-populate/tree/v0.1.0) (2017-10-02) 51 | 52 | 53 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-shallow-populate 2 | 3 | [![Build Status](https://travis-ci.org/Mattchewone/feathers-shallow-populate.png?branch=master)](https://travis-ci.org/Mattchewone/feathers-shallow-populate) 4 | 6 | 7 | > Feathers Shallow Populate 8 | 9 | The fastest FeathersJS hook for populating relational data. 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install feathers-shallow-populate --save 15 | ``` 16 | 17 | ## Complete Example 18 | 19 | Here's an example of using `feathers-shallow-populate`. 20 | 21 | ```js 22 | const { shallowPopulate } = require('feathers-shallow-populate') 23 | 24 | const options = { 25 | include: [ 26 | { 27 | service: 'tags', 28 | nameAs: 'tags', 29 | keyHere: 'tagIds', 30 | keyThere: '_id', 31 | asArray: true, // by default 32 | params: {} // by default 33 | }, 34 | { 35 | service: 'tags', 36 | nameAs: 'tags', 37 | params: function(params, context) { 38 | return { 39 | query: { 40 | userId: this.userId, 41 | companyId: this.companyId 42 | } 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | 49 | app.service('posts').hooks({ 50 | after: { 51 | all: shallowPopulate(options) 52 | } 53 | }); 54 | ``` 55 | 56 | ## Options for the hook 57 | - `include` (**required**) Can be one `include` object or an array of `include` objects. See table below 58 | - `catchOnError` (_optional_) Wether the hook continues populating, if an error occurs (e.g. because of missing authentication) or throws. Also can be set per `include` individually 59 | 60 | ## Options per include 61 | 62 | | **Option** | **Description** | 63 | |------------------|-----------------| 64 | | `service` | The service to reference

**required**
**Type:** `String` | 65 | | `nameAs` | The property to be assigned to on this entry

**required**
**Type:** `String` | 66 | | `keyHere` | The primary or secondary key for this entry

**required if `params` is not complex (most of the time)**
**Type:** `String` | 67 | | `keyThere` | The primary or secondary key for the referenced entry/entries

**required if `keyHere` is defined**
**Type:** `String` | 68 | | `asArray` | Is the referenced item a single entry or an array of entries?

**optional - default:** `true`
**Type:** `Boolean` 69 | | `requestPerItem` | Decided wether your `params` object/function runs against each item individually or bundled. Most of the time you don't need this.

**optional - default:
- `false`** (if `keyHere` and `keyThere` are defined)
- **`true`** (if `keyHere` and `keyThere` are not defined)
**Type:** `String` 70 | | `catchOnError` | Wether the hook continues populating, if an error occurs (e.g. because of missing authentication) or throws. Also can be set on the prior options

**optional - default:** `false`
**Type:**: `Boolean` | 71 | | `params` | Additional params to be passed to the underlying service.
You can mutate the passed `params` object or return a newly created `params` object which gets merged deeply
Merged deeply after the params are generated internally.
**ProTip #1:** You can use this for adding a '$select' property or passing authentication and user data from 'context' to 'params' to restrict accesss
**ProTip #2:** If you don't define `keyHere` and `keyThere` or set `requestPerItem` to `true` the function has access to the _`this` keyword_ being the individual item the request will be made for.
**ProTip #3**: You can skip a `requestPerItem` if it returns `undefined`.
**ProTip #4**: The hook whats for async functions!

**optional - default:** `{}`
**Possible types:**
- `Object`: _will be merged with params - simple requests_
- `Function(params, context, { path, service }) => params`: _needs to return the `params` or a new one which gets merged deeply - more complex_
- `Function(params, context, { path, service }) => Promise`
- `[Object | Function]` | 72 | 73 | ## Multiple Populates 74 | ```js 75 | const { shallowPopulate } = require('feathers-shallow-populate') 76 | 77 | const options = { 78 | include: [ 79 | { 80 | service: 'tags', 81 | nameAs: 'tags', 82 | keyHere: 'tagIds', 83 | keyThere: '_id', 84 | asArray: true, 85 | params: {} 86 | }, 87 | { 88 | service: 'comments', 89 | nameAs: 'comments', 90 | keyHere: 'commentIds', 91 | keyThere: '_id', 92 | asArray: true, 93 | params: {} 94 | } 95 | ] 96 | } 97 | 98 | app.service('posts').hooks({ 99 | after: { 100 | all: shallowPopulate(options) 101 | } 102 | }); 103 | 104 | // result.data 105 | [ 106 | { 107 | id: 1, 108 | title: 'About Timothy', 109 | tagIds: [1, 2] 110 | tags: [ 111 | { 112 | id: 1, 113 | name: 'tag 1' 114 | }, 115 | { 116 | id: 2, 117 | name: 'tag 2' 118 | } 119 | ], 120 | commentIds: [3, 5], 121 | comments: [ 122 | { 123 | id: 3, 124 | title: 'My first comment' 125 | }, 126 | { 127 | id: 5, 128 | title: 'Another comment' 129 | } 130 | ] 131 | } 132 | ] 133 | ``` 134 | 135 | ## As Object 136 | ```js 137 | const { shallowPopulate } = require('feathers-shallow-populate') 138 | 139 | const options = { 140 | include: { 141 | service: 'users', 142 | nameAs: 'publisher', 143 | keyHere: 'publisherId', 144 | keyThere: 'id', 145 | asArray: false, 146 | params: {} 147 | } 148 | } 149 | 150 | app.service('posts').hooks({ 151 | after: { 152 | all: shallowPopulate(options) 153 | } 154 | }); 155 | 156 | // result.data 157 | [ 158 | { 159 | id: 1, 160 | title: 'About Timothy', 161 | publisherId: 2, 162 | publisher: { 163 | id: 2, 164 | name: 'Timothy' 165 | } 166 | } 167 | ] 168 | ``` 169 | 170 | This will go through all the hook.data/hook.result and will create a single query to lookup the tags, it will then populate them back onto the data. 171 | 172 | ## License 173 | 174 | Copyright (c) 2020 175 | 176 | Licensed under the [MIT license](LICENSE). 177 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | shallowPopulate: require('./shallow-populate') 3 | } 4 | -------------------------------------------------------------------------------- /lib/shallow-populate.js: -------------------------------------------------------------------------------- 1 | const _get = require('lodash/get') 2 | const _set = require('lodash/set') 3 | const _isEqual = require('lodash/isEqual') 4 | const _has = require('lodash/has') 5 | 6 | const { 7 | assertIncludes, 8 | chainedParams, 9 | shouldCatchOnError 10 | } = require('./utils') 11 | const set = require('lodash/set') 12 | 13 | const defaults = { 14 | include: undefined, 15 | catchOnError: false 16 | } 17 | 18 | /** 19 | * @callback ParamsFunction 20 | * @param {object} [params] 21 | * @param {object} [context] 22 | * @returns {(undefined|object|Promise)} 23 | */ 24 | 25 | /** 26 | * The options parameter for the shallowPopulate hook 27 | * @typedef {object} Include 28 | * @property {string} service - The related service path 29 | * @property {string} nameAs - The property of this item the related item gets populated to - supports dot notation 30 | * @property {string} [keyHere] - The name of the key on this item (if defined also needs `keyThere` property; can be skipped if `params` is defined - for more complicated relations;) - supports dot notation 31 | * @property {string} [keyThere] - The name of the key on this item (if defined also needs `keyThere` property; can be skipped if `params` is defined - for more complicated relations;) - supports dot notation 32 | * @property {boolean} [asArray=true] - if true: this[nameAs] becomes an array, if false: this[nameAs] becomes an object 33 | * @property {boolean} [requestPerItem] - run request per every item or grouped 34 | * @property {boolean} [catchOnError=false] - if true: continue populating when an error occurs 35 | * @property {ParamsFunction} [params={}] - optional params object/function for custom queries 36 | */ 37 | 38 | /** 39 | * 40 | * @param {object} options 41 | * @param {Include|Include[]} options.include 42 | * @param {boolean} [options.catchOnError=false] 43 | */ 44 | module.exports = function (options) { 45 | options = Object.assign({}, defaults, options) 46 | 47 | // Make an array of includes 48 | const includes = [].concat(options.include || []) 49 | 50 | if (!includes.length) { 51 | throw new Error('shallowPopulate hook: You must provide one or more relationships in the `include` option.') 52 | } 53 | 54 | assertIncludes(includes) 55 | 56 | const cumulatedIncludes = includes.filter(include => !include.requestPerItem) 57 | 58 | const includesByKeyHere = cumulatedIncludes.reduce((includes, include) => { 59 | if (_has(include, 'keyHere') && !includes[include.keyHere]) { 60 | includes[include.keyHere] = include 61 | } 62 | return includes 63 | }, {}) 64 | 65 | const keysHere = Object.keys(includesByKeyHere) 66 | 67 | const includesPerItem = includes.filter(include => include.requestPerItem) 68 | 69 | return async function shallowPopulate (context) { 70 | const { app, type } = context 71 | let data = type === 'before' 72 | ? context.data 73 | : context.method === 'find' 74 | ? (context.result.data || context.result) 75 | : context.result 76 | 77 | data = [].concat(data || []) 78 | 79 | if (!data.length) { 80 | return context 81 | } 82 | 83 | const dataMap = data.reduce((byKeyHere, current) => { 84 | keysHere.forEach(key => { 85 | byKeyHere[key] = byKeyHere[key] || {} 86 | const keyHere = _get(current, key) 87 | 88 | if (keyHere !== undefined) { 89 | if (Array.isArray(keyHere)) { 90 | if (!includesByKeyHere[key].asArray) { 91 | mapDataWithId(byKeyHere, key, keyHere[0], current) 92 | } else { 93 | keyHere.forEach(hereKey => mapDataWithId(byKeyHere, key, hereKey, current)) 94 | } 95 | } else { 96 | mapDataWithId(byKeyHere, key, keyHere, current) 97 | } 98 | } 99 | }) 100 | 101 | return byKeyHere 102 | }, {}) 103 | 104 | // const dataMap = { 105 | // keyHere: { 106 | // trackIds: { 107 | // 1: [{}] 108 | // } 109 | // }, 110 | // keyThere: { 111 | // foo: [...keyHeres].map(here => { 112 | // here[nameAs] = tracksResponse[index] 113 | // }) 114 | // } 115 | // } 116 | 117 | let cumulatedResults = cumulatedIncludes.map(async (include) => { 118 | let result 119 | try { 120 | result = await makeCumulatedRequest(app, include, dataMap, context) 121 | } catch (err) { 122 | if (!shouldCatchOnError(options, include)) throw err 123 | return { include } 124 | } 125 | return result 126 | }) 127 | 128 | cumulatedResults = await Promise.all(cumulatedResults) 129 | 130 | cumulatedResults.forEach(result => { 131 | if (!result) return 132 | const { include } = result 133 | if (!result.response) { 134 | data.forEach(item => { 135 | set(item, include.nameAs, (include.asArray) ? [] : {}) 136 | }) 137 | return 138 | } 139 | const { params, response } = result 140 | setItems(data, include, params, response) 141 | }) 142 | 143 | const promisesPerIncludeAndItem = [] 144 | 145 | includesPerItem.forEach(include => { 146 | const promisesPerItem = data.map(async item => { 147 | try { 148 | await makeRequestPerItem(item, app, include, context) 149 | } catch (err) { 150 | if (!shouldCatchOnError(options, include)) throw err 151 | set(item, include.nameAs, (include.asArray) ? [] : {}) 152 | } 153 | }) 154 | promisesPerIncludeAndItem.push(...promisesPerItem) 155 | }) 156 | 157 | await Promise.all(promisesPerIncludeAndItem) 158 | 159 | return context 160 | } 161 | } 162 | 163 | async function makeCumulatedRequest (app, include, dataMap, context) { 164 | const { keyHere, keyThere } = include 165 | 166 | let params = { paginate: false } 167 | 168 | if (_has(include, 'keyHere') && _has(include, 'keyThere')) { 169 | const keyVals = dataMap[keyHere] 170 | let keysHere = Object.keys(keyVals) || [] 171 | keysHere = keysHere.map(k => keyVals[k].key) 172 | Object.assign(params, { query: { [keyThere]: { $in: keysHere } } }) 173 | } 174 | 175 | const paramsFromInclude = (Array.isArray(include.params)) 176 | ? include.params 177 | : [include.params] 178 | 179 | const service = app.service(include.service) 180 | 181 | const target = { 182 | path: include.service, 183 | service 184 | } 185 | 186 | params = await chainedParams([params, ...paramsFromInclude], context, target) 187 | 188 | // modify params 189 | let query = params.query || {} 190 | 191 | query = Object.assign({}, query) 192 | 193 | // remove $skip to prevent unintended results and regard it afterwards 194 | if (query.$skip) { delete query.$skip } 195 | 196 | // remove $limit to prevent unintended results and regard it afterwards 197 | if (query.$limit) { delete query.$limit } 198 | 199 | // if $select hasn't ${keyThere} add it and delete it afterwards 200 | if (query.$select && !query.$select.includes(keyThere)) { 201 | query.$select = [...query.$select, keyThere] 202 | } 203 | 204 | const response = await service.find(Object.assign({}, params, { query })) 205 | 206 | return { 207 | include, 208 | params, 209 | response 210 | } 211 | } 212 | 213 | async function makeRequestPerItem (item, app, include, context) { 214 | const { nameAs, asArray } = include 215 | const paramsFromInclude = (Array.isArray(include.params)) 216 | ? include.params 217 | : [include.params] 218 | 219 | const paramsOptions = { 220 | thisKey: item, 221 | skipWhenUndefined: true 222 | } 223 | 224 | const service = app.service(include.service) 225 | 226 | const target = { 227 | path: include.service, 228 | service 229 | } 230 | 231 | const params = await chainedParams([{ paginate: false }, ...paramsFromInclude], context, target, paramsOptions) 232 | 233 | if (!params) { 234 | (asArray) 235 | ? _set(item, nameAs, []) 236 | : _set(item, nameAs, null) 237 | return 238 | } 239 | const response = await service.find(params) 240 | const relatedItems = response.data || response 241 | 242 | if (asArray) { 243 | _set(item, nameAs, relatedItems) 244 | } else { 245 | const relatedItem = (relatedItems.length > 0) ? relatedItems[0] : null 246 | _set(item, nameAs, relatedItem) 247 | } 248 | } 249 | 250 | function setItems (data, include, params, response) { 251 | const relatedItems = response.data || response 252 | const { nameAs, keyThere, asArray } = include 253 | 254 | data.forEach(item => { 255 | const keyHere = _get(item, include.keyHere) 256 | 257 | if (keyHere !== undefined) { 258 | if (Array.isArray(keyHere)) { 259 | if (!asArray) { 260 | const items = getRelatedItems(keyHere[0], relatedItems, include, params) 261 | if (items !== undefined) { _set(item, nameAs, items) } 262 | } else { 263 | _set(item, nameAs, getRelatedItems(keyHere, relatedItems, include, params)) 264 | } 265 | } else { 266 | const items = getRelatedItems(keyHere, relatedItems, include, params) 267 | if (items !== undefined) { _set(item, nameAs, items) } 268 | } 269 | } 270 | }) 271 | 272 | if (params.query.$select && !params.query.$select.includes(keyThere)) { 273 | relatedItems.forEach(item => { 274 | delete item[keyThere] 275 | }) 276 | } 277 | } 278 | 279 | function getRelatedItems (ids, relatedItems, include, params) { 280 | const { keyThere, asArray } = include 281 | const skip = _get(params, 'query.$skip', 0) 282 | const limit = _get(params, 'query.$limit', Math.max) 283 | ids = [].concat(ids || []) 284 | let skipped = 0 285 | let itemOrItems = (asArray) ? [] : undefined 286 | 287 | let isDone = false 288 | for (let i = 0, n = relatedItems.length; i < n; i++) { 289 | if (isDone) { break } 290 | const currentItem = relatedItems[i] 291 | 292 | for (let j = 0, m = ids.length; j < m; j++) { 293 | const id = ids[j] 294 | let currentId 295 | // Allow populating on nested array of objects like key[0].name, key[1].name 296 | // If keyThere includes a dot, we're looking for a nested prop. This checks if that nested prop is an array. 297 | // If it's an array, we assume it to be an array of objects. 298 | // It splits the key only on the first dot which allows populating on nested keys inside the array of objects. 299 | if (keyThere.includes('.') && Array.isArray(currentItem[keyThere.slice(0, keyThere.indexOf('.'))])) { 300 | // The name of the array is everything leading up to the first dot. 301 | const arrayName = keyThere.split('.')[0] 302 | // The rest will be handed to getByDot as the path to the prop 303 | const nestedProp = keyThere.slice(keyThere.indexOf('.') + 1) 304 | // Map over the array to grab each nestedProp's value. 305 | currentId = currentItem[arrayName].map(nestedItem => { 306 | const keyThereVal = _get(nestedItem, nestedProp) 307 | return keyThereVal 308 | }) 309 | } else { 310 | const keyThereVal = _get(currentItem, keyThere) 311 | currentId = keyThereVal 312 | } 313 | if (asArray) { 314 | if ((Array.isArray(currentId) && currentId.includes(id)) || _isEqual(currentId, id)) { 315 | if (skipped < skip) { 316 | skipped++ 317 | continue 318 | } 319 | itemOrItems.push(currentItem) 320 | if (itemOrItems.length >= limit) { 321 | isDone = true 322 | break 323 | } 324 | } 325 | } else { 326 | if (_isEqual(currentId, id)) { 327 | if (skipped < skip) { 328 | skipped++ 329 | continue 330 | } 331 | itemOrItems = currentItem 332 | isDone = true 333 | break 334 | } 335 | } 336 | } 337 | } 338 | 339 | return itemOrItems 340 | } 341 | 342 | function mapDataWithId (byKeyHere, key, keyHere, current) { 343 | byKeyHere[key][keyHere] = byKeyHere[key][keyHere] || { 344 | key: keyHere, 345 | vals: [] 346 | } 347 | byKeyHere[key][keyHere].vals.push(current) 348 | return byKeyHere 349 | } 350 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const _has = require('lodash/has') 2 | const _isEmpty = require('lodash/isEmpty') 3 | const _isFunction = require('lodash/isFunction') 4 | const _merge = require('lodash/merge') 5 | const _uniqBy = require('lodash/uniqBy') 6 | 7 | const requiredIncludeAttrs = [ 8 | 'service', 9 | 'nameAs', 10 | 'asArray', 11 | 'params' 12 | ] 13 | 14 | const isDynamicParams = (params) => { 15 | if (!params) return false 16 | if (Array.isArray(params)) { 17 | return params.some(p => isDynamicParams(p)) 18 | } else { 19 | return !_isEmpty(params) || _isFunction(params) 20 | } 21 | } 22 | 23 | const shouldCatchOnError = (options, include) => { 24 | if (include.catchOnError !== undefined) return !!include.catchOnError 25 | if (options.catchOnError !== undefined) return !!options.catchOnError 26 | return false 27 | } 28 | 29 | const assertIncludes = (includes) => { 30 | includes.forEach(include => { 31 | // Create default `asArray` property 32 | if (!_has(include, 'asArray')) { 33 | include.asArray = true 34 | } 35 | // Create default `params` property 36 | if (!_has(include, 'params')) { 37 | include.params = {} 38 | } 39 | // Create default `requestPerItem` property 40 | if (!_has(include, 'requestPerItem')) { 41 | include.requestPerItem = (!_has(include, 'keyHere') && !_has(include, 'keyThere')) 42 | } 43 | 44 | const isDynamic = isDynamicParams(include.params) 45 | 46 | const requiredAttrs = (isDynamic) 47 | ? requiredIncludeAttrs 48 | : [...requiredIncludeAttrs, 'keyHere', 'keyThere'] 49 | 50 | requiredAttrs.forEach(attr => { 51 | if (!_has(include, attr)) { 52 | throw new Error('shallowPopulate hook: Every `include` must contain `service`, `nameAs` and (`keyHere` and `keyThere`) or `params` properties') 53 | } 54 | }) 55 | 56 | // if is dynamicParams and `keyHere` is defined, also `keyThere` must be defined 57 | if ( 58 | isDynamic && 59 | Object.keys(include).filter(key => key === 'keyHere' || key === 'keyThere').length === 1 60 | ) { 61 | throw new Error('shallowPopulate hook: Every `include` with attribute `KeyHere` or `keyThere` also needs the other attribute defined') 62 | } 63 | 64 | if (include.requestPerItem && (_has(include, 'keyHere') || _has(include, 'keyThere'))) { 65 | throw new Error('shallowPopulate hook: The attributes `keyHere` and `keyThere` are useless when you set `requestPerItem: true`. You should remove these properties') 66 | } 67 | }) 68 | 69 | const uniqueNameAs = _uniqBy(includes, 'nameAs') 70 | if (uniqueNameAs.length !== includes.length) { 71 | throw new Error('shallowPopulate hook: Every `ìnclude` must have a unique `nameAs` property') 72 | } 73 | } 74 | 75 | const chainedParams = async (paramsArr, context, target, options = {}) => { 76 | if (!paramsArr) return undefined 77 | if (!Array.isArray(paramsArr)) paramsArr = [paramsArr] 78 | const { thisKey, skipWhenUndefined } = options 79 | 80 | const resultingParams = {} 81 | for (let i = 0, n = paramsArr.length; i < n; i++) { 82 | let params = paramsArr[i] 83 | if (_isFunction(params)) { 84 | params = (thisKey == null) 85 | ? params(resultingParams, context, target) 86 | : params.call(thisKey, resultingParams, context, target) 87 | params = await Promise.resolve(params) 88 | } 89 | if (!params && skipWhenUndefined) return undefined 90 | if (params !== resultingParams) _merge(resultingParams, params) 91 | } 92 | 93 | return resultingParams 94 | } 95 | 96 | module.exports = { 97 | assertIncludes, 98 | chainedParams, 99 | shouldCatchOnError 100 | } 101 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | -u bdd --recursive --exit test/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-shallow-populate", 3 | "description": "Feathers Shallow Populate", 4 | "version": "2.5.1", 5 | "homepage": "https://github.com/Mattchewone/feathers-shallow-populate.git", 6 | "main": "lib/", 7 | "keywords": [ 8 | "feathers", 9 | "feathers-plugin" 10 | ], 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Mattchewone/feathers-shallow-populate.git" 15 | }, 16 | "author": { 17 | "name": "Feathers contributors", 18 | "email": "hello@feathersjs.com", 19 | "url": "https://feathersjs.com" 20 | }, 21 | "contributors": [ 22 | "Matt Chaffe (https://mattchaffe.uk)" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/Mattchewone/feathers-shallow-populate.git/issues" 26 | }, 27 | "scripts": { 28 | "publish": "git push origin --tags && git push origin", 29 | "release:pre": "npm version prerelease && npm publish --tag pre", 30 | "release:patch": "npm version patch && npm publish", 31 | "release:minor": "npm version minor && npm publish", 32 | "release:major": "npm version major && npm publish", 33 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 34 | "lint": "standard lib/*.js lib/**/*.js test/*.js test/**/*.js --fix", 35 | "mocha": "mocha --opts mocha.opts", 36 | "coverage": "nyc npm run mocha", 37 | "test": "npm run lint && npm run coverage" 38 | }, 39 | "standard": { 40 | "envs": [ 41 | "mocha" 42 | ] 43 | }, 44 | "directories": { 45 | "lib": "lib" 46 | }, 47 | "dependencies": { 48 | "lodash": "^4.17.20" 49 | }, 50 | "devDependencies": { 51 | "@feathersjs/errors": "^4.5.10", 52 | "chai": "^4.2.0", 53 | "feathers-memory": "^4.1.0", 54 | "mocha": "^6.2.3", 55 | "nyc": "^15.1.0", 56 | "sift": "^11.0.10", 57 | "standard": "^13.1.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/shallow-populate.general.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { shallowPopulate: makePopulate } = require('../lib/index') 3 | const memory = require('feathers-memory') 4 | const sift = require('sift').default 5 | const { NotAuthenticated } = require('@feathersjs/errors') 6 | 7 | const services = { 8 | posts: memory({ 9 | store: { 10 | 111: { id: '111', name: 'My Monkey and Me', userId: '11' }, 11 | 222: { id: '222', name: 'I forgot why I love you', userId: '11' }, 12 | 333: { id: '333', name: 'If I were a banana...', userId: '22' }, 13 | 444: { id: 444, name: 'One, two, three, one, two, three, drink', userId: '33' }, 14 | 555: { id: 555, name: 'Im gonna live like tomorrow doesnt exist', userId: 44 }, 15 | 666: { id: 666, name: 'I feel the love, feel the love', userId: 44 } 16 | } 17 | }), 18 | users: memory({ 19 | store: { 20 | 11: { id: '11', name: 'Joe Bloggs', postsId: ['111'], orgId: 'org1' }, 21 | 22: { id: '22', name: 'Jane Bloggs', postsId: '333', orgId: 'org2' }, 22 | 33: { id: '33', name: 'John Smith', postsId: ['111', '222'], orgId: 3 }, 23 | 44: { id: 44, name: 'Muhammad Li', postsId: [444, '555'], orgId: 4 } 24 | }, 25 | matcher: query => { 26 | return items => { 27 | const s = Object.assign({}, query) 28 | items = [].concat(items || []) 29 | return !!sift(s, items).length 30 | } 31 | } 32 | }), 33 | taskSets: memory({ 34 | store: { 35 | ts1: { id: 'ts1', name: 'Task Set 1' }, 36 | ts2: { id: 'ts2', name: 'Task Set 2' }, 37 | ts3: { id: 'ts3', name: 'Task Set 3' }, 38 | 4: { id: 4, name: 'Task Set 4' }, 39 | 5: { id: 5, name: 'Task Set 5' }, 40 | ts6: { id: 'ts6', name: 'Task Set 6' } 41 | } 42 | }), 43 | tasks: memory({ 44 | store: { 45 | task1: { id: 'task1', name: 'Task 1 - belongs with TaskSet1', taskSet: { taskSetId: 'ts1' }, userId: '11' }, 46 | task2: { id: 'task2', name: 'Task 2 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '22' }, 47 | task3: { id: 'task3', name: 'Task 3 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '11' }, 48 | task4: { id: 'task4', name: 'Task 4 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 49 | task5: { id: 'task5', name: 'Task 5 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 50 | task6: { id: 'task6', name: 'Task 6 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: '33' }, 51 | 7: { id: 7, name: 'Task 7 - belongs with TaskSet4', taskSet: { taskSetId: 4 } }, 52 | task8: { id: 'task8', name: 'Task 8 - belongs with TaskSet5', taskSet: { taskSetId: 5 } }, 53 | 9: { id: 9, name: 'Task 9 - belongs with TaskSet6', taskSet: { taskSetId: 'ts6' } } 54 | } 55 | }), 56 | comments: memory({ 57 | store: { 58 | 11111: { id: '11111', name: 'The Best Sounds This Summer', postsId: ['222'], userId: '11' }, 59 | 22222: { id: '22222', name: 'Chillstation', postsId: ['333'], userId: '22' }, 60 | 33333: { id: '33333', name: 'Hard Hitting Bass', postsId: ['111', '222', '333'], userId: '33' }, 61 | 44444: { id: 44444, name: 'As long as skies are blue', postsId: ['111', 444, '555'], userId: 44 } 62 | }, 63 | matcher: query => { 64 | return items => { 65 | const s = Object.assign({}, query) 66 | items = [].concat(items || []) 67 | return !!sift(s, items).length 68 | } 69 | } 70 | }), 71 | tags: memory({ 72 | store: { 73 | 1111: { id: '1111', name: 'Trombones', userId: '11' }, 74 | 2222: { id: '2222', name: 'Trumpets', userId: '11' }, 75 | 3333: { id: '3333', name: 'Drums', userId: '22' }, 76 | 4444: { id: 4444, name: 'Guitars', userId: '33' }, 77 | 5555: { id: 5555, name: 'Violins', userId: 44 } 78 | } 79 | }), 80 | orgs: memory({ 81 | store: { 82 | org1: { id: 'org1', name: 'Southern Utah', memberCount: 21 }, 83 | org2: { id: 'org2', name: 'Northern Utah', memberCount: 99 }, 84 | 3: { id: 3, name: 'Northern Arizona', memberCount: 42 }, 85 | 4: { id: 4, name: 'Southern Arizona', memberCount: 23 } 86 | } 87 | }), 88 | environments: memory({ 89 | store: { 90 | env1: { 91 | id: 'env1', 92 | name: 'Bryce Canyon National Park', 93 | orgs: [ 94 | { orgId: 'org1', orgName: 'Southern Utah' } 95 | ] 96 | }, 97 | env2: { 98 | id: 'env2', 99 | name: 'Zion National Park', 100 | orgs: [ 101 | { orgId: 'org1', orgName: 'Southern Utah' } 102 | ] 103 | }, 104 | env3: { 105 | id: 'env3', 106 | name: 'Canyonlands National Park', 107 | orgs: [ 108 | { orgId: 'org2', orgName: 'Northern Utah' } 109 | ] 110 | }, 111 | 4: { 112 | id: 4, 113 | name: 'Grand Canyon National Park', 114 | orgs: [ 115 | { orgId: 3, orgName: 'Northern Arizona' } 116 | ] 117 | }, 118 | 5: { 119 | id: '5', 120 | name: 'Organ Pipe Cactus National Monument', 121 | orgs: [ 122 | { orgId: 4, orgName: 'Southern Arizona' } 123 | ] 124 | }, 125 | 6: { 126 | id: 6, 127 | name: 'Antelope Canyon', 128 | orgs: [ 129 | { orgId: 'org1', orgName: 'Southern Utah' } 130 | ] 131 | } 132 | } 133 | }), 134 | authenticatedService: memory({ 135 | store: { 136 | task1: { id: 'task1', name: 'Task 1 - belongs with TaskSet1', taskSet: { taskSetId: 'ts1' }, userId: '11' }, 137 | task2: { id: 'task2', name: 'Task 2 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '22' }, 138 | task3: { id: 'task3', name: 'Task 3 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '11' }, 139 | task4: { id: 'task4', name: 'Task 4 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 140 | task5: { id: 'task5', name: 'Task 5 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 141 | task6: { id: 'task6', name: 'Task 6 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: '33' }, 142 | 7: { id: 7, name: 'Task 7 - belongs with TaskSet4', taskSet: { taskSetId: 4 } }, 143 | task8: { id: 'task8', name: 'Task 8 - belongs with TaskSet5', taskSet: { taskSetId: 5 } }, 144 | 9: { id: 9, name: 'Task 9 - belongs with TaskSet6', taskSet: { taskSetId: 'ts6' } } 145 | } 146 | }) 147 | } 148 | 149 | const beforeAfter = [ 150 | { 151 | type: 'before', 152 | dataResult: 'data' 153 | }, 154 | { 155 | type: 'after', 156 | dataResult: 'result' 157 | } 158 | ] 159 | 160 | describe('general', () => { 161 | it('throws when used without an includes object', () => { 162 | assert.throws(() => { 163 | makePopulate() 164 | }, 'does not work with no includes object') 165 | }) 166 | 167 | it('throws when an includes array has missing properties', () => { 168 | const includesOptions = [ 169 | { 170 | include: {} 171 | }, 172 | { 173 | include: { 174 | service: 'posts', 175 | nameAs: 'posts', 176 | keyHere: 'postsId' 177 | } 178 | }, 179 | { 180 | include: { 181 | service: 'posts', 182 | nameAs: 'posts', 183 | keyThere: 'id' 184 | } 185 | }, 186 | { 187 | include: { 188 | service: 'posts', 189 | nameAs: 'posts', 190 | keyThere: 'id', 191 | params: { test: true } 192 | } 193 | }, 194 | { 195 | include: { 196 | service: 'posts', 197 | nameAs: 'posts', 198 | keyHere: 'id', 199 | params: { test: true } 200 | } 201 | }, 202 | { 203 | include: { 204 | service: 'posts', 205 | nameAs: 'posts', 206 | keyThere: 'id', 207 | params: () => true 208 | } 209 | }, 210 | { 211 | include: { 212 | service: 'posts', 213 | nameAs: 'posts', 214 | keyHere: 'id', 215 | params: () => true 216 | } 217 | }, 218 | { 219 | include: { 220 | service: 'posts', 221 | nameAs: 'posts', 222 | keyHere: 'id', 223 | params: {} 224 | } 225 | }, 226 | { 227 | include: { 228 | service: 'posts', 229 | nameAs: 'posts', 230 | keyThere: 'id', 231 | params: {} 232 | } 233 | }, 234 | { 235 | include: { 236 | service: 'posts', 237 | nameAs: 'posts', 238 | keyThere: 'id', 239 | requestPerItem: true, 240 | params: {} 241 | } 242 | } 243 | ] 244 | 245 | includesOptions.forEach(options => { 246 | assert.throws(() => { 247 | makePopulate(options) 248 | }, 'Every `include` must contain `service`, `nameAs` and (`keyHere` and `keyThere`) or properties') 249 | }) 250 | }) 251 | 252 | it('throws when an includes array has properties with same `nameAs` property', () => { 253 | const options = { 254 | include: [ 255 | { 256 | service: 'posts', 257 | nameAs: 'posts', 258 | keyHere: 'postsId', 259 | keyThere: 'id' 260 | }, 261 | { 262 | service: 'posts', 263 | nameAs: 'posts', 264 | keyHere: 'postsId', 265 | keyThere: 'id' 266 | } 267 | ] 268 | } 269 | 270 | assert.throws(() => { 271 | makePopulate(options) 272 | }, 'Every `include` should have unique `nameAs` property') 273 | }) 274 | 275 | it('does nothing if we have no data', async () => { 276 | for (const { type, dataResult } of beforeAfter) { 277 | const options = { 278 | include: { 279 | // from: 'users', 280 | service: 'posts', 281 | nameAs: 'posts', 282 | keyHere: 'postsId', 283 | keyThere: 'id' 284 | } 285 | } 286 | 287 | const context = { 288 | app: { 289 | service (path) { 290 | return services[path] 291 | } 292 | }, 293 | method: 'create', 294 | type, 295 | params: {}, 296 | [dataResult]: {} 297 | } 298 | 299 | const shallowPopulate = makePopulate(options) 300 | const response = await shallowPopulate(context) 301 | const result = response[dataResult] 302 | 303 | assert.deepStrictEqual(result, context[dataResult], 'data should not be touched') 304 | } 305 | }) 306 | 307 | it('works with falsy \'keyHere: 0\' value', async () => { 308 | for (const { type, dataResult } of beforeAfter) { 309 | const options = { 310 | include: { 311 | // from: 'posts', 312 | service: 'users', 313 | nameAs: 'user', 314 | keyHere: 'id', 315 | keyThere: 'userId' 316 | } 317 | } 318 | 319 | let calledFind = false 320 | 321 | const context = { 322 | app: { 323 | service (path) { 324 | return { 325 | find (params = {}) { 326 | calledFind = true 327 | assert.deepStrictEqual(params.query.userId.$in, [0], 'sets \'userId.$in\' accordingly') 328 | } 329 | } 330 | } 331 | }, 332 | method: 'create', 333 | type, 334 | params: {}, 335 | [dataResult]: { 336 | id: 0 337 | } 338 | } 339 | 340 | const shallowPopulate = makePopulate(options) 341 | await shallowPopulate(context) 342 | 343 | assert(calledFind, 'called find method') 344 | } 345 | }) 346 | 347 | describe('requestPerItem: false', () => { 348 | it('throws if populated request throws', async () => { 349 | for (const { type, dataResult } of beforeAfter) { 350 | const options = { 351 | include: { 352 | // from: 'users', 353 | service: 'tasks', 354 | nameAs: 'tasks', 355 | keyHere: 'id', 356 | keyThere: 'userId' 357 | } 358 | } 359 | const context = { 360 | app: { 361 | service (path) { 362 | return { 363 | find (params = {}) { 364 | throw new NotAuthenticated('not authenticated') 365 | } 366 | } 367 | } 368 | }, 369 | method: 'create', 370 | type, 371 | params: {}, 372 | [dataResult]: { 373 | id: '11' 374 | } 375 | } 376 | 377 | const shallowPopulate = makePopulate(options) 378 | 379 | await assert.rejects(shallowPopulate(context), 'throws because of lacking authentication') 380 | } 381 | }) 382 | 383 | it('does not throw if `options.catchOnError: true`', async () => { 384 | for (const { type, dataResult } of beforeAfter) { 385 | let reachedThrow = false 386 | 387 | const options = { 388 | include: [ 389 | { 390 | // from: 'users', 391 | service: 'posts', 392 | nameAs: 'posts', 393 | keyHere: 'id', 394 | keyThere: 'userId' 395 | }, 396 | { 397 | // from: 'users', 398 | service: 'posts', 399 | nameAs: 'post', 400 | keyHere: 'id', 401 | keyThere: 'userId', 402 | asArray: false 403 | } 404 | ], 405 | catchOnError: true 406 | } 407 | 408 | const context = { 409 | app: { 410 | service (path) { 411 | return { 412 | find (params = {}) { 413 | reachedThrow = true 414 | throw new NotAuthenticated('not authenticated') 415 | } 416 | } 417 | } 418 | }, 419 | method: 'create', 420 | type, 421 | params: {}, 422 | [dataResult]: { 423 | id: '11' 424 | } 425 | } 426 | 427 | const shallowPopulate = makePopulate(options) 428 | const response = await shallowPopulate(context) 429 | const { [dataResult]: result } = response 430 | 431 | assert(reachedThrow, 'throw was fired') 432 | assert(result.posts.length === 0, 'set empty array by default') 433 | assert.deepStrictEqual(result.post, {}, 'set empty object by default') 434 | } 435 | }) 436 | 437 | it('does not throw if `include.catchOnError: true`', async () => { 438 | for (const { type, dataResult } of beforeAfter) { 439 | let reachedThrow = false 440 | 441 | const options = { 442 | include: [ 443 | { 444 | // from: 'users', 445 | service: 'posts', 446 | nameAs: 'posts', 447 | keyHere: 'id', 448 | keyThere: 'userId', 449 | catchOnError: true 450 | }, 451 | { 452 | // from: 'users', 453 | service: 'posts', 454 | nameAs: 'post', 455 | keyHere: 'id', 456 | keyThere: 'userId', 457 | asArray: false, 458 | catchOnError: true 459 | } 460 | ], 461 | catchOnError: false 462 | } 463 | const context = { 464 | app: { 465 | service (path) { 466 | return { 467 | find (params = {}) { 468 | reachedThrow = true 469 | throw new NotAuthenticated('not authenticated') 470 | } 471 | } 472 | } 473 | }, 474 | method: 'create', 475 | type, 476 | params: {}, 477 | [dataResult]: { 478 | id: '11' 479 | } 480 | } 481 | 482 | const shallowPopulate = makePopulate(options) 483 | const response = await shallowPopulate(context) 484 | const { [dataResult]: result } = response 485 | 486 | assert(reachedThrow, 'throw was fired') 487 | assert(result.posts.length === 0, 'set empty array by default') 488 | assert.deepStrictEqual(result.post, {}, 'set empty object by default') 489 | } 490 | }) 491 | 492 | describe('params - requestPerItem: false', () => { 493 | it('can pass in custom params for lookup', async () => { 494 | for (const { type, dataResult } of beforeAfter) { 495 | const options = { 496 | include: { 497 | // from: 'users', 498 | service: 'posts', 499 | nameAs: 'posts', 500 | keyHere: 'postsId', 501 | keyThere: 'id', 502 | params: { fromCommentsPopulate: true } 503 | } 504 | } 505 | 506 | let hasCalledFind = false 507 | 508 | const context = { 509 | method: 'create', 510 | type, 511 | app: { 512 | service () { 513 | return { 514 | find (params = {}) { 515 | assert(params.fromCommentsPopulate === true, 'we have a custom param') 516 | hasCalledFind = true 517 | return [] 518 | } 519 | } 520 | } 521 | }, 522 | params: {}, 523 | [dataResult]: { 524 | id: '1' 525 | } 526 | } 527 | 528 | const shallowPopulate = makePopulate(options) 529 | 530 | await shallowPopulate(context) 531 | assert(hasCalledFind, 'checks were made') 532 | } 533 | }) 534 | 535 | it('can pass in custom params for lookup and merges them deeply', async () => { 536 | for (const { type, dataResult } of beforeAfter) { 537 | const options = { 538 | include: { 539 | // from: 'users', 540 | service: 'posts', 541 | nameAs: 'posts', 542 | keyHere: 'postsId', 543 | keyThere: 'id', 544 | params: { query: { $select: ['id'] } } 545 | } 546 | } 547 | 548 | let hasCalledFind = false 549 | 550 | const context = { 551 | method: 'create', 552 | type, 553 | app: { 554 | service () { 555 | return { 556 | find (params = {}) { 557 | assert.deepStrictEqual(params.query.id.$in, [], 'we have the params from shallow-populate') 558 | assert.deepStrictEqual(params.query.$select, ['id'], 'we have a merged query') 559 | hasCalledFind = true 560 | return [] 561 | } 562 | } 563 | } 564 | }, 565 | params: {}, 566 | [dataResult]: { 567 | id: '1' 568 | } 569 | } 570 | 571 | const shallowPopulate = makePopulate(options) 572 | 573 | await shallowPopulate(context) 574 | assert(hasCalledFind, 'checks were made') 575 | } 576 | }) 577 | 578 | it('can pass in custom params-function which overrides params', async () => { 579 | for (const { type, dataResult } of beforeAfter) { 580 | const options = { 581 | include: { 582 | // from: 'users', 583 | service: 'posts', 584 | nameAs: 'posts', 585 | keyHere: 'postsId', 586 | keyThere: 'id', 587 | params: (params, context) => { 588 | assert.deepStrictEqual(params.query.id.$in, [], 'we have the params from shallow-populate first') 589 | params.query.$select = ['id'] 590 | } 591 | } 592 | } 593 | 594 | let hasCalledFind = false 595 | 596 | const context = { 597 | method: 'create', 598 | type, 599 | app: { 600 | service () { 601 | return { 602 | find (params = {}) { 603 | assert.deepStrictEqual(params.query.id.$in, [], 'we have the params from shallow-populate') 604 | assert.deepStrictEqual(params.query.$select, ['id'], 'we have a merged query') 605 | hasCalledFind = true 606 | return [] 607 | } 608 | } 609 | } 610 | }, 611 | params: {}, 612 | [dataResult]: { 613 | id: '1' 614 | } 615 | } 616 | 617 | const shallowPopulate = makePopulate(options) 618 | 619 | await shallowPopulate(context) 620 | assert(hasCalledFind, 'checks were made') 621 | } 622 | }) 623 | 624 | it('can pass in custom params-function which returns params and merges them deeply', async () => { 625 | for (const { type, dataResult } of beforeAfter) { 626 | const options = { 627 | include: { 628 | // from: 'users', 629 | service: 'posts', 630 | nameAs: 'posts', 631 | keyHere: 'postsId', 632 | keyThere: 'id', 633 | params: () => { return { query: { $select: ['id'] } } } 634 | } 635 | } 636 | 637 | let hasCalledFind = false 638 | 639 | const context = { 640 | method: 'create', 641 | type, 642 | app: { 643 | service () { 644 | return { 645 | find (params = {}) { 646 | assert.deepStrictEqual(params.query.id.$in, [], 'we have the params from shallow-populate') 647 | assert.deepStrictEqual(params.query.$select, ['id'], 'we have a merged query') 648 | hasCalledFind = true 649 | return [] 650 | } 651 | } 652 | } 653 | }, 654 | params: {}, 655 | [dataResult]: { 656 | id: '1' 657 | } 658 | } 659 | 660 | const shallowPopulate = makePopulate(options) 661 | 662 | await shallowPopulate(context) 663 | assert(hasCalledFind, 'checks were made') 664 | } 665 | }) 666 | 667 | it('can pass in custom params-function with context', async () => { 668 | for (const { type, dataResult } of beforeAfter) { 669 | let paramsFunctionCalled = false 670 | 671 | const options = { 672 | include: { 673 | service: 'posts', 674 | nameAs: 'posts', 675 | keyHere: 'postsId', 676 | keyThere: 'id', 677 | params: (params, context) => { 678 | assert(context.method === 'create', 'we can pass the context to include') 679 | params.method = context.method 680 | paramsFunctionCalled = true 681 | } 682 | } 683 | } 684 | 685 | let hasCalledFind = false 686 | 687 | const context = { 688 | method: 'create', 689 | type, 690 | app: { 691 | service () { 692 | return { 693 | find (params = {}) { 694 | assert(params.method === 'create', 'we can manipulate the params based on the context') 695 | hasCalledFind = true 696 | return [] 697 | } 698 | } 699 | } 700 | }, 701 | params: {}, 702 | [dataResult]: { 703 | id: '1' 704 | } 705 | } 706 | 707 | const shallowPopulate = makePopulate(options) 708 | 709 | await shallowPopulate(context) 710 | assert(paramsFunctionCalled, 'params function was called') 711 | assert(hasCalledFind, 'checks were made') 712 | } 713 | }) 714 | 715 | it('calls params-function once even for multiple records', async () => { 716 | for (const { type, dataResult } of beforeAfter) { 717 | let calledIncludeUsersParams = false 718 | let calledIncludeCommentsParams = false 719 | 720 | const options = { 721 | include: [ 722 | { 723 | service: 'users', 724 | nameAs: 'users', 725 | keyHere: 'id', 726 | keyThere: 'postsId', 727 | params: () => { 728 | assert(!calledIncludeUsersParams, 'not called before -> only called once') 729 | calledIncludeUsersParams = true 730 | } 731 | }, 732 | { 733 | service: 'comments', 734 | nameAs: 'comments', 735 | keyHere: 'id', 736 | keyThere: 'postsId', 737 | params: () => { 738 | assert(!calledIncludeCommentsParams, 'not called before -> only called once') 739 | calledIncludeCommentsParams = true 740 | } 741 | } 742 | ] 743 | } 744 | const context = { 745 | app: { 746 | service (path) { 747 | return services[path] 748 | } 749 | }, 750 | method: 'create', 751 | type, 752 | params: {}, 753 | [dataResult]: [ 754 | { 755 | id: '333', 756 | name: 'If I were a banana...' 757 | }, 758 | { 759 | id: '111', 760 | name: 'My Monkey and Me' 761 | }, 762 | { 763 | id: 444, 764 | name: 'One, two, three, one, two, three, drink' 765 | } 766 | ] 767 | } 768 | 769 | const shallowPopulate = makePopulate(options) 770 | 771 | await shallowPopulate(context) 772 | assert(calledIncludeUsersParams, 'params function for users was called') 773 | assert(calledIncludeCommentsParams, 'params function for comments was called') 774 | } 775 | }) 776 | 777 | it('wait for params function that returns a promise', async () => { 778 | for (const { type, dataResult } of beforeAfter) { 779 | let calledAsyncFunction = false 780 | const options = { 781 | include: { 782 | service: 'posts', 783 | nameAs: 'posts', 784 | params: async (params, context) => { 785 | await new Promise(resolve => { setTimeout(resolve, 200) }) 786 | params.calledAsyncFunction = true 787 | calledAsyncFunction = true 788 | return params 789 | } 790 | } 791 | } 792 | 793 | let hasCalledFind = false 794 | 795 | const context = { 796 | method: 'create', 797 | type, 798 | app: { 799 | service () { 800 | return { 801 | find (params = {}) { 802 | assert(params.calledAsyncFunction, 'waited for async params function before find') 803 | hasCalledFind = true 804 | return [] 805 | } 806 | } 807 | } 808 | }, 809 | params: {}, 810 | [dataResult]: { 811 | id: '1' 812 | } 813 | } 814 | 815 | const shallowPopulate = makePopulate(options) 816 | await shallowPopulate(context) 817 | assert(calledAsyncFunction, 'waited for async params function') 818 | assert(hasCalledFind, 'checks were made') 819 | } 820 | }) 821 | 822 | it('can pass in params as array', async () => { 823 | for (const { type, dataResult } of beforeAfter) { 824 | let calledLastFunction = false 825 | 826 | const expected = { 827 | paginate: false, 828 | query: { 829 | postsId: { $in: ['1'] }, 830 | second: true, 831 | fourth: true 832 | }, 833 | third: true, 834 | fifth: true, 835 | sixth: true 836 | } 837 | 838 | const options = { 839 | include: [ 840 | { 841 | service: 'users', 842 | nameAs: 'users', 843 | keyHere: 'id', 844 | keyThere: 'postsId', 845 | params: [ 846 | {}, 847 | { query: { second: true } }, 848 | (params) => { 849 | assert(params.query.second, 'walked through before') 850 | params.third = true 851 | }, 852 | (params) => { 853 | assert(params.third, 'walked through before') 854 | return { query: { fourth: true } } 855 | }, 856 | async (params) => { 857 | assert(params.query.fourth, 'walked through before') 858 | await new Promise(resolve => setTimeout(resolve, 200)) 859 | params.fifth = true 860 | }, 861 | (params, context) => { 862 | assert(params.fifth, 'walked through before') 863 | if (context.app) { 864 | return { sixth: true } 865 | } 866 | }, 867 | (params) => { 868 | assert.deepStrictEqual(params, expected, 'params object is right') 869 | calledLastFunction = true 870 | } 871 | ] 872 | } 873 | ] 874 | } 875 | const context = { 876 | app: { 877 | service (path) { 878 | return services[path] 879 | } 880 | }, 881 | method: 'create', 882 | type, 883 | params: {}, 884 | [dataResult]: [ 885 | { 886 | id: '1' 887 | } 888 | ] 889 | } 890 | 891 | const shallowPopulate = makePopulate(options) 892 | 893 | await shallowPopulate(context) 894 | assert(calledLastFunction, 'all params were called') 895 | } 896 | }) 897 | 898 | it('params function has third \'target\' object', async () => { 899 | for (const { type, dataResult } of beforeAfter) { 900 | let paramsFunctionCalled = false 901 | 902 | const options = { 903 | include: { 904 | service: 'posts', 905 | nameAs: 'posts', 906 | keyHere: 'postsId', 907 | keyThere: 'id', 908 | params: (params, context, target) => { 909 | assert.ok(target, 'target is defined') 910 | assert(target.service && typeof target.service.find === 'function', 'target has service') 911 | assert.strictEqual(typeof target.path, 'string', 'target.path is string') 912 | params.path = target.path 913 | paramsFunctionCalled = true 914 | } 915 | } 916 | } 917 | 918 | let hasCalledFind = false 919 | 920 | const context = { 921 | method: 'create', 922 | type, 923 | app: { 924 | service () { 925 | return { 926 | find (params = {}) { 927 | assert.strictEqual(params.path, 'posts', 'we can manipulate the params based on the target') 928 | hasCalledFind = true 929 | return [] 930 | } 931 | } 932 | } 933 | }, 934 | params: {}, 935 | [dataResult]: { 936 | id: '1' 937 | } 938 | } 939 | 940 | const shallowPopulate = makePopulate(options) 941 | 942 | await shallowPopulate(context) 943 | assert(paramsFunctionCalled, 'params function was called') 944 | assert(hasCalledFind, 'checks were made') 945 | } 946 | }) 947 | }) 948 | }) 949 | 950 | describe('requestPerItem: true', () => { 951 | it('throws if populated request throws', async () => { 952 | for (const { type, dataResult } of beforeAfter) { 953 | const options = { 954 | include: { 955 | // from: 'users', 956 | service: 'posts', 957 | nameAs: 'posts', 958 | params: { fromCommentsPopulate: true } 959 | } 960 | } 961 | const context = { 962 | app: { 963 | service (path) { 964 | return { 965 | find (params = {}) { 966 | throw new NotAuthenticated('not authenticated') 967 | } 968 | } 969 | } 970 | }, 971 | method: 'create', 972 | type, 973 | params: {}, 974 | [dataResult]: { 975 | id: '11' 976 | } 977 | } 978 | 979 | const shallowPopulate = makePopulate(options) 980 | 981 | await assert.rejects(shallowPopulate(context), 'throws because of lacking authentication') 982 | } 983 | }) 984 | 985 | it('does not throw if `options.catchOnError: true`', async () => { 986 | let throwReached = false 987 | for (const { type, dataResult } of beforeAfter) { 988 | const options = { 989 | include: [ 990 | { 991 | // from: 'users', 992 | service: 'posts', 993 | nameAs: 'posts', 994 | params: { fromCommentsPopulate: true } 995 | }, 996 | { 997 | // from: 'users', 998 | service: 'posts', 999 | nameAs: 'post', 1000 | asArray: false, 1001 | params: { fromCommentsPopulate: true } 1002 | } 1003 | ], 1004 | catchOnError: true 1005 | } 1006 | const context = { 1007 | app: { 1008 | service (path) { 1009 | return { 1010 | find (params = {}) { 1011 | throwReached = true 1012 | throw new NotAuthenticated('not authenticated') 1013 | } 1014 | } 1015 | } 1016 | }, 1017 | method: 'create', 1018 | type, 1019 | params: {}, 1020 | [dataResult]: { 1021 | id: '11' 1022 | } 1023 | } 1024 | 1025 | const shallowPopulate = makePopulate(options) 1026 | 1027 | const response = await shallowPopulate(context) 1028 | const { [dataResult]: result } = response 1029 | 1030 | assert(throwReached, 'throw was fired') 1031 | assert(result.posts.length === 0, 'set empty array by default') 1032 | assert.deepStrictEqual(result.post, {}, 'set empty object by default') 1033 | } 1034 | }) 1035 | 1036 | it('does not throw if `include.catchOnError: true`', async () => { 1037 | let throwReached = false 1038 | for (const { type, dataResult } of beforeAfter) { 1039 | const options = { 1040 | include: [ 1041 | { 1042 | // from: 'users', 1043 | service: 'posts', 1044 | nameAs: 'posts', 1045 | params: { fromCommentsPopulate: true }, 1046 | catchOnError: true 1047 | }, 1048 | { 1049 | // from: 'users', 1050 | service: 'posts', 1051 | nameAs: 'post', 1052 | asArray: false, 1053 | params: { fromCommentsPopulate: true }, 1054 | catchOnError: true 1055 | } 1056 | ], 1057 | catchOnError: false 1058 | } 1059 | const context = { 1060 | app: { 1061 | service (path) { 1062 | return { 1063 | find (params = {}) { 1064 | throwReached = true 1065 | throw new NotAuthenticated('not authenticated') 1066 | } 1067 | } 1068 | } 1069 | }, 1070 | method: 'create', 1071 | type, 1072 | params: {}, 1073 | [dataResult]: { 1074 | id: '11' 1075 | } 1076 | } 1077 | 1078 | const shallowPopulate = makePopulate(options) 1079 | const response = await shallowPopulate(context) 1080 | const { [dataResult]: result } = response 1081 | 1082 | assert(throwReached, 'throw was fired') 1083 | assert(result.posts.length === 0, 'set empty array by default') 1084 | assert.deepStrictEqual(result.post, {}, 'set empty object by default') 1085 | } 1086 | }) 1087 | 1088 | it('can pass in custom params for lookup without `keyHere` and `keyThere`', async () => { 1089 | for (const { type, dataResult } of beforeAfter) { 1090 | const options = { 1091 | include: { 1092 | // from: 'users', 1093 | service: 'posts', 1094 | nameAs: 'posts', 1095 | params: { fromCommentsPopulate: true } 1096 | } 1097 | } 1098 | 1099 | let hasCalledFind = false 1100 | 1101 | const context = { 1102 | method: 'create', 1103 | type, 1104 | app: { 1105 | service () { 1106 | return { 1107 | find (params = {}) { 1108 | assert(params.fromCommentsPopulate === true, 'we have a custom param') 1109 | hasCalledFind = true 1110 | return [] 1111 | } 1112 | } 1113 | } 1114 | }, 1115 | params: {}, 1116 | [dataResult]: { 1117 | id: '1' 1118 | } 1119 | } 1120 | 1121 | const shallowPopulate = makePopulate(options) 1122 | 1123 | await shallowPopulate(context) 1124 | assert(hasCalledFind, 'checks were made') 1125 | } 1126 | }) 1127 | 1128 | it('can pass in custom params function without `keyThere` and ``keyHere`', () => { 1129 | const expected = { paginate: false } 1130 | const options = { 1131 | include: { 1132 | service: 'posts', 1133 | nameAs: 'posts', 1134 | params: (params, context) => { 1135 | assert.deepStrictEqual(params, expected, 'params just have paginate attribute') 1136 | return params 1137 | } 1138 | } 1139 | } 1140 | 1141 | assert.doesNotThrow(() => { 1142 | makePopulate(options) 1143 | }, 'does not throw error') 1144 | }) 1145 | 1146 | it('can pass params as nonempty object without `keyThere` and ``keyHere`', () => { 1147 | const options = { 1148 | include: { 1149 | service: 'posts', 1150 | nameAs: 'posts', 1151 | params: { 1152 | test: true 1153 | } 1154 | } 1155 | } 1156 | 1157 | assert.doesNotThrow(() => { 1158 | makePopulate(options) 1159 | }, 'does not throw error') 1160 | }) 1161 | 1162 | it('skip request if params returns undefined', async () => { 1163 | for (const { type, dataResult } of beforeAfter) { 1164 | const options = { 1165 | include: { 1166 | // from: 'users', 1167 | service: 'posts', 1168 | nameAs: 'posts', 1169 | params: () => {} 1170 | } 1171 | } 1172 | 1173 | let hasCalledFind = false 1174 | 1175 | const context = { 1176 | method: 'create', 1177 | type, 1178 | app: { 1179 | service () { 1180 | return { 1181 | find (params = {}) { 1182 | hasCalledFind = true 1183 | return [] 1184 | } 1185 | } 1186 | } 1187 | }, 1188 | params: {}, 1189 | [dataResult]: { 1190 | id: '1' 1191 | } 1192 | } 1193 | 1194 | const shallowPopulate = makePopulate(options) 1195 | 1196 | await shallowPopulate(context) 1197 | assert(!hasCalledFind, 'skip request if params function returns undefined') 1198 | } 1199 | }) 1200 | 1201 | it('can pass in custom params-function which overrides params', async () => { 1202 | for (const { type, dataResult } of beforeAfter) { 1203 | const options = { 1204 | include: { 1205 | // from: 'users', 1206 | service: 'posts', 1207 | nameAs: 'posts', 1208 | params: (params, context) => { 1209 | params.query = { id: 1 } 1210 | return params 1211 | } 1212 | } 1213 | } 1214 | 1215 | let hasCalledFind = false 1216 | 1217 | const context = { 1218 | method: 'create', 1219 | type, 1220 | app: { 1221 | service () { 1222 | return { 1223 | find (params = {}) { 1224 | assert(params.paginate === false, 'we have the params from shallow-populate') 1225 | assert(params.query.id === 1, 'we have a merged query') 1226 | hasCalledFind = true 1227 | return [] 1228 | } 1229 | } 1230 | } 1231 | }, 1232 | params: {}, 1233 | [dataResult]: { 1234 | id: '1' 1235 | } 1236 | } 1237 | 1238 | const shallowPopulate = makePopulate(options) 1239 | 1240 | await shallowPopulate(context) 1241 | assert(hasCalledFind, 'checks were made') 1242 | } 1243 | }) 1244 | 1245 | it('can pass in custom params-function which returns params and merges them deeply', async () => { 1246 | for (const { type, dataResult } of beforeAfter) { 1247 | const options = { 1248 | include: { 1249 | // from: 'users', 1250 | service: 'posts', 1251 | nameAs: 'posts', 1252 | params: () => { return { query: { $select: ['id'] } } } 1253 | } 1254 | } 1255 | 1256 | let hasCalledFind = false 1257 | 1258 | const context = { 1259 | method: 'create', 1260 | type, 1261 | app: { 1262 | service () { 1263 | return { 1264 | find (params = {}) { 1265 | assert(params.paginate === false, 'we have the params from shallow-populate') 1266 | assert.deepStrictEqual(params.query, { $select: ['id'] }, 'we have a merged query') 1267 | hasCalledFind = true 1268 | return [] 1269 | } 1270 | } 1271 | } 1272 | }, 1273 | params: {}, 1274 | [dataResult]: { 1275 | id: '1' 1276 | } 1277 | } 1278 | 1279 | const shallowPopulate = makePopulate(options) 1280 | 1281 | await shallowPopulate(context) 1282 | assert(hasCalledFind, 'checks were made') 1283 | } 1284 | }) 1285 | 1286 | it('can pass in custom params-function with context', async () => { 1287 | for (const { type, dataResult } of beforeAfter) { 1288 | let paramsFunctionCalled = false 1289 | 1290 | const options = { 1291 | include: { 1292 | service: 'posts', 1293 | nameAs: 'posts', 1294 | params: (params, context) => { 1295 | assert.strictEqual(context.method, 'create', 'we can pass the context to include') 1296 | params.method = context.method 1297 | paramsFunctionCalled = true 1298 | return params 1299 | } 1300 | } 1301 | } 1302 | 1303 | let hasCalledFind = false 1304 | 1305 | const context = { 1306 | method: 'create', 1307 | type, 1308 | app: { 1309 | service () { 1310 | return { 1311 | find (params = {}) { 1312 | assert.strictEqual(params.method, 'create', 'we can manipulate the params based on the context') 1313 | hasCalledFind = true 1314 | return [] 1315 | } 1316 | } 1317 | } 1318 | }, 1319 | params: {}, 1320 | [dataResult]: { 1321 | id: '1' 1322 | } 1323 | } 1324 | 1325 | const shallowPopulate = makePopulate(options) 1326 | 1327 | await shallowPopulate(context) 1328 | assert(paramsFunctionCalled, 'params function was called') 1329 | assert(hasCalledFind, 'checks were made') 1330 | } 1331 | }) 1332 | 1333 | it('access `this` keyword in custom params-function which matches the data item', async () => { 1334 | for (const { type, dataResult } of beforeAfter) { 1335 | let paramsFunctionCalled = false 1336 | 1337 | const item = { 1338 | id: '11', 1339 | name: 'Dumb Stuff', 1340 | meta: { 1341 | postsId: ['111', '222', '333', 444, 555, '666'] 1342 | } 1343 | } 1344 | 1345 | const options = { 1346 | include: { 1347 | service: 'posts', 1348 | nameAs: 'posts', 1349 | params: function (params, context) { 1350 | assert.deepStrictEqual(this, item, 'item from data is passed as `this` keyword') 1351 | assert.strictEqual(context.method, 'create', 'we can pass the context to include') 1352 | params.method = context.method 1353 | paramsFunctionCalled = true 1354 | return params 1355 | } 1356 | } 1357 | } 1358 | 1359 | let hasCalledFind = false 1360 | const context = { 1361 | method: 'create', 1362 | type, 1363 | app: { 1364 | service () { 1365 | return { 1366 | find (params = {}) { 1367 | assert(params.method === 'create', 'we can manipulate the params based on the context') 1368 | hasCalledFind = true 1369 | return [] 1370 | } 1371 | } 1372 | } 1373 | }, 1374 | params: {}, 1375 | [dataResult]: item 1376 | } 1377 | 1378 | const shallowPopulate = makePopulate(options) 1379 | 1380 | await shallowPopulate(context) 1381 | assert(paramsFunctionCalled, 'params function was called') 1382 | assert(hasCalledFind, 'checks were made') 1383 | } 1384 | }) 1385 | 1386 | it('calls params-function per include and item', async () => { 1387 | for (const { type, dataResult } of beforeAfter) { 1388 | const items = [ 1389 | { 1390 | id: '333', 1391 | name: 'If I were a banana...' 1392 | }, 1393 | { 1394 | id: '111', 1395 | name: 'My Monkey and Me' 1396 | }, 1397 | { 1398 | id: 444, 1399 | name: 'One, two, three, one, two, three, drink' 1400 | } 1401 | ] 1402 | 1403 | let calledUsersParamsNTimes = 0 1404 | let calledCommentsParamsNTimes = 0 1405 | 1406 | const options = { 1407 | include: [ 1408 | { 1409 | service: 'users', 1410 | nameAs: 'users', 1411 | params: () => { 1412 | calledUsersParamsNTimes++ 1413 | return {} 1414 | } 1415 | }, 1416 | { 1417 | service: 'comments', 1418 | nameAs: 'comments', 1419 | params: () => { 1420 | calledCommentsParamsNTimes++ 1421 | return {} 1422 | } 1423 | } 1424 | ] 1425 | } 1426 | const context = { 1427 | app: { 1428 | service (path) { 1429 | return services[path] 1430 | } 1431 | }, 1432 | method: 'create', 1433 | type, 1434 | params: {}, 1435 | [dataResult]: items 1436 | } 1437 | 1438 | const shallowPopulate = makePopulate(options) 1439 | 1440 | await shallowPopulate(context) 1441 | assert(calledUsersParamsNTimes === items.length, 'params function for users was called n times') 1442 | assert(calledCommentsParamsNTimes === items.length, 'params function for comments was called n times') 1443 | } 1444 | }) 1445 | 1446 | it('wait for params function that returns a promise', async () => { 1447 | for (const { type, dataResult } of beforeAfter) { 1448 | let calledAsyncFunction = false 1449 | const options = { 1450 | include: { 1451 | service: 'posts', 1452 | nameAs: 'posts', 1453 | params: async (params, context) => { 1454 | await new Promise(resolve => { setTimeout(resolve, 200) }) 1455 | params.calledAsyncFunction = true 1456 | calledAsyncFunction = true 1457 | return params 1458 | } 1459 | } 1460 | } 1461 | 1462 | let hasCalledFind = false 1463 | 1464 | const context = { 1465 | method: 'create', 1466 | type, 1467 | app: { 1468 | service () { 1469 | return { 1470 | find (params = {}) { 1471 | assert(params.calledAsyncFunction, 'waited for async params function before find') 1472 | hasCalledFind = true 1473 | return [] 1474 | } 1475 | } 1476 | } 1477 | }, 1478 | params: {}, 1479 | [dataResult]: { 1480 | id: '1' 1481 | } 1482 | } 1483 | 1484 | const shallowPopulate = makePopulate(options) 1485 | await shallowPopulate(context) 1486 | assert(calledAsyncFunction, 'waited for async params function') 1487 | assert(hasCalledFind, 'checks were made') 1488 | } 1489 | }) 1490 | 1491 | it('can define params as array', async () => { 1492 | for (const { type, dataResult } of beforeAfter) { 1493 | let calledLastFunction = false 1494 | 1495 | const expected = { 1496 | paginate: false, 1497 | query: { 1498 | second: true, 1499 | fourth: true 1500 | }, 1501 | third: true, 1502 | fifth: true, 1503 | sixth: true 1504 | } 1505 | 1506 | const options = { 1507 | include: [ 1508 | { 1509 | service: 'users', 1510 | nameAs: 'users', 1511 | params: [ 1512 | {}, 1513 | { query: { second: true } }, 1514 | (params) => { 1515 | assert(params.query.second, 'walked through before') 1516 | params.third = true 1517 | return params 1518 | }, 1519 | (params) => { 1520 | assert(params.third, 'walked through before') 1521 | return { query: { fourth: true } } 1522 | }, 1523 | async (params) => { 1524 | assert(params.query.fourth, 'walked through before') 1525 | await new Promise(resolve => setTimeout(resolve, 200)) 1526 | params.fifth = true 1527 | return params 1528 | }, 1529 | (params, context) => { 1530 | assert(params.fifth, 'walked through before') 1531 | if (context.app) { 1532 | return { sixth: true } 1533 | } 1534 | }, 1535 | (params) => { 1536 | assert.deepStrictEqual(params, expected, 'params object is right') 1537 | calledLastFunction = true 1538 | return params 1539 | } 1540 | ] 1541 | } 1542 | ] 1543 | } 1544 | const context = { 1545 | app: { 1546 | service (path) { 1547 | return services[path] 1548 | } 1549 | }, 1550 | method: 'create', 1551 | type, 1552 | params: {}, 1553 | [dataResult]: [ 1554 | { 1555 | id: '1' 1556 | } 1557 | ] 1558 | } 1559 | 1560 | const shallowPopulate = makePopulate(options) 1561 | 1562 | await shallowPopulate(context) 1563 | assert(calledLastFunction, 'all params were called') 1564 | } 1565 | }) 1566 | 1567 | it('params function has third \'target\' object', async () => { 1568 | for (const { type, dataResult } of beforeAfter) { 1569 | let paramsFunctionCalled = false 1570 | 1571 | const options = { 1572 | include: { 1573 | service: 'posts', 1574 | nameAs: 'posts', 1575 | params: (params, context, target) => { 1576 | assert.ok(target, 'target is defined') 1577 | assert(target.service && typeof target.service.find === 'function', 'target has service') 1578 | assert.strictEqual(typeof target.path, 'string', 'target.path is string') 1579 | params.path = target.path 1580 | paramsFunctionCalled = true 1581 | return params 1582 | } 1583 | } 1584 | } 1585 | 1586 | let hasCalledFind = false 1587 | 1588 | const context = { 1589 | method: 'create', 1590 | type, 1591 | app: { 1592 | service () { 1593 | return { 1594 | find (params = {}) { 1595 | assert.strictEqual(params.path, 'posts', 'we can manipulate the params based on the target') 1596 | hasCalledFind = true 1597 | return [] 1598 | } 1599 | } 1600 | } 1601 | }, 1602 | params: {}, 1603 | [dataResult]: { 1604 | id: '1' 1605 | } 1606 | } 1607 | 1608 | const shallowPopulate = makePopulate(options) 1609 | 1610 | await shallowPopulate(context) 1611 | assert(paramsFunctionCalled, 'params function was called') 1612 | assert(hasCalledFind, 'checks were made') 1613 | } 1614 | }) 1615 | }) 1616 | }) 1617 | -------------------------------------------------------------------------------- /test/shallow-populate.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { shallowPopulate: makePopulate } = require('../lib/index') 3 | const memory = require('feathers-memory') 4 | const sift = require('sift').default 5 | 6 | const services = { 7 | posts: memory({ 8 | store: { 9 | 111: { id: '111', name: 'My Monkey and Me', userId: '11' }, 10 | 222: { id: '222', name: 'I forgot why I love you', userId: '11' }, 11 | 333: { id: '333', name: 'If I were a banana...', userId: '22' }, 12 | 444: { id: 444, name: 'One, two, three, one, two, three, drink', userId: '33' }, 13 | 555: { id: 555, name: 'Im gonna live like tomorrow doesnt exist', userId: 44 }, 14 | 666: { id: 666, name: 'I feel the love, feel the love', userId: 44 } 15 | } 16 | }), 17 | users: memory({ 18 | store: { 19 | 11: { id: '11', name: 'Joe Bloggs', postsId: ['111'], orgId: 'org1' }, 20 | 22: { id: '22', name: 'Jane Bloggs', postsId: '333', orgId: 'org2' }, 21 | 33: { id: '33', name: 'John Smith', postsId: ['111', '222'], orgId: 3 }, 22 | 44: { id: 44, name: 'Muhammad Li', postsId: [444, '555'], orgId: 4 } 23 | }, 24 | matcher: query => { 25 | return items => { 26 | const s = Object.assign({}, query) 27 | items = [].concat(items || []) 28 | return !!sift(s, items).length 29 | } 30 | } 31 | }), 32 | taskSets: memory({ 33 | store: { 34 | ts1: { id: 'ts1', name: 'Task Set 1' }, 35 | ts2: { id: 'ts2', name: 'Task Set 2' }, 36 | ts3: { id: 'ts3', name: 'Task Set 3' }, 37 | 4: { id: 4, name: 'Task Set 4' }, 38 | 5: { id: 5, name: 'Task Set 5' }, 39 | ts6: { id: 'ts6', name: 'Task Set 6' } 40 | } 41 | }), 42 | tasks: memory({ 43 | store: { 44 | task1: { id: 'task1', name: 'Task 1 - belongs with TaskSet1', taskSet: { taskSetId: 'ts1' }, userId: '11' }, 45 | task2: { id: 'task2', name: 'Task 2 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '22' }, 46 | task3: { id: 'task3', name: 'Task 3 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '11' }, 47 | task4: { id: 'task4', name: 'Task 4 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 48 | task5: { id: 'task5', name: 'Task 5 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 49 | task6: { id: 'task6', name: 'Task 6 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: '33' }, 50 | 7: { id: 7, name: 'Task 7 - belongs with TaskSet4', taskSet: { taskSetId: 4 } }, 51 | task8: { id: 'task8', name: 'Task 8 - belongs with TaskSet5', taskSet: { taskSetId: 5 } }, 52 | 9: { id: 9, name: 'Task 9 - belongs with TaskSet6', taskSet: { taskSetId: 'ts6' } } 53 | } 54 | }), 55 | comments: memory({ 56 | store: { 57 | 11111: { id: '11111', name: 'The Best Sounds This Summer', postsId: ['222'], userId: '11' }, 58 | 22222: { id: '22222', name: 'Chillstation', postsId: ['333'], userId: '22' }, 59 | 33333: { id: '33333', name: 'Hard Hitting Bass', postsId: ['111', '222', '333'], userId: '33' }, 60 | 44444: { id: 44444, name: 'As long as skies are blue', postsId: ['111', 444, '555'], userId: 44 } 61 | }, 62 | matcher: query => { 63 | return items => { 64 | const s = Object.assign({}, query) 65 | items = [].concat(items || []) 66 | return !!sift(s, items).length 67 | } 68 | } 69 | }), 70 | tags: memory({ 71 | store: { 72 | 1111: { id: '1111', name: 'Trombones', userId: '11' }, 73 | 2222: { id: '2222', name: 'Trumpets', userId: '11' }, 74 | 3333: { id: '3333', name: 'Drums', userId: '22' }, 75 | 4444: { id: 4444, name: 'Guitars', userId: '33' }, 76 | 5555: { id: 5555, name: 'Violins', userId: 44 } 77 | } 78 | }), 79 | orgs: memory({ 80 | store: { 81 | org1: { id: 'org1', name: 'Southern Utah', memberCount: 21 }, 82 | org2: { id: 'org2', name: 'Northern Utah', memberCount: 99 }, 83 | 3: { id: 3, name: 'Northern Arizona', memberCount: 42 }, 84 | 4: { id: 4, name: 'Southern Arizona', memberCount: 23 } 85 | } 86 | }), 87 | environments: memory({ 88 | store: { 89 | env1: { 90 | id: 'env1', 91 | name: 'Bryce Canyon National Park', 92 | orgs: [ 93 | { orgId: 'org1', orgName: 'Southern Utah' } 94 | ] 95 | }, 96 | env2: { 97 | id: 'env2', 98 | name: 'Zion National Park', 99 | orgs: [ 100 | { orgId: 'org1', orgName: 'Southern Utah' } 101 | ] 102 | }, 103 | env3: { 104 | id: 'env3', 105 | name: 'Canyonlands National Park', 106 | orgs: [ 107 | { orgId: 'org2', orgName: 'Northern Utah' } 108 | ] 109 | }, 110 | 4: { 111 | id: 4, 112 | name: 'Grand Canyon National Park', 113 | orgs: [ 114 | { orgId: 3, orgName: 'Northern Arizona' } 115 | ] 116 | }, 117 | 5: { 118 | id: '5', 119 | name: 'Organ Pipe Cactus National Monument', 120 | orgs: [ 121 | { orgId: 4, orgName: 'Southern Arizona' } 122 | ] 123 | }, 124 | 6: { 125 | id: 6, 126 | name: 'Antelope Canyon', 127 | orgs: [ 128 | { orgId: 'org1', orgName: 'Southern Utah' } 129 | ] 130 | } 131 | } 132 | }), 133 | authenticatedService: memory({ 134 | store: { 135 | task1: { id: 'task1', name: 'Task 1 - belongs with TaskSet1', taskSet: { taskSetId: 'ts1' }, userId: '11' }, 136 | task2: { id: 'task2', name: 'Task 2 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '22' }, 137 | task3: { id: 'task3', name: 'Task 3 - belongs with TaskSet2', taskSet: { taskSetId: 'ts2' }, userId: '11' }, 138 | task4: { id: 'task4', name: 'Task 4 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 139 | task5: { id: 'task5', name: 'Task 5 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: 44 }, 140 | task6: { id: 'task6', name: 'Task 6 - belongs with TaskSet3', taskSet: { taskSetId: 'ts3' }, userId: '33' }, 141 | 7: { id: 7, name: 'Task 7 - belongs with TaskSet4', taskSet: { taskSetId: 4 } }, 142 | task8: { id: 'task8', name: 'Task 8 - belongs with TaskSet5', taskSet: { taskSetId: 5 } }, 143 | 9: { id: 9, name: 'Task 9 - belongs with TaskSet6', taskSet: { taskSetId: 'ts6' } } 144 | } 145 | }) 146 | } 147 | 148 | const beforeAfter = [ 149 | { 150 | type: 'before', 151 | dataResult: 'data' 152 | }, 153 | { 154 | type: 'after', 155 | dataResult: 'result' 156 | } 157 | ] 158 | 159 | describe('populating thing', () => { 160 | it('does nothing when data is empty', async () => { 161 | for (const { type, dataResult } of beforeAfter) { 162 | const options = { 163 | include: { 164 | // from: 'users', 165 | service: 'posts', 166 | nameAs: 'post', 167 | keyHere: 'postIds', 168 | keyThere: 'id', 169 | asArray: false 170 | } 171 | } 172 | const context = { 173 | app: { 174 | service (path) { 175 | return services[path] 176 | } 177 | }, 178 | method: 'create', 179 | type, 180 | params: {}, 181 | [dataResult]: {} 182 | } 183 | 184 | const shallowPopulate = makePopulate(options) 185 | const response = await shallowPopulate(context) 186 | const result = response[dataResult] 187 | 188 | assert.deepStrictEqual(result, context[dataResult], `${type}: data should not be touched`) 189 | } 190 | }) 191 | 192 | describe('Single Record:', () => { 193 | describe('Single data/result - Single Relationship:', () => { 194 | it('as object', async () => { 195 | for (const { type, dataResult } of beforeAfter) { 196 | const options = { 197 | include: { 198 | // from: 'users', 199 | service: 'posts', 200 | nameAs: 'post', 201 | keyHere: 'postIds', 202 | keyThere: 'id', 203 | asArray: false 204 | } 205 | } 206 | const context = { 207 | app: { 208 | service (path) { 209 | return services[path] 210 | } 211 | }, 212 | method: 'create', 213 | type, 214 | params: {}, 215 | [dataResult]: { 216 | id: '11', 217 | name: 'Dumb Stuff', 218 | postIds: '111' 219 | } 220 | } 221 | 222 | const shallowPopulate = makePopulate(options) 223 | 224 | const response = await shallowPopulate(context) 225 | const result = response[dataResult] 226 | 227 | assert(result.post, `${type}: post should have been populated`) 228 | assert(!Array.isArray(result.post), `${type}: post should not be an array`) 229 | assert(result.post.id === '111', `${type}: post has correct id`) 230 | } 231 | }) 232 | 233 | it('as object when array', async () => { 234 | for (const { type, dataResult } of beforeAfter) { 235 | const options = { 236 | include: { 237 | // from: 'users', 238 | service: 'posts', 239 | nameAs: 'post', 240 | keyHere: 'postIds', 241 | keyThere: 'id', 242 | asArray: false 243 | } 244 | } 245 | const context = { 246 | app: { 247 | service (path) { 248 | return services[path] 249 | } 250 | }, 251 | method: 'create', 252 | type, 253 | params: {}, 254 | [dataResult]: { 255 | id: '11', 256 | name: 'Dumb Stuff', 257 | postIds: ['111', '222', 444, '555'] 258 | } 259 | } 260 | 261 | const shallowPopulate = makePopulate(options) 262 | 263 | const response = await shallowPopulate(context) 264 | const result = response[dataResult] 265 | 266 | assert(result.post, `${type}: post should have been populated`) 267 | assert(!Array.isArray(result.post), `${type}: post should not be an array`) 268 | assert(result.post.id === '111', `${type}: tpost has correct id`) 269 | } 270 | }) 271 | 272 | it('does nothing if no populate data on item', async () => { 273 | for (const { type, dataResult } of beforeAfter) { 274 | const options = { 275 | include: { 276 | // from: 'users', 277 | service: 'posts', 278 | nameAs: 'posts', 279 | keyHere: 'postsId', 280 | keyThere: 'id', 281 | asArray: false 282 | } 283 | } 284 | const context = { 285 | app: { 286 | service (path) { 287 | return services[path] 288 | } 289 | }, 290 | method: 'create', 291 | type, 292 | params: {}, 293 | [dataResult]: { 294 | id: '11', 295 | name: 'Dumb Stuff' 296 | } 297 | } 298 | 299 | const shallowPopulate = makePopulate(options) 300 | 301 | const response = await shallowPopulate(context) 302 | const result = response[dataResult] 303 | 304 | assert(!result.posts, `${type}: posts should have not been populated`) 305 | } 306 | }) 307 | 308 | it('does nothing if keyHere of related item is null', async () => { 309 | for (const { type, dataResult } of beforeAfter) { 310 | const options = { 311 | include: { 312 | // from: 'users', 313 | service: 'posts', 314 | nameAs: 'posts', 315 | keyHere: 'postsId', 316 | keyThere: 'id', 317 | asArray: false 318 | } 319 | } 320 | const context = { 321 | app: { 322 | service (path) { 323 | return services[path] 324 | } 325 | }, 326 | method: 'create', 327 | type, 328 | params: {}, 329 | [dataResult]: { 330 | id: '11', 331 | name: 'Dumb Stuff', 332 | postsId: null 333 | } 334 | } 335 | 336 | const shallowPopulate = makePopulate(options) 337 | 338 | const response = await shallowPopulate(context) 339 | const result = response[dataResult] 340 | 341 | assert(!Object.prototype.hasOwnProperty.call(result, 'posts'), `${type}: post should have not been populated`) 342 | } 343 | }) 344 | 345 | it('populates from local keys dot notation', async () => { 346 | for (const { type, dataResult } of beforeAfter) { 347 | const options = { 348 | include: { 349 | // from: 'users', 350 | service: 'posts', 351 | nameAs: 'meta.posts', 352 | keyHere: 'meta.postsId', 353 | keyThere: 'id' 354 | } 355 | } 356 | const context = { 357 | app: { 358 | service (path) { 359 | return services[path] 360 | } 361 | }, 362 | method: 'create', 363 | type, 364 | params: {}, 365 | [dataResult]: { 366 | id: '11', 367 | name: 'Dumb Stuff', 368 | meta: { 369 | postsId: ['111', 444] 370 | } 371 | } 372 | } 373 | 374 | const shallowPopulate = makePopulate(options) 375 | 376 | const response = await shallowPopulate(context) 377 | const result = response[dataResult] 378 | assert(result.meta.posts.length, `${type}: posts should have been populated`) 379 | } 380 | }) 381 | 382 | it('populates from local keys', async () => { 383 | for (const { type, dataResult } of beforeAfter) { 384 | const options = { 385 | include: { 386 | // from: 'users', 387 | service: 'posts', 388 | nameAs: 'posts', 389 | keyHere: 'postsId', 390 | keyThere: 'id' 391 | } 392 | } 393 | const context = { 394 | app: { 395 | service (path) { 396 | return services[path] 397 | } 398 | }, 399 | method: 'create', 400 | type, 401 | params: {}, 402 | [dataResult]: { 403 | id: '11', 404 | name: 'Dumb Stuff', 405 | postsId: ['111', '222', '333', 444, 555, '666'] 406 | } 407 | } 408 | 409 | const shallowPopulate = makePopulate(options) 410 | const response = await shallowPopulate(context) 411 | const result = response[dataResult] 412 | 413 | assert(result.posts.length, `${type}: posts should have been populated`) 414 | } 415 | }) 416 | 417 | it.skip('populates empty nameAs property if no relatedItems', async () => { 418 | for (const { type, dataResult } of beforeAfter) { 419 | const options = { 420 | include: { 421 | // from: 'users', 422 | service: 'posts', 423 | nameAs: 'posts', 424 | keyHere: 'postsId', 425 | keyThere: 'id' 426 | } 427 | } 428 | const context = { 429 | app: { 430 | service (path) { 431 | return services[path] 432 | } 433 | }, 434 | method: 'create', 435 | type, 436 | params: {}, 437 | [dataResult]: { 438 | id: '11', 439 | name: 'Dumb Stuff' 440 | } 441 | } 442 | 443 | const shallowPopulate = makePopulate(options) 444 | const response = await shallowPopulate(context) 445 | const result = response[dataResult] 446 | 447 | assert(result.posts, `${type}: posts should have been populated`) 448 | } 449 | }) 450 | 451 | it('populates from foreign keys', async () => { 452 | for (const { type, dataResult } of beforeAfter) { 453 | const options = { 454 | include: { 455 | // from: 'posts', 456 | service: 'users', 457 | nameAs: 'users', 458 | keyHere: 'id', 459 | keyThere: 'postsId' 460 | } 461 | } 462 | const context = { 463 | app: { 464 | service (path) { 465 | return services[path] 466 | } 467 | }, 468 | method: 'create', 469 | type, 470 | params: {}, 471 | [dataResult]: { 472 | id: '111', 473 | name: 'My Monkey and Me' 474 | } 475 | } 476 | 477 | const shallowPopulate = makePopulate(options) 478 | const response = await shallowPopulate(context) 479 | const result = response[dataResult] 480 | 481 | assert(result.users, `${type}: should have users property`) 482 | } 483 | }) 484 | 485 | it('$select works without $keyThere', async () => { 486 | for (const { type, dataResult } of beforeAfter) { 487 | const options = { 488 | include: { 489 | // from: 'users', 490 | service: 'posts', 491 | nameAs: 'posts', 492 | keyHere: 'postsId', 493 | keyThere: 'id', 494 | params: { query: { $select: ['name'] } } 495 | } 496 | } 497 | const context = { 498 | app: { 499 | service (path) { 500 | return services[path] 501 | } 502 | }, 503 | method: 'create', 504 | type, 505 | params: {}, 506 | [dataResult]: { 507 | id: '11', 508 | name: 'Dumb Stuff', 509 | postsId: ['111', '222', '333', 444, 555, '666'] 510 | } 511 | } 512 | 513 | const shallowPopulate = makePopulate(options) 514 | const response = await shallowPopulate(context) 515 | const result = response[dataResult] 516 | 517 | assert(result.posts.length, `${type}: posts should have been populated`) 518 | result.posts.forEach(post => { 519 | const { name, ...rest } = post 520 | assert.deepStrictEqual(rest, {}, `${type}: only has name property`) 521 | }) 522 | } 523 | }) 524 | 525 | it('$skip works as intended', async () => { 526 | for (const { type, dataResult } of beforeAfter) { 527 | const options1 = { 528 | include: { 529 | // from: 'users', 530 | service: 'posts', 531 | nameAs: 'posts', 532 | keyHere: 'postsId', 533 | keyThere: 'id', 534 | params: { query: { } } 535 | } 536 | } 537 | const context1 = { 538 | app: { 539 | service (path) { 540 | return services[path] 541 | } 542 | }, 543 | method: 'create', 544 | type, 545 | params: {}, 546 | [dataResult]: { 547 | id: '11', 548 | name: 'Dumb Stuff', 549 | postsId: ['111', '222', '333', 444, 555, '666'] 550 | } 551 | } 552 | 553 | const shallowPopulate1 = makePopulate(options1) 554 | const response1 = await shallowPopulate1(context1) 555 | const user1 = response1[dataResult] 556 | 557 | const options2 = { 558 | include: { 559 | // from: 'users', 560 | service: 'posts', 561 | nameAs: 'posts', 562 | keyHere: 'postsId', 563 | keyThere: 'id', 564 | params: { query: { $skip: 1 } } 565 | } 566 | } 567 | const context2 = { 568 | app: { 569 | service (path) { 570 | return services[path] 571 | } 572 | }, 573 | method: 'create', 574 | type, 575 | params: {}, 576 | [dataResult]: { 577 | id: '11', 578 | name: 'Dumb Stuff', 579 | postsId: ['111', '222', '333', 444, 555, '666'] 580 | } 581 | } 582 | 583 | const shallowPopulate2 = makePopulate(options2) 584 | const response2 = await shallowPopulate2(context2) 585 | const user2 = response2[dataResult] 586 | 587 | assert(user1.posts.length - 1 === user2.posts.length, `${type}: skipped 1 item for user2`) 588 | } 589 | }) 590 | 591 | it('$limit works as intended', async () => { 592 | for (const { type, dataResult } of beforeAfter) { 593 | const options1 = { 594 | include: { 595 | // from: 'users', 596 | service: 'posts', 597 | nameAs: 'posts', 598 | keyHere: 'postsId', 599 | keyThere: 'id', 600 | params: { query: { } } 601 | } 602 | } 603 | const context1 = { 604 | app: { 605 | service (path) { 606 | return services[path] 607 | } 608 | }, 609 | method: 'create', 610 | type, 611 | params: {}, 612 | [dataResult]: { 613 | id: '11', 614 | name: 'Dumb Stuff', 615 | postsId: ['111', '222', '333', 444, 555, '666'] 616 | } 617 | } 618 | 619 | const shallowPopulate1 = makePopulate(options1) 620 | const response1 = await shallowPopulate1(context1) 621 | const user1 = response1[dataResult] 622 | 623 | const options2 = { 624 | include: { 625 | // from: 'users', 626 | service: 'posts', 627 | nameAs: 'posts', 628 | keyHere: 'postsId', 629 | keyThere: 'id', 630 | params: { query: { $limit: 2 } } 631 | } 632 | } 633 | const context2 = { 634 | app: { 635 | service (path) { 636 | return services[path] 637 | } 638 | }, 639 | method: 'create', 640 | type, 641 | params: {}, 642 | [dataResult]: { 643 | id: '11', 644 | name: 'Dumb Stuff', 645 | postsId: ['111', '222', '333', 444, 555, '666'] 646 | } 647 | } 648 | 649 | const shallowPopulate2 = makePopulate(options2) 650 | const response2 = await shallowPopulate2(context2) 651 | const user2 = response2[dataResult] 652 | 653 | assert(user1.posts.length > user2.posts.length, `${type}: user1 has more posts than user2`) 654 | assert(user2.posts.length === 2, `${type}: limited posts for user2`) 655 | } 656 | }) 657 | 658 | describe('requestPerItem: true', () => { 659 | it('populates with custom params $select works', async () => { 660 | for (const { type, dataResult } of beforeAfter) { 661 | const options = { 662 | include: { 663 | // from: 'posts', 664 | service: 'tasks', 665 | nameAs: 'tasks', 666 | params: (params, context) => { 667 | return { query: { $select: ['id'] } } 668 | } 669 | } 670 | } 671 | const context = { 672 | app: { 673 | service (path) { 674 | return services[path] 675 | } 676 | }, 677 | method: 'create', 678 | type, 679 | params: {}, 680 | // Data for a single track 681 | [dataResult]: { 682 | id: '111', 683 | name: 'My Monkey and Me' 684 | } 685 | } 686 | 687 | const shallowPopulate = makePopulate(options) 688 | const response = await shallowPopulate(context) 689 | const result = response[dataResult] 690 | 691 | const expected = Object.values(services.tasks.store).map(x => { return { id: x.id } }) 692 | assert.deepStrictEqual(result.tasks, expected, `${type}: populated all tasks with only 'id' attribute`) 693 | } 694 | }) 695 | 696 | it('populates with custom params function', async () => { 697 | for (const { type, dataResult } of beforeAfter) { 698 | const options = { 699 | include: { 700 | // from: 'posts', 701 | service: 'tasks', 702 | nameAs: 'tasks', 703 | params: function (params, context) { 704 | return { query: { userId: this.userId } } 705 | } 706 | } 707 | } 708 | const context = { 709 | app: { 710 | service (path) { 711 | return services[path] 712 | } 713 | }, 714 | method: 'create', 715 | type, 716 | params: {}, 717 | // Data for a single track 718 | [dataResult]: { 719 | id: '111', 720 | name: 'My Monkey and Me', 721 | userId: '11' 722 | } 723 | } 724 | 725 | const shallowPopulate = makePopulate(options) 726 | const response = await shallowPopulate(context) 727 | const result = response[dataResult] 728 | 729 | const expectedTasks = Object.values(services.tasks.store).filter(x => x.userId === '11') 730 | assert.deepStrictEqual(result.tasks, expectedTasks, `${type}: tasks populated correctly`) 731 | } 732 | }) 733 | }) 734 | 735 | it.skip('handles missing _id on create', async () => {}) 736 | }) 737 | 738 | describe('Single data/result - Multiple Relationship:', () => { 739 | it('as object', async () => { 740 | for (const { type, dataResult } of beforeAfter) { 741 | const options = { 742 | include: [ 743 | { 744 | // from: 'users', 745 | service: 'tags', 746 | nameAs: 'tags', 747 | keyHere: 'tagIds', 748 | keyThere: 'id' 749 | }, 750 | { 751 | // from: 'users', 752 | service: 'posts', 753 | nameAs: 'post', 754 | keyHere: 'postIds', 755 | keyThere: 'id', 756 | asArray: false 757 | } 758 | ] 759 | } 760 | const context = { 761 | app: { 762 | service (path) { 763 | return services[path] 764 | } 765 | }, 766 | method: 'create', 767 | type, 768 | params: {}, 769 | [dataResult]: { 770 | id: '11', 771 | name: 'Dumb Stuff', 772 | postIds: '111', 773 | tagIds: ['1111', 4444] 774 | } 775 | } 776 | 777 | const shallowPopulate = makePopulate(options) 778 | 779 | const response = await shallowPopulate(context) 780 | const result = response[dataResult] 781 | assert(result.post, 'post should have been populated') 782 | assert(!Array.isArray(result.post), 'post should not be an array') 783 | assert(result.post.id === '111', 'post has correct id') 784 | assert(Array.isArray(result.tags), 'tags is an array') 785 | } 786 | }) 787 | 788 | it('as object when array', async () => { 789 | for (const { type, dataResult } of beforeAfter) { 790 | const options = { 791 | include: [ 792 | { 793 | // from: 'users', 794 | service: 'tags', 795 | nameAs: 'tags', 796 | keyHere: 'tagIds', 797 | keyThere: 'id' 798 | }, 799 | { 800 | // from: 'users', 801 | service: 'posts', 802 | nameAs: 'post', 803 | keyHere: 'postIds', 804 | keyThere: 'id', 805 | asArray: false 806 | } 807 | ] 808 | } 809 | const context = { 810 | app: { 811 | service (path) { 812 | return services[path] 813 | } 814 | }, 815 | method: 'create', 816 | type, 817 | params: {}, 818 | [dataResult]: { 819 | id: '11', 820 | name: 'Dumb Stuff', 821 | postIds: ['111', '222', 444], 822 | tagIds: ['1111', '3333', 4444] 823 | } 824 | } 825 | 826 | const shallowPopulate = makePopulate(options) 827 | 828 | const response = await shallowPopulate(context) 829 | const result = response[dataResult] 830 | assert(result.post, 'post should have been populated') 831 | assert(!Array.isArray(result.post), 'post should not be an array') 832 | assert(result.post.id === '111', 'post has correct id') 833 | assert(Array.isArray(result.tags), 'tags is an array') 834 | } 835 | }) 836 | 837 | it('does nothing if some populate data on item does not exist', async () => { 838 | for (const { type, dataResult } of beforeAfter) { 839 | const options = { 840 | include: [ 841 | { 842 | // from: 'users', 843 | service: 'posts', 844 | nameAs: 'posts', 845 | keyHere: 'postsId', 846 | keyThere: 'id' 847 | }, 848 | { 849 | // from: 'users', 850 | service: 'tags', 851 | nameAs: 'tags', 852 | keyHere: 'tagIds', 853 | keyThere: 'id' 854 | } 855 | ] 856 | } 857 | const context = { 858 | app: { 859 | service (path) { 860 | return services[path] 861 | } 862 | }, 863 | method: 'create', 864 | type, 865 | params: {}, 866 | [dataResult]: { 867 | id: '11', 868 | name: 'Dumb Stuff', 869 | tagIds: ['1111', '3333', 4444] 870 | } 871 | } 872 | 873 | const shallowPopulate = makePopulate(options) 874 | 875 | const response = await shallowPopulate(context) 876 | const result = response[dataResult] 877 | assert(!result.posts, 'posts should have not been populated') 878 | assert(result.tags.length === 3, 'tags have been populated') 879 | } 880 | }) 881 | 882 | it('populates from local keys dot notation', async () => { 883 | for (const { type, dataResult } of beforeAfter) { 884 | const options = { 885 | include: [ 886 | { 887 | // from: 'users', 888 | service: 'posts', 889 | nameAs: 'meta.posts', 890 | keyHere: 'meta.postsId', 891 | keyThere: 'id' 892 | }, 893 | { 894 | // from: 'users', 895 | service: 'tags', 896 | nameAs: 'meta.tags', 897 | keyHere: 'meta.tagIds', 898 | keyThere: 'id' 899 | } 900 | ] 901 | } 902 | const context = { 903 | app: { 904 | service (path) { 905 | return services[path] 906 | } 907 | }, 908 | method: 'create', 909 | type, 910 | params: {}, 911 | [dataResult]: { 912 | id: '11', 913 | name: 'Dumb Stuff', 914 | meta: { 915 | postsId: ['111', '222', '333', 444], 916 | tagIds: ['1111', '3333', 4444] 917 | } 918 | } 919 | } 920 | 921 | const shallowPopulate = makePopulate(options) 922 | 923 | const response = await shallowPopulate(context) 924 | const result = response[dataResult] 925 | assert(result.meta.posts.length, 'posts should have been populated') 926 | assert(result.meta.tags.length, 'posts should have been populated') 927 | } 928 | }) 929 | 930 | it('populates from local keys', async () => { 931 | for (const { type, dataResult } of beforeAfter) { 932 | const options = { 933 | include: [ 934 | { 935 | // from: 'users', 936 | service: 'posts', 937 | nameAs: 'posts', 938 | keyHere: 'postsId', 939 | keyThere: 'id' 940 | }, 941 | { 942 | // from: 'users', 943 | service: 'tags', 944 | nameAs: 'tags', 945 | keyHere: 'tagIds', 946 | keyThere: 'id' 947 | } 948 | ] 949 | } 950 | const context = { 951 | app: { 952 | service (path) { 953 | return services[path] 954 | } 955 | }, 956 | method: 'create', 957 | type, 958 | params: {}, 959 | [dataResult]: { 960 | id: '11', 961 | name: 'Dumb Stuff', 962 | postsId: ['111', '222', '333', 444], 963 | tagIds: ['1111', '3333', 4444] 964 | } 965 | } 966 | 967 | const shallowPopulate = makePopulate(options) 968 | 969 | const response = await shallowPopulate(context) 970 | const result = response[dataResult] 971 | assert(result.posts.length, 'posts should have been populated') 972 | } 973 | }) 974 | 975 | it('populates from foreign keys', async () => { 976 | for (const { type, dataResult } of beforeAfter) { 977 | const options = { 978 | include: [ 979 | { 980 | service: 'users', 981 | nameAs: 'users', 982 | keyHere: 'id', 983 | keyThere: 'postsId' 984 | }, 985 | { 986 | service: 'comments', 987 | nameAs: 'comments', 988 | keyHere: 'id', 989 | keyThere: 'postsId' 990 | } 991 | ] 992 | } 993 | const context = { 994 | app: { 995 | service (path) { 996 | return services[path] 997 | } 998 | }, 999 | method: 'create', 1000 | type, 1001 | params: {}, 1002 | [dataResult]: { 1003 | id: '333', 1004 | name: 'If I were a banana...' 1005 | } 1006 | } 1007 | 1008 | const shallowPopulate = makePopulate(options) 1009 | 1010 | const response = await shallowPopulate(context) 1011 | const result = response[dataResult] 1012 | assert(result.users.length === 1, 'data should have correct users data') 1013 | assert(result.comments.length === 2, 'data should have correct comments data') 1014 | } 1015 | }) 1016 | 1017 | it('$select works without $keyThere', async () => { 1018 | for (const { type, dataResult } of beforeAfter) { 1019 | const options = { 1020 | include: [ 1021 | { 1022 | service: 'users', 1023 | nameAs: 'users', 1024 | keyHere: 'id', 1025 | keyThere: 'postsId', 1026 | params: { query: { $select: ['name'] } } 1027 | }, 1028 | { 1029 | service: 'comments', 1030 | nameAs: 'comments', 1031 | keyHere: 'id', 1032 | keyThere: 'postsId' 1033 | } 1034 | ] 1035 | } 1036 | const context = { 1037 | app: { 1038 | service (path) { 1039 | return services[path] 1040 | } 1041 | }, 1042 | method: 'create', 1043 | type, 1044 | params: {}, 1045 | [dataResult]: { 1046 | id: '333', 1047 | name: 'If I were a banana...' 1048 | } 1049 | } 1050 | 1051 | const shallowPopulate = makePopulate(options) 1052 | 1053 | const response = await shallowPopulate(context) 1054 | const result = response[dataResult] 1055 | assert(result.users.length, 'posts should have been populated') 1056 | result.users.forEach(user => { 1057 | const { name, ...rest } = user 1058 | assert.deepStrictEqual(rest, {}, 'only has name property') 1059 | }) 1060 | 1061 | const expectedComments = Object.values(services.comments.store).filter(comment => comment.postsId.includes('333')) 1062 | 1063 | assert(result.comments.length === 2, 'data should have correct comments data') 1064 | assert.deepStrictEqual(result.comments, expectedComments, 'comments are populated complete') 1065 | } 1066 | }) 1067 | 1068 | it('$skip works as intended', async () => { 1069 | for (const { type, dataResult } of beforeAfter) { 1070 | const options1 = { 1071 | include: [ 1072 | { 1073 | // from: 'users', 1074 | service: 'posts', 1075 | nameAs: 'posts', 1076 | keyHere: 'postsId', 1077 | keyThere: 'id', 1078 | params: { query: { } } 1079 | }, 1080 | { 1081 | service: 'comments', 1082 | nameAs: 'comments', 1083 | keyHere: 'id', 1084 | keyThere: 'userId' 1085 | } 1086 | ] 1087 | } 1088 | 1089 | const context1 = { 1090 | app: { 1091 | service (path) { 1092 | return services[path] 1093 | } 1094 | }, 1095 | method: 'create', 1096 | type, 1097 | params: {}, 1098 | [dataResult]: { 1099 | id: '11', 1100 | name: 'Dumb Stuff', 1101 | postsId: ['111', '222', '333', 444, 555, '666'] 1102 | } 1103 | } 1104 | 1105 | const shallowPopulate1 = makePopulate(options1) 1106 | 1107 | const { [dataResult]: user1 } = await shallowPopulate1(context1) 1108 | 1109 | const options2 = { 1110 | include: [ 1111 | { 1112 | // from: 'users', 1113 | service: 'posts', 1114 | nameAs: 'posts', 1115 | keyHere: 'postsId', 1116 | keyThere: 'id', 1117 | params: { query: { $skip: 1 } } 1118 | }, 1119 | { 1120 | service: 'comments', 1121 | nameAs: 'comments', 1122 | keyHere: 'id', 1123 | keyThere: 'userId' 1124 | } 1125 | ] 1126 | } 1127 | const context2 = { 1128 | app: { 1129 | service (path) { 1130 | return services[path] 1131 | } 1132 | }, 1133 | method: 'create', 1134 | type, 1135 | params: {}, 1136 | [dataResult]: { 1137 | id: '11', 1138 | name: 'Dumb Stuff', 1139 | postsId: ['111', '222', '333', 444, 555, '666'] 1140 | } 1141 | } 1142 | 1143 | const shallowPopulate2 = makePopulate(options2) 1144 | 1145 | const { [dataResult]: user2 } = await shallowPopulate2(context2) 1146 | 1147 | assert(user1.posts.length - 1 === user2.posts.length, 'skipped 1 item for user2') 1148 | assert(user1.comments.length > 0, 'at least some comments') 1149 | assert.deepStrictEqual(user1.comments, user2.comments, 'comments are populated the same') 1150 | } 1151 | }) 1152 | 1153 | it('$limit works as intended', async () => { 1154 | for (const { type, dataResult } of beforeAfter) { 1155 | const options1 = { 1156 | include: [ 1157 | { 1158 | // from: 'users', 1159 | service: 'posts', 1160 | nameAs: 'posts', 1161 | keyHere: 'postsId', 1162 | keyThere: 'id' 1163 | }, 1164 | { 1165 | service: 'comments', 1166 | nameAs: 'comments', 1167 | keyHere: 'id', 1168 | keyThere: 'userId' 1169 | } 1170 | ] 1171 | } 1172 | const context1 = { 1173 | app: { 1174 | service (path) { 1175 | return services[path] 1176 | } 1177 | }, 1178 | method: 'create', 1179 | type, 1180 | params: {}, 1181 | [dataResult]: { 1182 | id: '11', 1183 | name: 'Dumb Stuff', 1184 | postsId: ['111', '222', '333', 444, 555, '666'] 1185 | } 1186 | } 1187 | 1188 | const shallowPopulate1 = makePopulate(options1) 1189 | 1190 | const { [dataResult]: user1 } = await shallowPopulate1(context1) 1191 | 1192 | const options2 = { 1193 | include: [ 1194 | { 1195 | // from: 'users', 1196 | service: 'posts', 1197 | nameAs: 'posts', 1198 | keyHere: 'postsId', 1199 | keyThere: 'id', 1200 | params: { query: { $limit: 1 } } 1201 | }, 1202 | { 1203 | service: 'comments', 1204 | nameAs: 'comments', 1205 | keyHere: 'id', 1206 | keyThere: 'userId' 1207 | } 1208 | ] 1209 | } 1210 | const context2 = { 1211 | app: { 1212 | service (path) { 1213 | return services[path] 1214 | } 1215 | }, 1216 | method: 'create', 1217 | type, 1218 | params: {}, 1219 | [dataResult]: { 1220 | id: '11', 1221 | name: 'Dumb Stuff', 1222 | postsId: ['111', '222', '333', 444, 555, '666'] 1223 | } 1224 | } 1225 | 1226 | const shallowPopulate2 = makePopulate(options2) 1227 | 1228 | const { [dataResult]: user2 } = await shallowPopulate2(context2) 1229 | 1230 | assert(user1.posts.length > user2.posts.length, 'user1 has more posts than user2') 1231 | assert(user2.posts.length === 1, 'limited posts for user2') 1232 | assert.deepStrictEqual(user1.comments, user2.comments, 'comments are the same') 1233 | } 1234 | }) 1235 | 1236 | describe('requestPerItem: true', () => { 1237 | it('populates with custom params $select works', async () => { 1238 | for (const { type, dataResult } of beforeAfter) { 1239 | const options = { 1240 | include: [ 1241 | { 1242 | // from: 'posts', 1243 | service: 'tasks', 1244 | nameAs: 'tasks', 1245 | params: (params, context) => { return { query: { $select: ['id'] } } } 1246 | }, 1247 | { 1248 | // from: 'posts', 1249 | service: 'comments', 1250 | nameAs: 'comments', 1251 | params: (params, context) => { return { query: { $select: ['id'] } } } 1252 | } 1253 | ] 1254 | } 1255 | const context = { 1256 | app: { 1257 | service (path) { 1258 | return services[path] 1259 | } 1260 | }, 1261 | method: 'create', 1262 | type, 1263 | params: {}, 1264 | // Data for a single track 1265 | [dataResult]: { 1266 | id: '111', 1267 | name: 'My Monkey and Me' 1268 | } 1269 | } 1270 | 1271 | const shallowPopulate = makePopulate(options) 1272 | 1273 | const response = await shallowPopulate(context) 1274 | const result = response[dataResult] 1275 | const expectedTasks = Object.values(services.tasks.store).map(x => { return { id: x.id } }) 1276 | assert.deepStrictEqual(result.tasks, expectedTasks, 'populated all tasks with only `id` attribute') 1277 | 1278 | const expectedComments = Object.values(services.comments.store).map(x => { return { id: x.id } }) 1279 | assert.deepStrictEqual(result.comments, expectedComments, 'populated all tasks with only `id` attribute') 1280 | } 1281 | }) 1282 | 1283 | it('populates with custom params function', async () => { 1284 | for (const { type, dataResult } of beforeAfter) { 1285 | const options = { 1286 | include: [ 1287 | { 1288 | // from: 'posts', 1289 | service: 'tasks', 1290 | nameAs: 'tasks', 1291 | params: function (params, context) { 1292 | return { query: { userId: this.userId } } 1293 | } 1294 | }, 1295 | { 1296 | // from: 'posts', 1297 | service: 'tags', 1298 | nameAs: 'tags', 1299 | params: function (params, context) { 1300 | return { 1301 | query: { 1302 | userId: this.userId, 1303 | $select: ['id'] 1304 | } 1305 | } 1306 | } 1307 | }, 1308 | { 1309 | service: 'orgs', 1310 | nameAs: 'org', 1311 | asArray: false, 1312 | params: async function (params, context) { 1313 | const user = await context.app.service('users').get(this.userId) 1314 | return { query: { id: user.orgId } } 1315 | } 1316 | }, 1317 | { 1318 | // from: 'posts', 1319 | service: 'tags', 1320 | nameAs: 'tag', 1321 | asArray: false, 1322 | params: [ 1323 | function (params, context) { 1324 | return { 1325 | query: { 1326 | userId: this.userId 1327 | } 1328 | } 1329 | }, 1330 | { query: { $select: ['id'] } } 1331 | ] 1332 | }, 1333 | { 1334 | // from: 'posts', 1335 | service: 'tasks', 1336 | nameAs: 'nullTask', 1337 | asArray: false, 1338 | params: function (params, context) { 1339 | return undefined 1340 | } 1341 | }, 1342 | { 1343 | // from: 'posts', 1344 | service: 'tasks', 1345 | nameAs: 'emptyTasks', 1346 | params: function (params, context) { 1347 | return undefined 1348 | } 1349 | } 1350 | ] 1351 | } 1352 | const context = { 1353 | app: { 1354 | service (path) { 1355 | return services[path] 1356 | } 1357 | }, 1358 | method: 'create', 1359 | type, 1360 | params: {}, 1361 | // Data for a single track 1362 | [dataResult]: { 1363 | id: '111', 1364 | name: 'My Monkey and Me', 1365 | userId: '11' 1366 | } 1367 | } 1368 | 1369 | const shallowPopulate = makePopulate(options) 1370 | 1371 | const response = await shallowPopulate(context) 1372 | const result = response[dataResult] 1373 | const expectedTasks = Object.values(services.tasks.store).filter(x => x.userId === '11') 1374 | const expectedTags = Object.values(services.tags.store).filter(x => x.userId === result.userId).map(x => { return { id: x.id } }) 1375 | const user = Object.values(services.users.store).filter(x => x.id === result.userId)[0] 1376 | const expectedOrg = Object.values(services.orgs.store).filter(x => x.id === user.orgId)[0] 1377 | const expectedTag = expectedTags[0] 1378 | assert.deepStrictEqual(result.tasks, expectedTasks, 'tasks populated correctly') 1379 | assert.deepStrictEqual(result.tags, expectedTags, 'tags populated correctly') 1380 | assert.deepStrictEqual(result.org, expectedOrg, 'populated org correctly') 1381 | assert.deepStrictEqual(result.tag, expectedTag, 'single tag populated correctly') 1382 | assert(result.nullTask === null, 'set default to null') 1383 | assert.deepStrictEqual(result.emptyTasks, [], 'set default to empty array') 1384 | } 1385 | }) 1386 | }) 1387 | 1388 | it.skip('handles missing _id on create', async () => {}) 1389 | }) 1390 | }) 1391 | 1392 | describe('Multiple Records:', () => { 1393 | describe('Multiple data/result - Single Relationship:', () => { 1394 | it('as object', async () => { 1395 | for (const { type, dataResult } of beforeAfter) { 1396 | const options = { 1397 | include: { 1398 | // from: 'users', 1399 | service: 'posts', 1400 | nameAs: 'post', 1401 | keyHere: 'postIds', 1402 | keyThere: 'id', 1403 | asArray: false 1404 | } 1405 | } 1406 | const context = { 1407 | app: { 1408 | service (path) { 1409 | return services[path] 1410 | } 1411 | }, 1412 | method: 'create', 1413 | type, 1414 | params: {}, 1415 | [dataResult]: [ 1416 | { 1417 | id: '11', 1418 | name: 'Dumb Stuff', 1419 | postIds: ['111', '222', 444] 1420 | }, 1421 | { 1422 | id: '22', 1423 | name: 'Smart Stuff', 1424 | postIds: '222' 1425 | }, 1426 | { 1427 | id: '33', 1428 | name: 'Some Stuff', 1429 | postIds: ['111', 444] 1430 | } 1431 | ] 1432 | } 1433 | 1434 | const shallowPopulate = makePopulate(options) 1435 | 1436 | const response = await shallowPopulate(context) 1437 | const result = response[dataResult] 1438 | assert(result[0].post, 'post should have been populated') 1439 | assert(!Array.isArray(result[0].post), 'post should not be an array') 1440 | assert(result[0].post.id === '111', 'post has correct id') 1441 | assert(result[1].post, 'post should have been populated') 1442 | assert(!Array.isArray(result[1].post), 'post should not be an array') 1443 | assert(result[1].post.id === '222', 'post has correct id') 1444 | } 1445 | }) 1446 | 1447 | it('as object when array', async () => { 1448 | for (const { type, dataResult } of beforeAfter) { 1449 | const options = { 1450 | include: { 1451 | // from: 'users', 1452 | service: 'posts', 1453 | nameAs: 'post', 1454 | keyHere: 'postIds', 1455 | keyThere: 'id', 1456 | asArray: false 1457 | } 1458 | } 1459 | const context = { 1460 | app: { 1461 | service (path) { 1462 | return services[path] 1463 | } 1464 | }, 1465 | method: 'create', 1466 | type, 1467 | params: {}, 1468 | [dataResult]: [ 1469 | { 1470 | id: '11', 1471 | name: 'Dumb Stuff', 1472 | postIds: ['111', '222', 444] 1473 | }, 1474 | { 1475 | id: '22', 1476 | name: 'Smart Stuff', 1477 | postIds: ['222', '111', 444] 1478 | }, 1479 | { 1480 | id: 44, 1481 | name: 'Just Stuff', 1482 | postIds: [444, 111, '222'] 1483 | } 1484 | ] 1485 | } 1486 | 1487 | const shallowPopulate = makePopulate(options) 1488 | 1489 | const response = await shallowPopulate(context) 1490 | const result = response[dataResult] 1491 | assert(result[0].post, 'post should have been populated') 1492 | assert(!Array.isArray(result[0].post), 'post should not be an array') 1493 | assert(result[0].post.id === '111', 'post has correct id') 1494 | assert(result[1].post, 'post should have been populated') 1495 | assert(!Array.isArray(result[1].post), 'post should not be an array') 1496 | assert(result[1].post.id === '222', 'post has correct id') 1497 | } 1498 | }) 1499 | 1500 | it('does nothing if some populate data on item does not exist', async () => { 1501 | for (const { type, dataResult } of beforeAfter) { 1502 | const options = { 1503 | include: { 1504 | // from: 'users', 1505 | service: 'tags', 1506 | nameAs: 'tags', 1507 | keyHere: 'tagIds', 1508 | keyThere: 'id' 1509 | } 1510 | } 1511 | const context = { 1512 | app: { 1513 | service (path) { 1514 | return services[path] 1515 | } 1516 | }, 1517 | method: 'create', 1518 | type, 1519 | params: {}, 1520 | [dataResult]: [ 1521 | { 1522 | id: '11', 1523 | name: 'Dumb Stuff', 1524 | tagIds: ['1111', '3333', 4444] 1525 | }, 1526 | { 1527 | id: '22', 1528 | name: 'Smart Stuff' 1529 | }, 1530 | { 1531 | id: 44, 1532 | name: 'Just Stuff', 1533 | tagIds: [4444] 1534 | } 1535 | ] 1536 | } 1537 | 1538 | const shallowPopulate = makePopulate(options) 1539 | 1540 | const response = await shallowPopulate(context) 1541 | const result = response[dataResult] 1542 | assert(result[0].tags.length === 3, 'tags have been populated') 1543 | assert(!result[1].tags, 'tags have not been populated') 1544 | } 1545 | }) 1546 | 1547 | it('populates from local keys dot notation', async () => { 1548 | for (const { type, dataResult } of beforeAfter) { 1549 | const options = { 1550 | include: { 1551 | service: 'posts', 1552 | nameAs: 'meta.posts', 1553 | keyHere: 'meta.postsId', 1554 | keyThere: 'id' 1555 | } 1556 | } 1557 | const context = { 1558 | app: { 1559 | service (path) { 1560 | return services[path] 1561 | } 1562 | }, 1563 | method: 'create', 1564 | type, 1565 | params: {}, 1566 | [dataResult]: [ 1567 | { 1568 | id: '11', 1569 | name: 'Dumb Stuff', 1570 | meta: { 1571 | postsId: ['111', '333', 444] 1572 | } 1573 | }, 1574 | { 1575 | id: '22', 1576 | name: 'Dumb Stuff', 1577 | meta: { 1578 | postsId: ['222', '333', '111', 555] 1579 | } 1580 | }, 1581 | { 1582 | id: 44, 1583 | name: 'Integer Stuff', 1584 | meta: { 1585 | postsId: ['222', 555] 1586 | } 1587 | } 1588 | ] 1589 | } 1590 | 1591 | const shallowPopulate = makePopulate(options) 1592 | 1593 | const response = await shallowPopulate(context) 1594 | const result = response[dataResult] 1595 | assert(result[0].meta.posts.length === 3, 'result[0] posts should have been populated') 1596 | assert(result[1].meta.posts.length === 4, 'result[0] posts should have been populated') 1597 | } 1598 | }) 1599 | 1600 | it('populates from local keys', async () => { 1601 | for (const { type, dataResult } of beforeAfter) { 1602 | const options = { 1603 | include: { 1604 | // from: 'users', 1605 | service: 'posts', 1606 | nameAs: 'posts', 1607 | keyHere: 'postsId', 1608 | keyThere: 'id' 1609 | } 1610 | } 1611 | const context = { 1612 | app: { 1613 | service (path) { 1614 | return services[path] 1615 | } 1616 | }, 1617 | method: 'create', 1618 | type, 1619 | params: {}, 1620 | [dataResult]: [ 1621 | { 1622 | id: '11', 1623 | name: 'Dumb Stuff', 1624 | postsId: ['111', '222', 444, '555'] 1625 | }, 1626 | { 1627 | id: '22', 1628 | name: 'Smart Stuff', 1629 | postsId: ['333', 444, '555'] 1630 | } 1631 | ] 1632 | } 1633 | 1634 | const shallowPopulate = makePopulate(options) 1635 | 1636 | const response = await shallowPopulate(context) 1637 | const result = response[dataResult] 1638 | assert(result[0].posts.length === 3, 'result[0] should have correct posts data') 1639 | assert(result[1].posts.length === 2, 'result[1] should have correct posts data') 1640 | } 1641 | }) 1642 | 1643 | it('populates from foreign keys', async () => { 1644 | for (const { type, dataResult } of beforeAfter) { 1645 | const options = { 1646 | include: { 1647 | // from: 'posts', 1648 | service: 'users', 1649 | nameAs: 'users', 1650 | keyHere: 'id', 1651 | keyThere: 'postsId' 1652 | } 1653 | } 1654 | const context = { 1655 | app: { 1656 | service (path) { 1657 | return services[path] 1658 | } 1659 | }, 1660 | method: 'create', 1661 | type, 1662 | params: {}, 1663 | [dataResult]: [ 1664 | { 1665 | id: '111', 1666 | name: 'My Monkey and Me' 1667 | }, 1668 | { 1669 | id: '222', 1670 | name: 'I forgot why I love you' 1671 | }, 1672 | { 1673 | id: 444, 1674 | name: 'One, two, three, one, two, three, drink' 1675 | } 1676 | ] 1677 | } 1678 | 1679 | const shallowPopulate = makePopulate(options) 1680 | 1681 | const response = await shallowPopulate(context) 1682 | const result = response[dataResult] 1683 | result.forEach(item => { 1684 | assert(item.users, 'should have users property') 1685 | }) 1686 | } 1687 | }) 1688 | 1689 | it('$select works without $keyThere', async () => { 1690 | for (const { type, dataResult } of beforeAfter) { 1691 | const options = { 1692 | include: { 1693 | // from: 'users', 1694 | service: 'posts', 1695 | nameAs: 'posts', 1696 | keyHere: 'postsId', 1697 | keyThere: 'id', 1698 | params: { query: { $select: ['name'] } } 1699 | } 1700 | } 1701 | const context = { 1702 | app: { 1703 | service (path) { 1704 | return services[path] 1705 | } 1706 | }, 1707 | method: 'create', 1708 | type, 1709 | params: {}, 1710 | [dataResult]: [ 1711 | { 1712 | id: '11', 1713 | name: 'Dumb Stuff', 1714 | postsId: ['111', '222', 444, '555'] 1715 | }, 1716 | { 1717 | id: '22', 1718 | name: 'Smart Stuff', 1719 | postsId: ['333', 444, '555'] 1720 | } 1721 | ] 1722 | } 1723 | 1724 | const shallowPopulate = makePopulate(options) 1725 | const response = await shallowPopulate(context) 1726 | const result = response[dataResult] 1727 | 1728 | result.forEach(user => { 1729 | assert(user.posts.length, `${type}: posts should have been populated`) 1730 | user.posts.forEach(post => { 1731 | const { name, ...rest } = post 1732 | assert.deepStrictEqual(rest, {}, `${type}: only has name property`) 1733 | }) 1734 | }) 1735 | } 1736 | }) 1737 | 1738 | it('$skip works as intended', async () => { 1739 | for (const { type, dataResult } of beforeAfter) { 1740 | const options1 = { 1741 | include: { 1742 | // from: 'users', 1743 | service: 'posts', 1744 | nameAs: 'posts', 1745 | keyHere: 'postsId', 1746 | keyThere: 'id', 1747 | params: { query: { } } 1748 | } 1749 | } 1750 | const context1 = { 1751 | app: { 1752 | service (path) { 1753 | return services[path] 1754 | } 1755 | }, 1756 | method: 'create', 1757 | type, 1758 | params: {}, 1759 | [dataResult]: [ 1760 | { 1761 | id: '11', 1762 | name: 'Dumb Stuff', 1763 | postsId: ['111', '222', 444, '555'] 1764 | }, 1765 | { 1766 | id: '22', 1767 | name: 'Smart Stuff', 1768 | postsId: ['333', 444, '555'] 1769 | } 1770 | ] 1771 | } 1772 | 1773 | const shallowPopulate1 = makePopulate(options1) 1774 | const response1 = await shallowPopulate1(context1) 1775 | const users1 = response1[dataResult] 1776 | 1777 | const options2 = { 1778 | include: { 1779 | // from: 'users', 1780 | service: 'posts', 1781 | nameAs: 'posts', 1782 | keyHere: 'postsId', 1783 | keyThere: 'id', 1784 | params: { query: { $skip: 1 } } 1785 | } 1786 | } 1787 | const context2 = { 1788 | app: { 1789 | service (path) { 1790 | return services[path] 1791 | } 1792 | }, 1793 | method: 'create', 1794 | type, 1795 | params: {}, 1796 | [dataResult]: [ 1797 | { 1798 | id: '11', 1799 | name: 'Dumb Stuff', 1800 | postsId: ['111', '222', 444, '555'] 1801 | }, 1802 | { 1803 | id: '22', 1804 | name: 'Smart Stuff', 1805 | postsId: ['333', 444, '555'] 1806 | } 1807 | ] 1808 | } 1809 | 1810 | const shallowPopulate2 = makePopulate(options2) 1811 | const response2 = await shallowPopulate2(context2) 1812 | const users2 = response2[dataResult] 1813 | 1814 | users1.forEach((user1, i) => { 1815 | const user2 = users2[i] 1816 | assert(user1.posts.length - 1 === user2.posts.length, `${type}: skipped 1 item for user2`) 1817 | }) 1818 | } 1819 | }) 1820 | 1821 | it('$limit works as intended', async () => { 1822 | for (const { type, dataResult } of beforeAfter) { 1823 | const options1 = { 1824 | include: { 1825 | // from: 'users', 1826 | service: 'posts', 1827 | nameAs: 'posts', 1828 | keyHere: 'postsId', 1829 | keyThere: 'id', 1830 | params: { query: { } } 1831 | } 1832 | } 1833 | const context1 = { 1834 | app: { 1835 | service (path) { 1836 | return services[path] 1837 | } 1838 | }, 1839 | method: 'create', 1840 | type, 1841 | params: {}, 1842 | [dataResult]: [ 1843 | { 1844 | id: '11', 1845 | name: 'Dumb Stuff', 1846 | postsId: ['111', '222', 444, '555'] 1847 | }, 1848 | { 1849 | id: '22', 1850 | name: 'Smart Stuff', 1851 | postsId: ['333', 444, '555'] 1852 | } 1853 | ] 1854 | } 1855 | 1856 | const shallowPopulate1 = makePopulate(options1) 1857 | const response1 = await shallowPopulate1(context1) 1858 | const users1 = response1[dataResult] 1859 | 1860 | const options2 = { 1861 | include: { 1862 | // from: 'users', 1863 | service: 'posts', 1864 | nameAs: 'posts', 1865 | keyHere: 'postsId', 1866 | keyThere: 'id', 1867 | params: { query: { $limit: 1 } } 1868 | } 1869 | } 1870 | const context2 = { 1871 | app: { 1872 | service (path) { 1873 | return services[path] 1874 | } 1875 | }, 1876 | method: 'create', 1877 | type, 1878 | params: {}, 1879 | [dataResult]: [ 1880 | { 1881 | id: '11', 1882 | name: 'Dumb Stuff', 1883 | postsId: ['111', '222', 444, '555'] 1884 | }, 1885 | { 1886 | id: '22', 1887 | name: 'Smart Stuff', 1888 | postsId: ['333', 444, '555'] 1889 | } 1890 | ] 1891 | } 1892 | 1893 | const shallowPopulate2 = makePopulate(options2) 1894 | const response2 = await shallowPopulate2(context2) 1895 | const users2 = response2[dataResult] 1896 | 1897 | users1.forEach((user1, i) => { 1898 | const user2 = users2[i] 1899 | assert(user1.posts.length > user2.posts.length, `${type}: user1 has more posts than user2`) 1900 | assert(user2.posts.length === 1, `${type}: limited posts for user2`) 1901 | }) 1902 | } 1903 | }) 1904 | 1905 | describe('requestPerItem: true', () => { 1906 | it('populates with custom params $select works', async () => { 1907 | for (const { type, dataResult } of beforeAfter) { 1908 | const posts = [ 1909 | { 1910 | id: '111', 1911 | name: 'My Monkey and Me' 1912 | }, 1913 | { 1914 | id: '222', 1915 | name: 'I forgot why I love you' 1916 | }, 1917 | { 1918 | id: 444, 1919 | name: 'One, two, three, one, two, three, drink' 1920 | } 1921 | ] 1922 | 1923 | const options = { 1924 | include: { 1925 | // from: 'posts', 1926 | service: 'tasks', 1927 | nameAs: 'tasks', 1928 | params: (params, context) => { 1929 | return { query: { $select: ['id'] } } 1930 | } 1931 | } 1932 | } 1933 | const context = { 1934 | app: { 1935 | service (path) { 1936 | return services[path] 1937 | } 1938 | }, 1939 | method: 'create', 1940 | type, 1941 | params: {}, 1942 | // Data for a single track 1943 | [dataResult]: posts 1944 | } 1945 | 1946 | const shallowPopulate = makePopulate(options) 1947 | 1948 | const response = await shallowPopulate(context) 1949 | const result = response[dataResult] 1950 | 1951 | result.forEach(post => { 1952 | const expectedTasks = Object.values(services.tasks.store).map(x => { return { id: x.id } }) 1953 | assert.deepStrictEqual(post.tasks, expectedTasks, 'populated all tasks with only `id` attribute') 1954 | }) 1955 | } 1956 | }) 1957 | 1958 | it('populates with custom params function', async () => { 1959 | for (const { type, dataResult } of beforeAfter) { 1960 | const posts = [ 1961 | { 1962 | id: '111', 1963 | name: 'My Monkey and Me', 1964 | userId: '11' 1965 | }, 1966 | { 1967 | id: '222', 1968 | name: 'I forgot why I love you', 1969 | userId: '11' 1970 | }, 1971 | { 1972 | id: 444, 1973 | name: 'One, two, three, one, two, three, drink', 1974 | userId: 44 1975 | } 1976 | ] 1977 | 1978 | const options = { 1979 | include: { 1980 | // from: 'posts', 1981 | service: 'tasks', 1982 | nameAs: 'tasks', 1983 | params: function (params, context) { 1984 | return { query: { userId: this.userId } } 1985 | } 1986 | } 1987 | } 1988 | const context = { 1989 | app: { 1990 | service (path) { 1991 | return services[path] 1992 | } 1993 | }, 1994 | method: 'create', 1995 | type, 1996 | params: {}, 1997 | // Data for a single track 1998 | [dataResult]: posts 1999 | } 2000 | 2001 | const shallowPopulate = makePopulate(options) 2002 | 2003 | const response = await shallowPopulate(context) 2004 | const result = response[dataResult] 2005 | 2006 | result.forEach(post => { 2007 | const expectedTasks = Object.values(services.tasks.store).filter(x => x.userId === post.userId) 2008 | assert.deepStrictEqual(post.tasks, expectedTasks, 'tasks populated correctly') 2009 | }) 2010 | } 2011 | }) 2012 | }) 2013 | 2014 | it.skip('handles missing _id on create', async () => {}) 2015 | }) 2016 | 2017 | describe('Multiple data/result - Multiple Relationship:', () => { 2018 | it('as object', async () => { 2019 | for (const { type, dataResult } of beforeAfter) { 2020 | const options = { 2021 | include: [ 2022 | { 2023 | // from: 'users', 2024 | service: 'tags', 2025 | nameAs: 'tags', 2026 | keyHere: 'tagIds', 2027 | keyThere: 'id' 2028 | }, 2029 | { 2030 | // from: 'users', 2031 | service: 'posts', 2032 | nameAs: 'post', 2033 | keyHere: 'postIds', 2034 | keyThere: 'id', 2035 | asArray: false 2036 | } 2037 | ] 2038 | } 2039 | const context = { 2040 | app: { 2041 | service (path) { 2042 | return services[path] 2043 | } 2044 | }, 2045 | method: 'create', 2046 | type, 2047 | params: {}, 2048 | [dataResult]: [ 2049 | { 2050 | id: '11', 2051 | name: 'Dumb Stuff', 2052 | postIds: '111', 2053 | tagIds: ['1111', '3333', 4444] 2054 | }, 2055 | { 2056 | id: '22', 2057 | name: 'Smart Stuff', 2058 | postIds: '222', 2059 | tagIds: ['1111'] 2060 | }, 2061 | { 2062 | id: 33, 2063 | name: 'Just Stuff', 2064 | postIds: 444, 2065 | tagIds: ['1111', 4444] 2066 | } 2067 | ] 2068 | } 2069 | 2070 | const shallowPopulate = makePopulate(options) 2071 | 2072 | const response = await shallowPopulate(context) 2073 | const result = response[dataResult] 2074 | assert(result[0].post, 'post should have been populated') 2075 | assert(!Array.isArray(result[0].post), 'post should not be an array') 2076 | assert(result[0].post.id === '111', 'post has correct id') 2077 | assert(result[0].tags, 'tags should have been populated') 2078 | assert(Array.isArray(result[0].tags), 'tags should be an array') 2079 | 2080 | assert(result[1].post, 'post should have been populated') 2081 | assert(!Array.isArray(result[1].post), 'post should not be an array') 2082 | assert(result[1].post.id === '222', 'post has correct id') 2083 | assert(result[1].tags, 'tags should have been populated') 2084 | assert(Array.isArray(result[1].tags), 'tags should be an array') 2085 | } 2086 | }) 2087 | 2088 | it('as object when array', async () => { 2089 | for (const { type, dataResult } of beforeAfter) { 2090 | const options = { 2091 | include: [ 2092 | { 2093 | // from: 'users', 2094 | service: 'tags', 2095 | nameAs: 'tags', 2096 | keyHere: 'tagIds', 2097 | keyThere: 'id' 2098 | }, 2099 | { 2100 | // from: 'users', 2101 | service: 'posts', 2102 | nameAs: 'post', 2103 | keyHere: 'postIds', 2104 | keyThere: 'id', 2105 | asArray: false 2106 | } 2107 | ] 2108 | } 2109 | const context = { 2110 | app: { 2111 | service (path) { 2112 | return services[path] 2113 | } 2114 | }, 2115 | method: 'create', 2116 | type, 2117 | params: {}, 2118 | [dataResult]: [ 2119 | { 2120 | id: '11', 2121 | name: 'Dumb Stuff', 2122 | postIds: ['111', '222', 444], 2123 | tagIds: ['1111', '3333', 4444] 2124 | }, 2125 | { 2126 | id: '22', 2127 | name: 'Smart Stuff', 2128 | postIds: ['222', 444], 2129 | tagIds: ['1111'] 2130 | } 2131 | ] 2132 | } 2133 | 2134 | const shallowPopulate = makePopulate(options) 2135 | 2136 | const response = await shallowPopulate(context) 2137 | const result = response[dataResult] 2138 | assert(result[0].post, 'post should have been populated') 2139 | assert(!Array.isArray(result[0].post), 'post should not be an array') 2140 | assert(result[0].post.id === '111', 'post has correct id') 2141 | assert(result[0].tags, 'tags should have been populated') 2142 | assert(Array.isArray(result[0].tags), 'tags should be an array') 2143 | 2144 | assert(result[1].post, 'post should have been populated') 2145 | assert(!Array.isArray(result[1].post), 'post should not be an array') 2146 | assert(result[1].post.id === '222', 'post has correct id') 2147 | assert(result[1].tags, 'tags should have been populated') 2148 | assert(Array.isArray(result[1].tags), 'tags should be an array') 2149 | } 2150 | }) 2151 | 2152 | it('does nothing if some populate data on item does not exist', async () => { 2153 | for (const { type, dataResult } of beforeAfter) { 2154 | const options = { 2155 | include: [ 2156 | { 2157 | // from: 'users', 2158 | service: 'tags', 2159 | nameAs: 'tags', 2160 | keyHere: 'tagIds', 2161 | keyThere: 'id' 2162 | }, 2163 | { 2164 | // from: 'users', 2165 | service: 'posts', 2166 | nameAs: 'posts', 2167 | keyHere: 'postIds', 2168 | keyThere: 'id' 2169 | } 2170 | ] 2171 | } 2172 | const context = { 2173 | app: { 2174 | service (path) { 2175 | return services[path] 2176 | } 2177 | }, 2178 | method: 'create', 2179 | type, 2180 | params: {}, 2181 | [dataResult]: [ 2182 | { 2183 | id: '11', 2184 | name: 'Dumb Stuff', 2185 | postIds: ['111', '222', '333', 444] 2186 | }, 2187 | { 2188 | id: '22', 2189 | name: 'Smart Stuff', 2190 | postIds: ['111', '333', 555] 2191 | } 2192 | ] 2193 | } 2194 | 2195 | const shallowPopulate = makePopulate(options) 2196 | 2197 | const response = await shallowPopulate(context) 2198 | const result = response[dataResult] 2199 | assert(result[0].posts.length === 4, 'posts have been populated') 2200 | assert(!result[0].tags, 'tags have not been populated') 2201 | assert(!result[1].tags, 'tags have not been populated') 2202 | assert(result[1].posts.length === 3, 'posts have been populated') 2203 | } 2204 | }) 2205 | 2206 | it('populates from local keys', async () => { 2207 | for (const { type, dataResult } of beforeAfter) { 2208 | const options = { 2209 | include: [ 2210 | { 2211 | // from: 'users', 2212 | service: 'posts', 2213 | nameAs: 'posts', 2214 | keyHere: 'postsId', 2215 | keyThere: 'id' 2216 | }, 2217 | { 2218 | // from: 'users', 2219 | service: 'tags', 2220 | nameAs: 'tags', 2221 | keyHere: 'tagIds', 2222 | keyThere: 'id' 2223 | } 2224 | ] 2225 | } 2226 | const context = { 2227 | app: { 2228 | service (path) { 2229 | return services[path] 2230 | } 2231 | }, 2232 | method: 'create', 2233 | type, 2234 | params: {}, 2235 | [dataResult]: [ 2236 | { 2237 | id: '11', 2238 | name: 'Dumb Stuff', 2239 | postsId: ['111', '222', '333'], 2240 | tagIds: ['1111', '3333'] 2241 | }, 2242 | { 2243 | id: '22', 2244 | name: 'Smart Stuff', 2245 | postsId: ['111', '333'], 2246 | tagIds: ['3333'] 2247 | } 2248 | ] 2249 | } 2250 | 2251 | const shallowPopulate = makePopulate(options) 2252 | 2253 | const response = await shallowPopulate(context) 2254 | const result = response[dataResult] 2255 | assert(result[0].posts.length === 3, 'result[0] should have correct posts data') 2256 | assert(result[0].tags.length === 2, 'result[0] should have correct tags data') 2257 | 2258 | assert(result[1].posts.length === 2, 'result[1] should have correct posts data') 2259 | assert(result[1].tags.length === 1, 'result[1] should have correct tags data') 2260 | } 2261 | }) 2262 | 2263 | it('populates from foreign keys', async () => { 2264 | for (const { type, dataResult } of beforeAfter) { 2265 | const options = { 2266 | include: [ 2267 | { 2268 | service: 'users', 2269 | nameAs: 'users', 2270 | keyHere: 'id', 2271 | keyThere: 'postsId' 2272 | }, 2273 | { 2274 | service: 'comments', 2275 | nameAs: 'comments', 2276 | keyHere: 'id', 2277 | keyThere: 'postsId' 2278 | } 2279 | ] 2280 | } 2281 | const context = { 2282 | app: { 2283 | service (path) { 2284 | return services[path] 2285 | } 2286 | }, 2287 | method: 'create', 2288 | type, 2289 | params: {}, 2290 | [dataResult]: [ 2291 | { 2292 | id: '333', 2293 | name: 'If I were a banana...' 2294 | }, 2295 | { 2296 | id: '111', 2297 | name: 'My Monkey and Me' 2298 | } 2299 | ] 2300 | } 2301 | 2302 | const shallowPopulate = makePopulate(options) 2303 | 2304 | const response = await shallowPopulate(context) 2305 | const result = response[dataResult] 2306 | assert(result[0].users.length === 1, 'result[0] should have correct users data') 2307 | assert(result[0].comments.length === 2, 'result[0] should have correct comments data') 2308 | 2309 | assert(result[1].users.length === 2, 'result[1] should have correct users data') 2310 | assert(result[1].comments.length === 2, 'result[1] should have correct comments data') 2311 | } 2312 | }) 2313 | 2314 | it('$select works without $keyThere', async () => { 2315 | for (const { type, dataResult } of beforeAfter) { 2316 | const posts = [ 2317 | { 2318 | id: '333', 2319 | name: 'If I were a banana...' 2320 | }, 2321 | { 2322 | id: '111', 2323 | name: 'My Monkey and Me' 2324 | } 2325 | ] 2326 | 2327 | const options = { 2328 | include: [ 2329 | { 2330 | // from posts 2331 | service: 'users', 2332 | nameAs: 'users', 2333 | keyHere: 'id', 2334 | keyThere: 'postsId', 2335 | params: { query: { $select: ['name'] } } 2336 | }, 2337 | { 2338 | // from posts 2339 | service: 'comments', 2340 | nameAs: 'comments', 2341 | keyHere: 'id', 2342 | keyThere: 'postsId' 2343 | } 2344 | ] 2345 | } 2346 | const context = { 2347 | app: { 2348 | service (path) { 2349 | return services[path] 2350 | } 2351 | }, 2352 | method: 'create', 2353 | type, 2354 | params: {}, 2355 | [dataResult]: posts 2356 | } 2357 | 2358 | const shallowPopulate = makePopulate(options) 2359 | 2360 | const response = await shallowPopulate(context) 2361 | const result = response[dataResult] 2362 | 2363 | result.forEach((post, i) => { 2364 | assert(post.users.length, 'posts should have been populated') 2365 | post.users.forEach(user => { 2366 | const { name, ...rest } = user 2367 | assert.deepStrictEqual(rest, {}, 'only has name property') 2368 | }) 2369 | 2370 | const expectedComments = Object.values(services.comments.store).filter(comment => comment.postsId.includes(posts[i].id)) 2371 | 2372 | assert(post.comments.length === 2, 'data should have correct comments data') 2373 | assert.deepStrictEqual(post.comments, expectedComments, 'comments are populated complete') 2374 | }) 2375 | } 2376 | }) 2377 | 2378 | it('$skip works as intended', async () => { 2379 | for (const { type, dataResult } of beforeAfter) { 2380 | const options1 = { 2381 | include: [ 2382 | { 2383 | // from: 'users', 2384 | service: 'posts', 2385 | nameAs: 'posts', 2386 | keyHere: 'postsId', 2387 | keyThere: 'id', 2388 | params: { query: { } } 2389 | }, 2390 | { 2391 | service: 'comments', 2392 | nameAs: 'comments', 2393 | keyHere: 'id', 2394 | keyThere: 'userId' 2395 | } 2396 | ] 2397 | } 2398 | 2399 | const context1 = { 2400 | app: { 2401 | service (path) { 2402 | return services[path] 2403 | } 2404 | }, 2405 | method: 'create', 2406 | type, 2407 | params: {}, 2408 | [dataResult]: [ 2409 | { 2410 | id: '11', 2411 | name: 'Dumb Stuff', 2412 | postsId: ['111', '222', 444, '555'] 2413 | }, 2414 | { 2415 | id: '22', 2416 | name: 'Smart Stuff', 2417 | postsId: ['333', 444, '555'] 2418 | } 2419 | ] 2420 | } 2421 | 2422 | const shallowPopulate1 = makePopulate(options1) 2423 | 2424 | const { [dataResult]: users1 } = await shallowPopulate1(context1) 2425 | 2426 | const options2 = { 2427 | include: [ 2428 | { 2429 | // from: 'users', 2430 | service: 'posts', 2431 | nameAs: 'posts', 2432 | keyHere: 'postsId', 2433 | keyThere: 'id', 2434 | params: { query: { $skip: 1 } } 2435 | }, 2436 | { 2437 | service: 'comments', 2438 | nameAs: 'comments', 2439 | keyHere: 'id', 2440 | keyThere: 'userId' 2441 | } 2442 | ] 2443 | } 2444 | const context2 = { 2445 | app: { 2446 | service (path) { 2447 | return services[path] 2448 | } 2449 | }, 2450 | method: 'create', 2451 | type, 2452 | params: {}, 2453 | [dataResult]: [ 2454 | { 2455 | id: '11', 2456 | name: 'Dumb Stuff', 2457 | postsId: ['111', '222', 444, '555'] 2458 | }, 2459 | { 2460 | id: '22', 2461 | name: 'Smart Stuff', 2462 | postsId: ['333', 444, '555'] 2463 | } 2464 | ] 2465 | } 2466 | 2467 | const shallowPopulate2 = makePopulate(options2) 2468 | 2469 | const { [dataResult]: users2 } = await shallowPopulate2(context2) 2470 | 2471 | users1.forEach((user1, i) => { 2472 | const user2 = users2[i] 2473 | 2474 | assert(user1.posts.length - 1 === user2.posts.length, 'skipped 1 item for user2') 2475 | assert(user1.comments.length > 0, 'at least some comments') 2476 | assert.deepStrictEqual(user1.comments, user2.comments, 'comments are populated the same') 2477 | }) 2478 | } 2479 | }) 2480 | 2481 | it('$limit works as intended', async () => { 2482 | for (const { type, dataResult } of beforeAfter) { 2483 | const options1 = { 2484 | include: [ 2485 | { 2486 | // from: 'users', 2487 | service: 'posts', 2488 | nameAs: 'posts', 2489 | keyHere: 'postsId', 2490 | keyThere: 'id' 2491 | }, 2492 | { 2493 | service: 'comments', 2494 | nameAs: 'comments', 2495 | keyHere: 'id', 2496 | keyThere: 'userId' 2497 | } 2498 | ] 2499 | } 2500 | const context1 = { 2501 | app: { 2502 | service (path) { 2503 | return services[path] 2504 | } 2505 | }, 2506 | method: 'create', 2507 | type, 2508 | params: {}, 2509 | [dataResult]: [ 2510 | { 2511 | id: '11', 2512 | name: 'Dumb Stuff', 2513 | postsId: ['111', '222', 444, '555'] 2514 | }, 2515 | { 2516 | id: '22', 2517 | name: 'Smart Stuff', 2518 | postsId: ['333', 444, '555'] 2519 | } 2520 | ] 2521 | } 2522 | 2523 | const shallowPopulate1 = makePopulate(options1) 2524 | 2525 | const { [dataResult]: users1 } = await shallowPopulate1(context1) 2526 | 2527 | const options2 = { 2528 | include: [ 2529 | { 2530 | // from: 'users', 2531 | service: 'posts', 2532 | nameAs: 'posts', 2533 | keyHere: 'postsId', 2534 | keyThere: 'id', 2535 | params: { query: { $limit: 1 } } 2536 | }, 2537 | { 2538 | service: 'comments', 2539 | nameAs: 'comments', 2540 | keyHere: 'id', 2541 | keyThere: 'userId' 2542 | } 2543 | ] 2544 | } 2545 | const context2 = { 2546 | app: { 2547 | service (path) { 2548 | return services[path] 2549 | } 2550 | }, 2551 | method: 'create', 2552 | type, 2553 | params: {}, 2554 | [dataResult]: [ 2555 | { 2556 | id: '11', 2557 | name: 'Dumb Stuff', 2558 | postsId: ['111', '222', 444, '555'] 2559 | }, 2560 | { 2561 | id: '22', 2562 | name: 'Smart Stuff', 2563 | postsId: ['333', 444, '555'] 2564 | } 2565 | ] 2566 | } 2567 | 2568 | const shallowPopulate2 = makePopulate(options2) 2569 | 2570 | const { [dataResult]: users2 } = await shallowPopulate2(context2) 2571 | 2572 | users1.forEach((user1, i) => { 2573 | const user2 = users2[i] 2574 | 2575 | assert(user1.posts.length > user2.posts.length, 'user1 has more posts than user2') 2576 | assert(user2.posts.length === 1, 'limited posts for user2') 2577 | assert.deepStrictEqual(user1.comments, user2.comments, 'comments are the same') 2578 | }) 2579 | } 2580 | }) 2581 | 2582 | describe('requestPerItem: true', () => { 2583 | it('populates with custom params $select works', async () => { 2584 | for (const { type, dataResult } of beforeAfter) { 2585 | const posts = [ 2586 | { 2587 | id: '111', 2588 | name: 'My Monkey and Me' 2589 | }, 2590 | { 2591 | id: '222', 2592 | name: 'I forgot why I love you' 2593 | }, 2594 | { 2595 | id: 444, 2596 | name: 'One, two, three, one, two, three, drink' 2597 | } 2598 | ] 2599 | 2600 | const options = { 2601 | include: [ 2602 | { 2603 | // from: 'posts', 2604 | service: 'tasks', 2605 | nameAs: 'tasks', 2606 | params: (params, context) => { return { query: { $select: ['id'] } } } 2607 | }, 2608 | { 2609 | // from: 'posts', 2610 | service: 'comments', 2611 | nameAs: 'comments', 2612 | params: (params, context) => { return { query: { $select: ['id'] } } } 2613 | } 2614 | ] 2615 | } 2616 | const context = { 2617 | app: { 2618 | service (path) { 2619 | return services[path] 2620 | } 2621 | }, 2622 | method: 'create', 2623 | type, 2624 | params: {}, 2625 | // Data for a single track 2626 | [dataResult]: posts 2627 | } 2628 | 2629 | const shallowPopulate = makePopulate(options) 2630 | 2631 | const response = await shallowPopulate(context) 2632 | const result = response[dataResult] 2633 | 2634 | result.forEach(post => { 2635 | const expectedTasks = Object.values(services.tasks.store).map(x => { return { id: x.id } }) 2636 | assert.deepStrictEqual(post.tasks, expectedTasks, 'populated all tasks with only `id` attribute') 2637 | 2638 | const expectedComments = Object.values(services.comments.store).map(x => { return { id: x.id } }) 2639 | assert.deepStrictEqual(post.comments, expectedComments, 'populated all tasks with only `id` attribute') 2640 | }) 2641 | } 2642 | }) 2643 | 2644 | it('populates with custom params function', async () => { 2645 | for (const { type, dataResult } of beforeAfter) { 2646 | const posts = [ 2647 | { 2648 | id: '111', 2649 | name: 'My Monkey and Me', 2650 | userId: '11' 2651 | }, 2652 | { 2653 | id: '222', 2654 | name: 'I forgot why I love you', 2655 | userId: '11' 2656 | }, 2657 | { 2658 | id: 444, 2659 | name: 'One, two, three, one, two, three, drink', 2660 | userId: 44 2661 | } 2662 | ] 2663 | 2664 | const options = { 2665 | include: [ 2666 | { 2667 | // from: 'posts', 2668 | service: 'tasks', 2669 | nameAs: 'tasks', 2670 | params: function (params, context) { 2671 | return { query: { userId: this.userId } } 2672 | } 2673 | }, 2674 | { 2675 | // from: 'posts', 2676 | service: 'tags', 2677 | nameAs: 'tags', 2678 | params: function (params, context) { 2679 | return { 2680 | query: { 2681 | userId: this.userId, 2682 | $select: ['id'] 2683 | } 2684 | } 2685 | } 2686 | }, 2687 | { 2688 | service: 'orgs', 2689 | nameAs: 'org', 2690 | asArray: false, 2691 | params: async function (params, context) { 2692 | const user = await context.app.service('users').get(this.userId) 2693 | return { query: { id: user.orgId } } 2694 | } 2695 | }, 2696 | { 2697 | // from: 'posts', 2698 | service: 'tags', 2699 | nameAs: 'tag', 2700 | asArray: false, 2701 | params: [ 2702 | function (params, context) { 2703 | return { 2704 | query: { 2705 | userId: this.userId 2706 | } 2707 | } 2708 | }, 2709 | { query: { $select: ['id'] } } 2710 | ] 2711 | }, 2712 | { 2713 | // from: 'posts', 2714 | service: 'tasks', 2715 | nameAs: 'nullTask', 2716 | asArray: false, 2717 | params: function (params, context) { 2718 | return undefined 2719 | } 2720 | }, 2721 | { 2722 | // from: 'posts', 2723 | service: 'tasks', 2724 | nameAs: 'emptyTasks', 2725 | params: function (params, context) { 2726 | return undefined 2727 | } 2728 | } 2729 | ] 2730 | } 2731 | const context = { 2732 | app: { 2733 | service (path) { 2734 | return services[path] 2735 | } 2736 | }, 2737 | method: 'create', 2738 | type, 2739 | params: {}, 2740 | // Data for a single track 2741 | [dataResult]: posts 2742 | } 2743 | 2744 | const shallowPopulate = makePopulate(options) 2745 | 2746 | const response = await shallowPopulate(context) 2747 | const result = response[dataResult] 2748 | 2749 | result.forEach(post => { 2750 | const expectedTasks = Object.values(services.tasks.store).filter(x => x.userId === post.userId) 2751 | const expectedTags = Object.values(services.tags.store).filter(x => x.userId === post.userId).map(x => { return { id: x.id } }) 2752 | const user = Object.values(services.users.store).filter(x => x.id === post.userId)[0] 2753 | const expectedOrg = Object.values(services.orgs.store).filter(x => x.id === user.orgId)[0] 2754 | const expectedTag = expectedTags[0] 2755 | assert.deepStrictEqual(post.tasks, expectedTasks, 'tasks populated correctly') 2756 | assert.deepStrictEqual(post.tags, expectedTags, 'tags populated correctly') 2757 | assert.deepStrictEqual(post.org, expectedOrg, 'populated org correctly') 2758 | assert.deepStrictEqual(post.tag, expectedTag, 'single tag populated correctly') 2759 | assert(post.nullTask === null, 'set default to null') 2760 | assert.deepStrictEqual(post.emptyTasks, [], 'set default to empty array') 2761 | }) 2762 | } 2763 | }) 2764 | }) 2765 | 2766 | it.skip('handles missing _id on create', async () => {}) 2767 | }) 2768 | }) 2769 | }) 2770 | --------------------------------------------------------------------------------