├── .editorconfig ├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── config └── index.js ├── errors └── E_MISSING_CONFIG.md ├── index.js ├── package.json ├── providers └── FeudProvider.js ├── src ├── Feud.js ├── Middleware │ └── TenantAware.js ├── ModelHook.js ├── Relations │ ├── BelongsToMany.js │ ├── HasMany.js │ └── HasOne.js ├── TenantAware.js └── ValidatorRules.js └── test ├── functional ├── belongsToMany.spec.js ├── feud-provider.spec.js ├── hasMany.spec.js └── hasOne.spec.js └── unit ├── helpers ├── Comment.js ├── Group.js ├── Post.js ├── Profile.js ├── User.js └── fixtures.js └── tenantAware.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.json] 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brainnit/adonisjs-feud/043ff58b0fac10473eab0f1374595224f6b23a4d/.env.example -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | build 3 | dist 4 | node_modules 5 | stats.json 6 | .DS_Store 7 | .idea 8 | .vscode 9 | *.log 10 | yarn.lock 11 | *.sqlite 12 | .env 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2019 João Pedro Barros, 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdonisJs Feud 2 | 3 | Adonis Feud allows you to serve multiple tenants within the same Adonis application while keeping tenant specific data logically separated for fully independent multi-domain/SaaS setups. 4 | 5 | ## Instalation 6 | 7 | Use npm or yarn to install the package: 8 | 9 | ```sh 10 | npm -i @brainnit/adonisjs-feud 11 | # or 12 | yarn add @brainnit/adonisjs-feud 13 | ``` 14 | 15 | Add Feud to the list of service providers at `start/app.js`: 16 | 17 | ```js 18 | const providers = [ 19 | // ... 20 | '@brainnit/adonisjs-feud/providers/FeudProvider' 21 | ]; 22 | ``` 23 | 24 | ## Setup 25 | 26 | Copy `config/index.js` to your app config folder and name it `feud.js`. Don't forget to setup your environment variables. 27 | 28 | ## Usage 29 | 30 | Add `@provider:Feud` trait to your models and define only the methods you want to override to change default behaviour: 31 | 32 | ```js 33 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 34 | const Model = use('Model'); 35 | 36 | class User extends Model { 37 | static get traits () { 38 | return ['@provider:TenantAware'] 39 | } 40 | } 41 | 42 | module.exports = Users 43 | ``` 44 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | 5 | /** 6 | * ------------------------------------------------------------------------ 7 | * Default tenant column 8 | * ------------------------------------------------------------------------ 9 | * 10 | * Here you can specify the default tenant column name for models. Don't 11 | * forget to add this column into your migrations. 12 | */ 13 | tenantColumn: 'tenant_id' 14 | } 15 | -------------------------------------------------------------------------------- /errors/E_MISSING_CONFIG.md: -------------------------------------------------------------------------------- 1 | # E_MISSING_CONFIG 2 | 3 | ### Why This Error Occurred 4 | This error occurs when the search engine you are trying to use has not been defined inside `config/feud.js` file. 5 | 6 | ### Possible Ways to Fix It 7 | - Make sure to define the search engine connection inside `config/feud.js` file. Check [example config file](https://github.com/brainnit/adonisjs-feud/master/blob/config/index.js) for reference. 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @brainnit/adonisjs-feud 3 | * 4 | * (c) João Pedro Barros 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | 'use strict' 11 | 12 | module.exports = {} 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brainnit/adonisjs-feud", 3 | "version": "0.1.2", 4 | "description": "", 5 | "keywords": [ 6 | "adonis", 7 | "adonis-framework", 8 | "adonis-feud", 9 | "multi-tenancy", 10 | "tenant" 11 | ], 12 | "main": "index.js", 13 | "repository": "git@github.com:brainnit/adonisjs-feud.git", 14 | "scripts": { 15 | "test": "jest --coverage --runInBand", 16 | "lint": "standard" 17 | }, 18 | "author": "João Pedro Barros ", 19 | "license": "MIT", 20 | "standard": { 21 | "globals": [ 22 | "use" 23 | ], 24 | "env": { 25 | "node": true, 26 | "jest": true, 27 | "es6": true 28 | } 29 | }, 30 | "dependencies": { 31 | "@adonisjs/generic-exceptions": "^3.0.1", 32 | "@adonisjs/validator": "^5.0.6" 33 | }, 34 | "devDependencies": { 35 | "@adonisjs/ace": "^5.0.8", 36 | "@adonisjs/fold": "^4.0.9", 37 | "@adonisjs/lucid": "^6.1.3", 38 | "@adonisjs/sink": "^1.0.17", 39 | "adonis-lucid-polymorphic": "^1.0.1", 40 | "dotenv": "^6.2.0", 41 | "jest": "^24.1.0", 42 | "sqlite3": "^4.0.6", 43 | "standard": "^12.0.1" 44 | }, 45 | "peerDependencies": { 46 | "@adonisjs/framework": "^5.0.12" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /providers/FeudProvider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ServiceProvider } = require('@adonisjs/fold') 4 | 5 | class FeudProvider extends ServiceProvider { 6 | /** 7 | * Register namespaces on IoC container. 8 | * 9 | * @method register 10 | * 11 | * @return {Elasticsearch} 12 | */ 13 | register () { 14 | this._registerFeud() 15 | this._registerTenantAwareMiddleware() 16 | this._registerTenantAwareTrait() 17 | } 18 | 19 | /** 20 | * Register feud provider under `Adonis/Addons/Feud` 21 | * namespace and an alias named `Feud`. 22 | * 23 | * @method _registerFeud 24 | * 25 | * @return {void} 26 | * 27 | * @private 28 | */ 29 | _registerFeud () { 30 | this.app.singleton('Adonis/Addons/Feud', (app) => { 31 | const Config = app.use('Adonis/Src/Config') 32 | const Feud = require('../src/Feud') 33 | return new Feud(Config) 34 | }) 35 | this.app.alias('Adonis/Addons/Feud', 'Feud') 36 | } 37 | 38 | /** 39 | * Register auth middleware under `Adonis/Middleware/TenantAware` namespace. 40 | * 41 | * @method _registerTenantAwareMiddleware 42 | * 43 | * @return {void} 44 | * 45 | * @private 46 | */ 47 | _registerTenantAwareMiddleware () { 48 | this.app.bind('Adonis/Middleware/TenantAware', (app) => { 49 | return new (require('../src/Middleware/TenantAware'))() 50 | }) 51 | } 52 | 53 | /** 54 | * Register searchable trait under `Adonis/Traits/TenantAware` namespace 55 | * and creates an alias named `TenantAware`. 56 | * 57 | * Supposed to be used to make your Lucid models searchable. 58 | * 59 | * @method _registerTenantAwareTrait 60 | * 61 | * @return {void} 62 | * 63 | * @private 64 | */ 65 | _registerTenantAwareTrait () { 66 | this.app.bind('Adonis/Traits/TenantAware', () => { 67 | const TenantAware = require('../src/TenantAware') 68 | return new TenantAware() 69 | }) 70 | this.app.alias('Adonis/Traits/TenantAware', 'TenantAware') 71 | } 72 | 73 | /** 74 | * Extends the Validator adding rules scoped to Feud. 75 | * 76 | * @method _registerValidatorRules 77 | * 78 | * @return {void} 79 | * 80 | * @private 81 | */ 82 | _registerValidatorRules () { 83 | try { 84 | const { extend } = this.app.use('Adonis/Addons/Validator') 85 | const Feud = this.app.use('Feud') 86 | const Database = this.app.use('Adonis/Src/Database') 87 | const validatorRules = new (require('../src/ValidatorRules'))( 88 | Feud, 89 | Database 90 | ) 91 | 92 | /** 93 | * Adds `feudUnique` rule. 94 | */ 95 | extend( 96 | 'feudUnique', 97 | validatorRules.unique.bind(validatorRules), 98 | '{{field}} has already been taken by someone else' 99 | ) 100 | 101 | /** 102 | * Adds `feudExists` rule. 103 | */ 104 | extend( 105 | 'feudExists', 106 | validatorRules.exists.bind(validatorRules), 107 | '{{field}} has not been found' 108 | ) 109 | } catch (error) { 110 | // fails silently 111 | } 112 | } 113 | 114 | /** 115 | * Attach context getter when all providers have 116 | * been registered 117 | * 118 | * @method boot 119 | * 120 | * @return {void} 121 | */ 122 | boot () { 123 | this._registerValidatorRules() 124 | } 125 | } 126 | 127 | module.exports = FeudProvider 128 | -------------------------------------------------------------------------------- /src/Feud.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { InvalidArgumentException } = require('@adonisjs/generic-exceptions') 4 | 5 | class Feud { 6 | /** 7 | * @param {typeof Adonis/Src/Config} Config 8 | */ 9 | constructor (Config) { 10 | this.Config = Config 11 | 12 | this._tenantColumn = Config.get('feud.tenantColumn', 'tenant_id') 13 | this._tenant = null 14 | } 15 | 16 | /** 17 | * Get the tenant column name to scope models. 18 | * 19 | * @return {String} 20 | */ 21 | getTenantColumn () { 22 | return this._tenantColumn 23 | } 24 | 25 | /** 26 | * Set current tenant. 27 | * 28 | * @param {*} tenant 29 | */ 30 | setTenant (tenant) { 31 | if (!tenant) { 32 | throw InvalidArgumentException.invalidParameter( 33 | 'Tenant cannot be null', 34 | tenant 35 | ) 36 | } 37 | 38 | this._tenant = tenant 39 | } 40 | 41 | /** 42 | * Get current tenant. 43 | * 44 | * @return {*} 45 | */ 46 | getTenant () { 47 | return this._tenant 48 | } 49 | 50 | /** 51 | * Determine if a tenant set. 52 | * 53 | * @return {Boolean} 54 | */ 55 | hasTenant () { 56 | return !!this.getTenant() 57 | } 58 | } 59 | 60 | module.exports = Feud 61 | -------------------------------------------------------------------------------- /src/Middleware/TenantAware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 4 | 5 | const { ioc } = require('@adonisjs/fold') 6 | const { HttpException } = require('@adonisjs/generic-exceptions') 7 | 8 | class TenantAware { 9 | /** 10 | * Make sure the tenant can be identified or fail. 11 | * 12 | * @throws {HttpException} 13 | * 14 | * @param {object} ctx 15 | * @param {Request} ctx.request 16 | * @param {Function} next 17 | */ 18 | async handle ({ request }, next) { 19 | const Feud = ioc.use('Feud') 20 | 21 | const tenant = request.header('Tenant') 22 | 23 | if (!tenant) { 24 | throw new HttpException( 25 | `The request Tenant is missing or invalid`, 26 | 403, 27 | 'E_UNKOWN_TENANT' 28 | ) 29 | } 30 | 31 | Feud.setTenant(tenant) 32 | 33 | // call next to advance with the request 34 | await next() 35 | } 36 | } 37 | 38 | module.exports = TenantAware 39 | -------------------------------------------------------------------------------- /src/ModelHook.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | 5 | const Feud = ioc.use('Feud') 6 | 7 | /** 8 | * @typedef {import('@adonisjs/lucid/src/Lucid/Model')} Model 9 | */ 10 | 11 | class ModelHook { 12 | /** 13 | * Adds the tenant attribute to restrict the new instances to the 14 | * current tenant scope. Called on the `beforeCreate` event. 15 | * 16 | * @param {Model} modelInstance 17 | * 18 | * @return {void} 19 | */ 20 | static addTenantScope (modelInstance) { 21 | if (!Feud.getTenantColumn()) { 22 | throw new Error('Tenant must be set before creating the model') 23 | } 24 | 25 | modelInstance[Feud.getTenantColumn()] = Feud.getTenant() 26 | } 27 | } 28 | 29 | module.exports = ModelHook 30 | -------------------------------------------------------------------------------- /src/Relations/BelongsToMany.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | 5 | const Feud = ioc.use('Feud') 6 | const BaseBelongsToMany = require('@adonisjs/lucid/src/Lucid/Relations/BelongsToMany') 7 | 8 | class BelongsToMany extends BaseBelongsToMany { 9 | /** 10 | * Saves the relationship to the pivot table 11 | * 12 | * @method _attachSingle 13 | * @async 14 | * 15 | * @param {Number|String} value 16 | * @param {Function} [pivotCallback] 17 | * @param {Object} [trx] 18 | * 19 | * @return {Object} Instance of pivot model 20 | * 21 | * @private 22 | */ 23 | async _attachSingle (value, pivotCallback, trx) { 24 | /** 25 | * The relationship values 26 | * 27 | * @type {Object} 28 | */ 29 | const pivotValues = { 30 | [this.relatedForeignKey]: value, 31 | [Feud.getTenantColumn()]: this.parentInstance.getTenant(), 32 | [this.foreignKey]: this.$primaryKeyValue 33 | } 34 | const pivotModel = this._newUpPivotModel() 35 | this._existingPivotInstances.push(pivotModel) 36 | pivotModel.fill(pivotValues) 37 | 38 | /** 39 | * Set $table, $timestamps, $connection when there 40 | * is no pre-defined pivot model. 41 | */ 42 | if (!this._PivotModel) { 43 | pivotModel.$table = this.$pivotTable 44 | pivotModel.$connection = this.RelatedModel.connection 45 | pivotModel.$withTimestamps = this._pivot.withTimestamps 46 | pivotModel.$primaryKey = this._pivot.pivotPrimaryKey 47 | } 48 | 49 | /** 50 | * If pivot callback is defined, do call it. This gives 51 | * chance to the user to set additional fields to the 52 | * model. 53 | */ 54 | if (typeof pivotCallback === 'function') { 55 | pivotCallback(pivotModel) 56 | } 57 | 58 | await pivotModel.save(trx) 59 | return pivotModel 60 | } 61 | } 62 | 63 | module.exports = BelongsToMany 64 | -------------------------------------------------------------------------------- /src/Relations/HasMany.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | 5 | const Feud = ioc.use('Feud') 6 | const BaseHasMany = require('@adonisjs/lucid/src/Lucid/Relations/HasMany') 7 | 8 | class HasMany extends BaseHasMany { 9 | /** 10 | * Saves the related instance to the database. Foreign 11 | * key is set automatically 12 | * 13 | * @method save 14 | * 15 | * @param {Object} relatedInstance 16 | * @param {Object} [trx] 17 | * 18 | * @return {Promise} 19 | */ 20 | async save (relatedInstance, trx) { 21 | await this._persistParentIfRequired(trx) 22 | relatedInstance[this.foreignKey] = this.$primaryKeyValue 23 | relatedInstance[Feud.getTenantColumn()] = this.parentInstance.getTenant() 24 | 25 | return relatedInstance.save(trx) 26 | } 27 | } 28 | 29 | module.exports = HasMany 30 | -------------------------------------------------------------------------------- /src/Relations/HasOne.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | 5 | const Feud = ioc.use('Feud') 6 | const BaseHasOne = require('@adonisjs/lucid/src/Lucid/Relations/HasOne') 7 | 8 | class HasOne extends BaseHasOne { 9 | /** 10 | * Saves the related instance to the database. Foreign 11 | * key is set automatically. 12 | * 13 | * NOTE: This method will persist the parent model if 14 | * not persisted already. 15 | * 16 | * @method save 17 | * 18 | * @param {Object} relatedInstance 19 | * @param {Object} [trx] 20 | * 21 | * @return {Promise} 22 | */ 23 | async save (relatedInstance, trx) { 24 | await this._persistParentIfRequired(trx) 25 | relatedInstance[this.foreignKey] = this.$primaryKeyValue 26 | relatedInstance[Feud.getTenantColumn()] = this.parentInstance.getTenant() 27 | 28 | return relatedInstance.save(trx) 29 | } 30 | } 31 | 32 | module.exports = HasOne 33 | -------------------------------------------------------------------------------- /src/TenantAware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | 5 | const Feud = ioc.use('Feud') 6 | const ModelHook = require('./ModelHook') 7 | const HasOne = require('./Relations/HasOne') 8 | const HasMany = require('./Relations/HasMany') 9 | const BelongsToMany = require('./Relations/BelongsToMany') 10 | 11 | class TenantAware { 12 | register (Model) { 13 | /** 14 | * Simply boot the trait to scope the model queries. 15 | */ 16 | this.constructor.bootTenantAware(Model) 17 | 18 | /** 19 | * Get the tenant for the model. 20 | * 21 | * @return {*} 22 | */ 23 | Model.prototype.getTenant = function () { 24 | return this.$attributes[Feud.getTenantColumn()] 25 | } 26 | 27 | /** 28 | * Returns an instance of @ref('HasOne') relation. 29 | * 30 | * @method hasOne 31 | * 32 | * @param {String|Class} relatedModel 33 | * @param {String} primaryKey 34 | * @param {String} foreignKey 35 | * 36 | * @return {HasOne} 37 | */ 38 | Model.prototype.hasOne = function (relatedModel, primaryKey, foreignKey) { 39 | relatedModel = typeof relatedModel === 'string' 40 | ? ioc.use(relatedModel) 41 | : relatedModel 42 | 43 | primaryKey = primaryKey || this.constructor.primaryKey 44 | foreignKey = foreignKey || this.constructor.foreignKey 45 | 46 | return new HasOne(this, relatedModel, primaryKey, foreignKey) 47 | } 48 | 49 | /** 50 | * Returns an instance of @ref('HasMany') relation 51 | * 52 | * @method hasMany 53 | * 54 | * @param {String|Class} relatedModel 55 | * @param {String} primaryKey 56 | * @param {String} foreignKey 57 | * 58 | * @return {HasMany} 59 | */ 60 | Model.prototype.hasMany = function (relatedModel, primaryKey, foreignKey) { 61 | relatedModel = 62 | typeof relatedModel === 'string' ? ioc.use(relatedModel) : relatedModel 63 | 64 | primaryKey = primaryKey || this.constructor.primaryKey 65 | foreignKey = foreignKey || this.constructor.foreignKey 66 | 67 | return new HasMany(this, relatedModel, primaryKey, foreignKey) 68 | } 69 | 70 | /** 71 | * Returns an instance of @ref('BelongsToMany') relation 72 | * 73 | * @method belongsToMany 74 | * 75 | * @param {Class|String} relatedModel 76 | * @param {String} foreignKey 77 | * @param {String} relatedForeignKey 78 | * @param {String} primaryKey 79 | * @param {String} relatedPrimaryKey 80 | * 81 | * @return {BelongsToMany} 82 | */ 83 | Model.prototype.belongsToMany = function ( 84 | relatedModel, 85 | foreignKey, 86 | relatedForeignKey, 87 | primaryKey, 88 | relatedPrimaryKey 89 | ) { 90 | relatedModel = 91 | typeof relatedModel === 'string' ? ioc.use(relatedModel) : relatedModel 92 | 93 | foreignKey = foreignKey || this.constructor.foreignKey 94 | relatedForeignKey = relatedForeignKey || relatedModel.foreignKey 95 | primaryKey = primaryKey || this.constructor.primaryKey 96 | relatedPrimaryKey = relatedPrimaryKey || relatedModel.primaryKey 97 | 98 | return new BelongsToMany( 99 | this, 100 | relatedModel, 101 | primaryKey, 102 | foreignKey, 103 | relatedPrimaryKey, 104 | relatedForeignKey 105 | ) 106 | } 107 | } 108 | 109 | /** 110 | * Boot the trait. 111 | * 112 | * @param {Model} Model 113 | * 114 | * @return {void} 115 | */ 116 | static bootTenantAware (Model) { 117 | this.registerObservers(Model) 118 | 119 | /** 120 | * Adds global scope to restrict all model queries to the current tenant. 121 | */ 122 | Model.addGlobalScope(query => { 123 | if (Feud.hasTenant() === false) { 124 | throw new Error('Tenant must be set before querying the model') 125 | } 126 | 127 | query.where(`${Model.table}.${Feud.getTenantColumn()}`, Feud.getTenant()) 128 | }, 'TenantAware') 129 | } 130 | 131 | /** 132 | * Register model event observers to automatically scope 133 | * new/existing instances. 134 | * 135 | * @param {Model} Model 136 | * 137 | * @return {void} 138 | */ 139 | static registerObservers (Model) { 140 | Model.addHook('beforeSave', ModelHook.addTenantScope) 141 | } 142 | } 143 | 144 | module.exports = TenantAware 145 | -------------------------------------------------------------------------------- /src/ValidatorRules.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | class ValidatorRules { 5 | constructor (Feud, Database) { 6 | this.Feud = Feud 7 | this.Database = Database 8 | } 9 | 10 | /** 11 | * Verifica se determinado valor é único na base de dados 12 | * no escopo de um Tenant. 13 | * 14 | * @method unique 15 | * 16 | * @param {Object} data Input 17 | * @param {String} field Nome do campo 18 | * @param {String} message Mensagem de erro 19 | * @param {Array} args Opções 20 | * @param {Function} get Função get 21 | * 22 | * @return {Promise} 23 | * 24 | * @example 25 | * ```js 26 | * email: 'feud_unique:users' // define table 27 | * email: 'feud_unique:users,user_email' // define table + field 28 | * email: 'feud_unique:users,user_email,id:1' // where id !== 1 29 | * 30 | * // Via new rule method 31 | * email: [rule('feud_unique', ['users', 'user_email', 'id', 1])] 32 | * ``` 33 | */ 34 | unique (data, field, message, args, get) { 35 | return new Promise((resolve, reject) => { 36 | const value = get(data, field) 37 | 38 | if (!value) { 39 | /** 40 | * Pula validação caso value não tenha sido definido. 41 | * A regra `required` deve cuidar disso. 42 | */ 43 | return resolve('validation skipped') 44 | } 45 | 46 | // Extraio valores da lista de argumentos 47 | const [table, fieldName, ignoreKey, ignoreValue] = args 48 | 49 | // Query base para selecionar where key=value no escopo do tenant atual 50 | const query = this.Database.table(table) 51 | .where(this.Feud.getTenantColumn(), this.Feud.getTenant()) 52 | .where(fieldName || field, value) 53 | 54 | // Se a chave de ignorar estiver definida, adiciona whereNot 55 | if (ignoreKey && ignoreValue) { 56 | query.whereNot(ignoreKey, ignoreValue) 57 | } 58 | 59 | query 60 | .then(rows => { 61 | /** 62 | * Unique validation fails when a row has been found 63 | */ 64 | if (rows && rows.length) { 65 | return reject(message) 66 | } 67 | return resolve('validation passed') 68 | }) 69 | .catch(reject) 70 | 71 | return this 72 | }) 73 | } 74 | 75 | /** 76 | * Verifica se determinado valor existe na base de dados 77 | * no escopo de um tenant. 78 | * 79 | * @method exists 80 | * 81 | * @param {Object} data Input 82 | * @param {String} field Nome do campo 83 | * @param {String} message Mensagem de erro 84 | * @param {Array} args Opções 85 | * @param {Function} get Função get 86 | * 87 | * @return {Promise} 88 | * 89 | * @example 90 | * ```js 91 | * email: 'feud_exists:users' // define table 92 | * email: 'feud_exists:users,user_email' // define table + field 93 | * 94 | * // Via new rule method 95 | * email: [rule('feud_exists', ['users', 'user_email'])] 96 | * ``` 97 | */ 98 | exists (data, field, message, args, get) { 99 | return new Promise((resolve, reject) => { 100 | const value = get(data, field) 101 | 102 | if (!value) { 103 | /** 104 | * Pula validação caso value não tenha sido definido. 105 | * A regra `required` deve cuidar disso. 106 | */ 107 | return resolve('validation skipped') 108 | } 109 | 110 | // Extraio valores da lista de argumentos 111 | const [table, fieldName] = args 112 | 113 | // Query base para selecionar where key=value no escopo do tenant atual 114 | const query = this.Database.table(table) 115 | .where(this.Feud.getTenantColumn(), this.Feud.getTenant()) 116 | .where(fieldName || field, value) 117 | 118 | query 119 | .then(rows => { 120 | /** 121 | * Unique validation fails when a row has NOT been found 122 | */ 123 | if (!rows || rows.length === 0) { 124 | return reject(message) 125 | } 126 | return resolve('validation passed') 127 | }) 128 | .catch(reject) 129 | 130 | return this 131 | }) 132 | } 133 | } 134 | 135 | module.exports = ValidatorRules 136 | -------------------------------------------------------------------------------- /test/functional/belongsToMany.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { ioc, registrar } = require('@adonisjs/fold') 5 | const { Config, setupResolver } = require('@adonisjs/sink') 6 | 7 | const fixtures = require('../unit/helpers/fixtures') 8 | 9 | beforeAll(async () => { 10 | ioc.singleton('Adonis/Src/Config', function () { 11 | const config = new Config() 12 | 13 | config.set('database', { 14 | connection: 'sqlite', 15 | sqlite: { 16 | client: 'sqlite3', 17 | connection: { 18 | filename: './testing.sqlite' 19 | }, 20 | useNullAsDefault: true, 21 | debug: false 22 | } 23 | }) 24 | 25 | config.set('feud', { tenantColumn: 'tenant_id' }) 26 | 27 | return config 28 | }) 29 | ioc.alias('Adonis/Src/Config', 'Config') 30 | 31 | await registrar 32 | .providers([ 33 | '@adonisjs/lucid/providers/LucidProvider', 34 | path.join(__dirname, '../../providers/FeudProvider') 35 | ]) 36 | .registerAndBoot() 37 | 38 | setupResolver() 39 | 40 | await fixtures.setupTables(ioc.use('Database')) 41 | }) 42 | 43 | afterAll(async () => { 44 | await fixtures.dropTables(ioc.use('Database')) 45 | }) 46 | 47 | afterEach(async () => { 48 | await fixtures.truncateTables(ioc.use('Database')) 49 | }) 50 | 51 | describe('belongsToMany Relation', () => { 52 | it('related new instance is scoped to tenant when saved', async () => { 53 | const Feud = ioc.use('Feud') 54 | Feud.setTenant(1) 55 | 56 | const User = require('../unit/helpers/User') 57 | User._bootIfNotBooted() 58 | 59 | const Group = require('../unit/helpers/Group') 60 | Group._bootIfNotBooted() 61 | 62 | const user = await User.create({ name: 'John Doe' }) 63 | await user.groups().create({ name: 'All users' }) 64 | 65 | const group = await user.groups().first() 66 | 67 | expect(group.toJSON()).toMatchObject({ 68 | __meta__: { 69 | pivot_group_id: 1, 70 | pivot_user_id: 1 71 | }, 72 | id: 1, 73 | name: 'All users', 74 | tenant_id: 1 75 | }) 76 | }) 77 | 78 | it('query returns only scoped results', async () => { 79 | const Feud = ioc.use('Feud') 80 | Feud.setTenant(1) 81 | 82 | const User = require('../unit/helpers/User') 83 | User._bootIfNotBooted() 84 | 85 | const user = await User.create({ name: 'John Doe' }) 86 | 87 | await ioc.use('Database').table('groups').insert([ 88 | { id: 1, tenant_id: 1, name: 'All users' }, 89 | { id: 2, tenant_id: 2, name: 'All users' } 90 | ]) 91 | 92 | await ioc.use('Database').table('group_members').insert([ 93 | { id: 1, tenant_id: 1, group_id: 1, user_id: user.id }, 94 | { id: 2, tenant_id: 2, group_id: 2, user_id: user.id } 95 | ]) 96 | 97 | const groups = await user.groups().fetch() 98 | 99 | expect(groups.toJSON()).toMatchObject([ 100 | { 101 | pivot: { 102 | group_id: 1, 103 | user_id: 1 104 | }, 105 | id: 1, 106 | name: 'All users', 107 | tenant_id: 1 108 | } 109 | ]) 110 | }) 111 | 112 | it('related instance is correctly updated', async () => { 113 | const Feud = ioc.use('Feud') 114 | Feud.setTenant(1) 115 | 116 | const User = require('../unit/helpers/User') 117 | User._bootIfNotBooted() 118 | 119 | const user = await User.create({ name: 'John Doe' }) 120 | 121 | await user.groups().create({ name: 'All users' }) 122 | await user.groups().where('name', 'All users').update({ name: 'foo' }) 123 | 124 | const comment = await user.groups().first() 125 | 126 | expect(comment.toJSON()).toMatchObject({ name: 'foo' }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /test/functional/feud-provider.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { ioc, registrar } = require('@adonisjs/fold') 5 | const { Config, Env, setupResolver } = require('@adonisjs/sink') 6 | 7 | beforeAll(async () => { 8 | ioc.singleton('Adonis/Src/Env', () => new Env()) 9 | ioc.alias('Adonis/Src/Env', 'Env') 10 | 11 | ioc.singleton('Adonis/Src/Config', function () { 12 | const config = new Config() 13 | config.set('feud', { 14 | tenantColumn: 'tenant_id' 15 | }) 16 | return config 17 | }) 18 | ioc.alias('Adonis/Src/Config', 'Config') 19 | 20 | await registrar 21 | .providers([ 22 | '@adonisjs/lucid/providers/LucidProvider', 23 | path.join(__dirname, '../../providers/FeudProvider') 24 | ]) 25 | .registerAndBoot() 26 | 27 | setupResolver() 28 | }) 29 | 30 | describe('FeudProvider', () => { 31 | it('Feud should be registered just fine', () => { 32 | const Feud = require('../../src/Feud') 33 | expect(ioc.use('Adonis/Addons/Feud')).toBeInstanceOf(Feud) 34 | expect(ioc.use('Feud')).toBeInstanceOf(Feud) 35 | }) 36 | 37 | it('TenantAware midleware should be registered just fine', () => { 38 | const TenantAware = require('../../src/Middleware/TenantAware') 39 | expect(ioc.use('Adonis/Middleware/TenantAware')).toBeInstanceOf(TenantAware) 40 | }) 41 | 42 | it('TenantAware trait should be registered just fine', () => { 43 | const TenantAware = require('../../src/TenantAware') 44 | expect(ioc.use('Adonis/Traits/TenantAware')).toBeInstanceOf(TenantAware) 45 | expect(ioc.use('TenantAware')).toBeInstanceOf(TenantAware) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/functional/hasMany.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { ioc, registrar } = require('@adonisjs/fold') 5 | const { Config, setupResolver } = require('@adonisjs/sink') 6 | 7 | const fixtures = require('../unit/helpers/fixtures') 8 | 9 | beforeAll(async () => { 10 | ioc.singleton('Adonis/Src/Config', function () { 11 | const config = new Config() 12 | 13 | config.set('database', { 14 | connection: 'sqlite', 15 | sqlite: { 16 | client: 'sqlite3', 17 | connection: { 18 | filename: './testing.sqlite' 19 | }, 20 | useNullAsDefault: true, 21 | debug: false 22 | } 23 | }) 24 | 25 | config.set('feud', { tenantColumn: 'tenant_id' }) 26 | 27 | return config 28 | }) 29 | ioc.alias('Adonis/Src/Config', 'Config') 30 | 31 | await registrar 32 | .providers([ 33 | '@adonisjs/lucid/providers/LucidProvider', 34 | path.join(__dirname, '../../providers/FeudProvider') 35 | ]) 36 | .registerAndBoot() 37 | 38 | setupResolver() 39 | 40 | await fixtures.setupTables(ioc.use('Database')) 41 | }) 42 | 43 | afterAll(async () => { 44 | await fixtures.dropTables(ioc.use('Database')) 45 | }) 46 | 47 | afterEach(async () => { 48 | await fixtures.truncateTables(ioc.use('Database')) 49 | }) 50 | 51 | describe('hasMany Relation', () => { 52 | it('related new instance is scoped to tenant when saved', async () => { 53 | const Feud = ioc.use('Feud') 54 | Feud.setTenant(1) 55 | 56 | const Post = require('../unit/helpers/Post') 57 | Post._bootIfNotBooted() 58 | 59 | const Comment = require('../unit/helpers/Comment') 60 | Comment._bootIfNotBooted() 61 | 62 | const post = await Post.create({ title: 'First post' }) 63 | 64 | const comment = new Comment() 65 | comment.body = 'Foo' 66 | await post.comments().save(comment) 67 | 68 | expect(comment.getTenant()).toEqual(1) 69 | expect(await post.comments().first()).toMatchObject(comment) 70 | }) 71 | 72 | it('query returns only scoped results', async () => { 73 | const Feud = ioc.use('Feud') 74 | Feud.setTenant(1) 75 | 76 | const Post = require('../unit/helpers/Post') 77 | Post._bootIfNotBooted() 78 | 79 | const post = await Post.create({ title: 'Another post' }) 80 | 81 | await ioc.use('Database').table('comments').insert([ 82 | { id: 1, tenant_id: 2, post_id: post.id, body: 'foo' }, 83 | { id: 2, tenant_id: 1, post_id: post.id, body: 'bar' } 84 | ]) 85 | 86 | const comment = await post.comments().first() 87 | 88 | expect(comment.toJSON()).toMatchObject( 89 | { id: 2, tenant_id: 1, post_id: post.id, body: 'bar' } 90 | ) 91 | }) 92 | 93 | it('related instance is correctly updated', async () => { 94 | const Feud = ioc.use('Feud') 95 | Feud.setTenant(1) 96 | 97 | const Post = require('../unit/helpers/Post') 98 | Post._bootIfNotBooted() 99 | 100 | const post = await Post.create({ title: 'Third post' }) 101 | 102 | await post.comments().create({ body: 'foo' }) 103 | await post.comments().where('body', 'foo').update({ body: 'bar' }) 104 | 105 | const comment = await post.comments().first() 106 | 107 | expect(comment.toJSON()).toMatchObject({ body: 'bar' }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/functional/hasOne.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { ioc, registrar } = require('@adonisjs/fold') 5 | const { Config, setupResolver } = require('@adonisjs/sink') 6 | 7 | const fixtures = require('../unit/helpers/fixtures') 8 | 9 | beforeAll(async () => { 10 | ioc.singleton('Adonis/Src/Config', function () { 11 | const config = new Config() 12 | 13 | config.set('database', { 14 | connection: 'sqlite', 15 | sqlite: { 16 | client: 'sqlite3', 17 | connection: { 18 | filename: './testing.sqlite' 19 | }, 20 | useNullAsDefault: true, 21 | debug: false 22 | } 23 | }) 24 | 25 | config.set('feud', { tenantColumn: 'tenant_id' }) 26 | 27 | return config 28 | }) 29 | ioc.alias('Adonis/Src/Config', 'Config') 30 | 31 | await registrar 32 | .providers([ 33 | '@adonisjs/lucid/providers/LucidProvider', 34 | path.join(__dirname, '../../providers/FeudProvider') 35 | ]) 36 | .registerAndBoot() 37 | 38 | setupResolver() 39 | 40 | await fixtures.setupTables(ioc.use('Database')) 41 | }) 42 | 43 | afterAll(async () => { 44 | await fixtures.dropTables(ioc.use('Database')) 45 | }) 46 | 47 | afterEach(async () => { 48 | await fixtures.truncateTables(ioc.use('Database')) 49 | }) 50 | 51 | describe('hasOne Relation', () => { 52 | it('related new instance is scoped to tenant when saved', async () => { 53 | const Feud = ioc.use('Feud') 54 | Feud.setTenant(1) 55 | 56 | const User = require('../unit/helpers/User') 57 | User._bootIfNotBooted() 58 | 59 | const Profile = require('../unit/helpers/Profile') 60 | Profile._bootIfNotBooted() 61 | 62 | const user = await User.create({ name: 'John Doe' }) 63 | 64 | const profile = new Profile() 65 | profile.bio = 'About me' 66 | await user.profile().save(profile) 67 | 68 | expect(profile.getTenant()).toEqual(1) 69 | expect(await user.profile().first()).toMatchObject(profile) 70 | }) 71 | 72 | it('query returns only scoped results', async () => { 73 | const Feud = ioc.use('Feud') 74 | Feud.setTenant(1) 75 | 76 | const User = require('../unit/helpers/User') 77 | User._bootIfNotBooted() 78 | 79 | const user = await User.create({ name: 'John Doe' }) 80 | 81 | await ioc.use('Database').table('profiles').insert([ 82 | { id: 1, tenant_id: 2, user_id: user.id, bio: 'foo' }, 83 | { id: 2, tenant_id: 1, user_id: user.id, bio: 'bar' } 84 | ]) 85 | 86 | const profile = await user.profile().first() 87 | 88 | expect(profile.toJSON()).toMatchObject( 89 | { id: 2, tenant_id: 1, user_id: user.id, bio: 'bar' } 90 | ) 91 | }) 92 | 93 | it('related instance is correctly updated', async () => { 94 | const Feud = ioc.use('Feud') 95 | Feud.setTenant(1) 96 | 97 | const User = require('../unit/helpers/User') 98 | User._bootIfNotBooted() 99 | 100 | const user = await User.create({ name: 'John Doe' }) 101 | 102 | await user.profile().create({ bio: 'foo' }) 103 | await user.profile().where('bio', 'foo').update({ bio: 'bar' }) 104 | 105 | const profile = await user.profile().fetch() 106 | 107 | expect(profile.toJSON()).toMatchObject({ bio: 'bar' }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/unit/helpers/Comment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | const Model = ioc.use('Model') 5 | 6 | class Comment extends Model { 7 | static get traits () { 8 | return ['@provider:TenantAware'] 9 | } 10 | } 11 | 12 | module.exports = Comment 13 | -------------------------------------------------------------------------------- /test/unit/helpers/Group.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | const Model = ioc.use('Model') 5 | 6 | class Group extends Model { 7 | static get traits () { 8 | return ['@provider:TenantAware'] 9 | } 10 | } 11 | 12 | module.exports = Group 13 | -------------------------------------------------------------------------------- /test/unit/helpers/Post.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | const Model = ioc.use('Model') 5 | const User = require('./User') 6 | const Comment = require('./Comment') 7 | 8 | class Post extends Model { 9 | static get traits () { 10 | return ['@provider:TenantAware'] 11 | } 12 | 13 | author () { 14 | return this.belongsTo(User) 15 | } 16 | 17 | comments () { 18 | return this.hasMany(Comment) 19 | } 20 | } 21 | 22 | module.exports = Post 23 | -------------------------------------------------------------------------------- /test/unit/helpers/Profile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | const Model = ioc.use('Model') 5 | 6 | class Profile extends Model { 7 | static get traits () { 8 | return ['@provider:TenantAware'] 9 | } 10 | } 11 | 12 | module.exports = Profile 13 | -------------------------------------------------------------------------------- /test/unit/helpers/User.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { ioc } = require('@adonisjs/fold') 4 | const Model = ioc.use('Model') 5 | const Profile = require('./Profile') 6 | const Group = require('./Group') 7 | 8 | class User extends Model { 9 | static get traits () { 10 | return ['@provider:TenantAware'] 11 | } 12 | 13 | profile () { 14 | return this.hasOne(Profile) 15 | } 16 | 17 | groups () { 18 | return this.belongsToMany(Group).pivotTable('group_members') 19 | } 20 | } 21 | 22 | module.exports = User 23 | -------------------------------------------------------------------------------- /test/unit/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | setupTables (db) { 5 | const tables = [ 6 | db.schema.createTable('users', function (table) { 7 | table.increments() 8 | table.integer('tenant_id').notNullable() 9 | table.string('name') 10 | table.timestamps() 11 | }), 12 | db.schema.createTable('profiles', function (table) { 13 | table.increments() 14 | table.integer('tenant_id').notNullable() 15 | table.integer('user_id').references('id').inTable('users') 16 | table.string('bio') 17 | table.timestamps() 18 | }), 19 | db.schema.createTable('groups', function (table) { 20 | table.increments() 21 | table.integer('tenant_id').notNullable() 22 | table.string('name') 23 | table.timestamps() 24 | }), 25 | db.schema.createTable('group_members', function (table) { 26 | table.increments() 27 | table.integer('tenant_id').notNullable() 28 | table.integer('group_id').references('id').inTable('groups') 29 | table.integer('user_id').references('id').inTable('users') 30 | table.timestamps() 31 | }), 32 | db.schema.createTable('posts', function (table) { 33 | table.increments() 34 | table.integer('tenant_id').notNullable() 35 | table.integer('user_id').references('id').inTable('users') 36 | table.string('title') 37 | table.timestamps() 38 | }), 39 | db.schema.createTable('comments', function (table) { 40 | table.increments() 41 | table.integer('tenant_id').notNullable() 42 | table.integer('post_id').references('id').inTable('posts') 43 | table.string('body') 44 | table.timestamps() 45 | }) 46 | ] 47 | return Promise.all(tables) 48 | }, 49 | truncateTables (db) { 50 | const tables = [ 51 | db.truncate('users'), 52 | db.truncate('profiles'), 53 | db.truncate('groups'), 54 | db.truncate('group_members'), 55 | db.truncate('posts'), 56 | db.truncate('comments') 57 | ] 58 | return Promise.all(tables) 59 | }, 60 | dropTables (db) { 61 | const tables = [ 62 | db.schema.dropTable('users'), 63 | db.schema.dropTable('profiles'), 64 | db.schema.dropTable('groups'), 65 | db.schema.dropTable('group_members'), 66 | db.schema.dropTable('posts'), 67 | db.schema.dropTable('comments') 68 | ] 69 | return Promise.all(tables) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/unit/tenantAware.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { ioc, registrar } = require('@adonisjs/fold') 5 | const { Config, setupResolver } = require('@adonisjs/sink') 6 | 7 | const fixtures = require('./helpers/fixtures') 8 | 9 | beforeAll(async () => { 10 | ioc.singleton('Adonis/Src/Config', function () { 11 | const config = new Config() 12 | 13 | config.set('database', { 14 | connection: 'sqlite', 15 | sqlite: { 16 | client: 'sqlite3', 17 | connection: { 18 | filename: './testing.sqlite' 19 | }, 20 | useNullAsDefault: true, 21 | debug: false 22 | } 23 | }) 24 | 25 | config.set('feud', { tenantColumn: 'tenant_id' }) 26 | 27 | return config 28 | }) 29 | ioc.alias('Adonis/Src/Config', 'Config') 30 | 31 | await registrar 32 | .providers([ 33 | '@adonisjs/lucid/providers/LucidProvider', 34 | path.join(__dirname, '../../providers/FeudProvider') 35 | ]) 36 | .registerAndBoot() 37 | 38 | setupResolver() 39 | 40 | await fixtures.setupTables(ioc.use('Database')) 41 | }) 42 | 43 | afterAll(async () => { 44 | await fixtures.dropTables(ioc.use('Database')) 45 | }) 46 | 47 | afterEach(async () => { 48 | await fixtures.truncateTables(ioc.use('Database')) 49 | }) 50 | 51 | describe('TenantAware', () => { 52 | it('new instance is scoped to tenant', async () => { 53 | // @todo move to functional tests 54 | const Feud = ioc.use('Feud') 55 | Feud.setTenant(1) 56 | 57 | const User = require('./helpers/User') 58 | User._bootIfNotBooted() 59 | const user = await User.create({ name: 'John Doe' }) 60 | 61 | expect(user.isDirty).toBe(false) 62 | expect(user.tenant_id).toEqual(1) 63 | }) 64 | 65 | it('hooks are registered', () => { 66 | const User = require('./helpers/User') 67 | User._bootIfNotBooted() 68 | expect(User.$hooks.before._handlers.save.length).toBe(1) 69 | }) 70 | 71 | it('getTenant returns model tenant value', async () => { 72 | const Feud = ioc.use('Feud') 73 | Feud.setTenant(2) 74 | 75 | const User = require('./helpers/User') 76 | User._bootIfNotBooted() 77 | 78 | const user = new User() 79 | user.newUp({ id: 1, tenant_id: 2, name: 'John Doe' }) 80 | 81 | expect(user.getTenant()).toEqual(2) 82 | }) 83 | 84 | it('model query is scoped to tenant', async () => { 85 | const Feud = ioc.use('Feud') 86 | Feud.setTenant(1) 87 | 88 | const User = require('./helpers/User') 89 | User._bootIfNotBooted() 90 | 91 | const query = User.query().toString() 92 | 93 | expect(query).toContain('where `users`.`tenant_id` = 1') 94 | }) 95 | }) 96 | --------------------------------------------------------------------------------