From 2096de6e17bbeb33ec89c714e952132be2907154 Mon Sep 17 00:00:00 2001 From: Harminder virk Date: Wed, 11 Dec 2019 21:20:03 +0530 Subject: [PATCH] feat: add method --- adonis-typings/model.ts | 16 +++- src/Orm/BaseModel/index.ts | 55 +++++++++--- test/orm/base-model.spec.ts | 168 +++++++++++++++++++++++++++++++++++- 3 files changed, 225 insertions(+), 14 deletions(-) diff --git a/adonis-typings/model.ts b/adonis-typings/model.ts index b48de2bc..6b8dbfb3 100644 --- a/adonis-typings/model.ts +++ b/adonis-typings/model.ts @@ -490,9 +490,19 @@ declare module '@ioc:Adonis/Lucid/Model' { /** * Read/write realtionships */ - $hasRelated (key: string): boolean - $setRelated (key: string, result: ModelContract | ModelContract[]): void - $getRelated (key: string, defaultValue?: any): ModelContract + $hasRelated (key: K): boolean + $setRelated ( + key: string, + result: ModelContract | ModelContract[] + ): void + $pushRelated ( + key: K, + result: ModelContract | ModelContract[], + ): void + $getRelated ( + key: K, + defaultValue?: any, + ): ModelContract /** * Consume the adapter result and hydrate the model diff --git a/src/Orm/BaseModel/index.ts b/src/Orm/BaseModel/index.ts index defaaa8e..47b28a28 100644 --- a/src/Orm/BaseModel/index.ts +++ b/src/Orm/BaseModel/index.ts @@ -43,6 +43,8 @@ import { ManyToMany } from '../Relations/ManyToMany' import { ensureRelation, isObject } from '../../utils' import { HasManyThrough } from '../Relations/HasManyThrough' +const MANY_RELATIONS = ['hasMany', 'manyToMany', 'hasManyThrough'] + function StaticImplements () { return (_t: T) => {} } @@ -797,14 +799,14 @@ export class BaseModel implements ModelContract { /** * Returns the related model or default value when model is missing */ - public $getRelated (key: string): any { + public $getRelated (key: any): any { return this.$preloaded[key] } /** * A boolean to know if relationship has been preloaded or not */ - public $hasRelated (key: string): boolean { + public $hasRelated (key: any): boolean { return this.$preloaded[key] !== undefined } @@ -812,7 +814,7 @@ export class BaseModel implements ModelContract { * Sets the related data on the model instance. The method internally handles * `one to one` or `many` relations */ - public $setRelated (key: string, models: ModelContract | ModelContract[]) { + public $setRelated (key: any, models: ModelContract | ModelContract[]) { const Model = this.constructor as typeof BaseModel const relation = Model.$relations.get(key as string) @@ -824,19 +826,52 @@ export class BaseModel implements ModelContract { } /** - * Create multiple for `hasMany` and one for `belongsTo` and `hasOne` + * Reset array before invoking $pushRelated */ - const manyRelationships = ['hasMany', 'manyToMany', 'hasManyThrough'] - if (manyRelationships.includes(relation.type)) { + if (MANY_RELATIONS.includes(relation.type)) { if (!Array.isArray(models)) { throw new Exception( - `${Model}.${key} must be an array (${manyRelationships.join(',')} relationships)`, + `${Model.name}.${key} must be an array when setting ${relation.type} relationship`, ) } - this.$preloaded[key] = models - } else { - this.$preloaded[key] = models as unknown as ModelContract + this.$preloaded[key] = [] } + + return this.$pushRelated(key, models) + } + + /** + * Push related adds to the existing related collection + */ + public $pushRelated (key: any, models: ModelContract | ModelContract[]) { + const Model = this.constructor as typeof BaseModel + const relation = Model.$relations.get(key as string) + + /** + * Ignore when relation is not defined + */ + if (!relation) { + return + } + + /** + * Create multiple for `hasMany` `manyToMany` and `hasManyThrough` + */ + if (MANY_RELATIONS.includes(relation.type)) { + this.$preloaded[key] = ((this.$preloaded[key] || []) as ModelContract[]).concat(models) + return + } + + /** + * Dis-allow setting multiple model instances for a one to one relationship + */ + if (Array.isArray(models)) { + throw new Error( + `${Model.name}.${key} cannot reference more than one instance of ${relation.relatedModel().name} model` + ) + } + + this.$preloaded[key] = models } /** diff --git a/test/orm/base-model.spec.ts b/test/orm/base-model.spec.ts index 226a0db1..cdcff88c 100644 --- a/test/orm/base-model.spec.ts +++ b/test/orm/base-model.spec.ts @@ -10,7 +10,7 @@ /// import test from 'japa' -import { column, computed, hasOne } from '../../src/Orm/Decorators' +import { column, computed, hasMany, hasOne } from '../../src/Orm/Decorators' import { getDb, cleanup, @@ -1310,6 +1310,172 @@ test.group('Base Model | relations', (group) => { }, }) }) + + test('push relationship', async (assert) => { + const adapter = new FakeAdapter() + class Profile extends BaseModel { + @column() + public username: string + + @column() + public userId: number + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @hasMany(() => Profile) + public profiles: Profile[] + } + + const user = new User() + Profile.$adapter = adapter + user.$consumeAdapterResult({ id: 1 }) + user.$pushRelated('profiles', await Profile.create({ username: 'nikk' })) + + assert.deepEqual(user.toJSON(), { + id: 1, + profiles: [ + { + username: 'nikk', + }, + ], + }) + }) + + test('push relationship to existing list', async (assert) => { + const adapter = new FakeAdapter() + class Profile extends BaseModel { + @column() + public username: string + + @column() + public userId: number + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @hasMany(() => Profile) + public profiles: Profile[] + } + + const user = new User() + Profile.$adapter = adapter + user.$consumeAdapterResult({ id: 1 }) + user.$setRelated('profiles', [await Profile.create({ username: 'virk' })]) + user.$pushRelated('profiles', await Profile.create({ username: 'nikk' })) + + assert.deepEqual(user.toJSON(), { + id: 1, + profiles: [ + { + username: 'virk', + }, + { + username: 'nikk', + }, + ], + }) + }) + + test('push an array of relationships', async (assert) => { + const adapter = new FakeAdapter() + class Profile extends BaseModel { + @column() + public username: string + + @column() + public userId: number + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @hasMany(() => Profile) + public profiles: Profile[] + } + + const user = new User() + Profile.$adapter = adapter + user.$consumeAdapterResult({ id: 1 }) + user.$pushRelated('profiles', [ + await Profile.create({ username: 'virk' }), + await Profile.create({ username: 'nikk' }), + ]) + + assert.deepEqual(user.toJSON(), { + id: 1, + profiles: [ + { + username: 'virk', + }, + { + username: 'nikk', + }, + ], + }) + }) + + test('raise error when pushing an array of relationships for hasOne', async (assert) => { + const adapter = new FakeAdapter() + class Profile extends BaseModel { + @column() + public username: string + + @column() + public userId: number + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @hasOne(() => Profile) + public profile: Profile + } + + const user = new User() + Profile.$adapter = adapter + user.$consumeAdapterResult({ id: 1 }) + + const profile = await Profile.create({ username: 'virk' }) + const profile1 = await Profile.create({ username: 'virk' }) + + const fn = () => user.$pushRelated('profile', [profile, profile1]) + assert.throw(fn, 'User.profile cannot reference more than one instance of Profile model') + }) + + test('raise error when setting single relationships for hasMany', async (assert) => { + const adapter = new FakeAdapter() + class Profile extends BaseModel { + @column() + public username: string + + @column() + public userId: number + } + + class User extends BaseModel { + @column({ primary: true }) + public id: number + + @hasMany(() => Profile) + public profiles: Profile[] + } + + const user = new User() + Profile.$adapter = adapter + user.$consumeAdapterResult({ id: 1 }) + + const profile = await Profile.create({ username: 'virk' }) + + const fn = () => user.$setRelated('profiles', profile) + assert.throw(fn, 'User.profiles must be an array when setting hasMany relationship') + }) }) test.group('Base Model | fetch', (group) => {