diff --git a/package-lock.json b/package-lock.json index 62839f7..4c35245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/database", - "version": "5.42.0", + "version": "5.43.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/database", - "version": "5.42.0", + "version": "5.43.0", "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1" diff --git a/package.json b/package.json index 9fb1840..ae7e367 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/database", - "version": "5.42.0", + "version": "5.43.0", "description": "The Athenna database handler for SQL/NoSQL.", "license": "MIT", "author": "João Lenon ", diff --git a/src/models/BaseModel.ts b/src/models/BaseModel.ts index ddbc6b0..cfeea17 100644 --- a/src/models/BaseModel.ts +++ b/src/models/BaseModel.ts @@ -600,6 +600,54 @@ export class BaseModel { return this[relation as any] } + public loadOnly(relation: string): Promise + public loadOnly>( + relation: K, + closure?: ( + query: ModelQueryBuilder< + Extract + > + ) => any + ): Promise + + /** + * Eager load a model relation without mutating + * the current model instance. + */ + public async loadOnly>( + relation: K | string, + closure?: ( + query: ModelQueryBuilder< + Extract + > + ) => any + ) { + const Model = this.constructor as any + const schema = Model.schema() + const generator = new ModelGenerator(Model, schema) + const copy = new Model() + const relations = schema.getRelationProperties() + + Object.keys(this).forEach(key => { + if (relations.includes(key)) { + return + } + + copy[key] = Json.copy(this[key]) + }) + + await generator.includeRelation( + copy, + schema.includeRelation(relation, closure) + ) + + if (relation.includes('.')) { + return Json.get(copy, relation) + } + + return copy[relation as any] + } + /** * Validate if model is persisted in database * or if it's a fresh instance. diff --git a/tests/unit/models/BaseModelTest.ts b/tests/unit/models/BaseModelTest.ts index 3126c1a..8fe173d 100644 --- a/tests/unit/models/BaseModelTest.ts +++ b/tests/unit/models/BaseModelTest.ts @@ -17,6 +17,7 @@ import { Product } from '#tests/fixtures/models/Product' import { ModelSchema } from '#src/models/schemas/ModelSchema' import { ModelFactory } from '#src/models/factories/ModelFactory' import { DatabaseProvider } from '#src/providers/DatabaseProvider' +import { ModelGenerator } from '#src/models/factories/ModelGenerator' import { ModelQueryBuilder } from '#src/models/builders/ModelQueryBuilder' import { NotFoundDataException } from '#src/exceptions/NotFoundDataException' import { Test, type Context, BeforeEach, AfterEach, Mock } from '@athenna/test' @@ -500,6 +501,54 @@ export default class BaseModelTest { assert.deepEqual(json, { id: '1', name: 'lenon', metadata3: 'metadata3' }) } + @Test() + public async shouldBeAbleToLoadOnlyOneObjectRelationFromInstanceWithoutMutatingIt({ assert }: Context) { + Mock.stub(ModelGenerator.prototype, 'includeRelation').callsFake((async (model: any) => { + const userModel = model as User + + userModel.profile = new Profile() + userModel.profile.id = '1' + + return userModel + }) as any) + + const user = new User() + + user.id = '1' + user.name = 'lenon' + + const profile = await user.loadOnly('profile') + + assert.instanceOf(profile, Profile) + assert.isUndefined(user.profile) + assert.deepEqual(user.id, '1') + assert.deepEqual(user.name, 'lenon') + } + + @Test() + public async shouldBeAbleToLoadOnlyOneArrayRelationFromInstanceWithoutMutatingIt({ assert }: Context) { + Mock.stub(ModelGenerator.prototype, 'includeRelation').callsFake((async (model: any) => { + const userModel = model as User + + userModel.products = [new Product()] + userModel.products[0].id = '1' + + return userModel + }) as any) + + const user = new User() + + user.id = '1' + user.name = 'lenon' + + const products = await user.loadOnly('products') + + assert.instanceOf(products[0], Product) + assert.isUndefined(user.products) + assert.deepEqual(user.id, '1') + assert.deepEqual(user.name, 'lenon') + } + @Test() public async shouldBeAbleToValidateThatAModelHasNotBeenPersistedInDatabase({ assert }: Context) { const user = new User() diff --git a/tests/unit/models/relations/BelongsTo/BelongsToRelationTest.ts b/tests/unit/models/relations/BelongsTo/BelongsToRelationTest.ts index 67a417d..c05e6b2 100644 --- a/tests/unit/models/relations/BelongsTo/BelongsToRelationTest.ts +++ b/tests/unit/models/relations/BelongsTo/BelongsToRelationTest.ts @@ -307,4 +307,13 @@ export default class BelongsToRelationTest { assert.instanceOf(product.user, User) assert.instanceOf(profile, Profile) } + + @Test() + public async shouldBeAbleToLoadNestedBelongsToRelationUsingLoadOnlyMethod({ assert }: Context) { + const product = await Product.find() + const profile = await product.loadOnly('user.profile') + + assert.isUndefined(product.user) + assert.instanceOf(profile, Profile) + } } diff --git a/tests/unit/models/relations/HasOne/HasOneRelationTest.ts b/tests/unit/models/relations/HasOne/HasOneRelationTest.ts index 4be05d3..7351a2d 100644 --- a/tests/unit/models/relations/HasOne/HasOneRelationTest.ts +++ b/tests/unit/models/relations/HasOne/HasOneRelationTest.ts @@ -133,4 +133,14 @@ export default class HasOneRelationTest { profile: { id: 1, userId: 1 } }) } + + @Test() + public async shouldBeAbleToLoadOneRelationFromInstanceWithoutMutatingTheModel({ assert }: Context) { + const user = await User.query().find() + const profile = await user.loadOnly('profile') + + assert.instanceOf(user, User) + assert.instanceOf(profile, Profile) + assert.isUndefined(user.profile) + } }