├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .husky └── commit-msg ├── .prettierignore ├── LICENSE.md ├── README.md ├── adonis-typings ├── auto-preload.ts ├── container.ts └── index.ts ├── bin └── test │ ├── config.ts │ ├── database.ts │ ├── index.ts │ └── japaTypes.ts ├── commands └── index.ts ├── instructions.md ├── package.json ├── pnpm-lock.yaml ├── providers └── AutoPreloadProvider.ts ├── src ├── Exceptions │ ├── WrongArgumentTypeException.ts │ └── WrongRelationshipTypeException.ts └── Mixins │ └── AutoPreload.ts ├── test-helpers └── index.ts ├── tests ├── auto_preload-multiple_pagination.spec.ts ├── auto_preload-multiple_rows.spec.ts ├── auto_preload-one_row.spec.ts └── auto_preload.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [MakeFile] 22 | indent_style = space 23 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. And following this guidelines will make your pull request easier to merge 4 | 5 | ## Prerequisites 6 | 7 | - Install [EditorConfig](http://editorconfig.org/) plugin for your code editor to make sure it uses correct settings. 8 | - Fork the repository and clone your fork. 9 | - Install dependencies: `npm install`. 10 | 11 | ## Coding style 12 | 13 | We make use of [standard](https://standardjs.com/) to lint our code. Standard does not need a config file and comes with set of non-configurable rules. 14 | 15 | ## Development work-flow 16 | 17 | Always make sure to lint and test your code before pushing it to the GitHub. 18 | 19 | ```bash 20 | npm test 21 | ``` 22 | 23 | Just lint the code 24 | 25 | ```bash 26 | npm run lint 27 | ``` 28 | 29 | **Make sure you add sufficient tests for the change**. 30 | 31 | ## Other notes 32 | 33 | - Do not change version number inside the `package.json` file. 34 | - Do not update `CHANGELOG.md` file. 35 | 36 | ## Need help? 37 | 38 | Feel free to ask. 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Melchyore # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: melchyore # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/melchyore', 'https://paypal.me/melchyore'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 6 | 7 | - Ensure the issue isn't already reported. 8 | - Ensure you are reporting the bug in the correct repo. 9 | 10 | *Delete the above section and the instructions in the sections below before submitting* 11 | 12 | ## Description 13 | 14 | If this is a feature request, explain why it should be added. Specific use-cases are best. 15 | 16 | For bug reports, please provide as much *relevant* info as possible. 17 | 18 | ## Package version 19 | 20 | 21 | ## Error Message & Stack Trace 22 | 23 | ## Relevant Information 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/Melchyore/adonis-auto-preload/blob/master/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - "Type: Security" 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: "Status: Abandoned" 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | linux: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 16.17.0 12 | - 18.x 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install 20 | run: npm install 21 | - name: Run tests 22 | run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .vscode 5 | .DS_STORE 6 | .env 7 | tmp 8 | test/__app 9 | .nyc_output 10 | .idea 11 | .vscode/ 12 | *.sublime-project 13 | *.sublime-workspace 14 | *.log 15 | dist 16 | shrinkwrap.yaml 17 | *.tgz 18 | package-lock.json 19 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Oussama Benhamed, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Adonis Auto-Preload

3 | 4 |

Auto-preload multiple relationships when retrieving Lucid models

5 | 6 |

7 | 8 | Build 9 | 10 | 11 | npm 12 | 13 | 14 | License: MIT 15 | 16 | Typescript 17 |

