From d17a5a01c7824831ea327714894d05c2b93c94d8 Mon Sep 17 00:00:00 2001 From: Jam Risser Date: Thu, 7 Feb 2019 19:19:15 -0600 Subject: [PATCH 1/3] feat(repository): add hasManyThrough support --- docs/site/HasManyThrough-relation.md | 205 ++++++++++++++ docs/site/Relations.md | 1 + .../imgs/hasManyThrough-relation-example.png | Bin 0 -> 49921 bytes packages/repository-tests/package.json | 1 + .../has-many-through.relation.acceptance.ts | 250 ++++++++++++++++++ .../fixtures/models/customer.model.ts | 4 + .../crud/relations/fixtures/models/index.ts | 3 + .../relations/fixtures/models/order.model.ts | 8 +- .../relations/fixtures/repositories/index.ts | 3 + .../fixtures/models/customer.model.ts | 47 ++++ .../__tests__/fixtures/models/order.model.ts | 46 ++++ .../__tests__/fixtures/models/seller.model.ts | 26 ++ .../repositories/customer.repository.ts | 80 ++++++ .../repositories/seller.repository.ts | 41 +++ .../relation.factory.integration.ts | 89 ++++++- ...as-many-through-repository-factory.unit.ts | 185 +++++++++++++ .../has-many-through-repository.factory.ts | 87 ++++++ .../has-many/has-many-through.helpers.ts | 164 ++++++++++++ .../has-many/has-many-through.repository.ts | 182 +++++++++++++ .../relations/has-many/has-many.decorator.ts | 10 +- .../src/relations/has-many/index.ts | 2 + .../src/relations/relation.types.ts | 37 +++ .../src/repositories/legacy-juggler-bridge.ts | 59 +++++ 23 files changed, 1524 insertions(+), 6 deletions(-) create mode 100644 docs/site/HasManyThrough-relation.md create mode 100644 docs/site/imgs/hasManyThrough-relation-example.png create mode 100644 packages/repository-tests/src/__tests__/acceptance/has-many-through.relation.acceptance.ts create mode 100644 packages/repository/src/__tests__/fixtures/models/customer.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/models/order.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/models/seller.model.ts create mode 100644 packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts create mode 100644 packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts create mode 100644 packages/repository/src/__tests__/unit/repositories/has-many-through-repository-factory.unit.ts create mode 100644 packages/repository/src/relations/has-many/has-many-through-repository.factory.ts create mode 100644 packages/repository/src/relations/has-many/has-many-through.helpers.ts create mode 100644 packages/repository/src/relations/has-many/has-many-through.repository.ts diff --git a/docs/site/HasManyThrough-relation.md b/docs/site/HasManyThrough-relation.md new file mode 100644 index 000000000000..6aa48a62a69d --- /dev/null +++ b/docs/site/HasManyThrough-relation.md @@ -0,0 +1,205 @@ +--- +lang: en +title: 'hasManyThrough Relation' +keywords: LoopBack 4.0, LoopBack 4 +sidebar: lb4_sidebar +permalink: /doc/en/lb4/HasMany-relation.html +--- + +{% include important.html content="The underlying implementation may change in the near future. +If some of the changes break backward-compatibility a semver-major may not +be released. +" %} + +## Overview + +A `hasManyThrough` relation sets up a many-to-many connection with another model. This relation indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model. For example, in an application for a medical practice where patients make appointments to see physicians, the relevant relation declarations might be: + +![hasManyThrough relation illustration](./imgs/hasManyThrough-relation-example.png) + +The `through` model, Appointment, has two foreign key properties, physicianId and patientId, that reference the primary keys in the declaring model, Physician, and the target model, Patient. + +## Defining a hasManyThrough Relation + +A `hasManyThrough` relation is defined in a model using the `@hasMany` decorator. + +The following example shows how to define a `hasManyThrough` between a `Customer` and `Seller` +model through an `Order` model. + +_models/customer.model.ts_ +```ts +import {Entity, property, hasMany} from '@loopback/repository'; +import {Order} from './order.model'; +import {Seller} from './seller.model'; + +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @hasMany(() => Seller, {through: () => Order}) + sellers?: Seller[]; + + constructor(data: Partial) { + super(data); + } +} +``` + +_models/seller.model.ts_ +```ts +import {Entity, property, hasMany} from '@loopback/repository'; +import {Order} from './order.model'; +import {Customer} from './customer.model'; + +export class Seller extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @hasMany(() => Customer, {through: () => Order}) + customers?: Customer[]; + + constructor(data: Partial) { + super(data); + } +} +``` + +_models/order.model.ts_ +```ts +import {Entity, property, belongsTo} from '@loopback/repository'; +import {Customer} from './customer.model'; +import {Seller} from './seller.model'; + +export class Order extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @belongsTo(() => Customer) + customerId?: number; + + @belongsTo(() => Seller) + sellerId?: number; + + constructor(data: Partial) { + super(data); + } +} +``` + +The definition of the `hasManyThrough` relation is inferred by using the `@hasMany` +decorator with a `through` property. The decorator takes in a function resolving +the target model class constructor. The `through` property takes in a function +resolving the through model class constructor. + +The decorated property name is used as the relation name and stored as part of +the source model definition's relation metadata. The property type metadata is +also preserved as an array of type `Seller` as part of the decoration. + +## Configuring a hasManyThrough relation + +The configuration and resolution of a `hasManyThrough` relation takes place at the +repository level. Once the `hasManyThrough` relation is defined on the source model, +then there are a couple of steps involved to configure it and use it. On the source +repository, the following are required: + +- In the constructor of your source repository class, use + [Dependency Injection](Dependency-injection.md) to receive a getter function + for obtaining an instance of the target repository and an instance of the + through repository. _Note: We need a getter function, accepting a string + repository name instead of a repository constructor, or a repository instance, + in order to break a cyclic dependency between two repositories referencing + eachother with a hasManyThrough relation._ + +- Declare a property with the factory function type + `HasManyThroughRepositoryFactory` + on the source repository class. +- call the `createHasManyThroughRepositoryFactoryFor` function in the constructor of + the source repository class with the relation name (decorated relation + property on the source model), target repository instance and through repository instance + and assign it the property mentioned above. + +_repositories/customer.repository.ts_ +```ts +import {Order, Customer, Seller} from '../models'; +import {OrderRepository, SellerRepository} from './order.repository'; +import { + DefaultCrudRepository, + juggler, + HasManyThroughRepositoryFactory, + repository, +} from '@loopback/repository'; +import {inject, Getter} from '@loopback/core'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id +> { + public readonly sellers: HasManyThroughRepositoryFactory< + Seller, + Order, + typeof Customer.prototype.id + >; + constructor( + @inject('datasources.db') protected db: juggler.DataSource, + @repository.getter('SellerRepository') + getSellerRepository: Getter, + @repository.getter('OrderRepository') + getOrderRepository: Getter, + ) { + super(Customer, db); + this.sellers = this.createHasManyThroughRepositoryFactoryFor( + 'sellers', + getSellerRepository, + getOrderRepository, + ); + } +} +``` + +The following CRUD APIs are now available in the constrained target repository +factory `orders` for instances of `customerRepository`: + +- `create` for creating a target model instance belonging to customer model + instance + ([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyThroughRepository.prototype.create)) +- `find` finding target model instance(s) belonging to customer model instance + ([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyThroughRepository.prototype.find)) +- `delete` for deleting target model instance(s) belonging to customer model + instance + ([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyThroughRepository.prototype.delete)) +- `patch` for patching target model instance(s) belonging to customer model + instance + ([API Docs](https://apidocs.strongloop.com/@loopback%2fdocs/repository.html#HasManyThroughRepository.prototype.patch)) + +## Using hasMany constrained repository in a controller + +```ts +import {post, param, requestBody} from '@loopback/rest'; +import {CustomerRepository} from '../repositories/'; +import {Customer, Seller} from '../models/'; +import {repository} from '@loopback/repository'; + +export class CustomerOrdersController { + constructor( + @repository(CustomerRepository) + protected customerRepository: CustomerRepository, + ) {} + + @post('/customers/{id}/order') + async createOrder( + @param.path.number('id') customerId: typeof Customer.prototype.id, + @requestBody() sellerData: Seller, + ): Promise { + return await this.customerRepository.sellers(customerId).create(sellerData); + } +} +``` diff --git a/docs/site/Relations.md b/docs/site/Relations.md index 9aacd6025451..cb7d625c651f 100644 --- a/docs/site/Relations.md +++ b/docs/site/Relations.md @@ -36,6 +36,7 @@ navigational property on the source repository. Here are the currently supported relations: - [HasMany](HasMany-relation.md) +- [HasManyThrough](HasManyThrough-relation.md) - [BelongsTo](BelongsTo-relation.md) - [HasOne](hasOne-relation.md) diff --git a/docs/site/imgs/hasManyThrough-relation-example.png b/docs/site/imgs/hasManyThrough-relation-example.png new file mode 100644 index 0000000000000000000000000000000000000000..ac3ec67c2b4e490aabef99a6d6d205bcc525764a GIT binary patch literal 49921 zcmbrkbyOVDvpq;O-tQKyY_=m%-iL-GaNjyACkG0Gs#Q@7uR; z|J_sPboV)ZyZXxKeyVO&g(=EQV0<9{00RSqAtfpL69xuO?%#7g3evx8;_J6hFfbp4 zEJZ{Vr9?z16@S~AS^hGGfsw?>N_J6EULgpX>I8O9<9w#11{1rZQqJ;7;v*=9N%<(#-*9UFLr`-BaPsR7%wV`hdR0TG3phj#L% zrIkny)jwP^`rF#=0GFs-KS;#(jxY*NqGJpG z&PI@nnpg38Z=m|uL-8R)``^?Qn*Cg2%}JsTKz3xi;$}MaPJSdpu1+fTwt2oMvT3J^ zR65MTXo02zB45D%k6}^%1(hB(4$!ieXH$~#bBv5ZPF~_E-FbcE;JVp zEuOkpw#dDeU-?GdUsP{s&Zn}4BC|EOew|mabP~WkeAUVztM5uug=w%s5u>-WX;TQ4159k+l_Vyb3LYwUB0OC6cGyeeUZXv zHx)Z5DlU={9Un)>OqnuyeZ+p>>*2DT_}kBz3F;xd2|89)I!;6l46|qt;`P&rWj6T` z=(Din^NEudnaDfK8qdO6AI2+)OR+@?dBlznM&t|pS6cyhIIlBp?C&2&x;iW>6AgK3X*rTEQN(g=k z?v-JM-So3W@xR8#&l4((B`Cm36JEq7(L!+(5mNkt2R|qh7KCOhvXWDF-X5J0Yz{G1%5_c1BK*9R5(`SE9^%q>$uMaaHV4Dv6uxUtx=M34*T#( zA-RUBp9qGC_ChoxcZ}2w7mcJ@7gCK=)no||InA)y{hs^_jTUN)G-xXX{-APx%!pzd zP&0@z(5^|TgPnv>y3RPc0l={?I z@dAp8-)Qg@q)EkuSLC^*TLs+&-9!q1VwBj1$&kK@@v#`#djL|_=2Rw(({sxLXFam z!i)lTIR=%eJL3J~n{gCM1y5>bsSBxyh35+0W?LHM8n{!?wWH7V_KT*ZvK zK7uQ^vg)FkX@++&pYCo@0RhzfW{Z05s^i( zL7>lg!Klo1#z>(_r-`Ux({Nhz+`l@IZmnzVH5_cUXIwv7v~k#2S<1Bar~AT-CtbH@ zgP>i$ed+=8;qwd4%XehH5Z91S%*SP?Ki}E|y%s*69q+VE-bC&p(|XX>&Pf$-Wob^* zO=3*uqoEUC6V5Yz`aR>Y(?a??j@g7cQs31+cV2ZZX;16yd0cKO>gdma&A`So-^y#t z3j>x6R-8n{aAI-75%Cn7UT8|SdYpQ7bNr*xmDN=-KR16Ee-uA%*PpI(FBXX5E8Od! z=Z(9i>!atT7X^eL2o_?;y*inVrB$X(44!qsJeQ)x*#waiHV8~v`fZ}%#KJzL=ngkxQPFi7!+HOkQ4I| zOA(Vv(O}f9-xrV6ili*!BcLF`_xSqfcj@EohD^xb`R<3kOJ;VRQV^dYJ z`n{SC^yDNIWLwzpVYq#z2eMUQ{Z^L)gr$V`keB$QA%>%<8(oMgG!7vpur6>$G-W_H z%vh>Q3SVjuPC=+~+qq8v)^EQwF*Y%uj=glIbiy*Q0n#uhOl=rm!=TH~N88EL56EW~ zX&bd%xKzc>6P$^Y{pP4?)}9*uCVMX#`89(&KohOrs&3O9vw7E_w_s>5$E7u9Wzb@k&RVOyi*9e*&VAQej18rYwTgN=e8MCK>QBkk}Rc~rEn z6tWpKT;HkapE}DsZRJDd9O0M z3(@Q7S@(ry^P$b_Ao@Lv1U>@k`I5QI<6Uo^@5F2Kbq2W9_qWOBSe=VEzPR2;rp z-X-wh_nP}+3?7asx2`lcW`|pkfp;%#+?w7nFoG~rqCzUJu;;qS zzB;N4{PkY-8QIt<)D76uFd}g@9J4}-*h+Lp1*oBv-@++BzacS%zkPr4C;e#_?nH6VT&Q$@W0jI(CeJf|#J1PZ4_7Qo!aFh7=D zzL83=;Nk!KjKfU&aEMGEe=AFB;H7B#Ss$sdsPsKb>iU}r<^Pdw+)2SD8~wsb3ew?f zQfG2yDP5npkfWK(`KSD_c{fic=j>POOYcKKB-LJ4Gt^q`7=-F(%g$ zOJB?{55B}Id3k%J16m@VzO8M{uJE172kYmoxBNNn+O{bBv9DV8e8-cxW`d9J{VtX_ ziFums4SR6qo<%?9nnLg62|U;WjlGhIGF96Pid!c?78jG4zUb0ES?c@a6@$nT-ai`g3 z)Y@fS5&Ye!@eK3&5#7f8%Ai_NOSBf#1~)75f6yUqqKNz7=)f`031Uf6U2yOX8&0#L zYaTUO(Qw{y!993qkD!0pOm{0UIfbODd_Tb3{9!Xik)=yChQ=9OLi4ghxR@iVD?VtU>fsZlMB;JRoj`i~*M_9C>1G-?6y=f_3yh;!LdH z|1C)X_=SGgJ4bq-ug$(~<{MN(hl{&%@-vGg6bG-qx6Ne2i7!+KoIMHa6gNXhZe`1d z866JKoHHKPGzE^#ExFK#J0DqDp8-er1GaPUFD^>Ij?kEH%N~da^PZx zHhl=z0daGFnX=tp6=5jB5Cv_KTmMs?X^z#(fWOLr&?Pw1GYaynD$V~ovRjtMK!Z# zIcM+u!?n7iNg?vncDV}=HQElEBP?$~=d30MAJIkm*4iWo+iT-wRVWD}Io+j6MP=H{ zlX2U)_giRXlzt;5Du7LkaP8DYf9$#$-NuhP$!}IQ76+yHf2fVY3yFJk&pa&IK6TkQ zk8xwHwyY*Y969u=h z&qxfuWJye)1YJKA)5pb-8vcjkGZDnAg~zSmj28_v-^xl* zx`kEjcjQ0z711Td{w#Jyo?cmznwrd3pkS8west=MSF^IT%q=REvD}G&fK$yC*r%PB zNTo3JLOC<`Y3JwVJNvTwr;|!WdrV;V#LMWX{YNx}4^x2qIu!d9OFFx7TW_e1JNVvg z)zv9`kz|u;kkblcK-^zVU3md`3<#QEogH<_YcqM&)76#x-^yF~88zz?4;g9P0|oF_ z>=(GXZEt^A{rXifIy%0^cYKia`@j^-CEcB?sbwxOYOcvzbp{g5+tSoYWuL>yW z()v)tiL3fzHCfg(MAlp zlB>mEjY#v$Qe%aAsk>X4&dExDR*@d&cTU;L%1ZE#|6oIoCF+8z>d!BB!S>p?EbB_A zyX`U;7Z-K8b)N?=$ui2PDQOjY$l!z>fO# zUD%*yZvL+5A8ET!DhRP~%6+RlqwtEj^w{~b>T;*lFjb&x${rYUe12zey%Ok)^YTad zc*zbW2pahO4-IcPQW_NCHElyLsOzFcheLw7@LP6w;m2Ft+XN1~NP^QZ^C1hu1nBoO zj$V}Ju6wJSD`IFVe-xFiLp+7!lg0#P%yOK}5}@#J=vUDW)}5Uq-DD|U6a3q_csT#< zCQM(k`|`*^QZXy*^OdWtmnDN19skQr9zpTIMstCEb-R7KwEbwQ@Kpb?nUp3uAk&^I zW3moS;rf^}Xw3La+Suz*)<5bC&W-hVq!_yikO?zSNfy$@{vZDWqQSncKMbKKs4_M+ z6Zq$S$R5317;mUVE!+ zEKd)<<&<1&%Q$0*l<=vGG~VC;CJ(Pn2}3{sZxR7XBH4GAm6he?M%}B#T_u8ud!{e~ z&ooYUP$;I7CY4~3F9CcIEB4pt%g5szV9ID#vEna8%MXG7&G_D_usYTDWFE$_Gaz!S zJRUtO47;~6(&8FRdUhp#lCH9qzuk6z!QD(DcI1I}WDuecuK&%?K9ewA_Zy=E^Q7Lz zr7rn+{Miib`rm4h^&qkI47Uv{RvcklmVI`*yx>#evg*pbi{TjbsUJycb3FwA;jil( za~QkZdhcRXpAQxDSOMP9%?FkU!#R~9SA+ifUYC@&s~s*+b2Kq-*G zg?)r?G&JGM?4A2hr9I}e;5hPc#Y<9@3>uSiQIU^3t%Xd(hsP_;zrljk49e_oWU-}EveDhQAq5qu)jaE6G_D+24z zBP5>EUnpofQP7e*#L&_Y!2(D8NHN#V%G$|I(-Ly7oGyUICZ;^#7DxhZl^FRv<1PJE zs9|VC>gPM3P0qg!AnNzo&2`5d328}nMy>qdM1Xf{(|U3y+nS}2$80Gg*k)-&SyxUK zVo@c^FXHk<{cF zdage5{%&xj0<=qCU0p5BZAxm=zMw8EADfe?gjU!(cY2mr5f;|Z)=auxIjow!=37q8 zfKI+6|JPE+?mc~l1`lrc23Xds63(Grxy(V;^4d2`ia9q+Vr;&qv$ll?qZ0|c##R;U zS`i(>$&C1#z+})cUdm__&e5Emi=0JmMEf)oHqSn({Z7mmmY%2XaDy&ZuX{h91Q!>s zd|hH$vj-s%W0q73`u`P=@T$kcnc&Rfywl@~H?J-Fb=mjE7|#9`hJq&*i+{uZj<;?Y zQ*(U#yQgVPck6*jI_Dj+)@TfsL013LjQV1*9PaM;0lmfD{R4h#RX|^xw3^jz1*@{9 z*NGdx9Fe-KTD}*D!Yp~=Ywz%HvJ0{XR|i}UAWLq#RhKsgi8MyQuerozN%2fx8SSwV zaQ#PEM~mJ@=$TvQqbl(%@}4v?y5`Gw}&?&Ido4EqlA zM9qc?C&6A~{x{zS6UHc%Y;0;0pK7unRc{OaRBI`mY2l!2=yryRY(WIp zi5oTab52s^>3wB6z+w?yoxhM+ZVv{7egZ3?Xo>RI&Q!6=4G4yWwAUT!-%Q$&UV$d;?Zx}ZJ*A^{b&htt>zG8CxM9oqY z{Zq48FBm3oFy(`;QG%L*gy{uY4KqN>tL$&|V)65Q)l5%9B&D0$CxzASpHO0#(C+5F zn7CdIjKHnTdNcLjnS_#&xr?3@^H1GEca0wQskRr>rYqh3l2qx8;h4MFOC zn52g8>b6E%VnG=^QogbecV}v46i-Fl6t$ak%r2I5D$D7TT@7nnSZV`RodPplQZ*V? z14`P;^k{ZR^#~c*x-4#u-fleiNix_75xM)r!tBr(DATWpBW&C>psQD@U-WLG4@OP z?k%AIya-4uuBgw;p!r(b*qLVQwd?dYGqj^w2OqsS!I5~RYcV$aN2|piwQ@soSU_Y; z9L)lIfu&^U2&OOiZ+Kpz4Xl@ChQRf3(t_chiIgUJXyeaC8D`;%<(b0)`x7VLA+!J~ zZ^nQeIultn1?1LY0&zi`Z2$?hwObiE6p1sR>jQj(QfHC!(|sTASqM(jeEwf|q`Xf( zykr07=oD2Yt zHDjGZ9Jhmqg2(ML!K>tF4=k*&-@PMHpUz7+9!3-_Eyr-I_62@*unDNFDq}u(O_{|D z%>%Nv`+izoB~%zSp58~EjdBGPE?H2X?4IbOYf#|c@FqSm*k+Jwn_x)u)d=6AWTQ-c zGfJg9ggF*BhChvw%0+7#`ihyFjpA>$C=5+5?;`e@cMR8gE5|)pREH>l_Gv*u@95aO#D;==Uw^%cUMn43`2S+hug`*#up>`t)$navTr=u zQqtW-C7=WlL6pfEcMYT4zl~I8_h|rxc1OWqv1^wusZ>{x7U(;Tx7$!d#7+r}SThia zq!1EA3vef+i2Ff^V}-J%NTjm$4V1NJgvX9t0VEQ%V!xXtb6f92hELylv++7{;vm?e zH!wmfH0w1!&r=4Byl z01@?UkrBdM_BbXEQx_RQtqlfJNaRctofAYeAjx+HD8Nh!uw$- z9*Zs>hd3ZHSj?ea7>B>A&$K7UXTqVco)p}^~C%@wNn!@F6kD{DYsd%-@<&Ub*qoH40d1$uE|^HbuxPtP+i zeAD%(Kv=A))xb|ob1Cz&tHGi$yx-h{5#Fm2_GDPo{7++VgXgbOfSS`Bg3_2tNtUmqCVi@xwP<`5Lyes)dx zxxdKo^fY0TZ-dJq%{;OkDhS7r#(@RwN9sMg31Iie>ZQF#({y~h=gdYBC@)OYR!S1epPkdcDb@Z8Fs>2wZH+w4rOXu?KZ{l%4Y(JB zGHx?{{Ak-qITw@mIw*5gk;*UuVoT-5xGBD+IB1%O(5kVKMflmzTVts!wZ;DIJtJ{l zznBr#dLBbox?$GsyOkC(L=w7g6P$(qDs@-O zaH#i7tER#0A6|0>CaH`efB{f=>#}7`1`RGfr{alSourWxF|$%om`RjF7K$ym0h*C_ z7Lhd|+R}foS@=7DK#gh%#hazc&!K55`7oYY!`s5$?;CJ~J{x&K5M6(ciJj#XqMCW` zRPKe7!GV(C6;AuT2yBU1e$dtqr^I$!O#UJj@dB^?_4Qc#lwE_-^dV0UV3|enSSo`G z*-ox+PgaPPs6^{NJMGL6&|s}+ODMI z`yqs!()^d;zrf$0kQ?ldJ|TE~J%iu^xTMI&llM~MO-Qk;5gp&;fC?VL1VpgAeFy`C zdj@8_<7;ilOzx(a?el+K4WU^8yUlX&A4lz3mzl|sL>~$lXsBx6pb$3JhceLtRmQgx z%+!S#(4fd!Ke_>IBjOt6JPBqfq4a1@jJ6N)Xo*lOo^N#X?sQcfdb(af9_RpFI;jT> zYP|fmDPZ{ysuZ^ddEoIig!*uUh-S>OTXaZC5ioT>Iz5O9fWtwx3fE_iA@cdUEL+jc zZf*{EE`0*Ye$W@k?0j0YZV57ZELRnM^t~1)tXif$p#FdY1bK@LVYAi=lkaBM>Djlq zEEfS_3@PE5dwHF6helKaNLLTF~s0=?h~0BTOB&+DjxfExcy(5b|6k&QQvF+&rlr3HU z3b`?8aai#GtWGB$quty-l60^L(o`@~KZ?$KZ|cvw2?BHk$i{XU!Uvpx{$djGjd)PM zlm4dt8DaJ91lT9V+i&q{a1wC2zF_gbl`W~xd*5b}`3hBep42U#EwAiAJMzcj3LvP* z%HoS6s%6fRdB4_NeVvs8n?!J@g?1lku0HtuDcE3cO}c)mw|u8G1jIcUKRBF3x-oS% zbezev@}GT*OR4HBWgSDdPwiC_k2}g4e}IW#Xll;5c?Mq zqPP3eqMWXVcLP+>E-8#i)2N-!^owpHu)5ebXVR66UV|cVd(_`qKGp^p^^dQuY)V%` zvX80hHpKJ^a)@3)ULhnMtL+uiVi$=)a~ntOz%)8_mzzj3`H#h*;{e?BDo)G0CNjnysgf$o!bsVAtS>4pCK zI^bxLg;iZ7SIwP;sq-q>=5{$@1Rf8_F3bm#%RLv(vzg-b33xz^=Vz;Oxjm@9SH1J$?m$>#M zSCCgr)E^5d?-|_!b$4JXSVwqLn{57Iuo>^mVC-=OJQU$R z@FZ2e&0yi}3z^sgZsi(mnfm*=kQLW2*Zm zh6udcpnoHQVLgLzm~|aOQo=Qbr39GDxhv)qyt>=U&A3}1Z20nMH}ZP+z6+PW@y!2f zy{d3r>ypv>yjj?-=MGc(s(&W^?%oZ7<>Lkgt(wg?YrNMkz4<>Jb;%V~i4DK+IqY_O z_v<~#L6aF;HyDh2+Dvmpb2aK}VZgsJ1Q@T7Mbp50-8xQI|4}JFah! zh_1nC#;EBe%9@%%L!z2OGgwH$uC3hI$9s!ds{Uv=WLlf

