├── .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 | [](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 |
--------------------------------------------------------------------------------