18 |
19 | 20 | ## **Pre-requisites** 21 | > Node.js >= 16.17.0 22 | 23 | ## **Installation** 24 | 25 | ```sh 26 | npm install @melchyore/adonis-auto-preload 27 | # or 28 | yarn add @melchyore/adonis-auto-preload 29 | # or 30 | pnpm install @melchyore/adonis-auto-preload 31 | ``` 32 | ## **Configure** 33 | ```sh 34 | node ace configure @melchyore/adonis-auto-preload 35 | ``` 36 | 37 | ## **Usage** 38 | Extend from the AutoPreload mixin and add a new `static $with` attribute. 39 | 40 | Adding `as const` to `$with` array will let the compiler know about your relationship names and infer them so you will have better intellisense when using `without` and `withOnly` methods. 41 | 42 | Relationships will be auto-preloaded for `find`, `all` and `paginate` queries. 43 | 44 | ### **Using relation name** 45 | ```ts 46 | // App/Models/User.ts 47 | 48 | import { BaseModel, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 49 | import { compose } from '@ioc:Adonis/Core/Helpers' 50 | 51 | import { AutoPreload } from '@ioc:Adonis/Addons/AutoPreload' 52 | 53 | import Post from 'App/Models/Post' 54 | 55 | class User extends compose(BaseModel, AutoPreload) { 56 | public static $with = ['posts'] as const 57 | 58 | @column({ isPrimary: true }) 59 | public id: number 60 | 61 | @column() 62 | public email: string 63 | 64 | @hasMany(() => Post) 65 | public posts: HasMany 66 | } 67 | ``` 68 | 69 | ```ts 70 | // App/Controllers/Http/UsersController.ts 71 | 72 | import User from 'App/Models/User' 73 | 74 | export default class UsersController { 75 | public async show() { 76 | return await User.find(1) // ⬅ Returns user with posts attached. 77 | } 78 | } 79 | ``` 80 | 81 | ### **Using function** 82 | You can also use functions to auto-preload relationships. The function will receive the model query builder as the only argument. 83 | 84 | ```ts 85 | // App/Models/User.ts 86 | 87 | import { BaseModel, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 88 | import { compose } from '@ioc:Adonis/Core/Helpers' 89 | 90 | import { AutoPreload } from '@ioc:Adonis/Addons/AutoPreload' 91 | 92 | import Post from 'App/Models/Post' 93 | 94 | class User extends compose(BaseModel, AutoPreload) { 95 | public static $with = [ 96 | (query: ModelQueryBuilderContract) => { 97 | query.preload('posts') 98 | } 99 | ] 100 | 101 | @column({ isPrimary: true }) 102 | public id: number 103 | 104 | @column() 105 | public email: string 106 | 107 | @hasMany(() => Post) 108 | public posts: HasMany 109 | } 110 | ``` 111 | 112 | ```ts 113 | // App/Controllers/Http/UsersController.ts 114 | 115 | import User from 'App/Models/User' 116 | 117 | export default class UsersController { 118 | public async show() { 119 | return await User.find(1) // ⬅ Returns user with posts attached. 120 | } 121 | } 122 | ``` 123 | 124 | ## **Nested relationships** 125 | You can auto-preload nested relationships using the dot "." between the parent model and the child model. In the following example, `User` -> hasMany -> `Post` -> hasMany -> `Comment`. 126 | 127 | ```ts 128 | // App/Models/Post.ts 129 | 130 | import { BaseModel, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 131 | import { compose } from '@ioc:Adonis/Core/Helpers' 132 | 133 | class Post extends BaseModel { 134 | @column({ isPrimary: true }) 135 | public id: number 136 | 137 | @column() 138 | public userId: number 139 | 140 | @column() 141 | public title: string 142 | 143 | @column() 144 | public content: string 145 | 146 | @hasMany(() => Comment) 147 | public comments: HasMany 148 | } 149 | ``` 150 | 151 | ```ts 152 | // App/Models/User.ts 153 | 154 | import { BaseModel, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 155 | import { compose } from '@ioc:Adonis/Core/Helpers' 156 | 157 | import { AutoPreload } from '@ioc:Adonis/Addons/AutoPreload' 158 | 159 | import Post from 'App/Models/Post' 160 | 161 | class User extends compose(BaseModel, AutoPreload) { 162 | public static $with = ['posts.comments'] as const 163 | 164 | @column({ isPrimary: true }) 165 | public id: number 166 | 167 | @column() 168 | public email: string 169 | 170 | @hasMany(() => Post) 171 | public posts: HasMany 172 | } 173 | ``` 174 | 175 | When retrieving a user, it will preload both `posts` and `comments` (`comments` will be attached to their `posts` parents objects). 176 | 177 | You can also use functions to auto-preload nested relationships. 178 | 179 | ```ts 180 | public static $with = [ 181 | (query: ModelQueryBuilderContract) => { 182 | query.preload('posts', (postsQuery) => { 183 | postsQuery.preload('comments') 184 | }) 185 | } 186 | ] 187 | ``` 188 | 189 | ## **Mixin methods** 190 | The `AutoPreload` mixin will add 3 methods to your models. We will explain all of them below. 191 | 192 | We will use the following model for our methods examples. 193 | 194 | ```ts 195 | // App/Models/User.ts 196 | 197 | import { BaseModel, column, hasOne, HasOne, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 198 | import { compose } from '@ioc:Adonis/Core/Helpers' 199 | 200 | import { AutoPreload } from '@ioc:Adonis/Addons/AutoPreload' 201 | 202 | import Profile from 'App/Models/Profile' 203 | import Post from 'App/Models/Post' 204 | 205 | class User extends compose(BaseModel, AutoPreload) { 206 | public static $with = ['posts', 'profile'] as const 207 | 208 | @column({ isPrimary: true }) 209 | public id: number 210 | 211 | @column() 212 | public email: string 213 | 214 | @hasOne(() => Profile) 215 | public profile: HasOne 216 | 217 | @hasMany(() => Post) 218 | public posts: HasMany 219 | } 220 | ``` 221 | 222 | ### **without** 223 | This method takes an array of relationship names as the only argument. All specified relationships will not be auto-preloaded. You cannot specify relationships registered using functions. 224 | 225 | ```ts 226 | // App/Controllers/Http/UsersController.ts 227 | 228 | import User from 'App/Models/User' 229 | 230 | export default class UsersController { 231 | public async show() { 232 | return await User.without(['posts']).find(1) // ⬅ Returns user with profile and without posts. 233 | } 234 | } 235 | ``` 236 | 237 | ### **withOnly** 238 | This method takes an array of relationship names as the only argument. Only specified relationships will be auto-preloaded. You cannot specify relationships registered using functions. 239 | 240 | ```ts 241 | // App/Controllers/Http/UsersController.ts 242 | 243 | import User from 'App/Models/User' 244 | 245 | export default class UsersController { 246 | public async show() { 247 | return await User.withOnly(['profile']).find(1) // ⬅ Returns user with profile and without posts. 248 | } 249 | } 250 | ``` 251 | 252 | ### **withoutAny** 253 | Exclude all relationships from being auto-preloaded. 254 | 255 | ```ts 256 | // App/Controllers/Http/UsersController.ts 257 | 258 | import User from 'App/Models/User' 259 | 260 | export default class UsersController { 261 | public async show() { 262 | return await User.withoutAny().find(1) // ⬅ Returns user without profile and posts. 263 | } 264 | } 265 | ``` 266 | 267 | > **Note** 268 | > 269 | > You can chain other model methods with mixin methods. For example, `await User.withoutAny().query().paginate(1)` 270 | 271 | ## **Limitations** 272 | - Consider the following scenario: `User` -> hasMany -> `Post` -> hasMany -> `Comments`. If you auto-preload `user` and `comments` from `Post` and you auto-preload `posts` from `User`, you will end-up in a infinite loop and your application will stop working. 273 | 274 | ## **Route model binding** 275 | When using route model binding, you cannot use `without`, `withOnly` and `withoutAny` methods in your controller. But, you can make use of [findForRequest](https://github.com/adonisjs/route-model-binding#change-lookup-logic) method. 276 | 277 | ```ts 278 | // App/Models/User.ts 279 | 280 | import { BaseModel, column, hasOne, HasOne, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' 281 | import { compose } from '@ioc:Adonis/Core/Helpers' 282 | 283 | import { AutoPreload } from '@ioc:Adonis/Addons/AutoPreload' 284 | 285 | import Profile from 'App/Models/Profile' 286 | import Post from 'App/Models/Post' 287 | 288 | class User extends compose(BaseModel, AutoPreload) { 289 | public static $with = ['posts', 'profile'] as const 290 | 291 | @column({ isPrimary: true }) 292 | public id: number 293 | 294 | @column() 295 | public email: string 296 | 297 | @hasOne(() => Profile) 298 | public profile: HasOne 299 | 300 | @hasMany(() => Post) 301 | public posts: HasMany 302 | 303 | public static findForRequest(ctx, param, value) { 304 | const lookupKey = param.lookupKey === '$primaryKey' ? 'id' : param.lookupKey 305 | 306 | return this 307 | .without(['posts']) // ⬅ Do not auto-preload posts when using route model binding. 308 | .query() 309 | .where(lookupKey, value) 310 | .firstOrFail() 311 | } 312 | } 313 | ``` 314 | 315 | ## **Run tests** 316 | 317 | ```sh 318 | npm run test 319 | ``` 320 | 321 | ## **Author** 322 | 323 | 👤 **Oussama Benhamed** 324 | 325 | * Twitter: [@Melchyore](https://twitter.com/Melchyore) 326 | * Github: [@Melchyore](https://github.com/Melchyore) 327 | 328 | ## 🤝 **Contributing** 329 | 330 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/Melchyore/adonis-auto-preload/issues). You can also take a look at the [contributing guide](https://github.com/Melchyore/adonis-auto-preload/blob/master/CONTRIBUTING.md). 331 | 332 | ## **Show your support** 333 | 334 | Give a ⭐️ if this project helped you! 335 | 336 | 337 | 338 | 339 | 340 | ## 📝 **License** 341 | 342 | Copyright © 2022 [Oussama Benhamed](https://github.com/Melchyore).
343 | This project is [MIT](https://github.com/Melchyore/adonis-auto-preload/blob/master/LICENSE.md) licensed. 344 | -------------------------------------------------------------------------------- /adonis-typings/auto-preload.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Addons/AutoPreload' { 2 | import type { LucidModel, ExtractModelRelations } from '@ioc:Adonis/Lucid/Orm' 3 | import type { NormalizeConstructor } from '@ioc:Adonis/Core/Helpers' 4 | 5 | type GetWith = T extends { $with: any } 6 | ? T['$with'][number] extends infer Item 7 | ? Item extends string 8 | ? Item 9 | : never 10 | : never 11 | : never 12 | 13 | export interface AutoPreloadMixin { 14 | >(superclass: T): T & { 15 | $with: any 16 | 17 | without(this: U, relationships: Array>): U 18 | withOnly(this: U, relationships: Array>): U 19 | withoutAny(this: U): U 20 | 21 | new (...args: Array): {} 22 | } 23 | } 24 | 25 | const AutoPreload: AutoPreloadMixin 26 | 27 | export { AutoPreload } 28 | } 29 | -------------------------------------------------------------------------------- /adonis-typings/container.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Application' { 2 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 3 | 4 | export interface ContainerBindings { 5 | 'Adonis/Addons/AutoPreload': { 6 | AutoPreload: AutoPreloadMixin 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /bin/test/config.ts: -------------------------------------------------------------------------------- 1 | import { resolve, join } from 'node:path' 2 | 3 | import { Filesystem } from '@poppinss/dev-utils' 4 | 5 | export const fs = new Filesystem(join(__dirname, 'app')) 6 | 7 | const databaseConfig = { 8 | connection: 'sqlite', 9 | connections: { 10 | sqlite: { 11 | client: 'sqlite', 12 | connection: { 13 | filename: resolve(__dirname, './app/tmp/database.sqlite'), 14 | }, 15 | }, 16 | }, 17 | } 18 | 19 | export async function createAppConfig() { 20 | await fs.add( 21 | 'config/app.ts', 22 | ` 23 | export const appKey = 'averylong32charsrandomsecretkey', 24 | export const http = { 25 | cookie: {}, 26 | trustProxy: () => true, 27 | } 28 | ` 29 | ) 30 | } 31 | 32 | export async function createDatabaseConfig() { 33 | await fs.add( 34 | 'config/database.ts', 35 | ` 36 | const databaseConfig = ${JSON.stringify(databaseConfig, null, 2)} 37 | export default databaseConfig 38 | ` 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /bin/test/database.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseContract } from '@ioc:Adonis/Lucid/Database' 2 | 3 | export async function createUsersTable(Database: DatabaseContract) { 4 | await Database.connection().schema.dropTableIfExists('users') 5 | await Database.connection().schema.createTable('users', (table) => { 6 | table.increments('id') 7 | table.string('email', 255).notNullable() 8 | table.string('name', 255).notNullable() 9 | table.timestamp('created_at', { useTz: true }) 10 | }) 11 | } 12 | 13 | export async function createPostsTable(Database: DatabaseContract) { 14 | await Database.connection().schema.dropTableIfExists('posts') 15 | await Database.connection().schema.createTable('posts', (table) => { 16 | table.increments('id') 17 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('') 18 | table.string('title', 100).notNullable() 19 | table.text('content', 'longtext').notNullable() 20 | table.timestamp('created_at', { useTz: true }) 21 | table.timestamp('updated_at', { useTz: true }) 22 | }) 23 | } 24 | 25 | export async function createCommentsTable(Database: DatabaseContract) { 26 | await Database.connection().schema.dropTableIfExists('comments') 27 | await Database.connection().schema.createTable('comments', (table) => { 28 | table.increments('id') 29 | table.integer('user_id').unsigned().references('id').inTable('users').onDelete('') 30 | table.integer('post_id').unsigned().references('id').inTable('posts').onDelete('') 31 | table.string('comment', 255).notNullable() 32 | table.timestamp('created_at', { useTz: true }) 33 | table.timestamp('updated_at', { useTz: true }) 34 | }) 35 | } 36 | 37 | export async function setupDatabase(Database: DatabaseContract) { 38 | await createUsersTable(Database) 39 | await createPostsTable(Database) 40 | await createCommentsTable(Database) 41 | } 42 | 43 | export async function cleanDatabase(Database: DatabaseContract) { 44 | await Database.connection().dropAllTables() 45 | await Database.manager.closeAll() 46 | } 47 | -------------------------------------------------------------------------------- /bin/test/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from '@japa/expect' 2 | import { assert } from '@japa/assert' 3 | import { specReporter } from '@japa/spec-reporter' 4 | import { runFailedTests } from '@japa/run-failed-tests' 5 | import { processCliArgs, configure, run } from '@japa/runner' 6 | 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Configure tests 10 | |-------------------------------------------------------------------------- 11 | | 12 | | The configure method accepts the configuration to configure the Japa 13 | | tests runner. 14 | | 15 | | The first method call "processCliArgs" process the command line arguments 16 | | and turns them into a config object. Using this method is not mandatory. 17 | | 18 | | Please consult japa.dev/runner-config for the config docs. 19 | */ 20 | 21 | configure({ 22 | ...processCliArgs(process.argv.slice(2)), 23 | ...{ 24 | files: ['tests/**/*.spec.ts'], 25 | plugins: [expect(), assert(), runFailedTests()], 26 | reporters: [specReporter()], 27 | importer: (filePath: string) => import(filePath), 28 | forceExit: true, 29 | }, 30 | }) 31 | 32 | /** 33 | * Setup context 34 | */ 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Run tests 39 | |-------------------------------------------------------------------------- 40 | | 41 | | The following "run" method is required to execute all the tests. 42 | | 43 | */ 44 | run() 45 | -------------------------------------------------------------------------------- /bin/test/japaTypes.ts: -------------------------------------------------------------------------------- 1 | import { Expect } from '@japa/expect' 2 | import { Assert } from '@japa/assert' 3 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 4 | 5 | declare module '@japa/runner' { 6 | interface TestContext { 7 | // notify TypeScript about custom context properties 8 | expect: Expect 9 | assert: Assert 10 | app: ApplicationContract 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | import { listDirectoryFiles } from '@adonisjs/core/build/standalone' 2 | import Application from '@ioc:Adonis/Core/Application' 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Exporting an array of commands 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Instead of manually exporting each file from this directory, we use the 10 | | helper `listDirectoryFiles` to recursively collect and export an array 11 | | of filenames. 12 | | 13 | | Couple of things to note: 14 | | 15 | | 1. The file path must be relative from the project root and not this directory. 16 | | 2. We must ignore this file to avoid getting into an infinite loop 17 | | 18 | */ 19 | export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) 20 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | The package has been configured successfully. You can now add the `AutoPreload` mixin to your models to auto-preload relationships. 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@melchyore/adonis-auto-preload", 3 | "version": "1.0.5", 4 | "description": "Auto-preload multiple relationships when retrieving Lucid models", 5 | "keywords": [ 6 | "typescript", 7 | "javascript", 8 | "ts", 9 | "js", 10 | "adonisjs", 11 | "package", 12 | "module", 13 | "template", 14 | "adonis", 15 | "framework", 16 | "node", 17 | "nodejs", 18 | "model", 19 | "lucid", 20 | "orm", 21 | "database", 22 | "sql", 23 | "sqlite", 24 | "mysql", 25 | "pg", 26 | "postgre", 27 | "postgresql", 28 | "oracle", 29 | "mssql", 30 | "auto", 31 | "preload", 32 | "eager", 33 | "load", 34 | "eager-loading" 35 | ], 36 | "author": { 37 | "name": "Oussama Benhamed", 38 | "email": "b.oussama@corposmart.dz" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/Melchyore/adonis-auto-preload.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/Melchyore/adonis-auto-preload/issues" 46 | }, 47 | "homepage": "https://github.com/Melchyore/adonis-auto-preload#readme", 48 | "scripts": { 49 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 50 | "dev": "node ace serve --watch", 51 | "build": "npm run compile", 52 | "start": "node server.js", 53 | "lint": "eslint . --ext=.ts", 54 | "format": "prettier --write .", 55 | "pretest": "npm run lint", 56 | "test": "node -r @adonisjs/require-ts/build/register bin/test", 57 | "clean": "del-cli build", 58 | "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", 59 | "compile": "npm run lint && npm run clean && tsc && npm run copyfiles", 60 | "prepublishOnly": "npm run build", 61 | "commit": "git-cz", 62 | "release": "np --message=\"chore(release): %s\"", 63 | "version": "npm run build", 64 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json Melchyore/adonis-auto-preload" 65 | }, 66 | "eslintConfig": { 67 | "extends": [ 68 | "plugin:adonis/typescriptPackage", 69 | "prettier" 70 | ], 71 | "plugins": [ 72 | "prettier" 73 | ], 74 | "rules": { 75 | "prettier/prettier": [ 76 | "error", 77 | { 78 | "endOfLine": "auto" 79 | } 80 | ] 81 | } 82 | }, 83 | "eslintIgnore": [ 84 | "build" 85 | ], 86 | "prettier": { 87 | "trailingComma": "es5", 88 | "semi": false, 89 | "singleQuote": true, 90 | "useTabs": false, 91 | "quoteProps": "consistent", 92 | "bracketSpacing": true, 93 | "arrowParens": "always", 94 | "printWidth": 100 95 | }, 96 | "devDependencies": { 97 | "@adonisjs/assembler": "^5.8.1", 98 | "@adonisjs/lucid": "^18.1.0", 99 | "@adonisjs/mrm-preset": "^5.0.3", 100 | "@adonisjs/require-ts": "^2.0.12", 101 | "@japa/expect": "^1.1.4", 102 | "@japa/preset-adonis": "^1.1.0", 103 | "@japa/runner": "^2.0.9", 104 | "@poppinss/dev-utils": "^2.0.3", 105 | "@types/node": "^18.7.13", 106 | "adonis-preset-ts": "^2.1.0", 107 | "commitizen": "^4.2.5", 108 | "copyfiles": "^2.4.1", 109 | "cz-conventional-changelog": "^3.3.0", 110 | "del-cli": "^5.0.0", 111 | "eslint": "^8.23.0", 112 | "eslint-config-prettier": "^8.5.0", 113 | "eslint-plugin-adonis": "^2.1.0", 114 | "eslint-plugin-prettier": "^4.2.1", 115 | "github-label-sync": "^2.2.0", 116 | "husky": "^8.0.1", 117 | "mrm": "^4.1.0", 118 | "np": "^7.6.2", 119 | "pino-pretty": "^9.1.0", 120 | "prettier": "^2.7.1", 121 | "sqlite3": "^5.0.11", 122 | "typescript": "~4.6", 123 | "youch": "^3.2.0", 124 | "youch-terminal": "^2.1.4" 125 | }, 126 | "dependencies": { 127 | "@adonisjs/core": "^5.8.6", 128 | "reflect-metadata": "^0.1.13" 129 | }, 130 | "peerDependencies": { 131 | "@adonisjs/lucid": "^18.0.0" 132 | }, 133 | "publishConfig": { 134 | "tag": "latest", 135 | "access": "public" 136 | }, 137 | "mrmConfig": { 138 | "core": false, 139 | "license": "MIT", 140 | "services": [ 141 | "github-actions" 142 | ], 143 | "minNodeVersion": "16.13.1", 144 | "probotApps": [ 145 | "stale", 146 | "lock" 147 | ], 148 | "runGhActionsOnWindows": false 149 | }, 150 | "license": "MIT", 151 | "main": "./build/providers/AutoPreloadProvider.js", 152 | "types": "./build/adonis-typings/index.d.ts", 153 | "files": [ 154 | "build/adonis-typings", 155 | "build/providers", 156 | "build/src", 157 | "build/instructions.md" 158 | ], 159 | "config": { 160 | "commitizen": { 161 | "path": "cz-conventional-changelog" 162 | } 163 | }, 164 | "np": { 165 | "contents": ".", 166 | "anyBranch": false 167 | }, 168 | "adonisjs": { 169 | "instructionsMd": "./build/instructions.md", 170 | "types": "@melchyore/adonis-auto-preload", 171 | "providers": [ 172 | "@melchyore/adonis-auto-preload" 173 | ] 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /providers/AutoPreloadProvider.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | export default class AutoPreloadProvider { 4 | public static needsApplication = true 5 | 6 | constructor(protected app: ApplicationContract) {} 7 | 8 | public register() { 9 | this.app.container.singleton('Adonis/Addons/AutoPreload', () => { 10 | const { AutoPreload } = require('../src/Mixins/AutoPreload') 11 | 12 | return { AutoPreload } 13 | }) 14 | } 15 | 16 | public async boot() {} 17 | 18 | public async ready() {} 19 | 20 | public async shutdown() {} 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/WrongArgumentTypeException.ts: -------------------------------------------------------------------------------- 1 | import { Exception } from '@poppinss/utils' 2 | 3 | export default class WrongArgumentTypeException extends Exception { 4 | public static invoke(method: string) { 5 | return new this( 6 | `The method ${method} accepts only an array of strings`, 7 | 500, 8 | 'E_WRONG_ARGUMENT_TYPE' 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Exceptions/WrongRelationshipTypeException.ts: -------------------------------------------------------------------------------- 1 | import { Exception } from '@poppinss/utils' 2 | 3 | export default class WrongRelationshipTypeException extends Exception { 4 | public static invoke(model: string) { 5 | return new this( 6 | `The model "${model}" has wrong relationships to be auto-preloaded. Only string and function types are allowed`, 7 | 500, 8 | 'E_WRONG_RELATIONSHIP_TYPE' 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Mixins/AutoPreload.ts: -------------------------------------------------------------------------------- 1 | import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm' 2 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 3 | 4 | import WrongRelationshipTypeException from '../Exceptions/WrongRelationshipTypeException' 5 | import WrongArgumentTypeException from '../Exceptions/WrongArgumentTypeException' 6 | 7 | export const AutoPreload: AutoPreloadMixin = (superclass) => { 8 | class AutoPreloadModel extends superclass { 9 | public static $with: Array = [] 10 | 11 | protected static $originalWith: Array = [] 12 | 13 | public static boot() { 14 | if (this.booted) { 15 | return 16 | } 17 | 18 | if (this.$with.length > 0) { 19 | const isWrongType = this.$with.every((relationship) => { 20 | return !['function', 'string'].includes(typeof relationship) 21 | }) 22 | 23 | if (isWrongType) { 24 | throw WrongRelationshipTypeException.invoke(this.name) 25 | } 26 | } 27 | 28 | super.boot() 29 | 30 | this.$originalWith = [...this.$with] 31 | 32 | for (const hook of ['fetch', 'find'] as const) { 33 | this.before(hook, (query: any) => { 34 | this.handleAutoPreload(query) 35 | }) 36 | } 37 | 38 | this.before( 39 | 'paginate', 40 | ([_, query]: [ 41 | ModelQueryBuilderContract, 42 | ModelQueryBuilderContract 43 | ]) => { 44 | this.handleAutoPreload(query, false) 45 | } 46 | ) 47 | } 48 | 49 | public static without(relationships: any): any { 50 | this.checkArrayOfRelationships('without', relationships) 51 | 52 | this.$with = this.$with.filter((relationship) => { 53 | if (typeof relationship === 'string') { 54 | return !relationships.includes(relationship) 55 | } else if (typeof relationship === 'function') { 56 | return relationship 57 | } else { 58 | throw WrongArgumentTypeException.invoke(relationship) 59 | } 60 | }) 61 | 62 | return this 63 | } 64 | 65 | public static withOnly(relationships: any): any { 66 | this.checkArrayOfRelationships('withOnly', relationships) 67 | 68 | this.$with = this.$with.filter((relationship) => { 69 | if (typeof relationship === 'string') { 70 | return relationships.includes(relationship) 71 | } else if (typeof relationship === 'function') { 72 | return relationship 73 | } else { 74 | throw WrongArgumentTypeException.invoke(relationship) 75 | } 76 | }) 77 | 78 | return this 79 | } 80 | 81 | public static withoutAny(): any { 82 | this.$with = [] 83 | 84 | return this 85 | } 86 | 87 | private static handleAutoPreload( 88 | query: ModelQueryBuilderContract, 89 | restorePreloads = true 90 | ) { 91 | const preloads = this.$with 92 | 93 | if (preloads.length > 0) { 94 | for (const preload of preloads) { 95 | if (typeof preload === 'string') { 96 | if (preload.includes('.')) { 97 | this.handleNestedRelationships(query, preload.split('.') as any) 98 | } else { 99 | query.preload(preload as any) 100 | } 101 | } else if (typeof preload === 'function') { 102 | preload(query) 103 | } 104 | } 105 | } 106 | 107 | if (restorePreloads) { 108 | this.$with = [...this.$originalWith] 109 | } 110 | } 111 | 112 | /** 113 | * Recursive function to handle nested relationships. 114 | */ 115 | private static handleNestedRelationships( 116 | query: ModelQueryBuilderContract, 117 | relationships: any 118 | ) { 119 | if (relationships.length > 0) { 120 | const nextRelation = relationships.shift() 121 | 122 | if (nextRelation) { 123 | query.preload(nextRelation, (qb: any) => { 124 | if (relationships.length > 0) { 125 | this.handleNestedRelationships(qb, relationships) 126 | } 127 | }) 128 | } 129 | } 130 | } 131 | 132 | private static checkArrayOfRelationships(method: string, relationships: Array) { 133 | if (relationships.length > 0) { 134 | const isWrongType = relationships.every((relationship: any) => { 135 | return !['function', 'string'].includes(typeof relationship) 136 | }) 137 | 138 | if (isWrongType) { 139 | throw WrongArgumentTypeException.invoke(method) 140 | } 141 | } 142 | } 143 | } 144 | 145 | return AutoPreloadModel 146 | } 147 | -------------------------------------------------------------------------------- /test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 2 | 3 | import { Application } from '@adonisjs/application' 4 | 5 | import { fs, createAppConfig, createDatabaseConfig } from '../bin/test/config' 6 | 7 | export async function setupApp(): Promise { 8 | await fs.add('.env', '') 9 | await fs.add('./tmp/database.sqlite', '') 10 | await createAppConfig() 11 | await createDatabaseConfig() 12 | 13 | const app = new Application(fs.basePath, 'test', { 14 | providers: ['@adonisjs/core', '@adonisjs/lucid', '../../../providers/AutoPreloadProvider'], 15 | }) 16 | 17 | await app.setup() 18 | await app.registerProviders() 19 | await app.bootProviders() 20 | 21 | return app 22 | } 23 | -------------------------------------------------------------------------------- /tests/auto_preload-multiple_pagination.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseContract } from '@ioc:Adonis/Lucid/Database' 2 | import type { 3 | BaseModel as BaseModelContract, 4 | ColumnDecorator, 5 | BelongsTo, 6 | BelongsToDecorator, 7 | HasMany, 8 | HasManyDecorator, 9 | ModelQueryBuilderContract, 10 | } from '@ioc:Adonis/Lucid/Orm' 11 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 12 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 13 | 14 | import { test } from '@japa/runner' 15 | import { compose } from '@poppinss/utils/build/helpers' 16 | 17 | import { setupDatabase, cleanDatabase } from '../bin/test/database' 18 | import { fs } from '../bin/test/config' 19 | import { setupApp } from '../test-helpers' 20 | 21 | let db: DatabaseContract 22 | let BaseModel: typeof BaseModelContract 23 | let AutoPreload: AutoPreloadMixin 24 | let app: ApplicationContract 25 | let column: ColumnDecorator 26 | let belongsTo: BelongsToDecorator 27 | let hasMany: HasManyDecorator 28 | 29 | test.group('Auto preload - Pagination', (group) => { 30 | group.setup(async () => { 31 | app = await setupApp() 32 | db = app.container.resolveBinding('Adonis/Lucid/Database') 33 | BaseModel = app.container.resolveBinding('Adonis/Lucid/Orm').BaseModel 34 | AutoPreload = app.container.resolveBinding('Adonis/Addons/AutoPreload').AutoPreload 35 | column = app.container.resolveBinding('Adonis/Lucid/Orm').column 36 | belongsTo = app.container.resolveBinding('Adonis/Lucid/Orm').belongsTo 37 | hasMany = app.container.resolveBinding('Adonis/Lucid/Orm').hasMany 38 | }) 39 | 40 | group.each.setup(async () => { 41 | await setupDatabase(db) 42 | }) 43 | 44 | group.each.teardown(async () => { 45 | await cleanDatabase(db) 46 | }) 47 | 48 | group.teardown(async () => { 49 | await db.manager.closeAll() 50 | await fs.cleanup() 51 | }) 52 | 53 | test('using mixin should auto-preload relationships when using relation name', async ({ 54 | expect, 55 | }) => { 56 | class Post extends BaseModel { 57 | @column({ isPrimary: true }) 58 | public id: number 59 | 60 | @column() 61 | public userId: number 62 | 63 | @column() 64 | public title: string 65 | 66 | @column() 67 | public content: string 68 | } 69 | 70 | class User extends compose(BaseModel, AutoPreload) { 71 | public static $with = ['posts'] 72 | 73 | @column({ isPrimary: true }) 74 | public id: number 75 | 76 | @column() 77 | public email: string 78 | 79 | @column() 80 | public name: string 81 | 82 | @hasMany(() => Post) 83 | public posts: HasMany 84 | } 85 | 86 | const user = await User.create({ 87 | email: 'john@doe.com', 88 | name: 'John Doe', 89 | }) 90 | 91 | await user.related('posts').createMany([ 92 | { 93 | title: 'Test', 94 | content: 'Test content', 95 | }, 96 | { 97 | title: 'Foo', 98 | content: 'Foo content', 99 | }, 100 | ]) 101 | 102 | expect((await User.query().paginate(1)).at(0)?.$preloaded.posts).toHaveLength(2) 103 | }) 104 | 105 | test('using mixin should auto-preload relationships when using a function', async ({ 106 | expect, 107 | }) => { 108 | class Post extends BaseModel { 109 | @column({ isPrimary: true }) 110 | public id: number 111 | 112 | @column() 113 | public userId: number 114 | 115 | @column() 116 | public title: string 117 | 118 | @column() 119 | public content: string 120 | } 121 | 122 | class User extends compose(BaseModel, AutoPreload) { 123 | public static $with = [ 124 | (query: ModelQueryBuilderContract) => { 125 | query.preload('posts') 126 | }, 127 | ] 128 | 129 | @column({ isPrimary: true }) 130 | public id: number 131 | 132 | @column() 133 | public email: string 134 | 135 | @column() 136 | public name: string 137 | 138 | @hasMany(() => Post) 139 | public posts: HasMany 140 | } 141 | 142 | const user = await User.create({ 143 | email: 'john@doe.com', 144 | name: 'John Doe', 145 | }) 146 | 147 | await user.related('posts').createMany([ 148 | { 149 | title: 'Test', 150 | content: 'Test content', 151 | }, 152 | { 153 | title: 'Foo', 154 | content: 'Foo content', 155 | }, 156 | ]) 157 | 158 | expect((await User.query().paginate(1)).at(0)?.$preloaded.posts).toHaveLength(2) 159 | }) 160 | 161 | test('using mixin should auto-preload nested relationships when using relation name', async ({ 162 | expect, 163 | }) => { 164 | class Comment extends BaseModel { 165 | @column({ isPrimary: true }) 166 | public id: number 167 | 168 | @column() 169 | public userId: number 170 | 171 | @column() 172 | public postId: number 173 | 174 | @column() 175 | public comment: string 176 | } 177 | 178 | class Post extends BaseModel { 179 | @column({ isPrimary: true }) 180 | public id: number 181 | 182 | @column() 183 | public userId: number 184 | 185 | @column() 186 | public title: string 187 | 188 | @column() 189 | public content: string 190 | 191 | @hasMany(() => Comment) 192 | public comments: HasMany 193 | } 194 | 195 | class User extends compose(BaseModel, AutoPreload) { 196 | public static $with = ['posts.comments'] 197 | 198 | @column({ isPrimary: true }) 199 | public id: number 200 | 201 | @column() 202 | public email: string 203 | 204 | @column() 205 | public name: string 206 | 207 | @hasMany(() => Post) 208 | public posts: HasMany 209 | } 210 | 211 | const user = await User.create({ 212 | email: 'john@doe.com', 213 | name: 'John Doe', 214 | }) 215 | 216 | const post1 = new Post() 217 | post1.title = 'Test' 218 | post1.content = 'Test content' 219 | 220 | const post2 = new Post() 221 | post2.title = 'Foo' 222 | post2.content = 'Foo content' 223 | 224 | await user.related('posts').saveMany([post1, post2]) 225 | 226 | await post1.related('comments').create({ 227 | userId: 1, 228 | comment: 'Test', 229 | }) 230 | 231 | await post2.related('comments').createMany([ 232 | { 233 | userId: 1, 234 | comment: 'Bar', 235 | }, 236 | { 237 | userId: 1, 238 | comment: 'Foo', 239 | }, 240 | ]) 241 | 242 | expect( 243 | (await User.query().paginate(1)).at(0)?.$preloaded.posts[0].$preloaded.comments 244 | ).toHaveLength(1) 245 | expect( 246 | (await User.query().paginate(1)).at(0)?.$preloaded.posts[1].$preloaded.comments 247 | ).toHaveLength(2) 248 | }) 249 | 250 | test('using mixin should auto-preload nested relationships when using a function', async ({ 251 | expect, 252 | }) => { 253 | class Comment extends BaseModel { 254 | @column({ isPrimary: true }) 255 | public id: number 256 | 257 | @column() 258 | public userId: number 259 | 260 | @column() 261 | public postId: number 262 | 263 | @column() 264 | public comment: string 265 | } 266 | 267 | class Post extends BaseModel { 268 | @column({ isPrimary: true }) 269 | public id: number 270 | 271 | @column() 272 | public userId: number 273 | 274 | @column() 275 | public title: string 276 | 277 | @column() 278 | public content: string 279 | 280 | @hasMany(() => Comment) 281 | public comments: HasMany 282 | } 283 | 284 | class User extends compose(BaseModel, AutoPreload) { 285 | public static $with = [ 286 | (query: ModelQueryBuilderContract) => { 287 | query.preload('posts', (postsQuery) => { 288 | postsQuery.preload('comments') 289 | }) 290 | }, 291 | ] 292 | 293 | @column({ isPrimary: true }) 294 | public id: number 295 | 296 | @column() 297 | public email: string 298 | 299 | @column() 300 | public name: string 301 | 302 | @hasMany(() => Post) 303 | public posts: HasMany 304 | } 305 | 306 | const user = await User.create({ 307 | email: 'john@doe.com', 308 | name: 'John Doe', 309 | }) 310 | 311 | const post1 = new Post() 312 | post1.title = 'Test' 313 | post1.content = 'Test content' 314 | 315 | const post2 = new Post() 316 | post2.title = 'Foo' 317 | post2.content = 'Foo content' 318 | 319 | await user.related('posts').saveMany([post1, post2]) 320 | 321 | await post1.related('comments').create({ 322 | userId: 1, 323 | comment: 'Test', 324 | }) 325 | 326 | await post2.related('comments').createMany([ 327 | { 328 | userId: 1, 329 | comment: 'Bar', 330 | }, 331 | { 332 | userId: 1, 333 | comment: 'Foo', 334 | }, 335 | ]) 336 | 337 | expect( 338 | (await User.query().paginate(1)).at(0)?.$preloaded.posts[0].$preloaded.comments 339 | ).toHaveLength(1) 340 | expect( 341 | (await User.query().paginate(1)).at(0)?.$preloaded.posts[1].$preloaded.comments 342 | ).toHaveLength(2) 343 | }) 344 | 345 | test('using mixin should auto-preload multiple relationships when using relation names', async ({ 346 | expect, 347 | }) => { 348 | class Comment extends BaseModel { 349 | @column({ isPrimary: true }) 350 | public id: number 351 | 352 | @column() 353 | public userId: number 354 | 355 | @column() 356 | public postId: number 357 | 358 | @column() 359 | public comment: string 360 | } 361 | 362 | class Post extends compose(BaseModel, AutoPreload) { 363 | public static $with = ['user', 'comments'] 364 | 365 | @column({ isPrimary: true }) 366 | public id: number 367 | 368 | @column() 369 | public userId: number 370 | 371 | @column() 372 | public title: string 373 | 374 | @column() 375 | public content: string 376 | 377 | @belongsTo(() => User) 378 | public user: BelongsTo 379 | 380 | @hasMany(() => Comment) 381 | public comments: HasMany 382 | } 383 | 384 | class User extends BaseModel { 385 | @column({ isPrimary: true }) 386 | public id: number 387 | 388 | @column() 389 | public email: string 390 | 391 | @column() 392 | public name: string 393 | 394 | @hasMany(() => Post) 395 | public posts: HasMany 396 | } 397 | 398 | const user = await User.create({ 399 | email: 'john@doe.com', 400 | name: 'John Doe', 401 | }) 402 | 403 | const post1 = new Post() 404 | post1.title = 'Test' 405 | post1.content = 'Test content' 406 | 407 | const post2 = new Post() 408 | post2.title = 'Foo' 409 | post2.content = 'Foo content' 410 | 411 | await user.related('posts').saveMany([post1, post2]) 412 | 413 | await post1.related('comments').create({ 414 | userId: 1, 415 | comment: 'Test', 416 | }) 417 | 418 | await post2.related('comments').createMany([ 419 | { 420 | userId: 1, 421 | comment: 'Bar', 422 | }, 423 | { 424 | userId: 1, 425 | comment: 'Foo', 426 | }, 427 | ]) 428 | 429 | const posts = await Post.query().paginate(1) 430 | 431 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 432 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 433 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(1) 434 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 435 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 436 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(2) 437 | }) 438 | 439 | test('using mixin should auto-preload multiple relationships when using functions', async ({ 440 | expect, 441 | }) => { 442 | class Comment extends BaseModel { 443 | @column({ isPrimary: true }) 444 | public id: number 445 | 446 | @column() 447 | public userId: number 448 | 449 | @column() 450 | public postId: number 451 | 452 | @column() 453 | public comment: string 454 | } 455 | 456 | class Post extends compose(BaseModel, AutoPreload) { 457 | public static $with = [ 458 | (query: ModelQueryBuilderContract) => { 459 | query.preload('user') 460 | }, 461 | (query: ModelQueryBuilderContract) => { 462 | query.preload('comments') 463 | }, 464 | ] 465 | 466 | @column({ isPrimary: true }) 467 | public id: number 468 | 469 | @column() 470 | public userId: number 471 | 472 | @column() 473 | public title: string 474 | 475 | @column() 476 | public content: string 477 | 478 | @belongsTo(() => User) 479 | public user: BelongsTo 480 | 481 | @hasMany(() => Comment) 482 | public comments: HasMany 483 | } 484 | 485 | class User extends BaseModel { 486 | @column({ isPrimary: true }) 487 | public id: number 488 | 489 | @column() 490 | public email: string 491 | 492 | @column() 493 | public name: string 494 | 495 | @hasMany(() => Post) 496 | public posts: HasMany 497 | } 498 | 499 | const user = await User.create({ 500 | email: 'john@doe.com', 501 | name: 'John Doe', 502 | }) 503 | 504 | const post1 = new Post() 505 | post1.title = 'Test' 506 | post1.content = 'Test content' 507 | 508 | const post2 = new Post() 509 | post2.title = 'Foo' 510 | post2.content = 'Foo content' 511 | 512 | await user.related('posts').saveMany([post1, post2]) 513 | 514 | await post1.related('comments').create({ 515 | userId: 1, 516 | comment: 'Test', 517 | }) 518 | 519 | await post2.related('comments').createMany([ 520 | { 521 | userId: 1, 522 | comment: 'Bar', 523 | }, 524 | { 525 | userId: 1, 526 | comment: 'Foo', 527 | }, 528 | ]) 529 | 530 | const posts = await Post.query().paginate(1) 531 | 532 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 533 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 534 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(1) 535 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 536 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 537 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(2) 538 | }) 539 | 540 | test('using mixin should auto-preload multiple relationships when using relation names and functions', async ({ 541 | expect, 542 | }) => { 543 | class Comment extends BaseModel { 544 | @column({ isPrimary: true }) 545 | public id: number 546 | 547 | @column() 548 | public userId: number 549 | 550 | @column() 551 | public postId: number 552 | 553 | @column() 554 | public comment: string 555 | } 556 | 557 | class Post extends compose(BaseModel, AutoPreload) { 558 | public static $with = [ 559 | 'user', 560 | (query: ModelQueryBuilderContract) => { 561 | query.preload('comments') 562 | }, 563 | ] 564 | 565 | @column({ isPrimary: true }) 566 | public id: number 567 | 568 | @column() 569 | public userId: number 570 | 571 | @column() 572 | public title: string 573 | 574 | @column() 575 | public content: string 576 | 577 | @belongsTo(() => User) 578 | public user: BelongsTo 579 | 580 | @hasMany(() => Comment) 581 | public comments: HasMany 582 | } 583 | 584 | class User extends BaseModel { 585 | @column({ isPrimary: true }) 586 | public id: number 587 | 588 | @column() 589 | public email: string 590 | 591 | @column() 592 | public name: string 593 | 594 | @hasMany(() => Post) 595 | public posts: HasMany 596 | } 597 | 598 | const user = await User.create({ 599 | email: 'john@doe.com', 600 | name: 'John Doe', 601 | }) 602 | 603 | const post1 = new Post() 604 | post1.title = 'Test' 605 | post1.content = 'Test content' 606 | 607 | const post2 = new Post() 608 | post2.title = 'Foo' 609 | post2.content = 'Foo content' 610 | 611 | await user.related('posts').saveMany([post1, post2]) 612 | 613 | await post1.related('comments').create({ 614 | userId: 1, 615 | comment: 'Test', 616 | }) 617 | 618 | await post2.related('comments').createMany([ 619 | { 620 | userId: 1, 621 | comment: 'Bar', 622 | }, 623 | { 624 | userId: 1, 625 | comment: 'Foo', 626 | }, 627 | ]) 628 | 629 | const posts = await Post.query().paginate(1) 630 | 631 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 632 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 633 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(1) 634 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 635 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 636 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(2) 637 | }) 638 | 639 | test('without method should exclude specified relationships from being auto-preloaded', async ({ 640 | expect, 641 | }) => { 642 | class Comment extends BaseModel { 643 | @column({ isPrimary: true }) 644 | public id: number 645 | 646 | @column() 647 | public userId: number 648 | 649 | @column() 650 | public postId: number 651 | 652 | @column() 653 | public comment: string 654 | } 655 | 656 | class Post extends compose(BaseModel, AutoPreload) { 657 | public static $with = ['user', 'comments'] as const 658 | 659 | @column({ isPrimary: true }) 660 | public id: number 661 | 662 | @column() 663 | public userId: number 664 | 665 | @column() 666 | public title: string 667 | 668 | @column() 669 | public content: string 670 | 671 | @belongsTo(() => User) 672 | public user: BelongsTo 673 | 674 | @hasMany(() => Comment) 675 | public comments: HasMany 676 | } 677 | 678 | class User extends BaseModel { 679 | @column({ isPrimary: true }) 680 | public id: number 681 | 682 | @column() 683 | public email: string 684 | 685 | @column() 686 | public name: string 687 | 688 | @hasMany(() => Post) 689 | public posts: HasMany 690 | } 691 | 692 | const user = await User.create({ 693 | email: 'john@doe.com', 694 | name: 'John Doe', 695 | }) 696 | 697 | const post1 = new Post() 698 | post1.title = 'Test' 699 | post1.content = 'Test content' 700 | 701 | const post2 = new Post() 702 | post2.title = 'Foo' 703 | post2.content = 'Foo content' 704 | 705 | await user.related('posts').saveMany([post1, post2]) 706 | 707 | await post1.related('comments').create({ 708 | userId: 1, 709 | comment: 'Test', 710 | }) 711 | 712 | await post2.related('comments').createMany([ 713 | { 714 | userId: 1, 715 | comment: 'Bar', 716 | }, 717 | { 718 | userId: 1, 719 | comment: 'Foo', 720 | }, 721 | ]) 722 | 723 | const posts = await Post.without(['comments']).query().paginate(1) 724 | 725 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 726 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 727 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 728 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 729 | }) 730 | 731 | test('auto-preloaded array should be restored after using without method', async ({ expect }) => { 732 | class Comment extends BaseModel { 733 | @column({ isPrimary: true }) 734 | public id: number 735 | 736 | @column() 737 | public userId: number 738 | 739 | @column() 740 | public postId: number 741 | 742 | @column() 743 | public comment: string 744 | } 745 | 746 | class Post extends compose(BaseModel, AutoPreload) { 747 | public static $with = ['user', 'comments'] as const 748 | 749 | @column({ isPrimary: true }) 750 | public id: number 751 | 752 | @column() 753 | public userId: number 754 | 755 | @column() 756 | public title: string 757 | 758 | @column() 759 | public content: string 760 | 761 | @belongsTo(() => User) 762 | public user: BelongsTo 763 | 764 | @hasMany(() => Comment) 765 | public comments: HasMany 766 | } 767 | 768 | class User extends BaseModel { 769 | @column({ isPrimary: true }) 770 | public id: number 771 | 772 | @column() 773 | public email: string 774 | 775 | @column() 776 | public name: string 777 | 778 | @hasMany(() => Post) 779 | public posts: HasMany 780 | } 781 | 782 | const user = await User.create({ 783 | email: 'john@doe.com', 784 | name: 'John Doe', 785 | }) 786 | 787 | const post1 = new Post() 788 | post1.title = 'Test' 789 | post1.content = 'Test content' 790 | 791 | const post2 = new Post() 792 | post2.title = 'Foo' 793 | post2.content = 'Foo content' 794 | 795 | await user.related('posts').saveMany([post1, post2]) 796 | 797 | await post1.related('comments').create({ 798 | userId: 1, 799 | comment: 'Test', 800 | }) 801 | 802 | await post2.related('comments').createMany([ 803 | { 804 | userId: 1, 805 | comment: 'Bar', 806 | }, 807 | { 808 | userId: 1, 809 | comment: 'Foo', 810 | }, 811 | ]) 812 | 813 | const posts = await Post.without(['comments']).query().paginate(1) 814 | 815 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 816 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 817 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 818 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 819 | 820 | expect(Post.$with).toStrictEqual(['user', 'comments']) 821 | }) 822 | 823 | test('without method should throw an exception when relationship is not a string', async ({ 824 | expect, 825 | }) => { 826 | class Comment extends BaseModel { 827 | @column({ isPrimary: true }) 828 | public id: number 829 | 830 | @column() 831 | public userId: number 832 | 833 | @column() 834 | public postId: number 835 | 836 | @column() 837 | public comment: string 838 | } 839 | 840 | class Post extends compose(BaseModel, AutoPreload) { 841 | public static $with = ['user', 'comments'] as const 842 | 843 | @column({ isPrimary: true }) 844 | public id: number 845 | 846 | @column() 847 | public userId: number 848 | 849 | @column() 850 | public title: string 851 | 852 | @column() 853 | public content: string 854 | 855 | @belongsTo(() => User) 856 | public user: BelongsTo 857 | 858 | @hasMany(() => Comment) 859 | public comments: HasMany 860 | } 861 | 862 | class User extends BaseModel { 863 | @column({ isPrimary: true }) 864 | public id: number 865 | 866 | @column() 867 | public email: string 868 | 869 | @column() 870 | public name: string 871 | 872 | @hasMany(() => Post) 873 | public posts: HasMany 874 | } 875 | 876 | const user = await User.create({ 877 | email: 'john@doe.com', 878 | name: 'John Doe', 879 | }) 880 | 881 | const post1 = new Post() 882 | post1.title = 'Test' 883 | post1.content = 'Test content' 884 | 885 | const post2 = new Post() 886 | post2.title = 'Foo' 887 | post2.content = 'Foo content' 888 | 889 | await user.related('posts').saveMany([post1, post2]) 890 | 891 | await post1.related('comments').create({ 892 | userId: 1, 893 | comment: 'Test', 894 | }) 895 | 896 | await post2.related('comments').createMany([ 897 | { 898 | userId: 1, 899 | comment: 'Bar', 900 | }, 901 | { 902 | userId: 1, 903 | comment: 'Foo', 904 | }, 905 | ]) 906 | 907 | // @ts-ignore 908 | expect(async () => await Post.without([5]).query().paginate(1)).rejects.toThrowError() 909 | }) 910 | 911 | test('withOnly method should auto-preload only specified relationships', async ({ expect }) => { 912 | class Comment extends BaseModel { 913 | @column({ isPrimary: true }) 914 | public id: number 915 | 916 | @column() 917 | public userId: number 918 | 919 | @column() 920 | public postId: number 921 | 922 | @column() 923 | public comment: string 924 | } 925 | 926 | class Post extends compose(BaseModel, AutoPreload) { 927 | public static $with = ['user', 'comments'] as const 928 | 929 | @column({ isPrimary: true }) 930 | public id: number 931 | 932 | @column() 933 | public userId: number 934 | 935 | @column() 936 | public title: string 937 | 938 | @column() 939 | public content: string 940 | 941 | @belongsTo(() => User) 942 | public user: BelongsTo 943 | 944 | @hasMany(() => Comment) 945 | public comments: HasMany 946 | } 947 | 948 | class User extends BaseModel { 949 | @column({ isPrimary: true }) 950 | public id: number 951 | 952 | @column() 953 | public email: string 954 | 955 | @column() 956 | public name: string 957 | 958 | @hasMany(() => Post) 959 | public posts: HasMany 960 | } 961 | 962 | const user = await User.create({ 963 | email: 'john@doe.com', 964 | name: 'John Doe', 965 | }) 966 | 967 | const post1 = new Post() 968 | post1.title = 'Test' 969 | post1.content = 'Test content' 970 | 971 | const post2 = new Post() 972 | post2.title = 'Foo' 973 | post2.content = 'Foo content' 974 | 975 | await user.related('posts').saveMany([post1, post2]) 976 | 977 | await post1.related('comments').create({ 978 | userId: 1, 979 | comment: 'Test', 980 | }) 981 | 982 | await post2.related('comments').createMany([ 983 | { 984 | userId: 1, 985 | comment: 'Bar', 986 | }, 987 | { 988 | userId: 1, 989 | comment: 'Foo', 990 | }, 991 | ]) 992 | 993 | const posts = await Post.withOnly(['user']).query().paginate(1) 994 | 995 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 996 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 997 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 998 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 999 | }) 1000 | 1001 | test('auto-preloaded array should be restored after using withOnly method', async ({ 1002 | expect, 1003 | }) => { 1004 | class Comment extends BaseModel { 1005 | @column({ isPrimary: true }) 1006 | public id: number 1007 | 1008 | @column() 1009 | public userId: number 1010 | 1011 | @column() 1012 | public postId: number 1013 | 1014 | @column() 1015 | public comment: string 1016 | } 1017 | 1018 | class Post extends compose(BaseModel, AutoPreload) { 1019 | public static $with = ['user', 'comments'] as const 1020 | 1021 | @column({ isPrimary: true }) 1022 | public id: number 1023 | 1024 | @column() 1025 | public userId: number 1026 | 1027 | @column() 1028 | public title: string 1029 | 1030 | @column() 1031 | public content: string 1032 | 1033 | @belongsTo(() => User) 1034 | public user: BelongsTo 1035 | 1036 | @hasMany(() => Comment) 1037 | public comments: HasMany 1038 | } 1039 | 1040 | class User extends BaseModel { 1041 | @column({ isPrimary: true }) 1042 | public id: number 1043 | 1044 | @column() 1045 | public email: string 1046 | 1047 | @column() 1048 | public name: string 1049 | 1050 | @hasMany(() => Post) 1051 | public posts: HasMany 1052 | } 1053 | 1054 | const user = await User.create({ 1055 | email: 'john@doe.com', 1056 | name: 'John Doe', 1057 | }) 1058 | 1059 | const post1 = new Post() 1060 | post1.title = 'Test' 1061 | post1.content = 'Test content' 1062 | 1063 | const post2 = new Post() 1064 | post2.title = 'Foo' 1065 | post2.content = 'Foo content' 1066 | 1067 | await user.related('posts').saveMany([post1, post2]) 1068 | 1069 | await post1.related('comments').create({ 1070 | userId: 1, 1071 | comment: 'Test', 1072 | }) 1073 | 1074 | await post2.related('comments').createMany([ 1075 | { 1076 | userId: 1, 1077 | comment: 'Bar', 1078 | }, 1079 | { 1080 | userId: 1, 1081 | comment: 'Foo', 1082 | }, 1083 | ]) 1084 | 1085 | const posts = await Post.withOnly(['user']).query().paginate(1) 1086 | 1087 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 1088 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1089 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 1090 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1091 | 1092 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1093 | }) 1094 | 1095 | test('withOnly method should throw an exception when relationship is not a string', async ({ 1096 | expect, 1097 | }) => { 1098 | class Comment extends BaseModel { 1099 | @column({ isPrimary: true }) 1100 | public id: number 1101 | 1102 | @column() 1103 | public userId: number 1104 | 1105 | @column() 1106 | public postId: number 1107 | 1108 | @column() 1109 | public comment: string 1110 | } 1111 | 1112 | class Post extends compose(BaseModel, AutoPreload) { 1113 | public static $with = ['user', 'comments'] as const 1114 | 1115 | @column({ isPrimary: true }) 1116 | public id: number 1117 | 1118 | @column() 1119 | public userId: number 1120 | 1121 | @column() 1122 | public title: string 1123 | 1124 | @column() 1125 | public content: string 1126 | 1127 | @belongsTo(() => User) 1128 | public user: BelongsTo 1129 | 1130 | @hasMany(() => Comment) 1131 | public comments: HasMany 1132 | } 1133 | 1134 | class User extends BaseModel { 1135 | @column({ isPrimary: true }) 1136 | public id: number 1137 | 1138 | @column() 1139 | public email: string 1140 | 1141 | @column() 1142 | public name: string 1143 | 1144 | @hasMany(() => Post) 1145 | public posts: HasMany 1146 | } 1147 | 1148 | const user = await User.create({ 1149 | email: 'john@doe.com', 1150 | name: 'John Doe', 1151 | }) 1152 | 1153 | const post1 = new Post() 1154 | post1.title = 'Test' 1155 | post1.content = 'Test content' 1156 | 1157 | const post2 = new Post() 1158 | post2.title = 'Foo' 1159 | post2.content = 'Foo content' 1160 | 1161 | await user.related('posts').saveMany([post1, post2]) 1162 | 1163 | await post1.related('comments').create({ 1164 | userId: 1, 1165 | comment: 'Test', 1166 | }) 1167 | 1168 | await post2.related('comments').createMany([ 1169 | { 1170 | userId: 1, 1171 | comment: 'Bar', 1172 | }, 1173 | { 1174 | userId: 1, 1175 | comment: 'Foo', 1176 | }, 1177 | ]) 1178 | 1179 | // @ts-ignore 1180 | expect(async () => await Post.withOnly([5]).query().paginate(1)).rejects.toThrowError() 1181 | }) 1182 | 1183 | test('withoutAny method should not auto-preload any relationship', async ({ expect }) => { 1184 | class Comment extends BaseModel { 1185 | @column({ isPrimary: true }) 1186 | public id: number 1187 | 1188 | @column() 1189 | public userId: number 1190 | 1191 | @column() 1192 | public postId: number 1193 | 1194 | @column() 1195 | public comment: string 1196 | } 1197 | 1198 | class Post extends compose(BaseModel, AutoPreload) { 1199 | public static $with = ['user', 'comments'] as const 1200 | 1201 | @column({ isPrimary: true }) 1202 | public id: number 1203 | 1204 | @column() 1205 | public userId: number 1206 | 1207 | @column() 1208 | public title: string 1209 | 1210 | @column() 1211 | public content: string 1212 | 1213 | @belongsTo(() => User) 1214 | public user: BelongsTo 1215 | 1216 | @hasMany(() => Comment) 1217 | public comments: HasMany 1218 | } 1219 | 1220 | class User extends BaseModel { 1221 | @column({ isPrimary: true }) 1222 | public id: number 1223 | 1224 | @column() 1225 | public email: string 1226 | 1227 | @column() 1228 | public name: string 1229 | 1230 | @hasMany(() => Post) 1231 | public posts: HasMany 1232 | } 1233 | 1234 | const user = await User.create({ 1235 | email: 'john@doe.com', 1236 | name: 'John Doe', 1237 | }) 1238 | 1239 | const post1 = new Post() 1240 | post1.title = 'Test' 1241 | post1.content = 'Test content' 1242 | 1243 | const post2 = new Post() 1244 | post2.title = 'Foo' 1245 | post2.content = 'Foo content' 1246 | 1247 | await user.related('posts').saveMany([post1, post2]) 1248 | 1249 | await post1.related('comments').create({ 1250 | userId: 1, 1251 | comment: 'Test', 1252 | }) 1253 | 1254 | await post2.related('comments').createMany([ 1255 | { 1256 | userId: 1, 1257 | comment: 'Bar', 1258 | }, 1259 | { 1260 | userId: 1, 1261 | comment: 'Foo', 1262 | }, 1263 | ]) 1264 | 1265 | const posts = await Post.withoutAny().query().paginate(1) 1266 | 1267 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('user') 1268 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1269 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('user') 1270 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1271 | }) 1272 | 1273 | test('auto-preloaded array should be restored after using withoutAny method', async ({ 1274 | expect, 1275 | }) => { 1276 | class Comment extends BaseModel { 1277 | @column({ isPrimary: true }) 1278 | public id: number 1279 | 1280 | @column() 1281 | public userId: number 1282 | 1283 | @column() 1284 | public postId: number 1285 | 1286 | @column() 1287 | public comment: string 1288 | } 1289 | 1290 | class Post extends compose(BaseModel, AutoPreload) { 1291 | public static $with = ['user', 'comments'] as const 1292 | 1293 | @column({ isPrimary: true }) 1294 | public id: number 1295 | 1296 | @column() 1297 | public userId: number 1298 | 1299 | @column() 1300 | public title: string 1301 | 1302 | @column() 1303 | public content: string 1304 | 1305 | @belongsTo(() => User) 1306 | public user: BelongsTo 1307 | 1308 | @hasMany(() => Comment) 1309 | public comments: HasMany 1310 | } 1311 | 1312 | class User extends BaseModel { 1313 | @column({ isPrimary: true }) 1314 | public id: number 1315 | 1316 | @column() 1317 | public email: string 1318 | 1319 | @column() 1320 | public name: string 1321 | 1322 | @hasMany(() => Post) 1323 | public posts: HasMany 1324 | } 1325 | 1326 | const user = await User.create({ 1327 | email: 'john@doe.com', 1328 | name: 'John Doe', 1329 | }) 1330 | 1331 | const post1 = new Post() 1332 | post1.title = 'Test' 1333 | post1.content = 'Test content' 1334 | 1335 | const post2 = new Post() 1336 | post2.title = 'Foo' 1337 | post2.content = 'Foo content' 1338 | 1339 | await user.related('posts').saveMany([post1, post2]) 1340 | 1341 | await post1.related('comments').create({ 1342 | userId: 1, 1343 | comment: 'Test', 1344 | }) 1345 | 1346 | await post2.related('comments').createMany([ 1347 | { 1348 | userId: 1, 1349 | comment: 'Bar', 1350 | }, 1351 | { 1352 | userId: 1, 1353 | comment: 'Foo', 1354 | }, 1355 | ]) 1356 | 1357 | const posts = await Post.withoutAny().query().paginate(1) 1358 | 1359 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('user') 1360 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1361 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('user') 1362 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1363 | 1364 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1365 | }) 1366 | }) 1367 | -------------------------------------------------------------------------------- /tests/auto_preload-multiple_rows.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseContract } from '@ioc:Adonis/Lucid/Database' 2 | import type { 3 | BaseModel as BaseModelContract, 4 | ColumnDecorator, 5 | BelongsTo, 6 | BelongsToDecorator, 7 | HasMany, 8 | HasManyDecorator, 9 | ModelQueryBuilderContract, 10 | } from '@ioc:Adonis/Lucid/Orm' 11 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 12 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 13 | 14 | import { test } from '@japa/runner' 15 | import { compose } from '@poppinss/utils/build/helpers' 16 | 17 | import { setupDatabase, cleanDatabase } from '../bin/test/database' 18 | import { fs } from '../bin/test/config' 19 | import { setupApp } from '../test-helpers' 20 | 21 | let db: DatabaseContract 22 | let BaseModel: typeof BaseModelContract 23 | let AutoPreload: AutoPreloadMixin 24 | let app: ApplicationContract 25 | let column: ColumnDecorator 26 | let belongsTo: BelongsToDecorator 27 | let hasMany: HasManyDecorator 28 | 29 | test.group('Auto preload - Multiple rows', (group) => { 30 | group.setup(async () => { 31 | app = await setupApp() 32 | db = app.container.resolveBinding('Adonis/Lucid/Database') 33 | BaseModel = app.container.resolveBinding('Adonis/Lucid/Orm').BaseModel 34 | AutoPreload = app.container.resolveBinding('Adonis/Addons/AutoPreload').AutoPreload 35 | column = app.container.resolveBinding('Adonis/Lucid/Orm').column 36 | belongsTo = app.container.resolveBinding('Adonis/Lucid/Orm').belongsTo 37 | hasMany = app.container.resolveBinding('Adonis/Lucid/Orm').hasMany 38 | }) 39 | 40 | group.each.setup(async () => { 41 | await setupDatabase(db) 42 | }) 43 | 44 | group.each.teardown(async () => { 45 | await cleanDatabase(db) 46 | }) 47 | 48 | group.teardown(async () => { 49 | await db.manager.closeAll() 50 | await fs.cleanup() 51 | }) 52 | 53 | test('using mixin should auto-preload relationships when using relation name', async ({ 54 | expect, 55 | }) => { 56 | class Post extends BaseModel { 57 | @column({ isPrimary: true }) 58 | public id: number 59 | 60 | @column() 61 | public userId: number 62 | 63 | @column() 64 | public title: string 65 | 66 | @column() 67 | public content: string 68 | } 69 | 70 | class User extends compose(BaseModel, AutoPreload) { 71 | public static $with = ['posts'] 72 | 73 | @column({ isPrimary: true }) 74 | public id: number 75 | 76 | @column() 77 | public email: string 78 | 79 | @column() 80 | public name: string 81 | 82 | @hasMany(() => Post) 83 | public posts: HasMany 84 | } 85 | 86 | const user1 = await User.create({ 87 | email: 'john@doe.com', 88 | name: 'John Doe', 89 | }) 90 | 91 | await user1.related('posts').createMany([ 92 | { 93 | title: 'Test', 94 | content: 'Test content', 95 | }, 96 | { 97 | title: 'Foo', 98 | content: 'Foo content', 99 | }, 100 | ]) 101 | 102 | const user2 = await User.create({ 103 | email: 'Anna@doe.com', 104 | name: 'Anna Doe', 105 | }) 106 | 107 | await user2.related('posts').createMany([ 108 | { 109 | title: 'Test', 110 | content: 'Test content', 111 | }, 112 | ]) 113 | 114 | expect((await User.all()).at(0)?.$preloaded.posts).toHaveLength(1) 115 | expect((await User.all()).at(1)?.$preloaded.posts).toHaveLength(2) 116 | }) 117 | 118 | test('using mixin should auto-preload relationships when using a function', async ({ 119 | expect, 120 | }) => { 121 | class Post extends BaseModel { 122 | @column({ isPrimary: true }) 123 | public id: number 124 | 125 | @column() 126 | public userId: number 127 | 128 | @column() 129 | public title: string 130 | 131 | @column() 132 | public content: string 133 | } 134 | 135 | class User extends compose(BaseModel, AutoPreload) { 136 | public static $with = [ 137 | (query: ModelQueryBuilderContract) => { 138 | query.preload('posts') 139 | }, 140 | ] 141 | 142 | @column({ isPrimary: true }) 143 | public id: number 144 | 145 | @column() 146 | public email: string 147 | 148 | @column() 149 | public name: string 150 | 151 | @hasMany(() => Post) 152 | public posts: HasMany 153 | } 154 | 155 | const user = await User.create({ 156 | email: 'john@doe.com', 157 | name: 'John Doe', 158 | }) 159 | 160 | await user.related('posts').createMany([ 161 | { 162 | title: 'Test', 163 | content: 'Test content', 164 | }, 165 | { 166 | title: 'Foo', 167 | content: 'Foo content', 168 | }, 169 | ]) 170 | 171 | const user2 = await User.create({ 172 | email: 'Anna@doe.com', 173 | name: 'Anna Doe', 174 | }) 175 | 176 | await user2.related('posts').createMany([ 177 | { 178 | title: 'Test', 179 | content: 'Test content', 180 | }, 181 | ]) 182 | 183 | expect((await User.all()).at(0)?.$preloaded.posts).toHaveLength(1) 184 | expect((await User.all()).at(1)?.$preloaded.posts).toHaveLength(2) 185 | }) 186 | 187 | test('using mixin should auto-preload nested relationships when using relation name', async ({ 188 | expect, 189 | }) => { 190 | class Comment extends BaseModel { 191 | @column({ isPrimary: true }) 192 | public id: number 193 | 194 | @column() 195 | public userId: number 196 | 197 | @column() 198 | public postId: number 199 | 200 | @column() 201 | public comment: string 202 | } 203 | 204 | class Post extends BaseModel { 205 | @column({ isPrimary: true }) 206 | public id: number 207 | 208 | @column() 209 | public userId: number 210 | 211 | @column() 212 | public title: string 213 | 214 | @column() 215 | public content: string 216 | 217 | @hasMany(() => Comment) 218 | public comments: HasMany 219 | } 220 | 221 | class User extends compose(BaseModel, AutoPreload) { 222 | public static $with = ['posts.comments'] 223 | 224 | @column({ isPrimary: true }) 225 | public id: number 226 | 227 | @column() 228 | public email: string 229 | 230 | @column() 231 | public name: string 232 | 233 | @hasMany(() => Post) 234 | public posts: HasMany 235 | } 236 | 237 | const user = await User.create({ 238 | email: 'john@doe.com', 239 | name: 'John Doe', 240 | }) 241 | 242 | const post1 = new Post() 243 | post1.title = 'Test' 244 | post1.content = 'Test content' 245 | 246 | const post2 = new Post() 247 | post2.title = 'Foo' 248 | post2.content = 'Foo content' 249 | 250 | await user.related('posts').saveMany([post1, post2]) 251 | 252 | await post1.related('comments').create({ 253 | userId: 1, 254 | comment: 'Test', 255 | }) 256 | 257 | await post2.related('comments').createMany([ 258 | { 259 | userId: 1, 260 | comment: 'Bar', 261 | }, 262 | { 263 | userId: 1, 264 | comment: 'Foo', 265 | }, 266 | ]) 267 | 268 | expect((await User.all()).at(0)?.$preloaded.posts[0].$preloaded.comments).toHaveLength(1) 269 | expect((await User.all()).at(0)?.$preloaded.posts[1].$preloaded.comments).toHaveLength(2) 270 | }) 271 | 272 | test('using mixin should auto-preload nested relationships when using a function', async ({ 273 | expect, 274 | }) => { 275 | class Comment extends BaseModel { 276 | @column({ isPrimary: true }) 277 | public id: number 278 | 279 | @column() 280 | public userId: number 281 | 282 | @column() 283 | public postId: number 284 | 285 | @column() 286 | public comment: string 287 | } 288 | 289 | class Post extends BaseModel { 290 | @column({ isPrimary: true }) 291 | public id: number 292 | 293 | @column() 294 | public userId: number 295 | 296 | @column() 297 | public title: string 298 | 299 | @column() 300 | public content: string 301 | 302 | @hasMany(() => Comment) 303 | public comments: HasMany 304 | } 305 | 306 | class User extends compose(BaseModel, AutoPreload) { 307 | public static $with = [ 308 | (query: ModelQueryBuilderContract) => { 309 | query.preload('posts', (postsQuery) => { 310 | postsQuery.preload('comments') 311 | }) 312 | }, 313 | ] 314 | 315 | @column({ isPrimary: true }) 316 | public id: number 317 | 318 | @column() 319 | public email: string 320 | 321 | @column() 322 | public name: string 323 | 324 | @hasMany(() => Post) 325 | public posts: HasMany 326 | } 327 | 328 | const user = await User.create({ 329 | email: 'john@doe.com', 330 | name: 'John Doe', 331 | }) 332 | 333 | const post1 = new Post() 334 | post1.title = 'Test' 335 | post1.content = 'Test content' 336 | 337 | const post2 = new Post() 338 | post2.title = 'Foo' 339 | post2.content = 'Foo content' 340 | 341 | await user.related('posts').saveMany([post1, post2]) 342 | 343 | await post1.related('comments').create({ 344 | userId: 1, 345 | comment: 'Test', 346 | }) 347 | 348 | await post2.related('comments').createMany([ 349 | { 350 | userId: 1, 351 | comment: 'Bar', 352 | }, 353 | { 354 | userId: 1, 355 | comment: 'Foo', 356 | }, 357 | ]) 358 | 359 | expect((await User.all()).at(0)?.$preloaded.posts[0].$preloaded.comments).toHaveLength(1) 360 | expect((await User.all()).at(0)?.$preloaded.posts[1].$preloaded.comments).toHaveLength(2) 361 | }) 362 | 363 | test('using mixin should auto-preload multiple relationships when using relation names', async ({ 364 | expect, 365 | }) => { 366 | class Comment extends BaseModel { 367 | @column({ isPrimary: true }) 368 | public id: number 369 | 370 | @column() 371 | public userId: number 372 | 373 | @column() 374 | public postId: number 375 | 376 | @column() 377 | public comment: string 378 | } 379 | 380 | class Post extends compose(BaseModel, AutoPreload) { 381 | public static $with = ['user', 'comments'] 382 | 383 | @column({ isPrimary: true }) 384 | public id: number 385 | 386 | @column() 387 | public userId: number 388 | 389 | @column() 390 | public title: string 391 | 392 | @column() 393 | public content: string 394 | 395 | @belongsTo(() => User) 396 | public user: BelongsTo 397 | 398 | @hasMany(() => Comment) 399 | public comments: HasMany 400 | } 401 | 402 | class User extends BaseModel { 403 | @column({ isPrimary: true }) 404 | public id: number 405 | 406 | @column() 407 | public email: string 408 | 409 | @column() 410 | public name: string 411 | 412 | @hasMany(() => Post) 413 | public posts: HasMany 414 | } 415 | 416 | const user = await User.create({ 417 | email: 'john@doe.com', 418 | name: 'John Doe', 419 | }) 420 | 421 | const post1 = new Post() 422 | post1.title = 'Test' 423 | post1.content = 'Test content' 424 | 425 | const post2 = new Post() 426 | post2.title = 'Foo' 427 | post2.content = 'Foo content' 428 | 429 | await user.related('posts').saveMany([post1, post2]) 430 | 431 | await post1.related('comments').create({ 432 | userId: 1, 433 | comment: 'Test', 434 | }) 435 | 436 | await post2.related('comments').createMany([ 437 | { 438 | userId: 1, 439 | comment: 'Bar', 440 | }, 441 | { 442 | userId: 1, 443 | comment: 'Foo', 444 | }, 445 | ]) 446 | 447 | const posts = await Post.all() 448 | 449 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 450 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 451 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(2) 452 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 453 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 454 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(1) 455 | }) 456 | 457 | test('using mixin should auto-preload multiple relationships when using functions', async ({ 458 | expect, 459 | }) => { 460 | class Comment extends BaseModel { 461 | @column({ isPrimary: true }) 462 | public id: number 463 | 464 | @column() 465 | public userId: number 466 | 467 | @column() 468 | public postId: number 469 | 470 | @column() 471 | public comment: string 472 | } 473 | 474 | class Post extends compose(BaseModel, AutoPreload) { 475 | public static $with = [ 476 | (query: ModelQueryBuilderContract) => { 477 | query.preload('user') 478 | }, 479 | (query: ModelQueryBuilderContract) => { 480 | query.preload('comments') 481 | }, 482 | ] 483 | 484 | @column({ isPrimary: true }) 485 | public id: number 486 | 487 | @column() 488 | public userId: number 489 | 490 | @column() 491 | public title: string 492 | 493 | @column() 494 | public content: string 495 | 496 | @belongsTo(() => User) 497 | public user: BelongsTo 498 | 499 | @hasMany(() => Comment) 500 | public comments: HasMany 501 | } 502 | 503 | class User extends BaseModel { 504 | @column({ isPrimary: true }) 505 | public id: number 506 | 507 | @column() 508 | public email: string 509 | 510 | @column() 511 | public name: string 512 | 513 | @hasMany(() => Post) 514 | public posts: HasMany 515 | } 516 | 517 | const user = await User.create({ 518 | email: 'john@doe.com', 519 | name: 'John Doe', 520 | }) 521 | 522 | const post1 = new Post() 523 | post1.title = 'Test' 524 | post1.content = 'Test content' 525 | 526 | const post2 = new Post() 527 | post2.title = 'Foo' 528 | post2.content = 'Foo content' 529 | 530 | await user.related('posts').saveMany([post1, post2]) 531 | 532 | await post1.related('comments').create({ 533 | userId: 1, 534 | comment: 'Test', 535 | }) 536 | 537 | await post2.related('comments').createMany([ 538 | { 539 | userId: 1, 540 | comment: 'Bar', 541 | }, 542 | { 543 | userId: 1, 544 | comment: 'Foo', 545 | }, 546 | ]) 547 | 548 | const posts = await Post.all() 549 | 550 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 551 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 552 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(2) 553 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 554 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 555 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(1) 556 | }) 557 | 558 | test('using mixin should auto-preload multiple relationships when using relation names and functions', async ({ 559 | expect, 560 | }) => { 561 | class Comment extends BaseModel { 562 | @column({ isPrimary: true }) 563 | public id: number 564 | 565 | @column() 566 | public userId: number 567 | 568 | @column() 569 | public postId: number 570 | 571 | @column() 572 | public comment: string 573 | } 574 | 575 | class Post extends compose(BaseModel, AutoPreload) { 576 | public static $with = [ 577 | 'user', 578 | (query: ModelQueryBuilderContract) => { 579 | query.preload('comments') 580 | }, 581 | ] 582 | 583 | @column({ isPrimary: true }) 584 | public id: number 585 | 586 | @column() 587 | public userId: number 588 | 589 | @column() 590 | public title: string 591 | 592 | @column() 593 | public content: string 594 | 595 | @belongsTo(() => User) 596 | public user: BelongsTo 597 | 598 | @hasMany(() => Comment) 599 | public comments: HasMany 600 | } 601 | 602 | class User extends BaseModel { 603 | @column({ isPrimary: true }) 604 | public id: number 605 | 606 | @column() 607 | public email: string 608 | 609 | @column() 610 | public name: string 611 | 612 | @hasMany(() => Post) 613 | public posts: HasMany 614 | } 615 | 616 | const user = await User.create({ 617 | email: 'john@doe.com', 618 | name: 'John Doe', 619 | }) 620 | 621 | const post1 = new Post() 622 | post1.title = 'Test' 623 | post1.content = 'Test content' 624 | 625 | const post2 = new Post() 626 | post2.title = 'Foo' 627 | post2.content = 'Foo content' 628 | 629 | await user.related('posts').saveMany([post1, post2]) 630 | 631 | await post1.related('comments').create({ 632 | userId: 1, 633 | comment: 'Test', 634 | }) 635 | 636 | await post2.related('comments').createMany([ 637 | { 638 | userId: 1, 639 | comment: 'Bar', 640 | }, 641 | { 642 | userId: 1, 643 | comment: 'Foo', 644 | }, 645 | ]) 646 | 647 | const posts = await Post.all() 648 | 649 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 650 | expect(posts.at(0)?.$preloaded).toHaveProperty('comments') 651 | expect(posts.at(0)?.$preloaded.comments).toHaveLength(2) 652 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 653 | expect(posts.at(1)?.$preloaded).toHaveProperty('comments') 654 | expect(posts.at(1)?.$preloaded.comments).toHaveLength(1) 655 | }) 656 | 657 | test('without method should exclude specified relationships from being auto-preloaded', async ({ 658 | expect, 659 | }) => { 660 | class Comment extends BaseModel { 661 | @column({ isPrimary: true }) 662 | public id: number 663 | 664 | @column() 665 | public userId: number 666 | 667 | @column() 668 | public postId: number 669 | 670 | @column() 671 | public comment: string 672 | } 673 | 674 | class Post extends compose(BaseModel, AutoPreload) { 675 | public static $with = ['user', 'comments'] as const 676 | 677 | @column({ isPrimary: true }) 678 | public id: number 679 | 680 | @column() 681 | public userId: number 682 | 683 | @column() 684 | public title: string 685 | 686 | @column() 687 | public content: string 688 | 689 | @belongsTo(() => User) 690 | public user: BelongsTo 691 | 692 | @hasMany(() => Comment) 693 | public comments: HasMany 694 | } 695 | 696 | class User extends BaseModel { 697 | @column({ isPrimary: true }) 698 | public id: number 699 | 700 | @column() 701 | public email: string 702 | 703 | @column() 704 | public name: string 705 | 706 | @hasMany(() => Post) 707 | public posts: HasMany 708 | } 709 | 710 | const user = await User.create({ 711 | email: 'john@doe.com', 712 | name: 'John Doe', 713 | }) 714 | 715 | const post1 = new Post() 716 | post1.title = 'Test' 717 | post1.content = 'Test content' 718 | 719 | const post2 = new Post() 720 | post2.title = 'Foo' 721 | post2.content = 'Foo content' 722 | 723 | await user.related('posts').saveMany([post1, post2]) 724 | 725 | await post1.related('comments').create({ 726 | userId: 1, 727 | comment: 'Test', 728 | }) 729 | 730 | await post2.related('comments').createMany([ 731 | { 732 | userId: 1, 733 | comment: 'Bar', 734 | }, 735 | { 736 | userId: 1, 737 | comment: 'Foo', 738 | }, 739 | ]) 740 | 741 | const posts = await Post.without(['comments']).all() 742 | 743 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 744 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 745 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 746 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 747 | }) 748 | 749 | test('auto-preloaded array should be restored after using without method', async ({ expect }) => { 750 | class Comment extends BaseModel { 751 | @column({ isPrimary: true }) 752 | public id: number 753 | 754 | @column() 755 | public userId: number 756 | 757 | @column() 758 | public postId: number 759 | 760 | @column() 761 | public comment: string 762 | } 763 | 764 | class Post extends compose(BaseModel, AutoPreload) { 765 | public static $with = ['user', 'comments'] as const 766 | 767 | @column({ isPrimary: true }) 768 | public id: number 769 | 770 | @column() 771 | public userId: number 772 | 773 | @column() 774 | public title: string 775 | 776 | @column() 777 | public content: string 778 | 779 | @belongsTo(() => User) 780 | public user: BelongsTo 781 | 782 | @hasMany(() => Comment) 783 | public comments: HasMany 784 | } 785 | 786 | class User extends BaseModel { 787 | @column({ isPrimary: true }) 788 | public id: number 789 | 790 | @column() 791 | public email: string 792 | 793 | @column() 794 | public name: string 795 | 796 | @hasMany(() => Post) 797 | public posts: HasMany 798 | } 799 | 800 | const user = await User.create({ 801 | email: 'john@doe.com', 802 | name: 'John Doe', 803 | }) 804 | 805 | const post1 = new Post() 806 | post1.title = 'Test' 807 | post1.content = 'Test content' 808 | 809 | const post2 = new Post() 810 | post2.title = 'Foo' 811 | post2.content = 'Foo content' 812 | 813 | await user.related('posts').saveMany([post1, post2]) 814 | 815 | await post1.related('comments').create({ 816 | userId: 1, 817 | comment: 'Test', 818 | }) 819 | 820 | await post2.related('comments').createMany([ 821 | { 822 | userId: 1, 823 | comment: 'Bar', 824 | }, 825 | { 826 | userId: 1, 827 | comment: 'Foo', 828 | }, 829 | ]) 830 | 831 | const posts = await Post.without(['comments']).all() 832 | 833 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 834 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 835 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 836 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 837 | 838 | expect(Post.$with).toStrictEqual(['user', 'comments']) 839 | }) 840 | 841 | test('without method should throw an exception when relationship is not a string', async ({ 842 | expect, 843 | }) => { 844 | class Comment extends BaseModel { 845 | @column({ isPrimary: true }) 846 | public id: number 847 | 848 | @column() 849 | public userId: number 850 | 851 | @column() 852 | public postId: number 853 | 854 | @column() 855 | public comment: string 856 | } 857 | 858 | class Post extends compose(BaseModel, AutoPreload) { 859 | public static $with = ['user', 'comments'] as const 860 | 861 | @column({ isPrimary: true }) 862 | public id: number 863 | 864 | @column() 865 | public userId: number 866 | 867 | @column() 868 | public title: string 869 | 870 | @column() 871 | public content: string 872 | 873 | @belongsTo(() => User) 874 | public user: BelongsTo 875 | 876 | @hasMany(() => Comment) 877 | public comments: HasMany 878 | } 879 | 880 | class User extends BaseModel { 881 | @column({ isPrimary: true }) 882 | public id: number 883 | 884 | @column() 885 | public email: string 886 | 887 | @column() 888 | public name: string 889 | 890 | @hasMany(() => Post) 891 | public posts: HasMany 892 | } 893 | 894 | const user = await User.create({ 895 | email: 'john@doe.com', 896 | name: 'John Doe', 897 | }) 898 | 899 | const post1 = new Post() 900 | post1.title = 'Test' 901 | post1.content = 'Test content' 902 | 903 | const post2 = new Post() 904 | post2.title = 'Foo' 905 | post2.content = 'Foo content' 906 | 907 | await user.related('posts').saveMany([post1, post2]) 908 | 909 | await post1.related('comments').create({ 910 | userId: 1, 911 | comment: 'Test', 912 | }) 913 | 914 | await post2.related('comments').createMany([ 915 | { 916 | userId: 1, 917 | comment: 'Bar', 918 | }, 919 | { 920 | userId: 1, 921 | comment: 'Foo', 922 | }, 923 | ]) 924 | 925 | // @ts-ignore 926 | expect(async () => await Post.without([5]).all()).rejects.toThrowError() 927 | }) 928 | 929 | test('withOnly method should auto-preload only specified relationships', async ({ expect }) => { 930 | class Comment extends BaseModel { 931 | @column({ isPrimary: true }) 932 | public id: number 933 | 934 | @column() 935 | public userId: number 936 | 937 | @column() 938 | public postId: number 939 | 940 | @column() 941 | public comment: string 942 | } 943 | 944 | class Post extends compose(BaseModel, AutoPreload) { 945 | public static $with = ['user', 'comments'] as const 946 | 947 | @column({ isPrimary: true }) 948 | public id: number 949 | 950 | @column() 951 | public userId: number 952 | 953 | @column() 954 | public title: string 955 | 956 | @column() 957 | public content: string 958 | 959 | @belongsTo(() => User) 960 | public user: BelongsTo 961 | 962 | @hasMany(() => Comment) 963 | public comments: HasMany 964 | } 965 | 966 | class User extends BaseModel { 967 | @column({ isPrimary: true }) 968 | public id: number 969 | 970 | @column() 971 | public email: string 972 | 973 | @column() 974 | public name: string 975 | 976 | @hasMany(() => Post) 977 | public posts: HasMany 978 | } 979 | 980 | const user = await User.create({ 981 | email: 'john@doe.com', 982 | name: 'John Doe', 983 | }) 984 | 985 | const post1 = new Post() 986 | post1.title = 'Test' 987 | post1.content = 'Test content' 988 | 989 | const post2 = new Post() 990 | post2.title = 'Foo' 991 | post2.content = 'Foo content' 992 | 993 | await user.related('posts').saveMany([post1, post2]) 994 | 995 | await post1.related('comments').create({ 996 | userId: 1, 997 | comment: 'Test', 998 | }) 999 | 1000 | await post2.related('comments').createMany([ 1001 | { 1002 | userId: 1, 1003 | comment: 'Bar', 1004 | }, 1005 | { 1006 | userId: 1, 1007 | comment: 'Foo', 1008 | }, 1009 | ]) 1010 | 1011 | const posts = await Post.withOnly(['user']).all() 1012 | 1013 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 1014 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1015 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 1016 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1017 | }) 1018 | 1019 | test('auto-preloaded array should be restored after using withOnly method', async ({ 1020 | expect, 1021 | }) => { 1022 | class Comment extends BaseModel { 1023 | @column({ isPrimary: true }) 1024 | public id: number 1025 | 1026 | @column() 1027 | public userId: number 1028 | 1029 | @column() 1030 | public postId: number 1031 | 1032 | @column() 1033 | public comment: string 1034 | } 1035 | 1036 | class Post extends compose(BaseModel, AutoPreload) { 1037 | public static $with = ['user', 'comments'] as const 1038 | 1039 | @column({ isPrimary: true }) 1040 | public id: number 1041 | 1042 | @column() 1043 | public userId: number 1044 | 1045 | @column() 1046 | public title: string 1047 | 1048 | @column() 1049 | public content: string 1050 | 1051 | @belongsTo(() => User) 1052 | public user: BelongsTo 1053 | 1054 | @hasMany(() => Comment) 1055 | public comments: HasMany 1056 | } 1057 | 1058 | class User extends BaseModel { 1059 | @column({ isPrimary: true }) 1060 | public id: number 1061 | 1062 | @column() 1063 | public email: string 1064 | 1065 | @column() 1066 | public name: string 1067 | 1068 | @hasMany(() => Post) 1069 | public posts: HasMany 1070 | } 1071 | 1072 | const user = await User.create({ 1073 | email: 'john@doe.com', 1074 | name: 'John Doe', 1075 | }) 1076 | 1077 | const post1 = new Post() 1078 | post1.title = 'Test' 1079 | post1.content = 'Test content' 1080 | 1081 | const post2 = new Post() 1082 | post2.title = 'Foo' 1083 | post2.content = 'Foo content' 1084 | 1085 | await user.related('posts').saveMany([post1, post2]) 1086 | 1087 | await post1.related('comments').create({ 1088 | userId: 1, 1089 | comment: 'Test', 1090 | }) 1091 | 1092 | await post2.related('comments').createMany([ 1093 | { 1094 | userId: 1, 1095 | comment: 'Bar', 1096 | }, 1097 | { 1098 | userId: 1, 1099 | comment: 'Foo', 1100 | }, 1101 | ]) 1102 | 1103 | const posts = await Post.withOnly(['user']).all() 1104 | 1105 | expect(posts.at(0)?.$preloaded).toHaveProperty('user') 1106 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1107 | expect(posts.at(1)?.$preloaded).toHaveProperty('user') 1108 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1109 | 1110 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1111 | }) 1112 | 1113 | test('withOnly method should throw an exception when relationship is not a string', async ({ 1114 | expect, 1115 | }) => { 1116 | class Comment extends BaseModel { 1117 | @column({ isPrimary: true }) 1118 | public id: number 1119 | 1120 | @column() 1121 | public userId: number 1122 | 1123 | @column() 1124 | public postId: number 1125 | 1126 | @column() 1127 | public comment: string 1128 | } 1129 | 1130 | class Post extends compose(BaseModel, AutoPreload) { 1131 | public static $with = ['user', 'comments'] as const 1132 | 1133 | @column({ isPrimary: true }) 1134 | public id: number 1135 | 1136 | @column() 1137 | public userId: number 1138 | 1139 | @column() 1140 | public title: string 1141 | 1142 | @column() 1143 | public content: string 1144 | 1145 | @belongsTo(() => User) 1146 | public user: BelongsTo 1147 | 1148 | @hasMany(() => Comment) 1149 | public comments: HasMany 1150 | } 1151 | 1152 | class User extends BaseModel { 1153 | @column({ isPrimary: true }) 1154 | public id: number 1155 | 1156 | @column() 1157 | public email: string 1158 | 1159 | @column() 1160 | public name: string 1161 | 1162 | @hasMany(() => Post) 1163 | public posts: HasMany 1164 | } 1165 | 1166 | const user = await User.create({ 1167 | email: 'john@doe.com', 1168 | name: 'John Doe', 1169 | }) 1170 | 1171 | const post1 = new Post() 1172 | post1.title = 'Test' 1173 | post1.content = 'Test content' 1174 | 1175 | const post2 = new Post() 1176 | post2.title = 'Foo' 1177 | post2.content = 'Foo content' 1178 | 1179 | await user.related('posts').saveMany([post1, post2]) 1180 | 1181 | await post1.related('comments').create({ 1182 | userId: 1, 1183 | comment: 'Test', 1184 | }) 1185 | 1186 | await post2.related('comments').createMany([ 1187 | { 1188 | userId: 1, 1189 | comment: 'Bar', 1190 | }, 1191 | { 1192 | userId: 1, 1193 | comment: 'Foo', 1194 | }, 1195 | ]) 1196 | 1197 | // @ts-ignore 1198 | expect(async () => await Post.withOnly([5]).all()).rejects.toThrowError() 1199 | }) 1200 | 1201 | test('withoutAny method should not auto-preload any relationship', async ({ expect }) => { 1202 | class Comment extends BaseModel { 1203 | @column({ isPrimary: true }) 1204 | public id: number 1205 | 1206 | @column() 1207 | public userId: number 1208 | 1209 | @column() 1210 | public postId: number 1211 | 1212 | @column() 1213 | public comment: string 1214 | } 1215 | 1216 | class Post extends compose(BaseModel, AutoPreload) { 1217 | public static $with = ['user', 'comments'] as const 1218 | 1219 | @column({ isPrimary: true }) 1220 | public id: number 1221 | 1222 | @column() 1223 | public userId: number 1224 | 1225 | @column() 1226 | public title: string 1227 | 1228 | @column() 1229 | public content: string 1230 | 1231 | @belongsTo(() => User) 1232 | public user: BelongsTo 1233 | 1234 | @hasMany(() => Comment) 1235 | public comments: HasMany 1236 | } 1237 | 1238 | class User extends BaseModel { 1239 | @column({ isPrimary: true }) 1240 | public id: number 1241 | 1242 | @column() 1243 | public email: string 1244 | 1245 | @column() 1246 | public name: string 1247 | 1248 | @hasMany(() => Post) 1249 | public posts: HasMany 1250 | } 1251 | 1252 | const user = await User.create({ 1253 | email: 'john@doe.com', 1254 | name: 'John Doe', 1255 | }) 1256 | 1257 | const post1 = new Post() 1258 | post1.title = 'Test' 1259 | post1.content = 'Test content' 1260 | 1261 | const post2 = new Post() 1262 | post2.title = 'Foo' 1263 | post2.content = 'Foo content' 1264 | 1265 | await user.related('posts').saveMany([post1, post2]) 1266 | 1267 | await post1.related('comments').create({ 1268 | userId: 1, 1269 | comment: 'Test', 1270 | }) 1271 | 1272 | await post2.related('comments').createMany([ 1273 | { 1274 | userId: 1, 1275 | comment: 'Bar', 1276 | }, 1277 | { 1278 | userId: 1, 1279 | comment: 'Foo', 1280 | }, 1281 | ]) 1282 | 1283 | const posts = await Post.withoutAny().all() 1284 | 1285 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('user') 1286 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1287 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('user') 1288 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1289 | }) 1290 | 1291 | test('auto-preloaded array should be restored after using withoutAny method', async ({ 1292 | expect, 1293 | }) => { 1294 | class Comment extends BaseModel { 1295 | @column({ isPrimary: true }) 1296 | public id: number 1297 | 1298 | @column() 1299 | public userId: number 1300 | 1301 | @column() 1302 | public postId: number 1303 | 1304 | @column() 1305 | public comment: string 1306 | } 1307 | 1308 | class Post extends compose(BaseModel, AutoPreload) { 1309 | public static $with = ['user', 'comments'] as const 1310 | 1311 | @column({ isPrimary: true }) 1312 | public id: number 1313 | 1314 | @column() 1315 | public userId: number 1316 | 1317 | @column() 1318 | public title: string 1319 | 1320 | @column() 1321 | public content: string 1322 | 1323 | @belongsTo(() => User) 1324 | public user: BelongsTo 1325 | 1326 | @hasMany(() => Comment) 1327 | public comments: HasMany 1328 | } 1329 | 1330 | class User extends BaseModel { 1331 | @column({ isPrimary: true }) 1332 | public id: number 1333 | 1334 | @column() 1335 | public email: string 1336 | 1337 | @column() 1338 | public name: string 1339 | 1340 | @hasMany(() => Post) 1341 | public posts: HasMany 1342 | } 1343 | 1344 | const user = await User.create({ 1345 | email: 'john@doe.com', 1346 | name: 'John Doe', 1347 | }) 1348 | 1349 | const post1 = new Post() 1350 | post1.title = 'Test' 1351 | post1.content = 'Test content' 1352 | 1353 | const post2 = new Post() 1354 | post2.title = 'Foo' 1355 | post2.content = 'Foo content' 1356 | 1357 | await user.related('posts').saveMany([post1, post2]) 1358 | 1359 | await post1.related('comments').create({ 1360 | userId: 1, 1361 | comment: 'Test', 1362 | }) 1363 | 1364 | await post2.related('comments').createMany([ 1365 | { 1366 | userId: 1, 1367 | comment: 'Bar', 1368 | }, 1369 | { 1370 | userId: 1, 1371 | comment: 'Foo', 1372 | }, 1373 | ]) 1374 | 1375 | const posts = await Post.withoutAny().all() 1376 | 1377 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('user') 1378 | expect(posts.at(0)?.$preloaded).not.toHaveProperty('comments') 1379 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('user') 1380 | expect(posts.at(1)?.$preloaded).not.toHaveProperty('comments') 1381 | 1382 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1383 | }) 1384 | }) 1385 | -------------------------------------------------------------------------------- /tests/auto_preload-one_row.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseContract } from '@ioc:Adonis/Lucid/Database' 2 | import type { 3 | BaseModel as BaseModelContract, 4 | ColumnDecorator, 5 | BelongsTo, 6 | BelongsToDecorator, 7 | HasMany, 8 | HasManyDecorator, 9 | ModelQueryBuilderContract, 10 | } from '@ioc:Adonis/Lucid/Orm' 11 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 12 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 13 | 14 | import { test } from '@japa/runner' 15 | import { compose } from '@poppinss/utils/build/helpers' 16 | 17 | import { setupDatabase, cleanDatabase } from '../bin/test/database' 18 | import { fs } from '../bin/test/config' 19 | import { setupApp } from '../test-helpers' 20 | 21 | let db: DatabaseContract 22 | let BaseModel: typeof BaseModelContract 23 | let AutoPreload: AutoPreloadMixin 24 | let app: ApplicationContract 25 | let column: ColumnDecorator 26 | let belongsTo: BelongsToDecorator 27 | let hasMany: HasManyDecorator 28 | 29 | test.group('Auto preload - One row', (group) => { 30 | group.setup(async () => { 31 | app = await setupApp() 32 | db = app.container.resolveBinding('Adonis/Lucid/Database') 33 | BaseModel = app.container.resolveBinding('Adonis/Lucid/Orm').BaseModel 34 | AutoPreload = app.container.resolveBinding('Adonis/Addons/AutoPreload').AutoPreload 35 | column = app.container.resolveBinding('Adonis/Lucid/Orm').column 36 | belongsTo = app.container.resolveBinding('Adonis/Lucid/Orm').belongsTo 37 | hasMany = app.container.resolveBinding('Adonis/Lucid/Orm').hasMany 38 | }) 39 | 40 | group.each.setup(async () => { 41 | await setupDatabase(db) 42 | }) 43 | 44 | group.each.teardown(async () => { 45 | await cleanDatabase(db) 46 | }) 47 | 48 | group.teardown(async () => { 49 | await db.manager.closeAll() 50 | await fs.cleanup() 51 | }) 52 | 53 | test('using mixin should auto-preload relationships when using relation name', async ({ 54 | expect, 55 | }) => { 56 | class Post extends BaseModel { 57 | @column({ isPrimary: true }) 58 | public id: number 59 | 60 | @column() 61 | public userId: number 62 | 63 | @column() 64 | public title: string 65 | 66 | @column() 67 | public content: string 68 | } 69 | 70 | class User extends compose(BaseModel, AutoPreload) { 71 | public static $with = ['posts'] 72 | 73 | @column({ isPrimary: true }) 74 | public id: number 75 | 76 | @column() 77 | public email: string 78 | 79 | @column() 80 | public name: string 81 | 82 | @hasMany(() => Post) 83 | public posts: HasMany 84 | } 85 | 86 | const user = await User.create({ 87 | email: 'john@doe.com', 88 | name: 'John Doe', 89 | }) 90 | 91 | await user.related('posts').createMany([ 92 | { 93 | title: 'Test', 94 | content: 'Test content', 95 | }, 96 | { 97 | title: 'Foo', 98 | content: 'Foo content', 99 | }, 100 | ]) 101 | 102 | expect((await User.find(1))?.$preloaded.posts).toHaveLength(2) 103 | }) 104 | 105 | test('using mixin should auto-preload relationships when using a function', async ({ 106 | expect, 107 | }) => { 108 | class Post extends BaseModel { 109 | @column({ isPrimary: true }) 110 | public id: number 111 | 112 | @column() 113 | public userId: number 114 | 115 | @column() 116 | public title: string 117 | 118 | @column() 119 | public content: string 120 | } 121 | 122 | class User extends compose(BaseModel, AutoPreload) { 123 | public static $with = [ 124 | (query: ModelQueryBuilderContract) => { 125 | query.preload('posts') 126 | }, 127 | ] 128 | 129 | @column({ isPrimary: true }) 130 | public id: number 131 | 132 | @column() 133 | public email: string 134 | 135 | @column() 136 | public name: string 137 | 138 | @hasMany(() => Post) 139 | public posts: HasMany 140 | } 141 | 142 | const user = await User.create({ 143 | email: 'john@doe.com', 144 | name: 'John Doe', 145 | }) 146 | 147 | await user.related('posts').createMany([ 148 | { 149 | title: 'Test', 150 | content: 'Test content', 151 | }, 152 | { 153 | title: 'Foo', 154 | content: 'Foo content', 155 | }, 156 | ]) 157 | 158 | expect((await User.find(1))?.$preloaded.posts).toHaveLength(2) 159 | }) 160 | 161 | test('using mixin should auto-preload nested relationships when using relation name', async ({ 162 | expect, 163 | }) => { 164 | class Comment extends BaseModel { 165 | @column({ isPrimary: true }) 166 | public id: number 167 | 168 | @column() 169 | public userId: number 170 | 171 | @column() 172 | public postId: number 173 | 174 | @column() 175 | public comment: string 176 | } 177 | 178 | class Post extends BaseModel { 179 | @column({ isPrimary: true }) 180 | public id: number 181 | 182 | @column() 183 | public userId: number 184 | 185 | @column() 186 | public title: string 187 | 188 | @column() 189 | public content: string 190 | 191 | @hasMany(() => Comment) 192 | public comments: HasMany 193 | } 194 | 195 | class User extends compose(BaseModel, AutoPreload) { 196 | public static $with = ['posts.comments'] 197 | 198 | @column({ isPrimary: true }) 199 | public id: number 200 | 201 | @column() 202 | public email: string 203 | 204 | @column() 205 | public name: string 206 | 207 | @hasMany(() => Post) 208 | public posts: HasMany 209 | } 210 | 211 | const user = await User.create({ 212 | email: 'john@doe.com', 213 | name: 'John Doe', 214 | }) 215 | 216 | const post1 = new Post() 217 | post1.title = 'Test' 218 | post1.content = 'Test content' 219 | 220 | const post2 = new Post() 221 | post2.title = 'Foo' 222 | post2.content = 'Foo content' 223 | 224 | await user.related('posts').saveMany([post1, post2]) 225 | 226 | await post1.related('comments').create({ 227 | userId: 1, 228 | comment: 'Test', 229 | }) 230 | 231 | await post2.related('comments').createMany([ 232 | { 233 | userId: 1, 234 | comment: 'Bar', 235 | }, 236 | { 237 | userId: 1, 238 | comment: 'Foo', 239 | }, 240 | ]) 241 | 242 | expect((await User.find(1))?.$preloaded.posts[0].$preloaded.comments).toHaveLength(1) 243 | expect((await User.find(1))?.$preloaded.posts[1].$preloaded.comments).toHaveLength(2) 244 | }) 245 | 246 | test('using mixin should auto-preload nested relationships when using a function', async ({ 247 | expect, 248 | }) => { 249 | class Comment extends BaseModel { 250 | @column({ isPrimary: true }) 251 | public id: number 252 | 253 | @column() 254 | public userId: number 255 | 256 | @column() 257 | public postId: number 258 | 259 | @column() 260 | public comment: string 261 | } 262 | 263 | class Post extends BaseModel { 264 | @column({ isPrimary: true }) 265 | public id: number 266 | 267 | @column() 268 | public userId: number 269 | 270 | @column() 271 | public title: string 272 | 273 | @column() 274 | public content: string 275 | 276 | @hasMany(() => Comment) 277 | public comments: HasMany 278 | } 279 | 280 | class User extends compose(BaseModel, AutoPreload) { 281 | public static $with = [ 282 | (query: ModelQueryBuilderContract) => { 283 | query.preload('posts', (postsQuery) => { 284 | postsQuery.preload('comments') 285 | }) 286 | }, 287 | ] 288 | 289 | @column({ isPrimary: true }) 290 | public id: number 291 | 292 | @column() 293 | public email: string 294 | 295 | @column() 296 | public name: string 297 | 298 | @hasMany(() => Post) 299 | public posts: HasMany 300 | } 301 | 302 | const user = await User.create({ 303 | email: 'john@doe.com', 304 | name: 'John Doe', 305 | }) 306 | 307 | const post1 = new Post() 308 | post1.title = 'Test' 309 | post1.content = 'Test content' 310 | 311 | const post2 = new Post() 312 | post2.title = 'Foo' 313 | post2.content = 'Foo content' 314 | 315 | await user.related('posts').saveMany([post1, post2]) 316 | 317 | await post1.related('comments').create({ 318 | userId: 1, 319 | comment: 'Test', 320 | }) 321 | 322 | await post2.related('comments').createMany([ 323 | { 324 | userId: 1, 325 | comment: 'Bar', 326 | }, 327 | { 328 | userId: 1, 329 | comment: 'Foo', 330 | }, 331 | ]) 332 | 333 | expect((await User.find(1))?.$preloaded.posts[0].$preloaded.comments).toHaveLength(1) 334 | expect((await User.find(1))?.$preloaded.posts[1].$preloaded.comments).toHaveLength(2) 335 | }) 336 | 337 | test('using mixin should auto-preload multiple relationships when using relation names', async ({ 338 | expect, 339 | }) => { 340 | class Comment extends BaseModel { 341 | @column({ isPrimary: true }) 342 | public id: number 343 | 344 | @column() 345 | public userId: number 346 | 347 | @column() 348 | public postId: number 349 | 350 | @column() 351 | public comment: string 352 | } 353 | 354 | class Post extends compose(BaseModel, AutoPreload) { 355 | public static $with = ['user', 'comments'] 356 | 357 | @column({ isPrimary: true }) 358 | public id: number 359 | 360 | @column() 361 | public userId: number 362 | 363 | @column() 364 | public title: string 365 | 366 | @column() 367 | public content: string 368 | 369 | @belongsTo(() => User) 370 | public user: BelongsTo 371 | 372 | @hasMany(() => Comment) 373 | public comments: HasMany 374 | } 375 | 376 | class User extends BaseModel { 377 | @column({ isPrimary: true }) 378 | public id: number 379 | 380 | @column() 381 | public email: string 382 | 383 | @column() 384 | public name: string 385 | 386 | @hasMany(() => Post) 387 | public posts: HasMany 388 | } 389 | 390 | const user = await User.create({ 391 | email: 'john@doe.com', 392 | name: 'John Doe', 393 | }) 394 | 395 | const post1 = new Post() 396 | post1.title = 'Test' 397 | post1.content = 'Test content' 398 | 399 | const post2 = new Post() 400 | post2.title = 'Foo' 401 | post2.content = 'Foo content' 402 | 403 | await user.related('posts').saveMany([post1, post2]) 404 | 405 | await post1.related('comments').create({ 406 | userId: 1, 407 | comment: 'Test', 408 | }) 409 | 410 | await post2.related('comments').createMany([ 411 | { 412 | userId: 1, 413 | comment: 'Bar', 414 | }, 415 | { 416 | userId: 1, 417 | comment: 'Foo', 418 | }, 419 | ]) 420 | 421 | expect((await Post.find(1))?.$preloaded).toHaveProperty('user') 422 | expect((await Post.find(1))?.$preloaded).toHaveProperty('comments') 423 | expect((await Post.find(1))?.$preloaded.comments).toHaveLength(1) 424 | expect((await Post.find(2))?.$preloaded).toHaveProperty('user') 425 | expect((await Post.find(2))?.$preloaded).toHaveProperty('comments') 426 | expect((await Post.find(2))?.$preloaded.comments).toHaveLength(2) 427 | }) 428 | 429 | test('using mixin should auto-preload multiple relationships when using functions', async ({ 430 | expect, 431 | }) => { 432 | class Comment extends BaseModel { 433 | @column({ isPrimary: true }) 434 | public id: number 435 | 436 | @column() 437 | public userId: number 438 | 439 | @column() 440 | public postId: number 441 | 442 | @column() 443 | public comment: string 444 | } 445 | 446 | class Post extends compose(BaseModel, AutoPreload) { 447 | public static $with = [ 448 | (query: ModelQueryBuilderContract) => { 449 | query.preload('user') 450 | }, 451 | (query: ModelQueryBuilderContract) => { 452 | query.preload('comments') 453 | }, 454 | ] 455 | 456 | @column({ isPrimary: true }) 457 | public id: number 458 | 459 | @column() 460 | public userId: number 461 | 462 | @column() 463 | public title: string 464 | 465 | @column() 466 | public content: string 467 | 468 | @belongsTo(() => User) 469 | public user: BelongsTo 470 | 471 | @hasMany(() => Comment) 472 | public comments: HasMany 473 | } 474 | 475 | class User extends BaseModel { 476 | @column({ isPrimary: true }) 477 | public id: number 478 | 479 | @column() 480 | public email: string 481 | 482 | @column() 483 | public name: string 484 | 485 | @hasMany(() => Post) 486 | public posts: HasMany 487 | } 488 | 489 | const user = await User.create({ 490 | email: 'john@doe.com', 491 | name: 'John Doe', 492 | }) 493 | 494 | const post1 = new Post() 495 | post1.title = 'Test' 496 | post1.content = 'Test content' 497 | 498 | const post2 = new Post() 499 | post2.title = 'Foo' 500 | post2.content = 'Foo content' 501 | 502 | await user.related('posts').saveMany([post1, post2]) 503 | 504 | await post1.related('comments').create({ 505 | userId: 1, 506 | comment: 'Test', 507 | }) 508 | 509 | await post2.related('comments').createMany([ 510 | { 511 | userId: 1, 512 | comment: 'Bar', 513 | }, 514 | { 515 | userId: 1, 516 | comment: 'Foo', 517 | }, 518 | ]) 519 | 520 | expect((await Post.find(1))?.$preloaded).toHaveProperty('user') 521 | expect((await Post.find(1))?.$preloaded).toHaveProperty('comments') 522 | expect((await Post.find(1))?.$preloaded.comments).toHaveLength(1) 523 | expect((await Post.find(2))?.$preloaded).toHaveProperty('user') 524 | expect((await Post.find(2))?.$preloaded).toHaveProperty('comments') 525 | expect((await Post.find(2))?.$preloaded.comments).toHaveLength(2) 526 | }) 527 | 528 | test('using mixin should auto-preload multiple relationships when using relation names and functions', async ({ 529 | expect, 530 | }) => { 531 | class Comment extends BaseModel { 532 | @column({ isPrimary: true }) 533 | public id: number 534 | 535 | @column() 536 | public userId: number 537 | 538 | @column() 539 | public postId: number 540 | 541 | @column() 542 | public comment: string 543 | } 544 | 545 | class Post extends compose(BaseModel, AutoPreload) { 546 | public static $with = [ 547 | 'user', 548 | (query: ModelQueryBuilderContract) => { 549 | query.preload('comments') 550 | }, 551 | ] 552 | 553 | @column({ isPrimary: true }) 554 | public id: number 555 | 556 | @column() 557 | public userId: number 558 | 559 | @column() 560 | public title: string 561 | 562 | @column() 563 | public content: string 564 | 565 | @belongsTo(() => User) 566 | public user: BelongsTo 567 | 568 | @hasMany(() => Comment) 569 | public comments: HasMany 570 | } 571 | 572 | class User extends BaseModel { 573 | @column({ isPrimary: true }) 574 | public id: number 575 | 576 | @column() 577 | public email: string 578 | 579 | @column() 580 | public name: string 581 | 582 | @hasMany(() => Post) 583 | public posts: HasMany 584 | } 585 | 586 | const user = await User.create({ 587 | email: 'john@doe.com', 588 | name: 'John Doe', 589 | }) 590 | 591 | const post1 = new Post() 592 | post1.title = 'Test' 593 | post1.content = 'Test content' 594 | 595 | const post2 = new Post() 596 | post2.title = 'Foo' 597 | post2.content = 'Foo content' 598 | 599 | await user.related('posts').saveMany([post1, post2]) 600 | 601 | await post1.related('comments').create({ 602 | userId: 1, 603 | comment: 'Test', 604 | }) 605 | 606 | await post2.related('comments').createMany([ 607 | { 608 | userId: 1, 609 | comment: 'Bar', 610 | }, 611 | { 612 | userId: 1, 613 | comment: 'Foo', 614 | }, 615 | ]) 616 | 617 | expect((await Post.find(1))?.$preloaded).toHaveProperty('user') 618 | expect((await Post.find(1))?.$preloaded).toHaveProperty('comments') 619 | expect((await Post.find(1))?.$preloaded.comments).toHaveLength(1) 620 | expect((await Post.find(2))?.$preloaded).toHaveProperty('user') 621 | expect((await Post.find(2))?.$preloaded).toHaveProperty('comments') 622 | expect((await Post.find(2))?.$preloaded.comments).toHaveLength(2) 623 | }) 624 | 625 | test('without method should exclude specified relationships from being auto-preloaded', async ({ 626 | expect, 627 | }) => { 628 | class Comment extends BaseModel { 629 | @column({ isPrimary: true }) 630 | public id: number 631 | 632 | @column() 633 | public userId: number 634 | 635 | @column() 636 | public postId: number 637 | 638 | @column() 639 | public comment: string 640 | } 641 | 642 | class Post extends compose(BaseModel, AutoPreload) { 643 | public static $with = ['user', 'comments'] as const 644 | 645 | @column({ isPrimary: true }) 646 | public id: number 647 | 648 | @column() 649 | public userId: number 650 | 651 | @column() 652 | public title: string 653 | 654 | @column() 655 | public content: string 656 | 657 | @belongsTo(() => User) 658 | public user: BelongsTo 659 | 660 | @hasMany(() => Comment) 661 | public comments: HasMany 662 | } 663 | 664 | class User extends BaseModel { 665 | @column({ isPrimary: true }) 666 | public id: number 667 | 668 | @column() 669 | public email: string 670 | 671 | @column() 672 | public name: string 673 | 674 | @hasMany(() => Post) 675 | public posts: HasMany 676 | } 677 | 678 | const user = await User.create({ 679 | email: 'john@doe.com', 680 | name: 'John Doe', 681 | }) 682 | 683 | const post1 = new Post() 684 | post1.title = 'Test' 685 | post1.content = 'Test content' 686 | 687 | const post2 = new Post() 688 | post2.title = 'Foo' 689 | post2.content = 'Foo content' 690 | 691 | await user.related('posts').saveMany([post1, post2]) 692 | 693 | await post1.related('comments').create({ 694 | userId: 1, 695 | comment: 'Test', 696 | }) 697 | 698 | await post2.related('comments').createMany([ 699 | { 700 | userId: 1, 701 | comment: 'Bar', 702 | }, 703 | { 704 | userId: 1, 705 | comment: 'Foo', 706 | }, 707 | ]) 708 | 709 | const post = (await Post.without(['comments']).find(1))! 710 | 711 | expect(post.$preloaded).toHaveProperty('user') 712 | expect(post.$preloaded).not.toHaveProperty('comments') 713 | }) 714 | 715 | test('auto-preloaded array should be restored after using without method', async ({ expect }) => { 716 | class Comment extends BaseModel { 717 | @column({ isPrimary: true }) 718 | public id: number 719 | 720 | @column() 721 | public userId: number 722 | 723 | @column() 724 | public postId: number 725 | 726 | @column() 727 | public comment: string 728 | } 729 | 730 | class Post extends compose(BaseModel, AutoPreload) { 731 | public static $with = ['user', 'comments'] as const 732 | 733 | @column({ isPrimary: true }) 734 | public id: number 735 | 736 | @column() 737 | public userId: number 738 | 739 | @column() 740 | public title: string 741 | 742 | @column() 743 | public content: string 744 | 745 | @belongsTo(() => User) 746 | public user: BelongsTo 747 | 748 | @hasMany(() => Comment) 749 | public comments: HasMany 750 | } 751 | 752 | class User extends BaseModel { 753 | @column({ isPrimary: true }) 754 | public id: number 755 | 756 | @column() 757 | public email: string 758 | 759 | @column() 760 | public name: string 761 | 762 | @hasMany(() => Post) 763 | public posts: HasMany 764 | } 765 | 766 | const user = await User.create({ 767 | email: 'john@doe.com', 768 | name: 'John Doe', 769 | }) 770 | 771 | const post1 = new Post() 772 | post1.title = 'Test' 773 | post1.content = 'Test content' 774 | 775 | const post2 = new Post() 776 | post2.title = 'Foo' 777 | post2.content = 'Foo content' 778 | 779 | await user.related('posts').saveMany([post1, post2]) 780 | 781 | await post1.related('comments').create({ 782 | userId: 1, 783 | comment: 'Test', 784 | }) 785 | 786 | await post2.related('comments').createMany([ 787 | { 788 | userId: 1, 789 | comment: 'Bar', 790 | }, 791 | { 792 | userId: 1, 793 | comment: 'Foo', 794 | }, 795 | ]) 796 | 797 | const post = (await Post.without(['comments']).find(1))! 798 | 799 | expect(post.$preloaded).toHaveProperty('user') 800 | expect(post.$preloaded).not.toHaveProperty('comments') 801 | 802 | expect(Post.$with).toStrictEqual(['user', 'comments']) 803 | }) 804 | 805 | test('without method should throw an exception when relationship is not a string', async ({ 806 | expect, 807 | }) => { 808 | class Comment extends BaseModel { 809 | @column({ isPrimary: true }) 810 | public id: number 811 | 812 | @column() 813 | public userId: number 814 | 815 | @column() 816 | public postId: number 817 | 818 | @column() 819 | public comment: string 820 | } 821 | 822 | class Post extends compose(BaseModel, AutoPreload) { 823 | public static $with = ['user', 'comments'] as const 824 | 825 | @column({ isPrimary: true }) 826 | public id: number 827 | 828 | @column() 829 | public userId: number 830 | 831 | @column() 832 | public title: string 833 | 834 | @column() 835 | public content: string 836 | 837 | @belongsTo(() => User) 838 | public user: BelongsTo 839 | 840 | @hasMany(() => Comment) 841 | public comments: HasMany 842 | } 843 | 844 | class User extends BaseModel { 845 | @column({ isPrimary: true }) 846 | public id: number 847 | 848 | @column() 849 | public email: string 850 | 851 | @column() 852 | public name: string 853 | 854 | @hasMany(() => Post) 855 | public posts: HasMany 856 | } 857 | 858 | const user = await User.create({ 859 | email: 'john@doe.com', 860 | name: 'John Doe', 861 | }) 862 | 863 | const post1 = new Post() 864 | post1.title = 'Test' 865 | post1.content = 'Test content' 866 | 867 | const post2 = new Post() 868 | post2.title = 'Foo' 869 | post2.content = 'Foo content' 870 | 871 | await user.related('posts').saveMany([post1, post2]) 872 | 873 | await post1.related('comments').create({ 874 | userId: 1, 875 | comment: 'Test', 876 | }) 877 | 878 | await post2.related('comments').createMany([ 879 | { 880 | userId: 1, 881 | comment: 'Bar', 882 | }, 883 | { 884 | userId: 1, 885 | comment: 'Foo', 886 | }, 887 | ]) 888 | 889 | // @ts-ignore 890 | expect(async () => await Post.without([5]).find(1)).rejects.toThrowError() 891 | }) 892 | 893 | test('withOnly method should auto-preload only specified relationships', async ({ expect }) => { 894 | class Comment extends BaseModel { 895 | @column({ isPrimary: true }) 896 | public id: number 897 | 898 | @column() 899 | public userId: number 900 | 901 | @column() 902 | public postId: number 903 | 904 | @column() 905 | public comment: string 906 | } 907 | 908 | class Post extends compose(BaseModel, AutoPreload) { 909 | public static $with = ['user', 'comments'] as const 910 | 911 | @column({ isPrimary: true }) 912 | public id: number 913 | 914 | @column() 915 | public userId: number 916 | 917 | @column() 918 | public title: string 919 | 920 | @column() 921 | public content: string 922 | 923 | @belongsTo(() => User) 924 | public user: BelongsTo 925 | 926 | @hasMany(() => Comment) 927 | public comments: HasMany 928 | } 929 | 930 | class User extends BaseModel { 931 | @column({ isPrimary: true }) 932 | public id: number 933 | 934 | @column() 935 | public email: string 936 | 937 | @column() 938 | public name: string 939 | 940 | @hasMany(() => Post) 941 | public posts: HasMany 942 | } 943 | 944 | const user = await User.create({ 945 | email: 'john@doe.com', 946 | name: 'John Doe', 947 | }) 948 | 949 | const post1 = new Post() 950 | post1.title = 'Test' 951 | post1.content = 'Test content' 952 | 953 | const post2 = new Post() 954 | post2.title = 'Foo' 955 | post2.content = 'Foo content' 956 | 957 | await user.related('posts').saveMany([post1, post2]) 958 | 959 | await post1.related('comments').create({ 960 | userId: 1, 961 | comment: 'Test', 962 | }) 963 | 964 | await post2.related('comments').createMany([ 965 | { 966 | userId: 1, 967 | comment: 'Bar', 968 | }, 969 | { 970 | userId: 1, 971 | comment: 'Foo', 972 | }, 973 | ]) 974 | 975 | const post = (await Post.withOnly(['user']).find(1))! 976 | 977 | expect(post.$preloaded).toHaveProperty('user') 978 | expect(post.$preloaded).not.toHaveProperty('comments') 979 | }) 980 | 981 | test('auto-preloaded array should be restored after using withOnly method', async ({ 982 | expect, 983 | }) => { 984 | class Comment extends BaseModel { 985 | @column({ isPrimary: true }) 986 | public id: number 987 | 988 | @column() 989 | public userId: number 990 | 991 | @column() 992 | public postId: number 993 | 994 | @column() 995 | public comment: string 996 | } 997 | 998 | class Post extends compose(BaseModel, AutoPreload) { 999 | public static $with = ['user', 'comments'] as const 1000 | 1001 | @column({ isPrimary: true }) 1002 | public id: number 1003 | 1004 | @column() 1005 | public userId: number 1006 | 1007 | @column() 1008 | public title: string 1009 | 1010 | @column() 1011 | public content: string 1012 | 1013 | @belongsTo(() => User) 1014 | public user: BelongsTo 1015 | 1016 | @hasMany(() => Comment) 1017 | public comments: HasMany 1018 | } 1019 | 1020 | class User extends BaseModel { 1021 | @column({ isPrimary: true }) 1022 | public id: number 1023 | 1024 | @column() 1025 | public email: string 1026 | 1027 | @column() 1028 | public name: string 1029 | 1030 | @hasMany(() => Post) 1031 | public posts: HasMany 1032 | } 1033 | 1034 | const user = await User.create({ 1035 | email: 'john@doe.com', 1036 | name: 'John Doe', 1037 | }) 1038 | 1039 | const post1 = new Post() 1040 | post1.title = 'Test' 1041 | post1.content = 'Test content' 1042 | 1043 | const post2 = new Post() 1044 | post2.title = 'Foo' 1045 | post2.content = 'Foo content' 1046 | 1047 | await user.related('posts').saveMany([post1, post2]) 1048 | 1049 | await post1.related('comments').create({ 1050 | userId: 1, 1051 | comment: 'Test', 1052 | }) 1053 | 1054 | await post2.related('comments').createMany([ 1055 | { 1056 | userId: 1, 1057 | comment: 'Bar', 1058 | }, 1059 | { 1060 | userId: 1, 1061 | comment: 'Foo', 1062 | }, 1063 | ]) 1064 | 1065 | const post = (await Post.withOnly(['user']).find(1))! 1066 | 1067 | expect(post.$preloaded).toHaveProperty('user') 1068 | expect(post.$preloaded).not.toHaveProperty('comments') 1069 | 1070 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1071 | }) 1072 | 1073 | test('withOnly method should throw an exception when relationship is not a string', async ({ 1074 | expect, 1075 | }) => { 1076 | class Comment extends BaseModel { 1077 | @column({ isPrimary: true }) 1078 | public id: number 1079 | 1080 | @column() 1081 | public userId: number 1082 | 1083 | @column() 1084 | public postId: number 1085 | 1086 | @column() 1087 | public comment: string 1088 | } 1089 | 1090 | class Post extends compose(BaseModel, AutoPreload) { 1091 | public static $with = ['user', 'comments'] as const 1092 | 1093 | @column({ isPrimary: true }) 1094 | public id: number 1095 | 1096 | @column() 1097 | public userId: number 1098 | 1099 | @column() 1100 | public title: string 1101 | 1102 | @column() 1103 | public content: string 1104 | 1105 | @belongsTo(() => User) 1106 | public user: BelongsTo 1107 | 1108 | @hasMany(() => Comment) 1109 | public comments: HasMany 1110 | } 1111 | 1112 | class User extends BaseModel { 1113 | @column({ isPrimary: true }) 1114 | public id: number 1115 | 1116 | @column() 1117 | public email: string 1118 | 1119 | @column() 1120 | public name: string 1121 | 1122 | @hasMany(() => Post) 1123 | public posts: HasMany 1124 | } 1125 | 1126 | const user = await User.create({ 1127 | email: 'john@doe.com', 1128 | name: 'John Doe', 1129 | }) 1130 | 1131 | const post1 = new Post() 1132 | post1.title = 'Test' 1133 | post1.content = 'Test content' 1134 | 1135 | const post2 = new Post() 1136 | post2.title = 'Foo' 1137 | post2.content = 'Foo content' 1138 | 1139 | await user.related('posts').saveMany([post1, post2]) 1140 | 1141 | await post1.related('comments').create({ 1142 | userId: 1, 1143 | comment: 'Test', 1144 | }) 1145 | 1146 | await post2.related('comments').createMany([ 1147 | { 1148 | userId: 1, 1149 | comment: 'Bar', 1150 | }, 1151 | { 1152 | userId: 1, 1153 | comment: 'Foo', 1154 | }, 1155 | ]) 1156 | 1157 | // @ts-ignore 1158 | expect(async () => await Post.withOnly([5]).find(1)).rejects.toThrowError() 1159 | }) 1160 | 1161 | test('withoutAny method should not auto-preload any relationship', async ({ expect }) => { 1162 | class Comment extends BaseModel { 1163 | @column({ isPrimary: true }) 1164 | public id: number 1165 | 1166 | @column() 1167 | public userId: number 1168 | 1169 | @column() 1170 | public postId: number 1171 | 1172 | @column() 1173 | public comment: string 1174 | } 1175 | 1176 | class Post extends compose(BaseModel, AutoPreload) { 1177 | public static $with = ['user', 'comments'] as const 1178 | 1179 | @column({ isPrimary: true }) 1180 | public id: number 1181 | 1182 | @column() 1183 | public userId: number 1184 | 1185 | @column() 1186 | public title: string 1187 | 1188 | @column() 1189 | public content: string 1190 | 1191 | @belongsTo(() => User) 1192 | public user: BelongsTo 1193 | 1194 | @hasMany(() => Comment) 1195 | public comments: HasMany 1196 | } 1197 | 1198 | class User extends BaseModel { 1199 | @column({ isPrimary: true }) 1200 | public id: number 1201 | 1202 | @column() 1203 | public email: string 1204 | 1205 | @column() 1206 | public name: string 1207 | 1208 | @hasMany(() => Post) 1209 | public posts: HasMany 1210 | } 1211 | 1212 | const user = await User.create({ 1213 | email: 'john@doe.com', 1214 | name: 'John Doe', 1215 | }) 1216 | 1217 | const post1 = new Post() 1218 | post1.title = 'Test' 1219 | post1.content = 'Test content' 1220 | 1221 | const post2 = new Post() 1222 | post2.title = 'Foo' 1223 | post2.content = 'Foo content' 1224 | 1225 | await user.related('posts').saveMany([post1, post2]) 1226 | 1227 | await post1.related('comments').create({ 1228 | userId: 1, 1229 | comment: 'Test', 1230 | }) 1231 | 1232 | await post2.related('comments').createMany([ 1233 | { 1234 | userId: 1, 1235 | comment: 'Bar', 1236 | }, 1237 | { 1238 | userId: 1, 1239 | comment: 'Foo', 1240 | }, 1241 | ]) 1242 | 1243 | const post = (await Post.withoutAny().find(1))! 1244 | 1245 | expect(post.$preloaded).not.toHaveProperty('user') 1246 | expect(post.$preloaded).not.toHaveProperty('comments') 1247 | }) 1248 | 1249 | test('auto-preloaded array should be restored after using withoutAny method', async ({ 1250 | expect, 1251 | }) => { 1252 | class Comment extends BaseModel { 1253 | @column({ isPrimary: true }) 1254 | public id: number 1255 | 1256 | @column() 1257 | public userId: number 1258 | 1259 | @column() 1260 | public postId: number 1261 | 1262 | @column() 1263 | public comment: string 1264 | } 1265 | 1266 | class Post extends compose(BaseModel, AutoPreload) { 1267 | public static $with = ['user', 'comments'] as const 1268 | 1269 | @column({ isPrimary: true }) 1270 | public id: number 1271 | 1272 | @column() 1273 | public userId: number 1274 | 1275 | @column() 1276 | public title: string 1277 | 1278 | @column() 1279 | public content: string 1280 | 1281 | @belongsTo(() => User) 1282 | public user: BelongsTo 1283 | 1284 | @hasMany(() => Comment) 1285 | public comments: HasMany 1286 | } 1287 | 1288 | class User extends BaseModel { 1289 | @column({ isPrimary: true }) 1290 | public id: number 1291 | 1292 | @column() 1293 | public email: string 1294 | 1295 | @column() 1296 | public name: string 1297 | 1298 | @hasMany(() => Post) 1299 | public posts: HasMany 1300 | } 1301 | 1302 | const user = await User.create({ 1303 | email: 'john@doe.com', 1304 | name: 'John Doe', 1305 | }) 1306 | 1307 | const post1 = new Post() 1308 | post1.title = 'Test' 1309 | post1.content = 'Test content' 1310 | 1311 | const post2 = new Post() 1312 | post2.title = 'Foo' 1313 | post2.content = 'Foo content' 1314 | 1315 | await user.related('posts').saveMany([post1, post2]) 1316 | 1317 | await post1.related('comments').create({ 1318 | userId: 1, 1319 | comment: 'Test', 1320 | }) 1321 | 1322 | await post2.related('comments').createMany([ 1323 | { 1324 | userId: 1, 1325 | comment: 'Bar', 1326 | }, 1327 | { 1328 | userId: 1, 1329 | comment: 'Foo', 1330 | }, 1331 | ]) 1332 | 1333 | const post = (await Post.withoutAny().find(1))! 1334 | 1335 | expect(post.$preloaded).not.toHaveProperty('user') 1336 | expect(post.$preloaded).not.toHaveProperty('comments') 1337 | 1338 | expect(Post.$with).toStrictEqual(['user', 'comments']) 1339 | }) 1340 | }) 1341 | -------------------------------------------------------------------------------- /tests/auto_preload.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseContract } from '@ioc:Adonis/Lucid/Database' 2 | import type { BaseModel as BaseModelContract, ColumnDecorator } from '@ioc:Adonis/Lucid/Orm' 3 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 4 | import type { AutoPreloadMixin } from '@ioc:Adonis/Addons/AutoPreload' 5 | 6 | import { test } from '@japa/runner' 7 | import { compose } from '@poppinss/utils/build/helpers' 8 | 9 | import { setupDatabase, cleanDatabase } from '../bin/test/database' 10 | import { fs } from '../bin/test/config' 11 | import { setupApp } from '../test-helpers' 12 | 13 | let db: DatabaseContract 14 | let BaseModel: typeof BaseModelContract 15 | let AutoPreload: AutoPreloadMixin 16 | let app: ApplicationContract 17 | let column: ColumnDecorator 18 | 19 | test.group('Auto preload', (group) => { 20 | group.setup(async () => { 21 | app = await setupApp() 22 | db = app.container.resolveBinding('Adonis/Lucid/Database') 23 | BaseModel = app.container.resolveBinding('Adonis/Lucid/Orm').BaseModel 24 | AutoPreload = app.container.resolveBinding('Adonis/Addons/AutoPreload').AutoPreload 25 | column = app.container.resolveBinding('Adonis/Lucid/Orm').column 26 | }) 27 | 28 | group.each.setup(async () => { 29 | await setupDatabase(db) 30 | }) 31 | 32 | group.each.teardown(async () => { 33 | await cleanDatabase(db) 34 | }) 35 | 36 | group.teardown(async () => { 37 | await db.manager.closeAll() 38 | await fs.cleanup() 39 | }) 40 | 41 | test('model extending from AutoPreload mixin should have an empty $with array', async ({ 42 | expect, 43 | }) => { 44 | class User extends compose(BaseModel, AutoPreload) { 45 | @column({ isPrimary: true }) 46 | public id: number 47 | 48 | @column() 49 | public email: string 50 | 51 | @column() 52 | public name: string 53 | } 54 | 55 | expect(User).toHaveProperty('$with') 56 | expect(User.$with).toHaveLength(0) 57 | }) 58 | 59 | test('should return $with relationships', async ({ expect }) => { 60 | class User extends compose(BaseModel, AutoPreload) { 61 | public static $with = ['posts'] 62 | 63 | @column({ isPrimary: true }) 64 | public id: number 65 | 66 | @column() 67 | public email: string 68 | 69 | @column() 70 | public name: string 71 | } 72 | 73 | expect(User).toHaveProperty('$with') 74 | expect(User.$with[0]).toStrictEqual('posts') 75 | }) 76 | 77 | test('filling $with with type other than string or function should throw an exception', async ({ 78 | expect, 79 | }) => { 80 | expect(() => { 81 | class User extends compose(BaseModel, AutoPreload) { 82 | public static $with = [1] 83 | 84 | @column({ isPrimary: true }) 85 | public id: number 86 | 87 | @column() 88 | public email: string 89 | 90 | @column() 91 | public name: string 92 | } 93 | 94 | User.boot() 95 | }).toThrowError( 96 | 'The model "User" has wrong relationships to be auto-preloaded. Only string and function types are allowed' 97 | ) 98 | }) 99 | 100 | test('model extending from a custom base model and AutoPreload mixin should have $with property', async ({ 101 | expect, 102 | }) => { 103 | class CustomBaseModel extends BaseModel {} 104 | 105 | class User extends compose(CustomBaseModel, AutoPreload) { 106 | @column({ isPrimary: true }) 107 | public id: number 108 | 109 | @column() 110 | public email: string 111 | 112 | @column() 113 | public name: string 114 | } 115 | 116 | expect(User).toHaveProperty('$with') 117 | expect(User.$with).toHaveLength(0) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "emitDecoratorMetadata": true, 5 | "skipLibCheck": true, 6 | "outDir": "build", 7 | "rootDir": "./", 8 | "paths": {}, 9 | "types": ["@types/node", "@adonisjs/core", "@adonisjs/lucid"] 10 | }, 11 | "include": ["**/*"], 12 | "exclude": ["./node_modules", "./build"], 13 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 14 | "files": ["./adonis-typings/index.ts"] 15 | } 16 | --------------------------------------------------------------------------------