KWW#pl2z`7I~HHxGG% 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 literal 0 HcmV?d00001 diff --git a/packages/repository-tests/package.json b/packages/repository-tests/package.json index db053c2fbaba..62e38ae48fbb 100644 --- a/packages/repository-tests/package.json +++ b/packages/repository-tests/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@loopback/build": "^1.7.1", "@loopback/repository": "^1.13.0", + "@loopback/core": "^1.9.3", "@types/debug": "^4.1.5", "@types/lodash": "^4.14.138", "@types/node": "^10.14.17", diff --git a/packages/repository-tests/src/__tests__/acceptance/has-many-through.relation.acceptance.ts b/packages/repository-tests/src/__tests__/acceptance/has-many-through.relation.acceptance.ts new file mode 100644 index 000000000000..a7ec8a5b4091 --- /dev/null +++ b/packages/repository-tests/src/__tests__/acceptance/has-many-through.relation.acceptance.ts @@ -0,0 +1,250 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as _ from 'lodash'; +import {Application} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import { + ApplicationWithRepositories, + juggler, + repository, + RepositoryMixin, +} from '@loopback/repository'; +import { + Order, + Seller, +} from '@loopback/repository/src/__tests__/fixtures/models'; +import { + CustomerRepository, + OrderRepository, + SellerRepository, +} from '@loopback/repository/src/__tests__/fixtures/repositories'; + +describe('HasManyThrough relation', () => { + // Given a Customer and Seller models - see definitions at the bottom + + let app: ApplicationWithRepositories; + let controller: CustomerController; + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + let sellerRepo: SellerRepository; + let existingCustomerId: number; + + before(givenApplicationWithMemoryDB); + before(givenBoundCrudRepositoriesForCustomerAndSeller); + before(givenCustomerController); + + beforeEach(async () => { + await sellerRepo.deleteAll(); + }); + + beforeEach(async () => { + existingCustomerId = (await givenPersistedCustomerInstance()).id; + }); + + it('can create an instance of the related model', async () => { + const seller = await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + expect(seller.toObject()).to.containDeep({ + name: 'Jam Risser', + }); + + const persisted = await sellerRepo.findById(seller.id); + expect(persisted.toObject()).to.deepEqual(seller.toObject()); + }); + + it('can find instances of the related model', async () => { + const seller = await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + const notMySeller = await controller.createCustomerSellers( + existingCustomerId + 1, + { + name: 'Mark Twain', + }, + {description: 'some order'}, + ); + const foundSellers = await controller.findCustomerSellers( + existingCustomerId, + ); + expect(foundSellers).to.containEql(seller); + expect(foundSellers).to.not.containEql(notMySeller); + + const persistedOrders = await orderRepo.find({ + where: { + customerId: existingCustomerId, + }, + }); + const persisted = await sellerRepo.find({ + where: { + or: persistedOrders.map((order: Order) => ({ + id: order.sellerId, + })), + }, + }); + expect(persisted).to.deepEqual(foundSellers); + }); + + it('can patch many instances', async () => { + await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + const patchObject = {name: 'Mark Twain'}; + const arePatched = await controller.patchCustomerSellers( + existingCustomerId, + patchObject, + ); + expect(arePatched.count).to.equal(2); + const patchedData = _.map( + await controller.findCustomerSellers(existingCustomerId), + d => _.pick(d, ['name']), + ); + expect(patchedData).to.eql([ + { + name: 'Mark Twain', + }, + { + name: 'Mark Twain', + }, + ]); + }); + + it('can delete many instances', async () => { + await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + const deletedSellers = await controller.deleteCustomerSellers( + existingCustomerId, + ); + expect(deletedSellers.count).to.equal(2); + const relatedSellers = await controller.findCustomerSellers( + existingCustomerId, + ); + expect(relatedSellers).to.be.empty(); + }); + + it("does not delete instances that don't belong to the constrained instance", async () => { + await controller.createCustomerSellers( + existingCustomerId, + { + name: 'Jam Risser', + }, + {description: 'some order'}, + ); + const newSeller = { + name: 'Mark Twain', + }; + await sellerRepo.create(newSeller); + await controller.deleteCustomerSellers(existingCustomerId); + const sellers = await sellerRepo.find(); + expect(sellers).to.have.length(1); + expect(_.pick(sellers[0], ['name'])).to.eql({ + name: 'Mark Twain', + }); + }); + + it('does not create an array of the related model', async () => { + await expect( + customerRepo.create({ + name: 'a customer', + sellers: [ + { + name: 'Mark Twain', + }, + ], + }), + ).to.be.rejectedWith(/`sellers` is not defined/); + }); + + // This should be enforced by the database to avoid race conditions + it.skip('reject create request when the customer does not exist'); + + class CustomerController { + constructor( + @repository(CustomerRepository) + protected customerRepository: CustomerRepository, + ) {} + + async createCustomerSellers( + customerId: number, + sellerData: Partial, + orderData?: Partial, + ): Promise { + return this.customerRepository + .sellers(customerId) + .create(sellerData, orderData); + } + + async findCustomerSellers(customerId: number) { + return this.customerRepository.sellers(customerId).find(); + } + + async patchCustomerSellers(customerId: number, seller: Partial) { + return this.customerRepository.sellers(customerId).patch(seller); + } + + async deleteCustomerSellers(customerId: number) { + return this.customerRepository.sellers(customerId).delete(); + } + } + + function givenApplicationWithMemoryDB() { + class TestApp extends RepositoryMixin(Application) {} + + app = new TestApp(); + app.dataSource(new juggler.DataSource({name: 'db', connector: 'memory'})); + } + + async function givenBoundCrudRepositoriesForCustomerAndSeller() { + app.repository(CustomerRepository); + app.repository(OrderRepository); + app.repository(SellerRepository); + customerRepo = await app.getRepository(CustomerRepository); + orderRepo = await app.getRepository(OrderRepository); + sellerRepo = await app.getRepository(SellerRepository); + } + + async function givenCustomerController() { + app.controller(CustomerController); + controller = await app.get( + 'controllers.CustomerController', + ); + } + + async function givenPersistedCustomerInstance() { + return customerRepo.create({name: 'a customer'}); + } +}); diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts index 6d189eb850ff..28cdb9991e4d 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts @@ -18,6 +18,7 @@ import {BelongsToAccessor} from '@loopback/repository/src'; import {MixedIdType} from '../../../../helpers.repository-tests'; import {Address, AddressWithRelations} from './address.model'; import {Order, OrderWithRelations} from './order.model'; +import {Seller} from './seller.model'; @model() export class Customer extends Entity { @@ -35,6 +36,9 @@ export class Customer extends Entity { @hasMany(() => Order) orders: Order[]; + @hasMany(() => Seller, {through: () => Order}) + sellers: Seller[]; + @hasOne(() => Address) address: Address; diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts index 67fe32851ea0..148188c54e57 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts @@ -7,3 +7,6 @@ export * from './address.model'; export * from './customer.model'; export * from './order.model'; export * from './shipment.model'; +export * from './product.model'; +export * from './shipment.model'; +export * from './seller.model'; diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts index ddf15cd2ea5d..a3d7a5fd22ee 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts @@ -14,6 +14,7 @@ import { import {MixedIdType} from '../../../../helpers.repository-tests'; import {Customer, CustomerWithRelations} from './customer.model'; import {Shipment, ShipmentWithRelations} from './shipment.model'; +import {Seller} from './seller.model'; @model() export class Order extends Entity { @@ -25,9 +26,9 @@ export class Order extends Entity { @property({ type: 'string', - required: true, + required: false, }) - description: string; + description?: string; @property({ type: 'boolean', @@ -40,6 +41,9 @@ export class Order extends Entity { @belongsTo(() => Shipment, {name: 'shipment'}) shipment_id: MixedIdType; + + @belongsTo(() => Seller) + sellerId: number; } export interface OrderRelations { diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts index 50970152cee5..1ab871744e3e 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts @@ -7,3 +7,6 @@ export * from './address.repository'; export * from './customer.repository'; export * from './order.repository'; export * from './shipment.repository'; +export * from './product.repository'; +export * from './shipment.repository'; +export * from './seller.repository'; diff --git a/packages/repository/src/__tests__/fixtures/models/customer.model.ts b/packages/repository/src/__tests__/fixtures/models/customer.model.ts new file mode 100644 index 000000000000..f03614557a62 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/models/customer.model.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {belongsTo, Entity, hasMany, hasOne, model, property} from '../../..'; +import {Address, AddressWithRelations} from './address.model'; +import {Order, OrderWithRelations} from './order.model'; +import {Seller} from './seller.model'; + +@model() +export class Customer extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + }) + name: string; + + @hasMany(() => Order) + orders: Order[]; + + @hasMany(() => Seller, {through: () => Order}) + sellers: Seller[]; + + @hasOne(() => Address) + address: Address; + + @hasMany(() => Customer, {keyTo: 'parentId'}) + customers?: Customer[]; + + @belongsTo(() => Customer) + parentId?: number; +} + +export interface CustomerRelations { + address?: AddressWithRelations; + orders?: OrderWithRelations[]; + customers?: CustomerWithRelations[]; + parentCustomer?: CustomerWithRelations; +} + +export type CustomerWithRelations = Customer & CustomerRelations; diff --git a/packages/repository/src/__tests__/fixtures/models/order.model.ts b/packages/repository/src/__tests__/fixtures/models/order.model.ts new file mode 100644 index 000000000000..fd7030e94d8e --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/models/order.model.ts @@ -0,0 +1,46 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {belongsTo, Entity, model, property} from '../../..'; +import {Customer, CustomerWithRelations} from './customer.model'; +import {Shipment, ShipmentWithRelations} from './shipment.model'; +import {Seller} from './seller.model'; + +@model() +export class Order extends Entity { + @property({ + type: 'string', + id: true, + }) + id: string; + + @property({ + type: 'string', + required: true, + }) + description: string; + + @property({ + type: 'boolean', + required: false, + }) + isShipped: boolean; + + @belongsTo(() => Customer) + customerId: number; + + @belongsTo(() => Seller) + sellerId: number; + + @belongsTo(() => Shipment, {name: 'shipment'}) + shipment_id: number; +} + +export interface OrderRelations { + customer?: CustomerWithRelations; + shipment?: ShipmentWithRelations; +} + +export type OrderWithRelations = Order & OrderRelations; diff --git a/packages/repository/src/__tests__/fixtures/models/seller.model.ts b/packages/repository/src/__tests__/fixtures/models/seller.model.ts new file mode 100644 index 000000000000..42cc585fbdee --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/models/seller.model.ts @@ -0,0 +1,26 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {hasMany, Entity, model, property} from '../../..'; +import {Customer} from './customer.model'; +import {Order} from './order.model'; + +@model() +export class Seller extends Entity { + @property({ + type: 'number', + id: true, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + name: string; + + @hasMany(() => Customer, {through: () => Order}) + customers: Customer[]; +} diff --git a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts new file mode 100644 index 000000000000..785a0517dfeb --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter, inject} from '@loopback/context'; +import { + BelongsToAccessor, + DefaultCrudRepository, + HasManyRepositoryFactory, + HasManyThroughRepositoryFactory, + juggler, + repository, +} from '../../..'; +import {HasOneRepositoryFactory} from '../../../'; +import {Address, Customer, CustomerRelations, Order, Seller} from '../models'; +import {AddressRepository} from './address.repository'; +import {SellerRepository} from './seller.repository'; +import {OrderRepository} from './order.repository'; + +export class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id, + CustomerRelations +> { + public readonly orders: HasManyRepositoryFactory< + Order, + typeof Customer.prototype.id + >; + public readonly address: HasOneRepositoryFactory< + Address, + typeof Customer.prototype.id + >; + public readonly customers: HasManyRepositoryFactory< + Customer, + typeof Customer.prototype.id + >; + public readonly sellers: HasManyThroughRepositoryFactory< + Seller, + Order, + typeof Customer.prototype.id + >; + public readonly parent: BelongsToAccessor< + Customer, + typeof Customer.prototype.id + >; + + constructor( + @inject('datasources.db') protected db: juggler.DataSource, + @repository.getter('OrderRepository') + orderRepositoryGetter: Getter, + @repository.getter('AddressRepository') + addressRepositoryGetter: Getter, + @repository.getter('SellerRepository') + sellerRepositoryGetter: Getter, + ) { + super(Customer, db); + this.orders = this.createHasManyRepositoryFactoryFor( + 'orders', + orderRepositoryGetter, + ); + this.sellers = this.createHasManyThroughRepositoryFactoryFor( + 'sellers', + sellerRepositoryGetter, + orderRepositoryGetter, + ); + this.address = this.createHasOneRepositoryFactoryFor( + 'address', + addressRepositoryGetter, + ); + this.customers = this.createHasManyRepositoryFactoryFor( + 'customers', + Getter.fromValue(this), + ); + this.parent = this.createBelongsToAccessorFor( + 'parent', + Getter.fromValue(this), + ); + } +} diff --git a/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts new file mode 100644 index 000000000000..a6b114721c05 --- /dev/null +++ b/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts @@ -0,0 +1,41 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter, inject} from '@loopback/context'; +import { + HasManyThroughRepositoryFactory, + DefaultCrudRepository, + juggler, + repository, +} from '../../..'; +import {Customer, Seller, Order} from '../models'; +import {CustomerRepository, OrderRepository} from '../repositories'; + +export class SellerRepository extends DefaultCrudRepository< + Seller, + typeof Seller.prototype.id +> { + public readonly customers: HasManyThroughRepositoryFactory< + Customer, + Order, + typeof Seller.prototype.id + >; + + constructor( + @inject('datasources.db') protected db: juggler.DataSource, + @repository.getter('CustomerRepository') + customerRepositoryGetter: Getter, + @repository.getter('OrderRepository') + orderRepositoryGetter: Getter, + ) { + super(Seller, db); + + this.customers = this.createHasManyThroughRepositoryFactoryFor( + 'customers', + customerRepositoryGetter, + orderRepositoryGetter, + ); + } +} diff --git a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts index e347ae765f08..64f2e3e77f57 100644 --- a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts +++ b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts @@ -9,6 +9,7 @@ import { BelongsToDefinition, createBelongsToAccessor, createHasManyRepositoryFactory, + createHasManyThroughRepositoryFactory, DefaultCrudRepository, Entity, EntityCrudRepository, @@ -17,21 +18,27 @@ import { HasManyDefinition, HasManyRepository, HasManyRepositoryFactory, + HasManyThroughRepository, + HasManyThroughDefinition, + HasManyThroughRepositoryFactory, juggler, ModelDefinition, RelationType, } from '../../..'; +import {Seller} from '../../fixtures/models'; // Given a Customer and Order models - see definitions at the bottom let db: juggler.DataSource; let customerRepo: EntityCrudRepository; let orderRepo: EntityCrudRepository; +let sellerRepo: EntityCrudRepository; let reviewRepo: EntityCrudRepository; describe('HasMany relation', () => { let existingCustomerId: number; let customerOrderRepo: HasManyRepository; + let customerSellerRepo: HasManyThroughRepository; let customerAuthoredReviewFactoryFn: HasManyRepositoryFactory< Review, typeof Customer.prototype.id @@ -81,6 +88,57 @@ describe('HasMany relation', () => { expect(orders).to.deepEqual(persistedOrders); }); + it('can create an instance of a related model through a junction table', async () => { + const seller = await customerSellerRepo.create( + { + name: 'Jam Risser', + }, + { + description: 'some order description', + }, + ); + const persisted = await sellerRepo.findById(seller.id); + + expect(seller).to.deepEqual(persisted); + }); + + it('can find an instance of a related model through a junction table', async () => { + const seller = await customerSellerRepo.create( + { + name: 'Jam Risser', + }, + { + description: 'some order description', + }, + ); + const notTheSeller = await sellerRepo.create( + { + name: 'Mark Twain', + }, + { + description: 'some order description', + }, + ); + + const persistedOrders = await orderRepo.find({ + where: { + customerId: existingCustomerId, + }, + }); + const persistedSellers = await sellerRepo.find({ + where: { + or: persistedOrders.map((order: Order) => ({ + id: order.sellerId, + })), + }, + }); + const sellers = await customerSellerRepo.find(); + + expect(sellers).to.containEql(seller); + expect(sellers).to.not.containEql(notTheSeller); + expect(sellers).to.deepEqual(persistedSellers); + }); + it('finds appropriate related model instances for multiple relations', async () => { // note(shimks): roundabout way of creating reviews with 'approves' // ideally, the review repository should have a approve function @@ -138,7 +196,24 @@ describe('HasMany relation', () => { Getter.fromValue(orderRepo), ); + const sellerFactoryFn: HasManyThroughRepositoryFactory< + Seller, + Order, + typeof Customer.prototype.id + > = createHasManyThroughRepositoryFactory< + Seller, + typeof Seller.prototype.id, + Order, + typeof Order.prototype.id, + typeof Customer.prototype.id + >( + Customer.definition.relations.sellers as HasManyThroughDefinition, + Getter.fromValue(sellerRepo), + Getter.fromValue(orderRepo), + ); + customerOrderRepo = orderFactoryFn(existingCustomerId); + customerSellerRepo = sellerFactoryFn(existingCustomerId); } function givenRepositoryFactoryFunctions() { @@ -208,11 +283,14 @@ describe('BelongsTo relation', () => { class Order extends Entity { id: number; description: string; - customerId: number; + customerId?: number; + sellerId?: number; static definition = new ModelDefinition('Order') .addProperty('id', {type: 'number', id: true}) .addProperty('description', {type: 'string', required: true}) + .addProperty('customerId', {type: 'number'}) + .addProperty('sellerId', {type: 'number'}) .addProperty('customerId', {type: 'number', required: true}) .addRelation({ name: 'customer', @@ -243,6 +321,7 @@ class Customer extends Entity { orders: Order[]; reviewsAuthored: Review[]; reviewsApproved: Review[]; + sellers: Seller[]; static definition: ModelDefinition = new ModelDefinition('Customer') .addProperty('id', {type: 'number', id: true}) @@ -273,6 +352,13 @@ class Customer extends Entity { source: Customer, target: () => Review, keyTo: 'approvedId', + }) + .addRelation({ + name: 'sellers', + type: RelationType.hasMany, + source: Customer, + target: () => Seller, + through: () => Order, }); } @@ -282,4 +368,5 @@ function givenCrudRepositories() { customerRepo = new DefaultCrudRepository(Customer, db); orderRepo = new DefaultCrudRepository(Order, db); reviewRepo = new DefaultCrudRepository(Review, db); + sellerRepo = new DefaultCrudRepository(Seller, db); } diff --git a/packages/repository/src/__tests__/unit/repositories/has-many-through-repository-factory.unit.ts b/packages/repository/src/__tests__/unit/repositories/has-many-through-repository-factory.unit.ts new file mode 100644 index 000000000000..d371d6a1ff02 --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/has-many-through-repository-factory.unit.ts @@ -0,0 +1,185 @@ +// Copyright IBM Corp. 2017,2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import {createStubInstance, expect} from '@loopback/testlab'; +import { + DefaultCrudRepository, + Entity, + HasManyThroughDefinition, + ModelDefinition, + RelationType, + createHasManyThroughRepositoryFactory, + juggler, +} from '../../..'; +import {TypeResolver} from '../../../type-resolver'; + +describe('createHasManyThroughRepositoryFactory', () => { + let customerRepo: CustomerRepository; + let orderRepo: OrderRepository; + + beforeEach(givenStubbedCustomerRepo); + + it('rejects relations with missing source', () => { + const relationMeta = givenHasManyThroughDefinition({ + source: undefined, + }); + + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(customerRepo), + ), + ).to.throw(/source model must be defined/); + }); + + it('rejects relations with missing target', () => { + const relationMeta = givenHasManyThroughDefinition({ + target: undefined, + }); + + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(customerRepo), + ), + ).to.throw(/target must be a type resolver/); + }); + + it('rejects relations with a target that is not a type resolver', () => { + const relationMeta = givenHasManyThroughDefinition({ + // tslint:disable-next-line:no-any + target: (Customer as unknown) as TypeResolver, + // the cast to any above is necessary to disable compile check + // we want to verify runtime assertion + }); + + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(orderRepo), + ), + ).to.throw(/target must be a type resolver/); + }); + + it('rejects relations with keyTo pointing to an unknown property', () => { + const relationMeta = givenHasManyThroughDefinition({ + target: () => Customer, + // Let the relation to use the default keyTo value "companyId" + // which does not exist on the Customer model! + keyTo: undefined, + }); + + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(orderRepo), + ), + ).to.throw(/through model Customer is missing.*foreign key companyId/); + }); + + it('rejects relations with missing "through"', () => { + const relationMeta = givenHasManyThroughDefinition({ + target: () => Customer, + through: undefined, + }); + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(orderRepo), + ), + ).to.throw(/through must be a type resolver/); + }); + + it('rejects relations with "through" that is not a type resolver', () => { + const relationMeta = givenHasManyThroughDefinition({ + target: () => Customer, + }); + relationMeta.through = (true as unknown) as TypeResolver< + Entity, + typeof Entity + >; + expect(() => + createHasManyThroughRepositoryFactory( + relationMeta, + Getter.fromValue(customerRepo), + Getter.fromValue(orderRepo), + ), + ).to.throw(/through must be a type resolver/); + }); + + /*------------- HELPERS ---------------*/ + + class Customer extends Entity { + static definition = new ModelDefinition('Customer').addProperty('id', { + type: Number, + id: true, + }); + id: number; + } + + class Order extends Entity { + static definition = new ModelDefinition('Order') + .addProperty('id', { + type: Number, + id: true, + }) + .addProperty('customerId', { + type: Number, + }); + id: number; + customerId: number; + } + + class CustomerRepository extends DefaultCrudRepository< + Customer, + typeof Customer.prototype.id + > { + constructor(dataSource: juggler.DataSource) { + super(Customer, dataSource); + } + } + + class OrderRepository extends DefaultCrudRepository< + Order, + typeof Order.prototype.id + > { + constructor(dataSource: juggler.DataSource) { + super(Order, dataSource); + } + } + + function givenStubbedCustomerRepo() { + customerRepo = createStubInstance(CustomerRepository); + } + + function givenHasManyThroughDefinition( + props?: Partial, + ): HasManyThroughDefinition { + class Company extends Entity { + static definition = new ModelDefinition('Company').addProperty('id', { + type: Number, + id: true, + }); + id: number; + } + + const defaults: HasManyThroughDefinition = { + type: RelationType.hasMany, + targetsMany: true, + name: 'customers', + target: () => Customer, + through: () => Order, + source: Company, + }; + + return Object.assign(defaults, props); + } +}); diff --git a/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts b/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts new file mode 100644 index 000000000000..9e42f9971f17 --- /dev/null +++ b/packages/repository/src/relations/has-many/has-many-through-repository.factory.ts @@ -0,0 +1,87 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as debugFactory from 'debug'; +import {DataObject} from '../../common-types'; +import {Entity} from '../../model'; +import {EntityCrudRepository} from '../../repositories/repository'; +import {Getter, HasManyThroughDefinition} from '../relation.types'; +import { + createTargetConstraint, + createThroughConstraint, + resolveHasManyThroughMetadata, +} from './has-many-through.helpers'; +import { + DefaultHasManyThroughRepository, + HasManyThroughRepository, +} from './has-many-through.repository'; + +const debug = debugFactory( + 'loopback:repository:has-many-through-repository-factory', +); + +export type HasManyThroughRepositoryFactory< + Target extends Entity, + Through extends Entity, + ForeignKeyType +> = (fkValue: ForeignKeyType) => HasManyThroughRepository; + +/** + * Enforces a constraint on a repository based on a relationship contract + * between models. For example, if a Customer model is related to an Order model + * via a HasMany relation, then, the relational repository returned by the + * factory function would be constrained by a Customer model instance's id(s). + * + * @param relationMetadata - The relation metadata used to describe the + * relationship and determine how to apply the constraint. + * @param targetRepositoryGetter - The repository which represents the target model of a + * relation attached to a datasource. + * @returns The factory function which accepts a foreign key value to constrain + * the given target repository + */ +export function createHasManyThroughRepositoryFactory< + Target extends Entity, + TargetID, + Through extends Entity, + ThroughID, + ForeignKeyType +>( + relationMetadata: HasManyThroughDefinition, + targetRepositoryGetter: Getter>, + throughRepositoryGetter: Getter>, +): HasManyThroughRepositoryFactory { + const meta = resolveHasManyThroughMetadata(relationMetadata); + debug('Resolved HasManyThrough relation metadata: %o', meta); + return function(fkValue?: ForeignKeyType) { + function getTargetContraint( + throughInstances: Through[], + ): DataObject { + return createTargetConstraint(meta, throughInstances); + } + function getThroughConstraint( + targetInstance?: Target, + ): DataObject { + const constriant: DataObject = createThroughConstraint< + Target, + Through, + ForeignKeyType + >(meta, fkValue, targetInstance); + return constriant; + } + return new DefaultHasManyThroughRepository< + Target, + TargetID, + EntityCrudRepository, + Through, + ThroughID, + EntityCrudRepository + >( + targetRepositoryGetter, + throughRepositoryGetter, + getTargetContraint, + getThroughConstraint, + ); + }; +} diff --git a/packages/repository/src/relations/has-many/has-many-through.helpers.ts b/packages/repository/src/relations/has-many/has-many-through.helpers.ts new file mode 100644 index 000000000000..7b3d7c846999 --- /dev/null +++ b/packages/repository/src/relations/has-many/has-many-through.helpers.ts @@ -0,0 +1,164 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as debugFactory from 'debug'; +import {DataObject} from '../../common-types'; +import {camelCase} from 'lodash'; +import {Entity} from '../../model'; +import {InvalidRelationError} from '../../errors'; +import {isTypeResolver} from '../../type-resolver'; +import {HasManyThroughDefinition} from '../relation.types'; + +const debug = debugFactory('loopback:repository:has-many-through-helpers'); + +/** + * Relation definition with optional metadata (e.g. `keyTo`) filled in. + * @internal + */ +export type HasManyThroughResolvedDefinition = HasManyThroughDefinition & { + keyTo: string; + keyThrough: string; + targetPrimaryKey: string; +}; + +/** + * Creates constraint used to query target + * @param relationMeta - hasManyThrough metadata to resolve + * @param throughInstances - Instances of through entities used to constrain the target + * @internal + */ +export function createTargetConstraint< + Target extends Entity, + Through extends Entity +>( + relationMeta: HasManyThroughResolvedDefinition, + throughInstances: Through[], +): DataObject { + const {targetPrimaryKey} = relationMeta; + const targetFkName = relationMeta.keyThrough; + const fkValues = throughInstances.map( + (throughInstance: Through) => + throughInstance[targetFkName as keyof Through], + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constraint: any = { + [targetPrimaryKey]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues}, + }; + return constraint; +} + +/** + * Creates constraint used to query through + * @param relationMeta - hasManyThrough metadata to resolve + * @param fkValue - Value of the foreign key used to constrain through + * @param targetInstance - Instance of target entity used to constrain through + * @internal + */ +export function createThroughConstraint< + Target extends Entity, + Through extends Entity, + ForeignKeyType +>( + relationMeta: HasManyThroughResolvedDefinition, + fkValue?: ForeignKeyType, + targetInstance?: Target, +): DataObject { + const {targetPrimaryKey} = relationMeta; + const targetFkName = relationMeta.keyThrough; + const sourceFkName = relationMeta.keyTo; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const constraint: any = {[sourceFkName]: fkValue}; + if (targetInstance) { + constraint[targetFkName] = targetInstance[targetPrimaryKey as keyof Target]; + } + return constraint; +} + +/** + * Resolves given hasMany metadata if target is specified to be a resolver. + * Mainly used to infer what the `keyTo` property should be from the target's + * belongsTo metadata + * @param relationMeta - hasManyThrough metadata to resolve + * @internal + */ +export function resolveHasManyThroughMetadata( + relationMeta: HasManyThroughDefinition, +): HasManyThroughResolvedDefinition { + if (!isTypeResolver(relationMeta.target)) { + const reason = 'target must be a type resolver'; + throw new InvalidRelationError(reason, relationMeta); + } + if (!isTypeResolver(relationMeta.through)) { + const reason = 'through must be a type resolver'; + throw new InvalidRelationError(reason, relationMeta); + } + + const throughModel = relationMeta.through(); + const throughModelProperties = + throughModel.definition && throughModel.definition.properties; + const targetModel = relationMeta.target(); + const targetModelProperties = + targetModel.definition && targetModel.definition.properties; + + // Make sure that if it already keys to the foreign key property, + // the key exists in the target model + if ( + relationMeta.keyTo && + throughModelProperties[relationMeta.keyTo] && + relationMeta.keyThrough && + throughModelProperties[relationMeta.keyThrough] && + relationMeta.targetPrimaryKey && + targetModelProperties[relationMeta.targetPrimaryKey] + ) { + // The explict cast is needed because of a limitation of type inference + return relationMeta as HasManyThroughResolvedDefinition; + } + + const sourceModel = relationMeta.source; + if (!sourceModel || !sourceModel.modelName) { + const reason = 'source model must be defined'; + throw new InvalidRelationError(reason, relationMeta); + } + + debug( + 'Resolved model %s from given metadata: %o', + targetModel.modelName, + targetModel, + ); + + debug( + 'Resolved model %s from given metadata: %o', + throughModel.modelName, + throughModel, + ); + + const sourceFkName = + relationMeta.keyTo || camelCase(sourceModel.modelName + '_id'); + const hasSourceFkProperty = throughModelProperties[sourceFkName]; + if (!hasSourceFkProperty) { + const reason = `through model ${targetModel.name} is missing definition of default foreign key ${sourceFkName}`; + throw new InvalidRelationError(reason, relationMeta); + } + + const targetFkName = + relationMeta.keyThrough || camelCase(targetModel.modelName + '_id'); + const hasTargetFkName = throughModelProperties[targetFkName]; + if (!hasTargetFkName) { + const reason = `through model ${throughModel.name} is missing definition of target foreign key ${targetFkName}`; + throw new InvalidRelationError(reason, relationMeta); + } + + const targetPrimaryKey = targetModel.definition.idProperties()[0]; + if (!targetPrimaryKey) { + const reason = `target model ${targetModel.modelName} does not have any primary key (id property)`; + throw new InvalidRelationError(reason, relationMeta); + } + + return Object.assign(relationMeta, { + keyTo: sourceFkName, + keyThrough: targetFkName, + targetPrimaryKey, + }); +} diff --git a/packages/repository/src/relations/has-many/has-many-through.repository.ts b/packages/repository/src/relations/has-many/has-many-through.repository.ts new file mode 100644 index 000000000000..5fd128112e08 --- /dev/null +++ b/packages/repository/src/relations/has-many/has-many-through.repository.ts @@ -0,0 +1,182 @@ +// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Node module: @loopback/example-todo +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import {Count, DataObject, Options} from '../../common-types'; +import {Entity} from '../../model'; +import {Filter, Where} from '../../query'; +import { + constrainDataObject, + constrainFilter, + constrainWhere, +} from '../../repositories/constraint-utils'; +import {EntityCrudRepository} from '../../repositories/repository'; + +/** + * CRUD operations for a target repository of a HasManyThrough relation + */ +export interface HasManyThroughRepository< + Target extends Entity, + Through extends Entity +> { + /** + * Create a target model instance + * @param targetModelData - The target model data + * @param throughModelData - The through model data + * @param options - Options for the operation + * @param throughOptions - Options passed to create through + * @returns A promise which resolves to the newly created target model instance + */ + create( + targetModelData: DataObject, + throughModelData?: DataObject, + options?: Options, + throughOptions?: Options, + ): Promise; + /** + * Find target model instance(s) + * @param filter - A filter object for where, order, limit, etc. + * @param options - Options for the operation + * @returns A promise which resolves with the found target instance(s) + */ + find( + filter?: Filter, + options?: Options, + throughOptions?: Options, + ): Promise; + /** + * Delete multiple target model instances + * @param where - Instances within the where scope are deleted + * @param options + * @returns A promise which resolves the deleted target model instances + */ + delete( + where?: Where, + options?: Options, + throughOptions?: Options, + ): Promise; + /** + * Patch multiple target model instances + * @param dataObject - The fields and their new values to patch + * @param where - Instances within the where scope are patched + * @param options + * @returns A promise which resolves the patched target model instances + */ + patch( + dataObject: DataObject, + where?: Where, + options?: Options, + throughOptions?: Options, + ): Promise; +} + +export class DefaultHasManyThroughRepository< + TargetEntity extends Entity, + TargetID, + TargetRepository extends EntityCrudRepository, + ThroughEntity extends Entity, + ThroughID, + ThroughRepository extends EntityCrudRepository +> implements HasManyThroughRepository { + /** + * Constructor of DefaultHasManyThroughEntityCrudRepository + * @param getTargetRepository - the getter of the related target model repository instance + * @param getThroughRepository - the getter of the related through model repository instance + * @param getTargetConstraint - the getter of the constraint used to query target + * @param getThroughConstraint - the getter of the constraint used to query through + * the hasManyThrough instance + */ + constructor( + public getTargetRepository: Getter, + public getThroughRepository: Getter, + public getTargetConstraint: ( + throughInstances: ThroughEntity[], + ) => DataObject, + public getThroughConstraint: ( + targetInstance?: TargetEntity, + ) => DataObject, + ) {} + + async create( + targetModelData: DataObject, + throughModelData: DataObject = {}, + options?: Options, + throughOptions?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + const throughRepository = await this.getThroughRepository(); + const targetInstance = await targetRepository.create( + targetModelData, + options, + ); + const throughConstraint = this.getThroughConstraint(targetInstance); + await throughRepository.create( + constrainDataObject(throughModelData, throughConstraint as DataObject< + ThroughEntity + >), + throughOptions, + ); + return targetInstance; + } + + async find( + filter?: Filter, + options?: Options, + throughOptions?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + const throughRepository = await this.getThroughRepository(); + const throughConstraint = this.getThroughConstraint(); + const throughInstances = await throughRepository.find( + constrainFilter(undefined, throughConstraint), + throughOptions, + ); + const targetConstraint = this.getTargetConstraint(throughInstances); + return targetRepository.find( + constrainFilter(filter, targetConstraint), + options, + ); + } + + async delete( + where?: Where, + options?: Options, + throughOptions?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + const throughRepository = await this.getThroughRepository(); + const throughConstraint = this.getThroughConstraint(); + const throughInstances = await throughRepository.find( + constrainFilter(undefined, throughConstraint), + throughOptions, + ); + const targetConstraint = this.getTargetConstraint(throughInstances); + return targetRepository.deleteAll( + constrainWhere(where, targetConstraint as Where), + options, + ); + } + + async patch( + dataObject: DataObject, + where?: Where, + options?: Options, + throughOptions?: Options, + ): Promise { + const targetRepository = await this.getTargetRepository(); + const throughRepository = await this.getThroughRepository(); + const throughConstraint = this.getThroughConstraint(); + const throughInstances = await throughRepository.find( + constrainFilter(undefined, throughConstraint), + throughOptions, + ); + const targetConstraint = this.getTargetConstraint(throughInstances); + return targetRepository.updateAll( + constrainDataObject(dataObject, targetConstraint), + constrainWhere(where, targetConstraint as Where), + options, + ); + } +} diff --git a/packages/repository/src/relations/has-many/has-many.decorator.ts b/packages/repository/src/relations/has-many/has-many.decorator.ts index 70f57f0bfd1c..51899626c4f3 100644 --- a/packages/repository/src/relations/has-many/has-many.decorator.ts +++ b/packages/repository/src/relations/has-many/has-many.decorator.ts @@ -1,11 +1,15 @@ -// Copyright IBM Corp. 2018,2019. All Rights Reserved. +// Copyright IBM Corp. 2017,2018,2019. All Rights Reserved. // Node module: @loopback/repository // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import {Entity, EntityResolver} from '../../model'; import {relation} from '../relation.decorator'; -import {HasManyDefinition, RelationType} from '../relation.types'; +import { + HasManyDefinition, + HasManyThroughDefinition, + RelationType, +} from '../relation.types'; /** * Decorator for hasMany @@ -17,7 +21,7 @@ import {HasManyDefinition, RelationType} from '../relation.types'; */ export function hasMany( targetResolver: EntityResolver, - definition?: Partial, + definition?: Partial, ) { return function(decoratedTarget: object, key: string) { const meta: HasManyDefinition = Object.assign( diff --git a/packages/repository/src/relations/has-many/index.ts b/packages/repository/src/relations/has-many/index.ts index 0025021d819a..ce8081ad3d10 100644 --- a/packages/repository/src/relations/has-many/index.ts +++ b/packages/repository/src/relations/has-many/index.ts @@ -6,3 +6,5 @@ export * from './has-many.decorator'; export * from './has-many.repository'; export * from './has-many-repository.factory'; +export * from './has-many-through-repository.factory'; +export * from './has-many-through.repository'; diff --git a/packages/repository/src/relations/relation.types.ts b/packages/repository/src/relations/relation.types.ts index 1bed6582653b..b8341f866593 100644 --- a/packages/repository/src/relations/relation.types.ts +++ b/packages/repository/src/relations/relation.types.ts @@ -68,6 +68,42 @@ export interface HasManyDefinition extends RelationDefinitionBase { keyTo?: string; } +export interface HasManyThroughDefinition extends RelationDefinitionBase { + type: RelationType.hasMany; + targetsMany: true; + + /** + * The through model of this relation. + * + * E.g. when a Customer has many Order instances and a Seller has many Order instances, + * then Order is through. + */ + through: TypeResolver; + + /** + * The foreign key used by the through model to reference the source model. + * + * E.g. when a Customer has many Order instances and a Seller has many Order instances, + * then keyTo is "customerId". + * Note that "customerId" is the default FK assumed by the framework, users + * can provide a custom FK name by setting "keyTo". + */ + keyTo?: string; + + /** + * The foreign key used by the through model to reference the target model. + * + * E.g. when a Customer has many Order instances and a Seller has many Order instances, + * then keyThrough is "sellerId". + */ + keyThrough?: string; + + /* + * The primary key in the target model when using through, e.g. Seller#id. + */ + targetPrimaryKey?: string; +} + export interface BelongsToDefinition extends RelationDefinitionBase { type: RelationType.belongsTo; targetsMany: false; @@ -102,6 +138,7 @@ export interface HasOneDefinition extends RelationDefinitionBase { */ export type RelationMetadata = | HasManyDefinition + | HasManyThroughDefinition | BelongsToDefinition | HasOneDefinition // TODO(bajtos) add other relation types and remove RelationDefinitionBase once diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 5606125d0129..e5380f84c127 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -23,9 +23,12 @@ import { BelongsToDefinition, createBelongsToAccessor, createHasManyRepositoryFactory, + createHasManyThroughRepositoryFactory, createHasOneRepositoryFactory, HasManyDefinition, HasManyRepositoryFactory, + HasManyThroughDefinition, + HasManyThroughRepositoryFactory, HasOneDefinition, HasOneRepositoryFactory, includeRelatedModels, @@ -258,6 +261,62 @@ export class DefaultCrudRepository< ); } + /** + * EXPIRMENTAL: The underlying implementation may change in the near future. + * If some of the changes break backward-compatibility a semver-major may not + * be released. + * + * Function to create a constrained relation repository factory + * + * ```ts + * class CustomerRepository extends DefaultCrudRepository< + * Customer, + * typeof Customer.prototype.id + * > { + * public readonly orders: HasManyRepositoryFactory; + * + * constructor( + * protected db: juggler.DataSource, + * orderRepository: EntityCrudRepository, + * ) { + * super(Customer, db); + * this.orders = this._createHasManyRepositoryFactoryFor( + * 'orders', + * orderRepository, + * ); + * } + * } + * ``` + * + * @param relationName Name of the relation defined on the source model + * @param targetRepo Target repository instance + * @param throughRepo Through repository instance + */ + protected createHasManyThroughRepositoryFactoryFor< + Target extends Entity, + TargetID, + Through extends Entity, + ThroughID, + ForeignKeyType + >( + relationName: string, + targetRepoGetter: Getter>, + throughRepositoryGetter: Getter>, + ): HasManyThroughRepositoryFactory { + const meta = this.entityClass.definition.relations[relationName]; + return createHasManyThroughRepositoryFactory< + Target, + TargetID, + Through, + ThroughID, + ForeignKeyType + >( + meta as HasManyThroughDefinition, + targetRepoGetter, + throughRepositoryGetter, + ); + } + /** * @deprecated * Function to create a belongs to accessor From cf5720fa883275dc35fb10b2dd97d793ad62822b Mon Sep 17 00:00:00 2001 From: Jam Risser Date: Sun, 6 Oct 2019 00:15:17 -0500 Subject: [PATCH 2/3] feat(repository) remapped test imports --- packages/repository/package.json | 1 + .../src/__tests__/fixtures/models/customer.model.ts | 5 ++++- .../src/__tests__/fixtures/models/order.model.ts | 5 ++++- .../fixtures/repositories/customer.repository.ts | 12 +++++++++--- .../fixtures/repositories/seller.repository.ts | 11 +++++++++-- .../repositories/relation.factory.integration.ts | 2 +- 6 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/repository/package.json b/packages/repository/package.json index c5c8819a492f..5bbdc1be55b3 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -20,6 +20,7 @@ "devDependencies": { "@loopback/build": "^2.0.13", "@loopback/eslint-config": "^4.1.1", + "@loopback/repository-tests": "^0.5.1", "@loopback/testlab": "^1.9.1", "@types/bson": "^4.0.0", "@types/lodash": "^4.14.141", diff --git a/packages/repository/src/__tests__/fixtures/models/customer.model.ts b/packages/repository/src/__tests__/fixtures/models/customer.model.ts index f03614557a62..90a7cf82bee6 100644 --- a/packages/repository/src/__tests__/fixtures/models/customer.model.ts +++ b/packages/repository/src/__tests__/fixtures/models/customer.model.ts @@ -4,7 +4,10 @@ // License text available at https://opensource.org/licenses/MIT import {belongsTo, Entity, hasMany, hasOne, model, property} from '../../..'; -import {Address, AddressWithRelations} from './address.model'; +import { + Address, + AddressWithRelations, +} from '@loopback/repository-tests/dist/crud/relations/fixtures/models/address.model'; import {Order, OrderWithRelations} from './order.model'; import {Seller} from './seller.model'; diff --git a/packages/repository/src/__tests__/fixtures/models/order.model.ts b/packages/repository/src/__tests__/fixtures/models/order.model.ts index fd7030e94d8e..58648c8092ff 100644 --- a/packages/repository/src/__tests__/fixtures/models/order.model.ts +++ b/packages/repository/src/__tests__/fixtures/models/order.model.ts @@ -5,7 +5,10 @@ import {belongsTo, Entity, model, property} from '../../..'; import {Customer, CustomerWithRelations} from './customer.model'; -import {Shipment, ShipmentWithRelations} from './shipment.model'; +import { + Shipment, + ShipmentWithRelations, +} from '@loopback/repository-tests/dist/crud/relations/fixtures/models/shipment.model'; import {Seller} from './seller.model'; @model() diff --git a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts index 785a0517dfeb..67f315b841c4 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts +++ b/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts @@ -13,10 +13,16 @@ import { repository, } from '../../..'; import {HasOneRepositoryFactory} from '../../../'; -import {Address, Customer, CustomerRelations, Order, Seller} from '../models'; -import {AddressRepository} from './address.repository'; +import { + Address, + Customer, + CustomerRelations, + Order, +} from '@loopback/repository-tests/dist/crud/relations/fixtures/models'; +import {Seller} from '../models/seller.model'; +import {AddressRepository} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories/address.repository'; import {SellerRepository} from './seller.repository'; -import {OrderRepository} from './order.repository'; +import {OrderRepository} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories/order.repository'; export class CustomerRepository extends DefaultCrudRepository< Customer, diff --git a/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts index a6b114721c05..0991f687f1ec 100644 --- a/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts +++ b/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts @@ -10,8 +10,15 @@ import { juggler, repository, } from '../../..'; -import {Customer, Seller, Order} from '../models'; -import {CustomerRepository, OrderRepository} from '../repositories'; +import { + Customer, + Order, +} from '@loopback/repository-tests/dist/crud/relations/fixtures/models'; +import {Seller} from '../models/seller.model'; +import { + CustomerRepository, + OrderRepository, +} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories'; export class SellerRepository extends DefaultCrudRepository< Seller, diff --git a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts index 64f2e3e77f57..5c7ce8ceb3de 100644 --- a/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts +++ b/packages/repository/src/__tests__/integration/repositories/relation.factory.integration.ts @@ -25,7 +25,7 @@ import { ModelDefinition, RelationType, } from '../../..'; -import {Seller} from '../../fixtures/models'; +import {Seller} from '../../fixtures/models/seller.model'; // Given a Customer and Order models - see definitions at the bottom let db: juggler.DataSource; From 2e847d4eb3913bea48d345c73f8f3c2399dee2a5 Mon Sep 17 00:00:00 2001 From: Jam Risser Date: Sun, 6 Oct 2019 00:29:12 -0500 Subject: [PATCH 3/3] feat(repository) fixed tests --- .../fixtures/models/customer.model.ts | 4 - .../crud/relations/fixtures/models/index.ts | 3 - .../relations/fixtures/models/order.model.ts | 8 +- .../relations/fixtures/repositories/index.ts | 3 - .../repositories/customer.repository.ts | 86 ------------------- .../repositories/seller.repository.ts | 48 ----------- 6 files changed, 2 insertions(+), 150 deletions(-) delete mode 100644 packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts delete mode 100644 packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts index 28cdb9991e4d..6d189eb850ff 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/customer.model.ts @@ -18,7 +18,6 @@ import {BelongsToAccessor} from '@loopback/repository/src'; import {MixedIdType} from '../../../../helpers.repository-tests'; import {Address, AddressWithRelations} from './address.model'; import {Order, OrderWithRelations} from './order.model'; -import {Seller} from './seller.model'; @model() export class Customer extends Entity { @@ -36,9 +35,6 @@ export class Customer extends Entity { @hasMany(() => Order) orders: Order[]; - @hasMany(() => Seller, {through: () => Order}) - sellers: Seller[]; - @hasOne(() => Address) address: Address; diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts index 148188c54e57..67fe32851ea0 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/index.ts @@ -7,6 +7,3 @@ export * from './address.model'; export * from './customer.model'; export * from './order.model'; export * from './shipment.model'; -export * from './product.model'; -export * from './shipment.model'; -export * from './seller.model'; diff --git a/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts index a3d7a5fd22ee..ddf15cd2ea5d 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/models/order.model.ts @@ -14,7 +14,6 @@ import { import {MixedIdType} from '../../../../helpers.repository-tests'; import {Customer, CustomerWithRelations} from './customer.model'; import {Shipment, ShipmentWithRelations} from './shipment.model'; -import {Seller} from './seller.model'; @model() export class Order extends Entity { @@ -26,9 +25,9 @@ export class Order extends Entity { @property({ type: 'string', - required: false, + required: true, }) - description?: string; + description: string; @property({ type: 'boolean', @@ -41,9 +40,6 @@ export class Order extends Entity { @belongsTo(() => Shipment, {name: 'shipment'}) shipment_id: MixedIdType; - - @belongsTo(() => Seller) - sellerId: number; } export interface OrderRelations { diff --git a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts index 1ab871744e3e..50970152cee5 100644 --- a/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts +++ b/packages/repository-tests/src/crud/relations/fixtures/repositories/index.ts @@ -7,6 +7,3 @@ export * from './address.repository'; export * from './customer.repository'; export * from './order.repository'; export * from './shipment.repository'; -export * from './product.repository'; -export * from './shipment.repository'; -export * from './seller.repository'; diff --git a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts deleted file mode 100644 index 67f315b841c4..000000000000 --- a/packages/repository/src/__tests__/fixtures/repositories/customer.repository.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Getter, inject} from '@loopback/context'; -import { - BelongsToAccessor, - DefaultCrudRepository, - HasManyRepositoryFactory, - HasManyThroughRepositoryFactory, - juggler, - repository, -} from '../../..'; -import {HasOneRepositoryFactory} from '../../../'; -import { - Address, - Customer, - CustomerRelations, - Order, -} from '@loopback/repository-tests/dist/crud/relations/fixtures/models'; -import {Seller} from '../models/seller.model'; -import {AddressRepository} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories/address.repository'; -import {SellerRepository} from './seller.repository'; -import {OrderRepository} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories/order.repository'; - -export class CustomerRepository extends DefaultCrudRepository< - Customer, - typeof Customer.prototype.id, - CustomerRelations -> { - public readonly orders: HasManyRepositoryFactory< - Order, - typeof Customer.prototype.id - >; - public readonly address: HasOneRepositoryFactory< - Address, - typeof Customer.prototype.id - >; - public readonly customers: HasManyRepositoryFactory< - Customer, - typeof Customer.prototype.id - >; - public readonly sellers: HasManyThroughRepositoryFactory< - Seller, - Order, - typeof Customer.prototype.id - >; - public readonly parent: BelongsToAccessor< - Customer, - typeof Customer.prototype.id - >; - - constructor( - @inject('datasources.db') protected db: juggler.DataSource, - @repository.getter('OrderRepository') - orderRepositoryGetter: Getter, - @repository.getter('AddressRepository') - addressRepositoryGetter: Getter, - @repository.getter('SellerRepository') - sellerRepositoryGetter: Getter, - ) { - super(Customer, db); - this.orders = this.createHasManyRepositoryFactoryFor( - 'orders', - orderRepositoryGetter, - ); - this.sellers = this.createHasManyThroughRepositoryFactoryFor( - 'sellers', - sellerRepositoryGetter, - orderRepositoryGetter, - ); - this.address = this.createHasOneRepositoryFactoryFor( - 'address', - addressRepositoryGetter, - ); - this.customers = this.createHasManyRepositoryFactoryFor( - 'customers', - Getter.fromValue(this), - ); - this.parent = this.createBelongsToAccessorFor( - 'parent', - Getter.fromValue(this), - ); - } -} diff --git a/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts b/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts deleted file mode 100644 index 0991f687f1ec..000000000000 --- a/packages/repository/src/__tests__/fixtures/repositories/seller.repository.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Getter, inject} from '@loopback/context'; -import { - HasManyThroughRepositoryFactory, - DefaultCrudRepository, - juggler, - repository, -} from '../../..'; -import { - Customer, - Order, -} from '@loopback/repository-tests/dist/crud/relations/fixtures/models'; -import {Seller} from '../models/seller.model'; -import { - CustomerRepository, - OrderRepository, -} from '@loopback/repository-tests/dist/crud/relations/fixtures/repositories'; - -export class SellerRepository extends DefaultCrudRepository< - Seller, - typeof Seller.prototype.id -> { - public readonly customers: HasManyThroughRepositoryFactory< - Customer, - Order, - typeof Seller.prototype.id - >; - - constructor( - @inject('datasources.db') protected db: juggler.DataSource, - @repository.getter('CustomerRepository') - customerRepositoryGetter: Getter, - @repository.getter('OrderRepository') - orderRepositoryGetter: Getter, - ) { - super(Seller, db); - - this.customers = this.createHasManyThroughRepositoryFactoryFor( - 'customers', - customerRepositoryGetter, - orderRepositoryGetter, - ); - } -}