From e2cbca8cbad022fa0a89dce76e41731e2d6c9c1f Mon Sep 17 00:00:00 2001 From: nvms Date: Fri, 30 Aug 2024 14:23:20 -0400 Subject: [PATCH 1/3] create models, migrations, ldm changes --- docs/logical_data_model.encoded | 2 +- docs/logical_data_model.puml | 60 +++++++- ...40830172706-populate-grant-replacements.js | 141 ++++++++++++++++++ ...101-create-grant-relationship-to-active.js | 91 +++++++++++ src/models/grant.js | 31 ---- src/models/grantRelationshipToActive.js | 61 ++++++++ src/models/grantReplacement.js | 25 ++++ src/models/grantReplacementType.js | 22 +++ 8 files changed, 398 insertions(+), 35 deletions(-) create mode 100644 src/migrations/20240830172706-populate-grant-replacements.js create mode 100644 src/migrations/20240830173101-create-grant-relationship-to-active.js create mode 100644 src/models/grantRelationshipToActive.js create mode 100644 src/models/grantReplacement.js create mode 100644 src/models/grantReplacementType.js diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded index a255266227..5094a65624 100644 --- a/docs/logical_data_model.encoded +++ b/docs/logical_data_model.encoded @@ -1 +1 @@ -xLt_RzqsalzTVuNW_Q5jyBBOjjS3pjWxhECuRN29OzXE5zkYC6Y9Ve-DHBubAQTkh__xWQI-a1GbaPAUaxJyoNuIoHbzE1mEPyZXFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOhpA2i1O-bkIfcdwZD0SqbDI49h-vUkl__Fal_wcFR_UeUmTXeobNoLf-hjavLJKXYn9wtPECexk4N-uX1dQ8uWv-or9dwGeJuzJX3dSGfeUft_VGWmfu33_doJTKe3vIxF0vTcBiykpyzEpg_JeM_6U7VqLvKbA-0xICymHVnYfyQOjXM1TyKaAMixsX8xHEp4AjlKp2WN39pmzJZag8UEWt2linS_qLu9XUfnYylxpBqJvQf_xb_yzIMpy1F6J_PwIUqYdGLp-VHM3TAJMMmRfus2Hvh20KwMJ51Ckwd3u2qzInW6lM7gSGboLGsYUag80juG2JlvSY0xWyG8Ly35mk4C57AabsGZ1mVVxXxxxMmAA5mWM-ILaUuBP31144zv0YCBj1su9AAvXkIy0XvA92qhA_QZ-SzSu36GfhJ6fXzQFFusXU0aPgMZsg03IckP8xPQjmq2a6fTTqQWS1PFuJ_wy6O7gngZ-Mv9YK2hSvVD4KhgZuc7Tf7zhV_-_fq_7kccu9wTxa52qHHWK4prdzDLIpR4DeBLk7sS5OV2gOdk8yrE2nL1e9nlztIK4ZE1cuaaXpfy3EIy98acAcFmoM3oSvOX3yIctX1GGY2fte62op2NdO0_QHeH0T3ERxBdsNJlHtGEQxG3z_Ajlllb2avccti2WVNFxCvGCdPAApb5yRGFUdTdMw7fAqXvfe6RkGywDBTVp9iarOFhOfisMm_bQVmrOFa7ztqUkDmdewfyNuWy75vJSenpc0_F0LAkEoQ1VPQAhhyaUPH-cdxaWFL9ViVP11e3_PEa3HF_JbqwRP1RLmoxg07qZV0W6Fr_EdcVRBpnLJ7y7AUpcri5fabLx0TIry8GqJSdnrjBKXjt4L3fV2uPtw2UeyuPLtTdfg_yzRL-_PJ1FKf3dY2E2GN5Ek70dPKeMn38Vkogq-jAj6_rLueGcdExT0Z3-Lc7uxC5lkH_Y6A1mYmWUxNXHu956yPDKUAEG78aTErybceJNVhzwZ9dZp-hcTjG1vUUQDCf1aLLY6PnV6VwUHbewpI508FII5Dqups61xY5zU0tvZkGzrlAA0gu-B8SPAa7n-TURBxzmYMktDExR-wKG6bdaPqRmhH0FlAt0riP2L2xW5cHAP4nUd3AlKSLLUGlfcxiBG3dAq5xuHxiEIT8zs4A14ApP44-CaZOdUqgs59IYprolQQDy-o_-NK76fH1OcbRugmreM7LR8YPan-ey-O9pQ2B90Tr2Va7seiE5W3l8InF1-O83X_o9D_U8yWeKpsjXS4bfg-zrVw4GfN1DhxVWhVKDDsMh1iZeOa2oTrXRI6TB2iea6YNOisioiigp6cpulmS5ob-C8lPzNMIkrgkNsARCFX7ZO0-sWCCt-3MaMsWirpcju86SsHh8PLbXb4cHVqMslglFpAetkhWsYtv4wyqUQnvVJfi9PtIAR89HXtBDYt8vuA3J--7VxkUGcHnKYrmdXklfIReNaptZx0r-koy8SBgl3ZiWpkuXvaelNg22y7EWx8TJY3caifyyX0HQhOnqlKsmXH8Rb4TQC5byb6YGoTkvMxc5Mdg2OLTJWttZfwZq6L6pMiJaxFtXfsylBy_Ehs-kNxu-kxe-UNtX6d_aFJOHTvlJ0T0uoFODlZt9x0zh4VDMvvugHwZiA_GhDB_c7NnmT7_ZaX0y34Y86KKDLc727HyQOyqi8s1quXgm5qAY19zQbBj0-DqpaBrhvSj0EpYA6GBLXujK4dpIB0WlTQ-EV6CzMHpL38rZXa1uIB-D6oHzNZR12fsnlNm-IoERKx5CP_HIFBn29Ie0HVpXMqgglLy8Crp-BJ8JLQtI1tB5mL-oeifX_lBkdF2Jl4JOqSc_Zu0r3THTQA1tdXM-BYvAkNy7s4iiLv1W8DxQys1POCb9y7G7yDP37uJC9sj7i-VFFRQ_3CFtLswFEIdoWJUjKqWrCMGjoXVIFT7tnXYukoOEpn7TLFMSci9LBSt5W32jhBraKLiicO44xuq6eAhTZghUdVXy8q2nGxWZG_QDCwnn1jQoNZ-LxZudhbrChu7XmEWIcw1FD6fs7uMRgWsfqCb_BXwVH1372SpKnAxFvXELsrczLyxLDTDTSeZmI5iE4Tn62vebqGwmdJxeQqxFJSU9_lWroT4TMgqJPRofZWTCzFJWojtFN9elxCf9TG4en3p4Z0ZP2lunMSer9zyZe8URF9Pt_VrYBoWfhqf0ETj1oAyvk2W0rfSyuzl5A7V7sPTk8vF8dykuqok4twsxgz-_AxST6y4pRPZZKV7N-qnf4NqRy6sX2kfmbhJXC6mWNEOa11XNe7XHuHpXxMk2-QxNP9OZryYgEyFrgXiJmtqfvG2w_LZotTKUPWWKYF6p4g1Ygf2p8OHbD4_E0vCSwOMjpIfrT7MAfLun9rDwO7L4_ulexod9dVKT8ryJUrBjFuWJRsfzRQoe_q1kFEc-S2iRX_qPf5N3C3sGfUtdSV0dRuMe_7G_OXVLRO4oOu_O3b2O_nD9fMMU6teflVhF0GU-7diL1tWn7a9zsHOA1jrmpQV7wMSldv-TNxzyyVBi-MV7nqVI_BQulb5rZlnHaX5WV4UwO8PXxLAO4PgVaafj8gcBi6m7Iz0NRErZFVnD_g5dXVGaok7XM_8UYnZd2HV6kxwX9ZrE_rQl-3F8s701_qwQsq5y_sAqZopqrnntERn36Eb-1XbbWnZS5d0WCdp7zrUGSuItHXrh20i9ogiPlXCXDgy1UzTMt0FUX-8rG0MavdQ2Mn4Bu-xjEzhCRU_dcHklRESMA6P-6RwtDoOzINycjuwzO4_4_YZX75bih578geAtMy9ObGbfrZIYZXZc40ESGH_OE4NRBQJj_iUsb3ughJRRqGaut1Bwe44-Ox-5N7BhFZF_UU5AmN1N5Kua24QSK8Ityg3pLLMMW2BeFLKRInNNNAqjgOx-UgJLELABWNi4tx_CtGrqp5tASaqhJQsZe0b6BX1FWqg_zVNBUnwv0enHGietT0im-jNUZldDvZ-yxIrO4hKZuahj-EurOqni23DEgsoy1fKQeDauE6s1IuIU1wqvkmvzWUIIojikl5FbfoMbRhWHbRRUKIko44wtygqgQ8Yjg7VKpO8Z4tIF6nLXQ5fhwX-5xOIGBTMEysOQiPnr7uEC4mQdbxSPeHHAUNmEGVuAORZSir4Bi0VrrbUadq1SACdV8IP-Uzc2oqKvN0GLVmyZalY8klZEuAab-N68i9-mvP13MY8CZE2MCHune6BB_NqdQOuusuh2ftVkYdPnNzOEvP8S_oule-GG0sFcmlrJaUHL971jeRELvAE9t1K7E0SqVpoZXDKKlOIsOKyJrBho9sOCFw9i_k7Kua7rOLI4Y_hcKVL6N8R4POXZnRgYbhDNp8VNSMfvKN-EjnzgyQB-4_DcgNBNg8GVVAP_zX0i6qsvIVuVGt5hlR7YmB5auR76pH_7rj6_PsrJfuTLj7I2UyRlxEfYXRXghufTuVNsWgRB_cWh9dl3dUMHqiILL3mZ-iuwbP6LSv3jIXEcmD20wa33HWoGk7vg1OSC6klWTJLTRp5-U5qNudHOrLs0k0Xkcv-wP9HyV0i1n4qFhTyesbOe5v7SMVAhdpnfEtlUkUwFxKns-FOeYx6bn_y7llEGUwkwJu6_y2iEzo7dAN1TPwphwYAVrZcA7XZzCnI4kyvGJ5IKvoTwPUmkU8oSPDuofUVfLZ-lvspLo4Yv5-h5EVDfUKqVV-lgl7HxVvmjh3DEbdqintQkhJDxnlCFS72dZwdVCHs_0pRwiPmvcIdNED6tu0Q7q55mw7C_sddQULaiPnyRkaEKyTDQ8VgKVOalnEa2kNjrklGvuLTYaU77H_rjNlBloRCewisoAEpDiIZio_3WMQgXmrEQBka6iYx9Awt4F3APpSk1VI7sRCWSVNxkYp0bpZ8oEi5UYVSVyUqP5-g0pWMaZ60CYwN6kjsUaVuNccbEGh-mTr2ati0k2FPo8gaqcSjs5yExEwNl9zqVq7dwIIaUbp3ByUrwiQVm3ltYIF9xvthS34_tkQUwBrm7_WvnB7M-iTOviAoSeMw4shoUemV976_-1G4DFJmsSkdJXr9jOmSDwMYksWeYwvauqV7PyR7u3JAHzFGUq0U4jpe0Mq_TYSSIgrsgjAfdb1gBcA532-ikRajtVbooXss2N3iHYEJs6SaVsktuJRubvqFdkSIAeX4y_3qA_iTzIe3mdD0SBTfQqffG-MBKDxNINa8Ep8HqpHC_1rw9IkWKIxUL8HAiUEVg3yab-p5auVVDlUjB7R7kZAqvooxGEDwcUrKKpjdCDXQwxrTsqG5innooZAAzSrIlR5bUGMbMQjaYVKzMXfmDP3qgO-Pch_21vRXFi3vhWl2sYolzsJ7npkSw9udhclb5PoSDxJuUbLSPeL0cpWJPRbKj9zn2NrfsB075LtO-rDe6kh6jDIOuwsN33-vVnE590VsEeXuY7EEJJU99MaB6L9B-DZQdqges9tcRo61xZhVxi1jHj0d6U-Hcc9ifpChEUWye0j8SDC-kUvi1WK6XGqwAmtFVsoPxtCfspOUF_0aIzU7eD_UGKfJ3jST8rP6OgDASqZvKPiPxK5Im7pdGIVj8hApK5yM08-oWEm9zEvmhERmdls-pyAdrm0VITfoyWovRZhkM4hxTrsRJ3C1Z6e0TAU7bWI-tsJ5vTmeTK65Y-VQARDKoS7rFZTlFLO1j5pHEZzEJoykJV_tuKpElIF1kPAaZaHT0VC99nNU4IW-o8tD0yv6kYsISJg4TmV3ScG3A265hd8XuDS3tCc45OXnbhDb3VYDiYfjIN9PnGser9lIvo0jrKmBwJXOGSwHCGPka_wv0WVZ33M2iec8FoKqEsEwM0DecomA5QbLxmIb06E7jZTfgnRHtJU1SPrT6oUM3HYQR2qEM-coNNZo2c39BtL6d6VilvbwiyCF9ythrvm5AjrGajETx-E7a1Ieuf7xHzgh4pkMlt6arUjt03oUlncwN7_AE2RFzHYiXdvN53M2zHxSFlKSTa1DJYoZxYoHJLKeqv2DzfqE7eMLiO_Z-XfBtD-OAWnIvusczdBjXQEleYLUJzyT7cFem3MmQYL3reRAyDX6YFaHZ5pD4lDnuEJpy4IetL16YKOY6RpYFQ8sW5VMwFrPHMONXPNia8rWZO4MSHd6AOyyBfWYF1S_8NRuH81iWwthV2902QFHgtWYI2wG8K0-Wk2fnmFBy-IiFIFA03ykj3nCtP0EStt2mxVcUHM49OljfWz-iZi3Se1pA8opzRPBglJFUfZAbtpY3CXFu15s374NA1La4o08eJc0EK4RkC8u0DG15x_OKGd6PEWBE-Dsz4QS1d296BepSj_L6W1g0ng_LZJZPE8qbCuXesMZ5USu3f0YE0vm8YWtAu0sh4PyMduXA3vNX-peZGYr2KNW6eOceMAWCNluPuUdWYT0CGXDXiM09kO3pJuTCo4MRVpfu9c_VZ3YPE0tS6nbXSiBak9Gm8ZM2bFi6lGZPD6HHn2RnYbBbWv8Z5Nv4QWYfgAk2i8fAQXAeYD0WGkuRrY9A9af3rxT5d5muHM7o02bET85GnN4tXLC4Q12WWPkr8ankc2945aHnvtuWH14U6vxXUxOYG3f04HWBcAbRiY9A1afn_zuKGX4P6ndtRJyH706EytQX28s4Pl1geOYFXa-K6mkNc0Xp69bjL3GkMEAYmGsCumQTY4o3J0J50iM-aoUY8b6PkkKdmkI6QU8Ye0YR6xO8aGtHo8r24I65KFz2eeYCG2PQFovK71SGHM0QO8RlBueOMHX5R1pO3D-Q535gC8gWMA3qh_A9k0XSt3VwH6mKDlOTHNzgN9BFqXF6I8QaxcL9aiXrUdhqvL7wzEcFVxajGFPkioy0mXk-Gzax31dxxwz-UlfK5j6E00k-sbyRUJ5jeawV-JPcvxr8ZzEjUBHHVxetIJ9oQuz5JLmF1dEBt6zfUeIFqbugwFHON4nbFjkaO91OuhYK8u3-r6qnwR55MnJPE9wPrLRdgdKLUpFKraY6ajlFPiY-rgF7KrjWC_IYhdb0tfeH4SgPh7T0lePxSfgtn-XJjVASmPogQ74sZw4cZjRZGYVU55g5ed51cujiysurQgR67FlGNkdLU72FaKrAkxUhqL8wSEexPQdLQ6hZOefmLtGrwiCZtIO-DAv3QBSRxTBZbErXXa2166-VRhhRnRLd9Xj9xzw1cJEvgdjStzzflzd8voyeANjr5d7GInSih1VhsSQmBWI5PIHx6P-anMGzN9ceU-TWUbpdExTUE8bR5mC4TRFZvWLfj9dFDxqdIfqq9iEAjaSc_viqDS8fToGwWLMa6i4Mlv9USlC04XwvvCKQH9JQg3_ZXHSnuuZf26MYFsKr5yurIlZ4SoSNAlfqiwpTdlMd4x1Cl51AGvAlIBYbFPJ6GCoswmNvirxQBM_S6UaKn5pFcmMb8nboe99svPokTRNrQkDwo9AXNBhhBlpsij56UhgrtGhK9-YvA8Ksx-HGWToysjGaro7KtwhT8xSTBRuA5xhHDKoyCxYMq6neCBFnPFbolsbvan8gV5gW65hcIyCEC8Qs6EQEXeAP6RAYDZrRQbxRFR5NZPj27zgetUkAMRENLzHIJhKq7-wUEXMJIjldWnemc8oUdhtz4gO_dTMkr5s0Gbpxeh8TAl9vQ1WBjjQShSTNS6lz3HXYRXDFtc2uKVzTloUgTVb83kFt9EdvGVs93kL94rRamAk-TtKL-cPw0uE--Mg0_cBVJUqwThXiWjjlHr_jZLRI5ToAgd0i_FFg__hJvpJLPh9jbdmBMxIF7IYBm7Yqj6tMjQdn6Mh13QvvD_sbBvlPlediHWSuqICU_irQcMIjM0aotzOqmjitszveD5XY9PWC6TDMpUEKKxJTGWhpPm-RxaVYaIjkbYgnS_GPogINusdJVzdMV1mMsqnlBaQj8iqKA1MVEnFQ4ePQIRxh6dG0LCls-lH7-ooyjnRpqoOZ690XzNAZmtK0BfpKfwYk9AAjHB2n4JCa42inCJP0_4R6IemCzOIJj9xNQfKks7OD-lJ1Ole1DbdfefeuipAjRPvsJMy6ZTOLEHdLL6vUHXJLKcuckyZcnQomEYSk5jZ6oLKoaevg0GTENM6-6XYlQt8nW8EJhf4OQAtPQbHTjmLNvS8zMJPFBTYjL2shHo6myTGl736V9pyUy05fTX7CSujg4mwooYTT33OyRgkxi73zRDF-rxco76xM5Vsrn7uuwRHTnC-WlwV7HxBc7XU3MkTZdTxZCk9jrJOGp69rEggxSGTubzBht7VQzqiVdlv9UYwDfNuXqZRsuFGEpILczgzEf1EjfETcMUIXQJJ1HSzJepI7_JnfFJ5hE8OsFGzHeW4zFxGfy6ktY_HZcsmUbG5NjqR8wUgUHwinyFNMbJK_gpstAxb-g6-kIUfS1xELb69Qj65h8JQZRJEyrRMiA_LPxNRurWqsu8Qsv9o6cbP9Vxu9fw2bkj6QtdTFPxJPs2EL-Bj9TwI4gf6acyVNskzBw__7T_akf-lSeZf4tB4dTJa7FeINqHl6ThiOpQ4mj4cZ7uAUGyjbDNPq-bQZsbk_R5y-9MgZY_KckEqkkqVRA6qV1RVMdWcjHsDzOa2qcAZ44BDL1HFLJKtXsYArulQUPdkeljjdTPg3ktwSF3XB9tN_hRVRd8j_flQM1rCrHAqceScQVYGqKEJiqxOIF_be8yNGVf-e6MEDlUcY-MPNKljyOVlseaEdOIusbj6HUw4hlXFxKWl0-DjzBavxYIWMhOsSK08kuxKXqRTSGrltSuqek7mS8g7qNTjn5BN-TfmvRTkRGhgsZJVXRRkYn7NZ_I1nQEjZlO1NG7rmCQv83Ap3GwcOiurgMfziGjlhqcc8NT4KgktVh7qQtA0zjsww6NPMMmwbsJMxM6Ew3h6t5Ewj5qbpSddX4OVxkfd-DljP9DegZUln-xRznjNGF9auWecFqjyrceHp1yrcZcmiCMP-ni0vHl-c7TOfypdoetkqlAdP9m9OR-FtAcJqVTy78k65sxeaJIk9XHoFMeRSaWQjJRYsksH1mqPyUDMc1rpU_qHyHwfcIw_vlFTmLNuCPwlkDqNh4l8IQkS9iBdoaFaSBmkGYTfXAUsNrPiV38MiIyPmN5tlAAcskvwDkJ5Vz-mryxtRM78mFa1kfEJqIRfH9bmdtUPggqAi7kfDXzvjZ9weOkqQMuxog-53DwAaSndkFempTxmm_tCsSqaC73XPgaHCT_PAPifMty8-Ru3PEvNdAvlQlnxSnJOxgcDx8VAvxU7X2FXI-1idfXdVdCke5W5ilVt-mrZv4MkFe3HTjYfmG8fbLXRLFQXplqqrqZWxvxQYlYr9_W5nNfL5yly0 \ No newline at end of file +xLthS-Kcil-klqBhV71swinkdhSghYIrZiVEJjrvk6nFjZQfBHUMi0uP2Su2ikFavd-_1UW124aWwNXc4dsoZmJThHzDqtJp-0VZWFAooACuFep17K5flAFa9eS5Y-yHtyO0PpbCuuIIUnRVGiYZE6Pl4Tu12eeuHnZoXnsC2ULROhnASi1O_5iKnt7y7Q4ff2Sa9pJ-wFFF_-Vugq-qvrTRbBp5aB7uioYEVvUYd8ZQa4LnHUSuuUWEyOSBcA0TWfYptwBuIOpI-2X6cCCCqlWe_Zg365B0OVfj56SKOFxSB71nUdPwSVhgvFGoFlXUl6S7VqDf51E-XVGKSyGV1cgyRaeXM5Ly4eD6iXsXG_oEZO9D_OH28V0nZuzYZWh8E2Xs2dlnC_wLO1WVnPZSV_nNKRxSfVxbVuyY6p_1_7Yte-GEqd4GvtzVoI0TgFM6GVeuMCGfpE5OwIZ6X8iw7Fx2evXnm4iM7yKGbqKVcYSapy4DeO2JFnUYFNYSeHm-1WwtA8S3jQGO8JZuVd_VTtzFG7w6O9vUGBOZc4q0GP3m1a8KRZum8o2_WiKk1nnA8SufANQ3zizTOp-GnRJ2fEfQF_rd2CKBo4X9aqS5a5Oy1setOkq78aLSDaLjSXHCuZ_vyte0gckZ-czHZ46XS9VF8ydfbeg75Kb_Qb__tzEdGytKtvBZBKWeoX06-KZB6Mqrrt9a0RoMRUCa9mnUbcm7SNf2iEWApM2Zlxk0W15S3pmnnEap8CSbmIIA4PDV1lE7qnHnA3xbXf0S0b7b5kHCbhdW7En0GqGeH3kooPPhtZK3tHsWoHRGZp_xyik_QZfJr-lfjAzlVePIe1DIqYc6ho7WtPXDZRQb9Ey10fLmxt9iRAIUDaWk0TFxDcms4awt-al0yW7Q_ovodK50D_yy5tyuEAVa3gKmxHa7ngWw1eDkbP6clY8xbBoRTkQ2zK1z9TexwG3-WwKF4DrFdpjiaabSzpkf0_IrY24OydmwUPnjll142VrFI64xjlOEawpS2Q2kH2Ia9YNxMyDJwtOZG-bqAnZUe1-WpUitTQSdB_xtf7dzXi4qYa6Q8eu81lahmaKuAL6w8ARvs5MXiPTks-Xl4aSiuNZhve3nDmh3xnxyfliXWG4ACqJmOCEB0e_KYv_48Xg5P4zinVaiqZIyYljLVSpiVrOpDxqEg3ZNg5CEygGKq-3qqeZtrDhIMWu10g4N96_xUGfpT0Vfmql8joJjY9QM573rOIbCK07oZp_VVFfNbxgsK-Eklry8fP55VcO4tmZrm5a9CXiBKJc76KYKofYoE7LUuuhBuWRIrxVdW7AKe9s8ZxOD4iKxa4614Ap444-CahOaEqeo2WhHPgxNj3Q-tHV_BI7ZKWaiIolYrGOqhBmf41EwOpMUVC4vD97aW6QGdvETQ72Xu0xsqaLmlk30eN_YpHsYl0JviqePN5hQs7izNYXKATpIwnIyhRuHfcpC01cS34ZnzkiBwM99HHb5em0xTaK6PdaMmLh-Ev2Zz6mGiH-g8bEjNZN5DdNsYUW7Ev013b_Zbv1cq9YkSrh2FGMpDP2BeiGeaIh_WcI-iy_4gZSukJQBlaVppOve7bzEwm5dS8Yiar7x2isBSYdWeBNxuT_k5v0PxLIpGYT6wEb9l1E3FUFiDNwpombmXg_sso5Exo3coYwUevpmLq3v3oSGSqXbEtae2RIe33IzpR214XkS1oYO8ZvADiYKxPNRk8LwUa9W5wdUjl4pLFg8g7ajOd3s_l2JDnSNfw-khg-Ulpozl3f-UUxgV-GUcuYxtUa0Q1nikmxVZYJsXpM8UPjpJvCZJFOb-XMQN_CMFZYQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc23otBdIFyzZkGl6tqoq0xE8iP0jMxorGIV5nF22_qg8vyOpsPdTKCpME6GBZFk8qR97rSDi4A7R6zOZvpCvjZkKndzb8yl4G5oW55_E5xIigTNmepNFyjDYDLhT9x2iN1NvAyJ67w_FOLuSTfWR2ZaNyV06iSgDlGHEyqBtnPN9Hn_oTWpx6SGOAUV6idWcM19GV1aot0cOSz21bFbivcJnzxx7WPXlUNxeWvoTEUDqrGI97eynRb2ncUw4PZ3DnT0uTdZwwhUWvDuQQMfg93c5xMJegeJPRCH8BtGWDGLc57LUzMVBwHeDWYtB8XkpeOLWS3ArsldifmdnFNDgTNmx1dT8dDq2SRDRcDmbtLcbNeu3yLZmyYYUC4PcnWrqVpaShDgDufvsgQyIRPHtgaAOS8Rh85zPHe2zXMx_ILfA_Cm8d--2L9qIrQh15blgdA1uoqTE_CtSzTcgxiAaXr3YZ4FCID25aA_ZvvofKaFo2WfviyfdVTzM9db1IN1Q2SRI7aLnoSvm3hKvxXvMAKk_FiSvUHIUHVoRpJ2uJVBhkgtxyfirQCu94sBF0evRk0knmaiAZocQDV_HV4M0c_EKn8NXOufl0MKeWJ6ClC5yuEnyAtJguI98iqsv8oJyKY-1ZaERcFSPddXw6EeozHNSLyw-mpAcp3vAcE5h1ZsZch7QYgd7xltm_2I2ESxO7sNjGdI1KiedBNKaeWatGBm9DjN90Kvifand_vffVaOTq8eWKhC_jr8ZOt7NsKVld5vvzyzQqWyVNVvRDlBVNgwbIujx6_QR4H_Q_juRo8Fy2u1FbPYLE4ivS4yss7P98Yrp1PTLxNgtQ0wpVCicz2Tr1QlhlSmGI1735-MW2KZnLo4ggME8oHwXHp4jDNDmmuOM89nWb9dVKTmw-cVwJSlPD8-bYGomXh_O5YwJA6wExrZabWa6iJXkcUfhj-OJz_zbQUsCy1G-oylCbHOYpKt08AqtyXQMpCpNlFPMhj6U30Dsscad5dm-iOdZU5Wa4tycFnKNpu_DdfgwldPyzFdnqU7Ht8ER0bzRMgS-2Fabo6WMz9cr4qffmK9eIc9wHKAxcg8YpPmW9q5LlxM8-Wh__w3uNqAChXQoZ8FHOB3Owvc47hgzhYe_YJAkoMGCRw2_WtqjG6uhkpmJQoq5-otwNm5c6a1_jagXjYS9F3dkKPjtnt2Jb7E3FgM5rOQ51QJWXW2hLv2TZRfk8Ey2uKBm7A8JNNzDo8X3Hthw7JrdhRY-gcBXfenXOeHduv2RDN1pw9VoOtZWMPJyGYHH6XdEraKSZAd3VhCbcP26tMBAAE6EOG0vn07zWuHLa6QRC8kIcbzqgBPjCx0OSPeGyQv1CMFScubnwuzXyNBXKCjsNkH89Wn0b5E9Ed3LoMHG4eWjv3jH4lS8tLMbpZu6y7Mfq3vSJV8NZ_F7KJQ8MxaELgAdMjWw01HaPfJe9Al_5DZddkUmICKKAArpG36Frghud-SlE5jxy52wAM9LsgRHSTCiR0XZ3Zj4o6Qr1f0UhLk-CUk4JeML1ieMtGCoIdGjv-uQqgFwuf5U5QLPfw1utAGZXEvNjLq51RKU-W7GK71kYED2hDLrEhwn-4BKUIbAhiLJsCM17pzq772OFZqzA4K0gbFZw387s1C9mVNgmKs8Dx4x3Ljm7FrEH_8YP-U_c2yrKvN0MLV0y3clY8jlZMaAbX-Nwei9-ofP13MY0CZ0opqHyne6BVorshQOvazuBAfpVkWdvnLzVsbHSO_YulfsGV7MFYnFr3qUH5D71ffREDvAEft2K7E0KqVJodX5KSlPIs5azJrAho1-OCFw9k_k7CubxjuMe8PxNDezgDTHgoJaCSCzO5hPmzOzw_ZLFJY-fplEDGioVr7rWqIvMxGWLyvoFzTm_1kb6N6vN_CGotrXeBTXnB5nwht_nnV1tzUvPsTJfOhPvESCluRI2VRH0NRLMnDQDQNb0tsHzjncIlUhKYKfgwaf8dH5_Ugev5vXmasyA4AJ0re1hGy143DAvxleb31urvyBAMhZCPtx-j2mawB1ekG5majyrFdTDBth-90E9cXDQl5sKbIaKcTvB-wjk_ciQUThZ2-tOmGlZoA4kmkMTmz7pdFjTMjPy2V-6N7Ev2DZNXgivGqrL5FgpDJ1qmkXkP27TjP1gfA3SnTShOJV0O1mDwwvH-MBxwUJLbgKP2YfqmBywHJpjS-F6Nhauxy_AFiuHrmlREzsNlt9MrSTPu_WvsptKnzTihvxVBJTyRD-Lus2MsrSwuHYMq5-nxyvssgNtQKRAJz-xT8ihuIMKGVSe-n-TYTu5SlJfU-XNXbsAIuIT7zMrU-kyfioZgBR8ex2snAEmhSE-6nhuTZ9ejQJkohiaBpSGyZXESTX3zkLYp8G_Az0jVX1bnbf310VPIl7-Fhx0vLnjqpY5b16EOptNMRNh66TpGF6iez0ix6qMg1Uo2OUTWCdLMxRpLnV3sLjdxqV07TEv-KeR7fSooVxltLS5Rz8lZfErRr-EwUxhtFLLjuTfvju-zuTETjJQ0PHtSEx3pp7aWiXv6ShpvDmGqTFFOmCrhxygq3MqGf8C7HfSGMS_4Zex3eoFn6oJYvE4xG1yGpdG0Ca_RYOTEDxnGPnkVKcfCOlesBImxkItSXrIJ7MnEgRPFuv7IVaVIXsOvVdtecVEXSrzYIT407luH1V_jFWL0U4fgZkPDBUbDg4ERr3Srsbv03inJ8UidVW6y0XNGgPPkBqESSkHzOKR8WSxlmrKOlWTW-t97hDrZQuwoCooEj-jUvhbpDBETXVRxbL-q1zynXIoXQEVirGjxbfTGkfNQaQs-Eoj3zWQo3fSXTxDJUCzwt6LOxJL1E6lpIdzsJBmFLPvoalAIX_nEFhct_NXKN2Q4GdbwBKYsYrev4HRwrF9Z2SW_imybhdoWQzjROqAopfKKmUsBE3pEeTzMr7saOtnEWNWI5v0nTLD6avjJPnlNEtdhp62pZxVzi6jHj4x6UvpDCFMuFkk8v3j04P3keBbttTiD24mAxNDGx1_xtjH7E2xirxpxp9zWKMsEVdq7AKKfNBgFb2sBaIhrBkrz7Ozw1_8ITZC7EciVZPMLkgeOi8KTj5kWR_eFB0zloH-Qep3LVE0TMJjEtaANByTTKmdVvckEj0t0Ono0BIdWnPvlcDeoUO4HxJ5XOlhsWcHg6hXyfCOTvyf1rYQDuwFu-EBY-Dy__QSULoHurxBKWJWNG7p2SSctXCe8ikFaWFeZNRSvEOD2MuBXA-V1JKOAMvD4vAi1lfC7LY36KSBgDUJNmgUuBiIDDTUaCcF2delpdAuBGD_A0cACbET0H_5_Iv0Z_d0D62kfM4DodGAslI60fdLQ4EIAglQ2Ku0mmjcrMch5j7TrubmNLsR9vODQ9-jRU-R6REVU_eEOSalTMQS9wo-cdcFtu_6pQlRd0KexLIIq4tluyVWCvHnIlsnxMM3dz5Ukj1gyy-F7unV7zbLl7j4nuRPVQoNaioA_1gppwEQXywW5ScHe4ML_aMpAKgc6VAIlT6Wnj9nj3E-lARc_DcSO94yTJwlft3L3cQCbNfYBDQhy9rrX6eIfPz782bKNAYjgiqBKXMZeTfGLBpTUZWZ_2fKRgXMYGOYqbO3MhNOhMWDehNRT7wlLM8NXPKSaLx06m5fn6iHfxNjUCAtmMF0CNAYz0cG3P5quiGAa01gy7ti5oGNI2b07e7LMue3bsRCN7f2a0H-MMXychga7EBtc8El4daLX2MBpQRtVhBR0NACKoYqitMMIwhqptwPEIhzs9cL6y0cwYtnLAHTaAq0MGEi0vW1bu2Bn5Q0Qe3MwFORGEaqT06Vu0kqLfnwSLenS6Bclp5u1gW4QljjwnSd6QUWmWnKj6wrrdFC0QGN70SuAYWrAxpN7ApuDFlst8gyWkK1Q0N81jABIGQN_rjSLeGQXLNbweLbHiL3flK9sXQU5PuMc5veL5WkMa6jS_tJ8X5pN-gU3P_GErnKd4wUUsqTNc9mmEQwsN94QGCfz3-CAsJXagugDuhLgbGzAZWiLLgJwa5fjLH1Mgv4Q1Mf5g1SXzprzAqecIdsN0AuukN1QbSa3f7MW2uHgxNTZEAq0zGBKSp_R2fCRfXKY2w9r_qSWH2iyDZpzjxMj0Ea0hS1Om8pTjRQYPA2qhW8TGsX5naR6VMVK2eu0ntb7rGgDXARtWgeL7msVLMyiNDRZLFkHoLP3uu9bdJIuWN7SmxGYKoEpWcA1Oj_ijA8YKHcxAsg5oGtJo7LIAsnks5Q8R8W6xgLQ4L88TQ-rMf4HoApUZmiLnpLMAp0rCFstOAqOMHYjs3YmwRzxQyMencW5nGAbUo7KYel0nNqlr8gDmiQ-wUfFZKjq61ypHX8aRI-BeaZEq-E3Jm_Yfqz_-kWc1ydRpIu726_u5YLlCMJicy_-y_IfBA9j0HPoDRysydhZIEq-yoLcdvuI7yMPUBHHVzetqLyGOoNyvX1eeiSOmZJc9Bw1uf98X64Qtn2-ZLD4HOwIO7OuMZ411SpZ131uQ0kVVld5vvzyDIAzVco7qAY4v8GULNM_bKM-_hjy-6qK3LnWPK4cRig3gSNrjpW-m0TnvrJq-4ZSqMO-syIWbrdak9ycWF_OnKdnaSLR5DaudfdL5fUgYNTx2zJKYFhkUPWia7t3JKUcfi2cxqLLSu-orTqwL39PRuzozRVak6oFqQPg-Zn1E5NHacoQG7Koi2E5phmkj0X7uwv25zlwtMBSJ8ivzA6Tqel9uEuZcvYsRnVZfMpWs7VAKelnrFh56EEsw6BS-tkwpRolNuVmRbOBnaSnsy44WHmntZmLZLhr6EsPAKRKkO9ZuaYkwXvLLzdQRpRo-HabHQVEB0aw9sSB5ynwjZ6y2y5X7ilEiLNIxRCUBipKlVmnFggE_hTUE9bRTsG4DRFZ4XBfj9dFDvqdofpKBiE2laSY_njrDS8vjonQXLMiQiKMlwBEEVy1B3okLH0JAL5ge_uC5vubZ2Cn9Oo9_fHLKZWdMk43pwsSRUd3pJow7UzFPs2vUA6S-ZHViN1hUoYFdiHpsmhoPpK_dK_ScQuLp5pFwrcnurpmG2FjEdwzijNMguth8OlwZfLUTkAtaqHhxMddTWjm7-7duXHAtYcX0hb-jQbOBiEekzwymouRBBw85BhJrS-yTRZcyAnvC9DuD7-wNrFUn8IAm1p8-wRvqh43ZA4CmzH1A5YOFYzgq_6bfj7kgBqYRYfzA56xrPMmPYilQAMSPcawt1rpAWQLfirxBM0m6_scWkPBcFvq4brfkm25k8f59LkKfzFGC2PiZJjRzQzWQjrL969i4qzUOxXH_rtT4qLTFfBT-9v4dM2kFzE-mzw45zdpgXiYNeI_UKQ3mTzy5HwYrYFDmL5f-mZjeFcMNvXQBrA7h3TZ9OKmJNpR6VmFZbvL6G17q2gOVcD_ggI2wb_1L5I58v5cKVzNZJ-5L7CWSJtfdKdHtZsYV2BZqLpQhAKvNSuIrocQgZakOAxqVekQg0hyllCc2lUMU-CBcswOOIfzimqtUbD6UehCdwnrzB2LN4Fcsuf7E7sJzaRZmIBhHD8G-sdLF8evHLa6AKo_UzYqFoAFmmKdcMtxK4V3S7SE1TjqNpOr7qV4vcX0bw1MaThTa8EC--YyEG8rcdpR4dG0LEdsylm7sooyjMRBqvnUqQD3wkL6XsiFKZYvZt5TI4KRZM13FQPF8HOIOgY3-FMCXGJfQmadQ_qWj4MQ86i9-hJ1RFu1_c_XuXevZ_5QUYhGajxqJX0Mb6JGKhbb6J1KSwKPRI9R5hF2ufAuH60RfrJHmFbbCoHmsWhpLKz_NLAA7XoSV8sJf9OQhGRDTLDmKIdSWsNzr8BTEjjdyOmFDfxwfNwco-Gd0zuZppPR_ChYMgVUhF9Hc-yEpv7huK9SFTiq_xLXGqT8Uy7OEB4VZWR8PydJ6rlho14jkPx8uDR6iyVN4mr4jhN56fD47S-WRzp9zBdysb2fzlqoNgf_adxuojm856c8FJZZK5EPcUshCsbLA-e4T6R5WcfBSDw92IdBOJ-FcfZF4Y6eJISM5Mb0JqyZ2_oQfJdTF3ARngKdhQ0nSgOLfl6g7NoTklBc9z5pX-shX_f2fgWw76EoaL922ngs5M8JQ9kfn9QoRjGdqMtnhGZMu8PISWv3DLNI7M_2EMXd8scaT-tJ2JHhVwGKVpVnbXHG4OdQSNofjTMq-dzuHVLylixbm7tC7Uj_vqlWmtWUltviizbezgNyJHxTTFyKNYwliUNJzHhLFePvUo0eKOkAh3V1jDlZO1gYj7aKJAtPH5prncftXg0OgPD49fiA9ggRbiAr76t1fLJrKbnzPTNYF7jNM3bbS9QEglPRRcK_5_-ChUCFvcgeMiL2qpLz3t6WojdRioL-yj56gg7zFb8ZnXBx00JvUWLOytmcVNlIeVCm9nlp-CXrvoNVYZN0EM3ypIuLsmD55ejMJaue0vVpMhFeUgwXrFr28mekdu18A7_NqYV5hRlqQGfjUm5EIzju4qxRbIMkY7UJnA7KXre1Bu1wxsHSiUYPXuToCKSRHLeVR4BfjUUlY1Ne5Agbzgnv6apG7bitxGWxYPR3QBPaPrjCrwa73OSxwqLKRtMVkPtahYBJtT8mYKLJfGuz_j3GhqCDzPEHxQxu8FvTuqauT_2inyZQ4sFcP_Ji6VKdxV1EcS_sAZsYvStDtdt1Eeli5s6Zhx7V4milt9PbQNf9B6GhsZpavX2q5ZpRuP8PX8tyyAAaUqBU_q9yIyfw-RJvlFDmKNcCOAiUDqNBCl9IQkTkkwdwaFXivGlGYIfWAVsNnSdtfaAMPMC4hgwsb9JPjkDZRipN7N_DVEzsbXo2zx0RA3c-Qt5G2DUkmgdMAXUpXshHeJUDmTrhh9bZorv-sR_1IPHqJiFT6v-czXVclwTfAekUpJrpZOWPpbx8JSdqxGBJEfYz9dgwiQdYwy5DR8kZ6hQJ8fzA3_TdmEV2NmSnpVpcL46r360JxVVlnYgEM4K4fUkIgSG1oUTvMLIFe4xzDaMa3dPEfQA-BNBjdOZbb1HdyIqbHLoG5ERiSFAHGUuD5qJ8_03pdDnlRaXkYqnYTxLshyLdNtprwTdPjvMeJuMeJwMexVb0_u0uBOiy-dy0 \ No newline at end of file diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml index 72586ae2ee..5102cd4e59 100644 --- a/docs/logical_data_model.puml +++ b/docs/logical_data_model.puml @@ -435,9 +435,27 @@ class GrantNumberLinks{ deletedAt : timestamp with time zone } +class GrantReplacementTypes{ + * id : integer : +!issue='column reference missing' mapsTo : integer : REFERENCES "GrantReplacementTypes".id +!issue='column should not allow null' * createdAt : timestamp with time zone : now() +!issue='column should not allow null' * name : text +!issue='column should not allow null' * updatedAt : timestamp with time zone : now() + deletedAt : timestamp with time zone +} + +class GrantReplacements{ + * id : integer : + grantReplacementTypeId : integer : REFERENCES "GrantReplacementTypes".id +!issue='column should not allow null' * replacedGrantId : integer : REFERENCES "Grants".id +!issue='column should not allow null' * replacingGrantId : integer : REFERENCES "Grants".id +!issue='column should not allow null' * createdAt : timestamp with time zone : now() +!issue='column should not allow null' * updatedAt : timestamp with time zone : now() +!issue='column type does not match model: date != timestamp with time zone' replacementDate : date +} + class Grants{ * id : integer - oldGrantId : integer : REFERENCES "Grants".id regionId : integer : REFERENCES "Regions".id * recipientId : integer : REFERENCES "Recipients".id * createdAt : timestamp with time zone : now() @@ -450,8 +468,6 @@ class Grants{ granteeName : varchar(255) grantSpecialistEmail : varchar(255) grantSpecialistName : varchar(255) - inactivationDate : timestamp with time zone - inactivationReason : enum programSpecialistEmail : varchar(255) programSpecialistName : varchar(255) startDate : timestamp with time zone @@ -1614,6 +1630,34 @@ class ZALGrantNumberLinks{ session_sig : text } +class ZALGrantReplacementTypes{ + * id : bigint : + * data_id : bigint + * dml_as : bigint + * dml_by : bigint + * dml_timestamp : timestamp with time zone + * dml_txid : uuid + * dml_type : enum + descriptor_id : integer + new_row_data : jsonb + old_row_data : jsonb + session_sig : text +} + +class ZALGrantReplacements{ + * id : bigint : + * data_id : bigint + * dml_as : bigint + * dml_by : bigint + * dml_timestamp : timestamp with time zone + * dml_txid : uuid + * dml_type : enum + descriptor_id : integer + new_row_data : jsonb + old_row_data : jsonb + session_sig : text +} + class ZALGrants{ * id : bigint : * data_id : bigint @@ -2432,6 +2476,9 @@ class ZALZAFilter{ Files "1" --[#black,plain,thickness=2]-- "1" ImportFiles : importFile, file Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNumberLink +!issue='associations need to be defined both directions' +!issue='associations need to be camel case' +Grants "1" --[#d54309,plain,thickness=2]-- "1" GrantReplacements : Grant ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses @@ -2481,12 +2528,16 @@ Goals "1" --[#black,dashed,thickness=2]--{ "n" Objectives : objectives, goal Goals "1" --[#black,dashed,thickness=2]--{ "n" SimScoreGoalCaches : scoreOne, scoreTwo, goalOne, goalTwo GrantNumberLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringClassSummaries : monitoringClassSummaries, grantNumberLink GrantNumberLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewGrantees, grantNumberLink +!issue='associations need to be camel case' +!issue='associations need to be camel case' +GrantReplacementTypes "1" --[#black,dashed,thickness=2]--{ "n" GrantReplacements : GrantReplacements, GrantReplacementType Grants "1" --[#black,dashed,thickness=2]--{ "n" ActivityRecipients : grant, activityRecipients Grants "1" --[#black,dashed,thickness=2]--{ "n" Goals : grant, goals Grants "1" --[#black,dashed,thickness=2]--{ "n" Grants : oldGrants, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" GroupGrants : groupGrants, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" ProgramPersonnel : programPersonnel, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" Programs : programs, grant +Grants "1" --[#black,dashed,thickness=2]--{ "n" undefined : grantRelationships, activeGrantRelationships Groups "1" --[#black,dashed,thickness=2]--{ "n" GroupCollaborators : group, groupCollaborators Groups "1" --[#black,dashed,thickness=2]--{ "n" GroupGrants : group, groupGrants ImportFiles "1" --[#black,dashed,thickness=2]--{ "n" ImportDataFiles : importFile, importDataFiles @@ -2600,4 +2651,7 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, roles Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes +!issue='association missing from models'!issue='associations need to be defined both directions' +GrantReplacementTypes o--[#yellow,bold,thickness=2]--o GrantReplacementTypes : missing-from-model + @enduml diff --git a/src/migrations/20240830172706-populate-grant-replacements.js b/src/migrations/20240830172706-populate-grant-replacements.js new file mode 100644 index 0000000000..f6b08b14ef --- /dev/null +++ b/src/migrations/20240830172706-populate-grant-replacements.js @@ -0,0 +1,141 @@ +const { prepMigration } = require('../lib/migration'); + +const { GRANT_INACTIVATION_REASONS } = require('../constants'); + +const inactivationReasons = Object.values(GRANT_INACTIVATION_REASONS); + +module.exports = { + up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + // Create GrantReplacementTypes table + await queryInterface.createTable('GrantReplacementTypes', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: Sequelize.TEXT, + allowNull: false, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + deletedAt: { + type: Sequelize.DATE, + allowNull: true, + }, + mapsTo: { + type: Sequelize.INTEGER, + references: { + model: 'GrantReplacementTypes', + key: 'id', + }, + allowNull: true, + }, + }, { transaction }); + + // Create GrantReplacements table + await queryInterface.createTable('GrantReplacements', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + replacedGrantId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Grants', + key: 'id', + }, + }, + replacingGrantId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Grants', + key: 'id', + }, + }, + grantReplacementTypeId: { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: 'GrantReplacementTypes', + key: 'id', + }, + }, + replacementDate: { + type: Sequelize.DATEONLY, + allowNull: true, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('NOW'), + }, + }, { transaction }); + + await queryInterface.sequelize.query(/* sql */` + INSERT INTO "GrantReplacements" ( + "replacedGrantId", + "replacingGrantId", + "replacementDate", + "createdAt", + "updatedAt" + ) + SELECT + gr1."oldGrantId" AS "replacedGrantId", + gr1."id" AS "replacingGrantId", + gr2."inactivationDate" AS "replacementDate", + gr1."createdAt", + gr1."updatedAt" + FROM "Grants" gr1 + JOIN "Grants" gr2 + ON gr1."oldGrantId" = gr2.id + WHERE gr1."oldGrantId" IS NOT NULL; + `, { transaction }); + + await queryInterface.removeColumn('Grants', 'oldGrantId', { transaction }); + await queryInterface.removeColumn('Grants', 'inactivationDate', { transaction }); + await queryInterface.removeColumn('Grants', 'inactivationReason', { transaction }); + }, + ), + + down: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + await queryInterface.addColumn('Grants', 'oldGrantId', { + type: Sequelize.INTEGER, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('Grants', 'inactivationDate', { + type: Sequelize.DATE, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('Grants', 'inactivationReason', { + type: Sequelize.ENUM(...inactivationReasons), + allowNull: true, + }, { transaction }); + + await queryInterface.dropTable('GrantReplacements', { transaction }); + await queryInterface.dropTable('GrantReplacementTypes', { transaction }); + }, + ), +}; diff --git a/src/migrations/20240830173101-create-grant-relationship-to-active.js b/src/migrations/20240830173101-create-grant-relationship-to-active.js new file mode 100644 index 0000000000..baf77731e0 --- /dev/null +++ b/src/migrations/20240830173101-create-grant-relationship-to-active.js @@ -0,0 +1,91 @@ +const { prepMigration } = require('../lib/migration'); + +module.exports = { + up: async (queryInterface) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + + await queryInterface.sequelize.query(/* sql */` + CREATE MATERIALIZED VIEW "GrantRelationshipToActive" AS + WITH RECURSIVE recursive_cte AS ( + -- Base query: Case 1: Select all Active grants from the "Grants" table + SELECT + g."id" AS "grantId", + g."id" AS "activeGrantId", + ARRAY[g."id"] AS "visited_grantIds" -- Initialize the array with the first grantId + FROM "Grants" g + WHERE g."status" = 'Active' + + UNION ALL + + -- Base query: Case 2: Select all inactive grants from the "Grants" table that have replaced other grants, but that have not been replaced + SELECT + g."id" AS "grantId", + NULL::int AS "activeGrantId", + ARRAY[g."id"] AS "visited_grantIds" -- Initialize the array with the first grantId + FROM "Grants" g + JOIN "GrantReplacements" gr1 + ON g.id = gr1."replacingGrantId" + LEFT JOIN "GrantReplacements" gr2 + ON g.id = gr2."replacedGrantId" + WHERE g.status != 'Active' + AND gr2.id IS NULL + + UNION ALL + + -- Base query: Case 3: Select all inactive grants from the "Grants" table that have never replaced other grants or been replaced + SELECT + g."id" AS "grantId", + NULL::int AS "activeGrantId", + ARRAY[g."id"] AS "visited_grantIds" -- Initialize the array with the first grantId + FROM "Grants" g + JOIN "GrantReplacements" gr + ON g.id = gr."replacingGrantId" + OR g.id = gr."replacedGrantId" + WHERE g.status != 'Active' + AND gr.id IS NULL + + UNION ALL + + -- Recursive query: Use an array to track visited grantIds + SELECT + g."id" AS "grantId", + rcte."activeGrantId", + "visited_grantIds" || g."id" -- Append the current grantId to the array + FROM recursive_cte rcte + JOIN "GrantReplacements" gr + ON rcte."grantId" = gr."replacingGrantId" + JOIN "Grants" g + ON g."id" = gr."replacedGrantId" + WHERE g."id" != ALL("visited_grantIds") -- Ensure the current grantId hasn't been visited + ) + SELECT DISTINCT + ROW_NUMBER() OVER (ORDER BY rcte."grantId", rcte."activeGrantId") AS "id", -- Add row number as "id" + rcte."grantId", + rcte."activeGrantId" + FROM recursive_cte rcte + WITH NO DATA; + `, { transaction }); + + await queryInterface.sequelize.query(/* sql */` + CREATE INDEX "idx_GrantRelationshipToActive_grantId_activeGrantId" + ON "GrantRelationshipToActive" ("grantId", "activeGrantId"); + `, { transaction }); + + // Initial refresh without CONCURRENTLY to populate the materialized view + await queryInterface.sequelize.query(/* sql */` + REFRESH MATERIALIZED VIEW "GrantRelationshipToActive"; + `, { transaction }); + }, + ), + + down: async (queryInterface) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + + await queryInterface.sequelize.query(/* sql */` + DROP MATERIALIZED VIEW IF EXISTS "GrantRelationshipToActive"; + `, { transaction }); + }, + ), +}; diff --git a/src/models/grant.js b/src/models/grant.js index 31a4238cd0..aa650ae070 100644 --- a/src/models/grant.js +++ b/src/models/grant.js @@ -7,25 +7,9 @@ const { beforeDestroy, } = require('./hooks/grant'); -const { GRANT_INACTIVATION_REASONS } = require('../constants'); - -const inactivationReasons = Object.values(GRANT_INACTIVATION_REASONS); - -/** - * Grants table. Stores grants. - * - * @param {} sequelize - * @param {*} DataTypes - */ export default (sequelize, DataTypes) => { class Grant extends Model { static associate(models) { - /** - * Associations: - * grantNumberLink: GrantNumberLink.grantId - id - * grant: id - GrantNumberLink.grantId - */ - Grant.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' }); Grant.belongsTo(models.Recipient, { foreignKey: 'recipientId', as: 'recipient' }); Grant.hasMany(models.Goal, { foreignKey: 'grantId', as: 'goals' }); @@ -66,11 +50,6 @@ export default (sequelize, DataTypes) => { number: { type: DataTypes.STRING, allowNull: false, - /* - We're not setting unique true here to allow - bulkCreate/updateOnDuplicate to properly match rows on just the id. - unique: true, - */ }, annualFundingMonth: DataTypes.STRING, cdi: { @@ -86,13 +65,10 @@ export default (sequelize, DataTypes) => { stateCode: DataTypes.STRING, startDate: DataTypes.DATE, endDate: DataTypes.DATE, - inactivationDate: DataTypes.DATE, - inactivationReason: DataTypes.ENUM(inactivationReasons), recipientId: { type: DataTypes.INTEGER, allowNull: false, }, - oldGrantId: DataTypes.INTEGER, deleted: { type: DataTypes.BOOLEAN, defaultValue: false, @@ -141,13 +117,6 @@ export default (sequelize, DataTypes) => { }, }, }, { - // defaultScope: { - // where: { - // deleted: false - // } - // }, - // }, - // { sequelize, modelName: 'Grant', hooks: { diff --git a/src/models/grantRelationshipToActive.js b/src/models/grantRelationshipToActive.js new file mode 100644 index 0000000000..2e91ed0544 --- /dev/null +++ b/src/models/grantRelationshipToActive.js @@ -0,0 +1,61 @@ +const { Model } = require('sequelize'); + +export default (sequelize, DataTypes) => { + class GrantRelationshipToActive extends Model { + static associate(models) { + GrantRelationshipToActive.belongsTo(models.Grant, { foreignKey: 'grantId', as: 'grant' }); + GrantRelationshipToActive.belongsTo(models.Grant, { foreignKey: 'activeGrantId', as: 'activeGrant' }); + + models.Grant.hasMany(GrantRelationshipToActive, { foreignKey: 'grantId', as: 'grantRelationships' }); + models.Grant.hasMany(GrantRelationshipToActive, { foreignKey: 'activeGrantId', as: 'activeGrantRelationships' }); + } + + // Static method to refresh the materialized view + static async refresh() { + try { + await sequelize.query('REFRESH MATERIALIZED VIEW "GrantRelationshipToActive";'); + console.log('Materialized view refreshed successfully'); + } catch (error) { + console.error('Error refreshing materialized view:', error); + throw error; + } + } + } + + GrantRelationshipToActive.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + grantId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + activeGrantId: { + type: DataTypes.INTEGER, + allowNull: true, + }, + }, { + sequelize, + timestamps: false, // Disable timestamps since this is a materialized view + freezeTableName: true, // Ensures Sequelize uses the exact table name provided + modelName: 'GrantRelationshipToActive', + }); + + // Override to prevent modifications + GrantRelationshipToActive.beforeCreate(() => { + throw new Error('Insertion not allowed on materialized view'); + }); + + GrantRelationshipToActive.beforeUpdate(() => { + throw new Error('Update not allowed on materialized view'); + }); + + GrantRelationshipToActive.beforeDestroy(() => { + throw new Error('Deletion not allowed on materialized view'); + }); + + return GrantRelationshipToActive; +}; diff --git a/src/models/grantReplacement.js b/src/models/grantReplacement.js new file mode 100644 index 0000000000..9c85516d01 --- /dev/null +++ b/src/models/grantReplacement.js @@ -0,0 +1,25 @@ +const { Model } = require('sequelize'); + +export default (sequelize, DataTypes) => { + class GrantReplacements extends Model { + static associate(models) { + GrantReplacements.belongsTo(models.GrantReplacementTypes, { foreignKey: 'grantReplacementTypeId' }); + GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacedGrantId' }); + GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacingGrantId' }); + } + } + + GrantReplacements.init({ + replacedGrantId: DataTypes.INTEGER, + replacingGrantId: DataTypes.INTEGER, + grantReplacementTypeId: DataTypes.INTEGER, + replacementDate: DataTypes.DATE, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + }, { + sequelize, + modelName: 'GrantReplacements', + }); + + return GrantReplacements; +}; diff --git a/src/models/grantReplacementType.js b/src/models/grantReplacementType.js new file mode 100644 index 0000000000..35a7d00341 --- /dev/null +++ b/src/models/grantReplacementType.js @@ -0,0 +1,22 @@ +const { Model } = require('sequelize'); + +export default (sequelize, DataTypes) => { + class GrantReplacementTypes extends Model { + static associate(models) { + GrantReplacementTypes.hasMany(models.GrantReplacements, { foreignKey: 'grantReplacementTypeId' }); + } + } + + GrantReplacementTypes.init({ + name: DataTypes.TEXT, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE, + deletedAt: DataTypes.DATE, + mapsTo: DataTypes.INTEGER, + }, { + sequelize, + modelName: 'GrantReplacementTypes', + }); + + return GrantReplacementTypes; +}; From 6abe4d4aedf6fa349d05602d8ad2579780ff3598 Mon Sep 17 00:00:00 2001 From: nvms Date: Fri, 30 Aug 2024 16:07:15 -0400 Subject: [PATCH 2/3] update associations, add hook, ensure individualHooks called --- src/goalServices/changeGoalStatus.test.js | 2 +- src/models/grant.js | 2 ++ src/models/grantReplacement.js | 3 +++ src/models/hooks/goalStatusChange.test.js | 2 +- src/models/hooks/grant.js | 13 +++++++++++++ src/scopes/activityReport/index.test.js | 1 + src/scopes/goals/index.test.js | 1 + src/services/objectives.test.js | 1 + .../trHoursOfTrainingByNationalCenter.test.js | 1 + src/widgets/trOverview.test.js | 1 + src/widgets/trReasonlist.test.js | 1 + src/widgets/trSessionsByTopics.test.js | 1 + 12 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/goalServices/changeGoalStatus.test.js b/src/goalServices/changeGoalStatus.test.js index 6d88dc4304..ee5c8d593f 100644 --- a/src/goalServices/changeGoalStatus.test.js +++ b/src/goalServices/changeGoalStatus.test.js @@ -55,7 +55,7 @@ describe('changeGoalStatus service', () => { afterAll(async () => { await db.Goal.destroy({ where: { id: goal.id }, force: true }); await db.GrantNumberLink.destroy({ where: { grantId: grant.id }, force: true }); - await db.Grant.destroy({ where: { id: grant.id } }); + await db.Grant.destroy({ where: { id: grant.id }, individualHooks: true }); await db.Recipient.destroy({ where: { id: recipient.id } }); await db.UserRole.destroy({ where: { userId: user.id } }); await db.Role.destroy({ where: { id: role.id } }); diff --git a/src/models/grant.js b/src/models/grant.js index aa650ae070..7035b88f0e 100644 --- a/src/models/grant.js +++ b/src/models/grant.js @@ -5,6 +5,7 @@ const { afterCreate, afterUpdate, beforeDestroy, + afterDestroy, } = require('./hooks/grant'); export default (sequelize, DataTypes) => { @@ -123,6 +124,7 @@ export default (sequelize, DataTypes) => { afterCreate: async (instance, options) => afterCreate(sequelize, instance, options), afterUpdate: async (instance, options) => afterUpdate(sequelize, instance, options), beforeDestroy: async (instance, options) => beforeDestroy(sequelize, instance, options), + afterDestroy: async (instance, options) => afterDestroy(sequelize, instance, options), }, }); return Grant; diff --git a/src/models/grantReplacement.js b/src/models/grantReplacement.js index 9c85516d01..939e646f7f 100644 --- a/src/models/grantReplacement.js +++ b/src/models/grantReplacement.js @@ -6,6 +6,9 @@ export default (sequelize, DataTypes) => { GrantReplacements.belongsTo(models.GrantReplacementTypes, { foreignKey: 'grantReplacementTypeId' }); GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacedGrantId' }); GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacingGrantId' }); + + models.Grant.hasMany(GrantReplacements, { foreignKey: 'replacedGrantId', as: 'replacedGrantReplacements' }); + models.Grant.hasMany(GrantReplacements, { foreignKey: 'replacingGrantId', as: 'replacingGrantReplacements' }); } } diff --git a/src/models/hooks/goalStatusChange.test.js b/src/models/hooks/goalStatusChange.test.js index 67d7511782..02131d7c1b 100644 --- a/src/models/hooks/goalStatusChange.test.js +++ b/src/models/hooks/goalStatusChange.test.js @@ -53,7 +53,7 @@ describe('GoalStatusChange hooks', () => { await User.destroy({ where: { id: user.id } }); await Goal.destroy({ where: { id: goal.id }, force: true }); await GrantNumberLink.destroy({ where: { grantId: grant.id }, force: true }); - await Grant.destroy({ where: { id: grant.id } }); + await Grant.destroy({ where: { id: grant.id }, individualHooks: true }); await Recipient.destroy({ where: { id: recipient.id } }); await sequelize.close(); }); diff --git a/src/models/hooks/grant.js b/src/models/hooks/grant.js index 9e419ed396..b860868673 100644 --- a/src/models/hooks/grant.js +++ b/src/models/hooks/grant.js @@ -6,12 +6,20 @@ import { const afterCreate = async (sequelize, instance, options) => { await Promise.all([ syncGrantNumberLink(sequelize, instance, options, 'number'), + sequelize.models.GrantRelationshipToActive.refresh(), ]); }; +const checkStatusChangeAndRefresh = async (sequelize, instance) => { + if (instance.changed('status')) { + await sequelize.models.GrantRelationshipToActive.refresh(); + } +}; + const afterUpdate = async (sequelize, instance, options) => { await Promise.all([ syncGrantNumberLink(sequelize, instance, options, 'number'), + checkStatusChangeAndRefresh(sequelize, instance), ]); }; @@ -21,8 +29,13 @@ const beforeDestroy = async (sequelize, instance, options) => { ]); }; +const afterDestroy = async (sequelize, instance, options) => { + await sequelize.models.GrantRelationshipToActive.refresh(); +}; + export { afterCreate, afterUpdate, beforeDestroy, + afterDestroy, }; diff --git a/src/scopes/activityReport/index.test.js b/src/scopes/activityReport/index.test.js index 416e6ce953..3d6878b98c 100644 --- a/src/scopes/activityReport/index.test.js +++ b/src/scopes/activityReport/index.test.js @@ -3519,6 +3519,7 @@ describe('filtersToScopes', () => { await Grant.destroy({ where: { id: grant.id }, + individualHooks: true, }); await Recipient.destroy({ diff --git a/src/scopes/goals/index.test.js b/src/scopes/goals/index.test.js index bade8276a8..4e41915d4a 100644 --- a/src/scopes/goals/index.test.js +++ b/src/scopes/goals/index.test.js @@ -1366,6 +1366,7 @@ describe('goal filtersToScopes', () => { where: { id: greatGrant.id, }, + individualHooks: true, }); await Recipient.destroy({ diff --git a/src/services/objectives.test.js b/src/services/objectives.test.js index ce18ff7bc8..fabfa3e8cc 100644 --- a/src/services/objectives.test.js +++ b/src/services/objectives.test.js @@ -535,6 +535,7 @@ describe('Objectives DB service', () => { id: grant.id, }, force: true, + individualHooks: true, }); await Recipient.destroy({ diff --git a/src/widgets/trHoursOfTrainingByNationalCenter.test.js b/src/widgets/trHoursOfTrainingByNationalCenter.test.js index a3c41e1cac..ec18b22c00 100644 --- a/src/widgets/trHoursOfTrainingByNationalCenter.test.js +++ b/src/widgets/trHoursOfTrainingByNationalCenter.test.js @@ -250,6 +250,7 @@ describe('TR hours of training by national center', () => { where: { id: [grant1.id, grant2.id, grant3.id, grant4.id, grant5.id], }, + individualHooks: true, }); // delete recipients diff --git a/src/widgets/trOverview.test.js b/src/widgets/trOverview.test.js index f9614c37d5..bf4dd8a57e 100644 --- a/src/widgets/trOverview.test.js +++ b/src/widgets/trOverview.test.js @@ -210,6 +210,7 @@ describe('TR overview widget', () => { where: { id: [grant1.id, grant2.id, grant3.id, grant4.id, grant5.id], }, + individualHooks: true, }); // delete recipients diff --git a/src/widgets/trReasonlist.test.js b/src/widgets/trReasonlist.test.js index 976438633a..885e74ad89 100644 --- a/src/widgets/trReasonlist.test.js +++ b/src/widgets/trReasonlist.test.js @@ -223,6 +223,7 @@ describe('TR reason list', () => { where: { id: [grant1.id, grant2.id, grant3.id, grant4.id, grant5.id], }, + individualHooks: true, }); // delete recipients diff --git a/src/widgets/trSessionsByTopics.test.js b/src/widgets/trSessionsByTopics.test.js index 725b12bcef..dbad9f0cd9 100644 --- a/src/widgets/trSessionsByTopics.test.js +++ b/src/widgets/trSessionsByTopics.test.js @@ -248,6 +248,7 @@ describe('TR sessions by topic', () => { where: { id: [grant1.id, grant2.id, grant3.id, grant4.id, grant5.id], }, + individualHooks: true, }); // delete recipients From 422ec1984b2dc86717559c2cd27b65cfc4ed0163 Mon Sep 17 00:00:00 2001 From: nvms Date: Fri, 30 Aug 2024 16:22:47 -0400 Subject: [PATCH 3/3] update model and ldm --- docs/logical_data_model.encoded | 2 +- docs/logical_data_model.puml | 55 ++++++++++--------- ...40830172706-populate-grant-replacements.js | 8 +-- ...101-create-grant-relationship-to-active.js | 8 +-- src/models/grantReplacement.js | 18 +++--- src/models/grantReplacementType.js | 2 +- 6 files changed, 49 insertions(+), 44 deletions(-) diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded index 5094a65624..8dcfea4f0d 100644 --- a/docs/logical_data_model.encoded +++ b/docs/logical_data_model.encoded @@ -1 +1 @@ -xLthS-Kcil-klqBhV71swinkdhSghYIrZiVEJjrvk6nFjZQfBHUMi0uP2Su2ikFavd-_1UW124aWwNXc4dsoZmJThHzDqtJp-0VZWFAooACuFep17K5flAFa9eS5Y-yHtyO0PpbCuuIIUnRVGiYZE6Pl4Tu12eeuHnZoXnsC2ULROhnASi1O_5iKnt7y7Q4ff2Sa9pJ-wFFF_-Vugq-qvrTRbBp5aB7uioYEVvUYd8ZQa4LnHUSuuUWEyOSBcA0TWfYptwBuIOpI-2X6cCCCqlWe_Zg365B0OVfj56SKOFxSB71nUdPwSVhgvFGoFlXUl6S7VqDf51E-XVGKSyGV1cgyRaeXM5Ly4eD6iXsXG_oEZO9D_OH28V0nZuzYZWh8E2Xs2dlnC_wLO1WVnPZSV_nNKRxSfVxbVuyY6p_1_7Yte-GEqd4GvtzVoI0TgFM6GVeuMCGfpE5OwIZ6X8iw7Fx2evXnm4iM7yKGbqKVcYSapy4DeO2JFnUYFNYSeHm-1WwtA8S3jQGO8JZuVd_VTtzFG7w6O9vUGBOZc4q0GP3m1a8KRZum8o2_WiKk1nnA8SufANQ3zizTOp-GnRJ2fEfQF_rd2CKBo4X9aqS5a5Oy1setOkq78aLSDaLjSXHCuZ_vyte0gckZ-czHZ46XS9VF8ydfbeg75Kb_Qb__tzEdGytKtvBZBKWeoX06-KZB6Mqrrt9a0RoMRUCa9mnUbcm7SNf2iEWApM2Zlxk0W15S3pmnnEap8CSbmIIA4PDV1lE7qnHnA3xbXf0S0b7b5kHCbhdW7En0GqGeH3kooPPhtZK3tHsWoHRGZp_xyik_QZfJr-lfjAzlVePIe1DIqYc6ho7WtPXDZRQb9Ey10fLmxt9iRAIUDaWk0TFxDcms4awt-al0yW7Q_ovodK50D_yy5tyuEAVa3gKmxHa7ngWw1eDkbP6clY8xbBoRTkQ2zK1z9TexwG3-WwKF4DrFdpjiaabSzpkf0_IrY24OydmwUPnjll142VrFI64xjlOEawpS2Q2kH2Ia9YNxMyDJwtOZG-bqAnZUe1-WpUitTQSdB_xtf7dzXi4qYa6Q8eu81lahmaKuAL6w8ARvs5MXiPTks-Xl4aSiuNZhve3nDmh3xnxyfliXWG4ACqJmOCEB0e_KYv_48Xg5P4zinVaiqZIyYljLVSpiVrOpDxqEg3ZNg5CEygGKq-3qqeZtrDhIMWu10g4N96_xUGfpT0Vfmql8joJjY9QM573rOIbCK07oZp_VVFfNbxgsK-Eklry8fP55VcO4tmZrm5a9CXiBKJc76KYKofYoE7LUuuhBuWRIrxVdW7AKe9s8ZxOD4iKxa4614Ap444-CahOaEqeo2WhHPgxNj3Q-tHV_BI7ZKWaiIolYrGOqhBmf41EwOpMUVC4vD97aW6QGdvETQ72Xu0xsqaLmlk30eN_YpHsYl0JviqePN5hQs7izNYXKATpIwnIyhRuHfcpC01cS34ZnzkiBwM99HHb5em0xTaK6PdaMmLh-Ev2Zz6mGiH-g8bEjNZN5DdNsYUW7Ev013b_Zbv1cq9YkSrh2FGMpDP2BeiGeaIh_WcI-iy_4gZSukJQBlaVppOve7bzEwm5dS8Yiar7x2isBSYdWeBNxuT_k5v0PxLIpGYT6wEb9l1E3FUFiDNwpombmXg_sso5Exo3coYwUevpmLq3v3oSGSqXbEtae2RIe33IzpR214XkS1oYO8ZvADiYKxPNRk8LwUa9W5wdUjl4pLFg8g7ajOd3s_l2JDnSNfw-khg-Ulpozl3f-UUxgV-GUcuYxtUa0Q1nikmxVZYJsXpM8UPjpJvCZJFOb-XMQN_CMFZYQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc23otBdIFyzZkGl6tqoq0xE8iP0jMxorGIV5nF22_qg8vyOpsPdTKCpME6GBZFk8qR97rSDi4A7R6zOZvpCvjZkKndzb8yl4G5oW55_E5xIigTNmepNFyjDYDLhT9x2iN1NvAyJ67w_FOLuSTfWR2ZaNyV06iSgDlGHEyqBtnPN9Hn_oTWpx6SGOAUV6idWcM19GV1aot0cOSz21bFbivcJnzxx7WPXlUNxeWvoTEUDqrGI97eynRb2ncUw4PZ3DnT0uTdZwwhUWvDuQQMfg93c5xMJegeJPRCH8BtGWDGLc57LUzMVBwHeDWYtB8XkpeOLWS3ArsldifmdnFNDgTNmx1dT8dDq2SRDRcDmbtLcbNeu3yLZmyYYUC4PcnWrqVpaShDgDufvsgQyIRPHtgaAOS8Rh85zPHe2zXMx_ILfA_Cm8d--2L9qIrQh15blgdA1uoqTE_CtSzTcgxiAaXr3YZ4FCID25aA_ZvvofKaFo2WfviyfdVTzM9db1IN1Q2SRI7aLnoSvm3hKvxXvMAKk_FiSvUHIUHVoRpJ2uJVBhkgtxyfirQCu94sBF0evRk0knmaiAZocQDV_HV4M0c_EKn8NXOufl0MKeWJ6ClC5yuEnyAtJguI98iqsv8oJyKY-1ZaERcFSPddXw6EeozHNSLyw-mpAcp3vAcE5h1ZsZch7QYgd7xltm_2I2ESxO7sNjGdI1KiedBNKaeWatGBm9DjN90Kvifand_vffVaOTq8eWKhC_jr8ZOt7NsKVld5vvzyzQqWyVNVvRDlBVNgwbIujx6_QR4H_Q_juRo8Fy2u1FbPYLE4ivS4yss7P98Yrp1PTLxNgtQ0wpVCicz2Tr1QlhlSmGI1735-MW2KZnLo4ggME8oHwXHp4jDNDmmuOM89nWb9dVKTmw-cVwJSlPD8-bYGomXh_O5YwJA6wExrZabWa6iJXkcUfhj-OJz_zbQUsCy1G-oylCbHOYpKt08AqtyXQMpCpNlFPMhj6U30Dsscad5dm-iOdZU5Wa4tycFnKNpu_DdfgwldPyzFdnqU7Ht8ER0bzRMgS-2Fabo6WMz9cr4qffmK9eIc9wHKAxcg8YpPmW9q5LlxM8-Wh__w3uNqAChXQoZ8FHOB3Owvc47hgzhYe_YJAkoMGCRw2_WtqjG6uhkpmJQoq5-otwNm5c6a1_jagXjYS9F3dkKPjtnt2Jb7E3FgM5rOQ51QJWXW2hLv2TZRfk8Ey2uKBm7A8JNNzDo8X3Hthw7JrdhRY-gcBXfenXOeHduv2RDN1pw9VoOtZWMPJyGYHH6XdEraKSZAd3VhCbcP26tMBAAE6EOG0vn07zWuHLa6QRC8kIcbzqgBPjCx0OSPeGyQv1CMFScubnwuzXyNBXKCjsNkH89Wn0b5E9Ed3LoMHG4eWjv3jH4lS8tLMbpZu6y7Mfq3vSJV8NZ_F7KJQ8MxaELgAdMjWw01HaPfJe9Al_5DZddkUmICKKAArpG36Frghud-SlE5jxy52wAM9LsgRHSTCiR0XZ3Zj4o6Qr1f0UhLk-CUk4JeML1ieMtGCoIdGjv-uQqgFwuf5U5QLPfw1utAGZXEvNjLq51RKU-W7GK71kYED2hDLrEhwn-4BKUIbAhiLJsCM17pzq772OFZqzA4K0gbFZw387s1C9mVNgmKs8Dx4x3Ljm7FrEH_8YP-U_c2yrKvN0MLV0y3clY8jlZMaAbX-Nwei9-ofP13MY0CZ0opqHyne6BVorshQOvazuBAfpVkWdvnLzVsbHSO_YulfsGV7MFYnFr3qUH5D71ffREDvAEft2K7E0KqVJodX5KSlPIs5azJrAho1-OCFw9k_k7CubxjuMe8PxNDezgDTHgoJaCSCzO5hPmzOzw_ZLFJY-fplEDGioVr7rWqIvMxGWLyvoFzTm_1kb6N6vN_CGotrXeBTXnB5nwht_nnV1tzUvPsTJfOhPvESCluRI2VRH0NRLMnDQDQNb0tsHzjncIlUhKYKfgwaf8dH5_Ugev5vXmasyA4AJ0re1hGy143DAvxleb31urvyBAMhZCPtx-j2mawB1ekG5majyrFdTDBth-90E9cXDQl5sKbIaKcTvB-wjk_ciQUThZ2-tOmGlZoA4kmkMTmz7pdFjTMjPy2V-6N7Ev2DZNXgivGqrL5FgpDJ1qmkXkP27TjP1gfA3SnTShOJV0O1mDwwvH-MBxwUJLbgKP2YfqmBywHJpjS-F6Nhauxy_AFiuHrmlREzsNlt9MrSTPu_WvsptKnzTihvxVBJTyRD-Lus2MsrSwuHYMq5-nxyvssgNtQKRAJz-xT8ihuIMKGVSe-n-TYTu5SlJfU-XNXbsAIuIT7zMrU-kyfioZgBR8ex2snAEmhSE-6nhuTZ9ejQJkohiaBpSGyZXESTX3zkLYp8G_Az0jVX1bnbf310VPIl7-Fhx0vLnjqpY5b16EOptNMRNh66TpGF6iez0ix6qMg1Uo2OUTWCdLMxRpLnV3sLjdxqV07TEv-KeR7fSooVxltLS5Rz8lZfErRr-EwUxhtFLLjuTfvju-zuTETjJQ0PHtSEx3pp7aWiXv6ShpvDmGqTFFOmCrhxygq3MqGf8C7HfSGMS_4Zex3eoFn6oJYvE4xG1yGpdG0Ca_RYOTEDxnGPnkVKcfCOlesBImxkItSXrIJ7MnEgRPFuv7IVaVIXsOvVdtecVEXSrzYIT407luH1V_jFWL0U4fgZkPDBUbDg4ERr3Srsbv03inJ8UidVW6y0XNGgPPkBqESSkHzOKR8WSxlmrKOlWTW-t97hDrZQuwoCooEj-jUvhbpDBETXVRxbL-q1zynXIoXQEVirGjxbfTGkfNQaQs-Eoj3zWQo3fSXTxDJUCzwt6LOxJL1E6lpIdzsJBmFLPvoalAIX_nEFhct_NXKN2Q4GdbwBKYsYrev4HRwrF9Z2SW_imybhdoWQzjROqAopfKKmUsBE3pEeTzMr7saOtnEWNWI5v0nTLD6avjJPnlNEtdhp62pZxVzi6jHj4x6UvpDCFMuFkk8v3j04P3keBbttTiD24mAxNDGx1_xtjH7E2xirxpxp9zWKMsEVdq7AKKfNBgFb2sBaIhrBkrz7Ozw1_8ITZC7EciVZPMLkgeOi8KTj5kWR_eFB0zloH-Qep3LVE0TMJjEtaANByTTKmdVvckEj0t0Ono0BIdWnPvlcDeoUO4HxJ5XOlhsWcHg6hXyfCOTvyf1rYQDuwFu-EBY-Dy__QSULoHurxBKWJWNG7p2SSctXCe8ikFaWFeZNRSvEOD2MuBXA-V1JKOAMvD4vAi1lfC7LY36KSBgDUJNmgUuBiIDDTUaCcF2delpdAuBGD_A0cACbET0H_5_Iv0Z_d0D62kfM4DodGAslI60fdLQ4EIAglQ2Ku0mmjcrMch5j7TrubmNLsR9vODQ9-jRU-R6REVU_eEOSalTMQS9wo-cdcFtu_6pQlRd0KexLIIq4tluyVWCvHnIlsnxMM3dz5Ukj1gyy-F7unV7zbLl7j4nuRPVQoNaioA_1gppwEQXywW5ScHe4ML_aMpAKgc6VAIlT6Wnj9nj3E-lARc_DcSO94yTJwlft3L3cQCbNfYBDQhy9rrX6eIfPz782bKNAYjgiqBKXMZeTfGLBpTUZWZ_2fKRgXMYGOYqbO3MhNOhMWDehNRT7wlLM8NXPKSaLx06m5fn6iHfxNjUCAtmMF0CNAYz0cG3P5quiGAa01gy7ti5oGNI2b07e7LMue3bsRCN7f2a0H-MMXychga7EBtc8El4daLX2MBpQRtVhBR0NACKoYqitMMIwhqptwPEIhzs9cL6y0cwYtnLAHTaAq0MGEi0vW1bu2Bn5Q0Qe3MwFORGEaqT06Vu0kqLfnwSLenS6Bclp5u1gW4QljjwnSd6QUWmWnKj6wrrdFC0QGN70SuAYWrAxpN7ApuDFlst8gyWkK1Q0N81jABIGQN_rjSLeGQXLNbweLbHiL3flK9sXQU5PuMc5veL5WkMa6jS_tJ8X5pN-gU3P_GErnKd4wUUsqTNc9mmEQwsN94QGCfz3-CAsJXagugDuhLgbGzAZWiLLgJwa5fjLH1Mgv4Q1Mf5g1SXzprzAqecIdsN0AuukN1QbSa3f7MW2uHgxNTZEAq0zGBKSp_R2fCRfXKY2w9r_qSWH2iyDZpzjxMj0Ea0hS1Om8pTjRQYPA2qhW8TGsX5naR6VMVK2eu0ntb7rGgDXARtWgeL7msVLMyiNDRZLFkHoLP3uu9bdJIuWN7SmxGYKoEpWcA1Oj_ijA8YKHcxAsg5oGtJo7LIAsnks5Q8R8W6xgLQ4L88TQ-rMf4HoApUZmiLnpLMAp0rCFstOAqOMHYjs3YmwRzxQyMencW5nGAbUo7KYel0nNqlr8gDmiQ-wUfFZKjq61ypHX8aRI-BeaZEq-E3Jm_Yfqz_-kWc1ydRpIu726_u5YLlCMJicy_-y_IfBA9j0HPoDRysydhZIEq-yoLcdvuI7yMPUBHHVzetqLyGOoNyvX1eeiSOmZJc9Bw1uf98X64Qtn2-ZLD4HOwIO7OuMZ411SpZ131uQ0kVVld5vvzyDIAzVco7qAY4v8GULNM_bKM-_hjy-6qK3LnWPK4cRig3gSNrjpW-m0TnvrJq-4ZSqMO-syIWbrdak9ycWF_OnKdnaSLR5DaudfdL5fUgYNTx2zJKYFhkUPWia7t3JKUcfi2cxqLLSu-orTqwL39PRuzozRVak6oFqQPg-Zn1E5NHacoQG7Koi2E5phmkj0X7uwv25zlwtMBSJ8ivzA6Tqel9uEuZcvYsRnVZfMpWs7VAKelnrFh56EEsw6BS-tkwpRolNuVmRbOBnaSnsy44WHmntZmLZLhr6EsPAKRKkO9ZuaYkwXvLLzdQRpRo-HabHQVEB0aw9sSB5ynwjZ6y2y5X7ilEiLNIxRCUBipKlVmnFggE_hTUE9bRTsG4DRFZ4XBfj9dFDvqdofpKBiE2laSY_njrDS8vjonQXLMiQiKMlwBEEVy1B3okLH0JAL5ge_uC5vubZ2Cn9Oo9_fHLKZWdMk43pwsSRUd3pJow7UzFPs2vUA6S-ZHViN1hUoYFdiHpsmhoPpK_dK_ScQuLp5pFwrcnurpmG2FjEdwzijNMguth8OlwZfLUTkAtaqHhxMddTWjm7-7duXHAtYcX0hb-jQbOBiEekzwymouRBBw85BhJrS-yTRZcyAnvC9DuD7-wNrFUn8IAm1p8-wRvqh43ZA4CmzH1A5YOFYzgq_6bfj7kgBqYRYfzA56xrPMmPYilQAMSPcawt1rpAWQLfirxBM0m6_scWkPBcFvq4brfkm25k8f59LkKfzFGC2PiZJjRzQzWQjrL969i4qzUOxXH_rtT4qLTFfBT-9v4dM2kFzE-mzw45zdpgXiYNeI_UKQ3mTzy5HwYrYFDmL5f-mZjeFcMNvXQBrA7h3TZ9OKmJNpR6VmFZbvL6G17q2gOVcD_ggI2wb_1L5I58v5cKVzNZJ-5L7CWSJtfdKdHtZsYV2BZqLpQhAKvNSuIrocQgZakOAxqVekQg0hyllCc2lUMU-CBcswOOIfzimqtUbD6UehCdwnrzB2LN4Fcsuf7E7sJzaRZmIBhHD8G-sdLF8evHLa6AKo_UzYqFoAFmmKdcMtxK4V3S7SE1TjqNpOr7qV4vcX0bw1MaThTa8EC--YyEG8rcdpR4dG0LEdsylm7sooyjMRBqvnUqQD3wkL6XsiFKZYvZt5TI4KRZM13FQPF8HOIOgY3-FMCXGJfQmadQ_qWj4MQ86i9-hJ1RFu1_c_XuXevZ_5QUYhGajxqJX0Mb6JGKhbb6J1KSwKPRI9R5hF2ufAuH60RfrJHmFbbCoHmsWhpLKz_NLAA7XoSV8sJf9OQhGRDTLDmKIdSWsNzr8BTEjjdyOmFDfxwfNwco-Gd0zuZppPR_ChYMgVUhF9Hc-yEpv7huK9SFTiq_xLXGqT8Uy7OEB4VZWR8PydJ6rlho14jkPx8uDR6iyVN4mr4jhN56fD47S-WRzp9zBdysb2fzlqoNgf_adxuojm856c8FJZZK5EPcUshCsbLA-e4T6R5WcfBSDw92IdBOJ-FcfZF4Y6eJISM5Mb0JqyZ2_oQfJdTF3ARngKdhQ0nSgOLfl6g7NoTklBc9z5pX-shX_f2fgWw76EoaL922ngs5M8JQ9kfn9QoRjGdqMtnhGZMu8PISWv3DLNI7M_2EMXd8scaT-tJ2JHhVwGKVpVnbXHG4OdQSNofjTMq-dzuHVLylixbm7tC7Uj_vqlWmtWUltviizbezgNyJHxTTFyKNYwliUNJzHhLFePvUo0eKOkAh3V1jDlZO1gYj7aKJAtPH5prncftXg0OgPD49fiA9ggRbiAr76t1fLJrKbnzPTNYF7jNM3bbS9QEglPRRcK_5_-ChUCFvcgeMiL2qpLz3t6WojdRioL-yj56gg7zFb8ZnXBx00JvUWLOytmcVNlIeVCm9nlp-CXrvoNVYZN0EM3ypIuLsmD55ejMJaue0vVpMhFeUgwXrFr28mekdu18A7_NqYV5hRlqQGfjUm5EIzju4qxRbIMkY7UJnA7KXre1Bu1wxsHSiUYPXuToCKSRHLeVR4BfjUUlY1Ne5Agbzgnv6apG7bitxGWxYPR3QBPaPrjCrwa73OSxwqLKRtMVkPtahYBJtT8mYKLJfGuz_j3GhqCDzPEHxQxu8FvTuqauT_2inyZQ4sFcP_Ji6VKdxV1EcS_sAZsYvStDtdt1Eeli5s6Zhx7V4milt9PbQNf9B6GhsZpavX2q5ZpRuP8PX8tyyAAaUqBU_q9yIyfw-RJvlFDmKNcCOAiUDqNBCl9IQkTkkwdwaFXivGlGYIfWAVsNnSdtfaAMPMC4hgwsb9JPjkDZRipN7N_DVEzsbXo2zx0RA3c-Qt5G2DUkmgdMAXUpXshHeJUDmTrhh9bZorv-sR_1IPHqJiFT6v-czXVclwTfAekUpJrpZOWPpbx8JSdqxGBJEfYz9dgwiQdYwy5DR8kZ6hQJ8fzA3_TdmEV2NmSnpVpcL46r360JxVVlnYgEM4K4fUkIgSG1oUTvMLIFe4xzDaMa3dPEfQA-BNBjdOZbb1HdyIqbHLoG5ERiSFAHGUuD5qJ8_03pdDnlRaXkYqnYTxLshyLdNtprwTdPjvMeJuMeJwMexVb0_u0uBOiy-dy0 \ No newline at end of file +xLrRS-MsaNxdh-0g3vDCdSdw4czL5CSLMIDDfdOkAadZLEnAgI0IXmSU4ZW1GCcAr_zz2m0l00YI0CcZcR7v91qIw6v-Q3GQtRZyZN50ULdaKHm_YS4TGMcyf-GsXmMBxn7VnG1dEKpZX99x5cyXv54Si_S8xm45HPmZ3Fd37iQ4yXqnNYKvO2p-EOhZEFwMq1JIKv8J6l_koo__v-JFdsdFhtSeUOyXO_4NKHp_8aKv5RKXY-AApad2qHtY3vSmGNi4CMS_H_7J6ANnSOmmXncayN7yTGOnf833z9iepYZ0_ALPuFBi_EpoxCtfsLLyz4AyPqT_H6aK4xw3z0npn1y6QhnjIY5OLNmQWqQoFQ53_4wCWatzX48Xy0cFZsEE2iWuAFOAU_4p_W_1C3wECRd__Hz4-NAV-fV_9Ofi_0Jpa_sUaZj8nq6S_tqbWtIWrZi6wUDW4QSmXsEbenWJBkfm-1EFOiO1Brby74DS5dredf8y1xU40av-MuZsu76ESlWOE6nH3WTgIJ52SF3z_hxlVa50VevWdbv2jYUOJG11aF06GXHkFZ0Z8B-6nJu774eXBYWfzeFsprtZC935jCAawbe__6i8nKl8I4cJHmMGLZm7QZTY7GOYHLmsHMro54pYF_dpPW2g6wFwpr6CGQ5mbyyZoMbBnSEAf3-rh__lsREXPkfloV4M91Hb28DyfEMCjffhER80NiksSP8J1gzAjeSuFILOT8LciD7VNKB02Au7dXZYz3cGuv9W4iM8oI-3-SDPYZWKdt9BI0w1gB85EPDbBZa7Uz0G4GhHZcoovPftpK1t1sYo0_JtNtprrHyqdQdhzNJQjxS_GYdGAIdf5CFN4F1kpAP6srAIzm21IhWFEJOsKayR99U0wMCRDXi9frlz9M3v1Er_5xdE8A2RVvwBFnnSK7A7KfZsZ0DZL1s3GRVAI5DVaprAFamxSy5weBuIxODq0F_1qWU8xgVF7JR99Aw7dTG1-bR440nvFfyypZPVUAg4_Zkai1rRUuT9Lku5KDSY4bAJalsjuQbrkv6XTBeL36_GJz3czPkwizDN_tlQl7x7O1h584qHHmI3_0tX8foKA5qGqtpigj3OIxTjz3U98vPml7LpmFWxXU6t3txJ_P3008KP8dYmuKM1H-h5JsCHZKAo9xRYV15f6bx5_Qe-PlO_gvcRNWTKd6lKAGTvKeffS7fbn7jgRMajXm21q4kIj_qyXJcwW_JXDUGxadP4IqiAkFgm52Qe0Fdllzny_WUNkhPJuww_NmYbaKL-P0JV2lN0MGao6mjHEOSPI9JAcB8uTLxZYilY5jAtsmj0EKhGNiH7smQ9uZt8e408LcA89qP9Mv8z9Ha5XMWprsjQ6z_kYt-Na76f19Qb5V5g0nfMtXQ82Tsn6a--u1oQI790CyWlo5uqk51m1tlf8ZXVSE3GF_7c3b5UWlnvfGmkBMtilHul5AeKRkbr2jvMtmXJDcQ034u6f7Y7zOLqiQIYZA9HW3sx9WEpF8jWBVyTo55wFeZOTrMHgLQlccAREljKz8Czo027R_5hI3DeJDSvBU61WjaQo4LHOfH8bNz5Cj_PPsBL6vnScqK_elbcHpIFBwVrW3CuHDR9g4E5vaMv573GsltmR_SBo8nsgbcX4oFqz3JUYy6USVOMlzld13Z3LuVj46Tt47Db5ozGJlWReFo7KuWvfBATF9G4MbG6cjvcs4293Su354mH7oKRPChsnctSGZqz8R2BL6_R-1agVKJKl1OnE7j_-EcxoykpDzStrozVdrrTdxo-SFK_SezDnDtkT00q3hPTXszF4ll36iIypRcdoH4c-mhz1vfVynu-E9g_3aa86NeqnCoY1fim4OuFpR4cbXCmE73r-0iXMO9FBSkT8_p-Mv2yR_JBG3iuYna2rOVBL19yKay8B_MeZdnZFPcTrGpDOuP0UCYuZHiaVLmsmGeTiRrYFdCpcsEvJ6VsKZoyGWNA0KNyuGKbvTulXHck_nQRaQfMwGE5uk1FILwcC7svVKFuILeWx6ZaNmR06WTg6vhedUQ5BukBCkv_1-mbbZD8q9DlxKImBB3a87Yw0pZpi0SXuydIsUpfOuyTByFm_ieTCITvslEc2Gef4dtUedmXmnFTQ8o1kslWyEpHTLNFeIdSrD9KT0ZJozfB4RN9afb8y5ve0CegxAZgRSNd6mcA7S8j6xAlWs5rCB3IzUgf3FSvSMrpUZKC1qQNs0nzib6JsoZSLQrPXGx-K_7u998uJc361dPzD1-ft8pgdNAkfXblaaEaHvfoYE0cMr1FYNQ0xVHENqdwnWoSw8zVanHjqMADAFDDLJvWfALxP-v-xj9qPLT1gWj08UOXRaJ8Kl2FoLEk9FaE0JtTv36zwwuNEwUak2e0vMm78R_cu287M9_o32yNezoTPv-xZ4mY_qRcdLuX_7xPLVt-JPcrOWIFjBw1n_z2Z9NmhqyAai8ybZahdCLRGcCERdFuH4Va4kvpaC02OXx_Re_m-x-8dHR75ARodp_UaZpzYn1JF8f_RcWd92yB7BCTAVDKB6MFGPwJ-nYNUVva65cB18LRI25EO2tf4mMI9jb8NEM5exXg62zfFGGIOvyadxxmrugbcIESBSqzr4_t9HhcpDEswNHROdPTktt7-kyuhYE9Y9bQTIXeTMDvcEO8osyebUkqHhXZSxWPk95eg4tllEG2mBW4-Nc9KuIprmJpNOTaaYBNY5oXwthLZgNzOu0qZT0T_vRlhlOmGI17Z9yL0ELZXPo4gjKKOwGwnPp4z8KK0_wUMErnIb9dVST8h-aVAVSV96W_5ayp8cY_eh2z35UwUxrZlXdaHOLXUg2PjL-4qP-zbPBsy_KGkoXlSvGOQq9tWCBq7qWQ6tEVu4kPnNiEEF3DqAcyt_dmaecNJQ5Wq8rKN7mSFtdv_EpDzSlpbsVFdnmV7ydfjYNrZQfpv8-INF61jwHDNXgpMmeJGjDJKgf5OrKHbknW0RgAhNsiHtpPVwc9GdeKvV3b6kGUYpKD9rnCrlK5TF5n_5aLuKiWO_q5_1jfQWFn_VbW6rdeBzdlKhX56EbnHPbLXrXCWdfMWcpv_WfoZd1crB6wiD2Wj9mGAnLgyngmzqt5FU1TA5u3b4DgBX-w5RblxbwkfoqhjzVlJLsVq8mjK8pyqaks5o8-ZNyajuwrdq_5UaWH39tjP578ofmtIq1PcGXjrYoYZXZc40ESG1_OE4LP1ccpthaffVTAYsRJEm676Q4F6kGJrbV949iU8_SV5wuK3DioTo91CEA48fp9Wmskog80547leTe8JRZ6QYs4SlJ78wtEaNBYxn2yVvcw2RJ2NSfoDPMwrb7G02C3GgT1fT_vvk4yjrW2nYWXnLkQ0Op-Oo-__chp9sz_2nP4hO2wKZiyEcICWGrXnccP3DUWqWBK2uN7FN29qFEWoL_ReET8JeMy_SAjAZ-kAHNXMbMQUaUDoa8uJkM5AcYehQXtr8w20mDqHngLKQofrVMFmbO-IKfLegmUHgpW_CUWuuJ1yMbfGYY5KX-VGv2-GXZEZsHM2kp1lOaDwpkEPshotn4JlxrymVcg7Aw2Ihw70KryH5kyDMHgM7uSAkodR2daa1O80oEPRVH73AZOOdFNQfeZhxwWykcDkw1VtDMrvLCGFj_bvPGSecDCdBX_AfgyY0OkhNIsaNmqbRlY0AU0fe_dbB2AevVIrk9fYbfL_iISuKVKpL-TMJoeFLmDuQosULIxiKv3rcc8OyQwe9MpPyo7jt5gUb5zZdSSQlQa_XEBPgborwY07toa__OXM3VgSgFoFuRXrbfZmOvZwS9ZzPjVZw-ZVy_Ihau7gzNJ2MuPlxtgimtYegqgTe5Komlg9lkrpPZi1RPMH4fJLrBoXEWhQTK9p9o3P5UOC0Mc1bG36ZvYWCPrZr9nwA0n3rxMqdKcuuUtTI51XqN31KYky3K_TKul-VWc0eYR4Lg_NfILA1MPt4dwg-yqQnfxsZaE7ziP3E7Beop1vXxtqVETXrnRrNuA_8bVSRWFsJ-5gpf3JLSL-RJsJNN0wEwP8jorPsgaejmlrIbZDy5Zd47ehLFwkVpgv_MKfHe9AVR2l3X7FkqSuyTVkZhjUCrVpnBM2Jj9FvPjUjVM7btZ-GVOgDV5r6zRdj-6EdqRtfIJQ9VOmJlZnfRGNd3eVNxQ_lbffzPEtYQuHPJnqyeW-fHzYI_4xWAvUdMyz1l2hyGamq-EwjkyzDzJPb7KMsHHs5jYKTXNuDwzatsR9JLRqdPaNPCNcebvd5muxSpx2x5cGvx9wHU-23FYB2630-oLUFyHtvPpeJReN43A2COmd-kis_ICuxwXUQnHw1TsDefK2zW5myuPQEfOt7chY-5sg-pzQFW3EdS_ASFZKcRPlztxgk0M_UGur7lhUbntJ_U-fwejN3ll3h4DVRgprWQmh3E21tQUESzUriD8ZZV_X826RXuRkEJjGvcseGs2TF1iwX8YwvdOqV7a-5Zy1eauvFWEq0U4Cns0pDDsug6352_LCSfdb1gJcBvD2yjERWltUTXa3vi3iUs3VwIqtvBqeLcENv-lvdpet9TOKZI0Xtz4mN-xJm4GdfAQetbJIteJwdmezKsDjXUG0_C0olg9Nm2lG0LqggNRon07AddVpH5oTlJ7SEk6xrdRljmHQ_VOcgFi38lZxRgNUUuSpMndeVr-vHSTm7TC8KiesZjxzSAUvHMKRgLsEgplpigGlO6imsN8tUmKFh0UTnbMUqqGpjgyqcyT4-_pcwSSshpaUSAJ3mbkFnuLLmcXK1wK2vBTebPEX8K-j_nO0l8lxFEFAn_enlRMc92iS_j5SFiY3azowBSLzH3f69zJ89x61MHCNShHvERKCShrJjuwCzZiusq_x4OKhPFn7cSpJ3qPq3gD-GvG1AGxQEvTztO30fD2XnmK-yU-T_LfaGlxDHy-yoTOL9kZtzv1Ib4Ab-uZPSjYf4gzI_jG9x7UWRo4tSo1pjf7OsMbhYe6R647xHRes_wpqSDRyhlcr4qr7_X0rewJzz3bot7NLCAt-PhJSmFm60TWHmfuUU3Bh7PCtfL4UqoOcBwz8fcQ1YuVgN77kNAGjKdZ-3W-kRmy-USF_wh75GdUZInr9Cu549zmKl8TeJA2x3WveFv8ryLHpg3GZc3undbmav72rYGH-Mg0x-I15OXnbB1w3VcDy2bk2d4vpWsfp9Ymv-8yfvi2qFUoW1WZvP7JKVpl4kI8Fzm0naegrf3Sf83jRWcWQPqM1BcYghtW522CS7QZbbenxLsTEDV5bLbo-Q3MoVg2eBansxaN3I6ct59trcd2-ekf1urzV_AyclrvWD9E5Gdjn1w-Fpb7EOVKBzjU5jWv_PKhhGQl5_d-vDM9VQkSHtHCkEqtMWdvhCXl0EiykdteF6g171bQHDcV99koLAeX7-ah7LeCBMSRm_iRIkvlp1a6oLF7qsewTyDGvcY9L-PYZIh_ILUO1c7gMJIoGjM5oehQh11r8HgwY6N5ousN8yA_GkM6QeLe429j9Q1rWTwArW1QgrttnserLc5usH795Qm1i5QS1d5Q-vuNJ2iy5Zm3TtUl0Da0MJS2h00fW0Qlubv1Se6q0jG1Q9sLE61v_loL1oHfm8UbreSbf2W0Jcyuo3gnPr5OWjXyMkytgoqmDoY5SWjBTvcaUk_CjwcJqc-SIPaHV8AkFLzLoWNP2f05q3g0EK0PkBDy1QW6Q8rkZo6qJbC7m1c-gtl5gGUd5ICNHkxRkLU0Qe26hz5USR9n6ZhCO4NBHciTvpn0Mi5nWBE2OeFIUwFoRneVpzkNQm0FUW6pio5yANHQoxkYrQGXzVHs3wddfHn113KD2ghtGRfX_-t8gwrgG5fHwQ3IVtVkYb23qAeylT2ig5Yez5x4kyBJmhD2qmlD2ei5IyXbkByvP48klVtDmJDw4-wAaubJpnskAqnEc1mNXAv83I3bliroXMmSibN5Hl5QjSe79SUr52jIVKYjjYe8gzL8ZGAr8jIBaFksmnKb4wM-gnHNd5muhSha0T8wq0L2jVQxCPnMW7e1whdbReN93TCAaGNHkeic4A8L7XkU_ZllLe1qW5PWB636RgnRKJ9GMjVIZY6q8cCZuxvvQmL7WEEywMk5HaBJUytNYe-6pwet5YxheRJzo6IheN71iauQtC0uRhDQaQcHMK6nGB7lnrfHaQYCtRYrmkI6QUHIhXKsDsmhH3P40xTxhGWfXBgNWwt826HMxyU5YkCoi1MO6fZ-kyHMZ2oCLcmSsFJVOBPYL6EqWcA1qhrMQyL5uEA-NMl5Hc5ZtnVscyObUXAVC4OI96qlYw98pjFZey-FucVFVlZTRGwIzvjz3X3Uy1rAtcF8s5-_-DUpPxA8jWLOoDNysiXBeYEr-yoNc7q6JNmSP-BHHlrftqJHm0rQ5ZTnLHzCBNO_n_4HFeexnxB7n-AIEFFP9WQzp2B71Ja1_CTkRegFBTwYoCRnpAosk5HDgDXReAP6r7VHn6I1x2jsEZ8r1JPzBwgQ1vIjxX6Xaibw1vIhloN7PNkCDbKppmd2gOgMPDC4glDe7IbqvdMXHJWQboMwsDRl5kDcMioX3-sKNam6Tn_PnBHzknWlPGF7lbEMMeodrY_66BT55-FUtzDjvdtzEeHtiz0kFexP7YO8v8ZnvgberAh7QCzCCQBE7I0JHt9LzxYwIzT-iv7FpoafEdLcIT0vkbjuBzQpZU5T20xpM7PEV98Ua_LmOQRkbOFnSNNtWFN2oTowf2EecHste4YdpNo-wJnHvgHs6HRqFHBvtwYh4SwvPT4gh69LAxRu5NKk6WXWuN6lWvY3Y58RzMUuy2nX73aTOKpqfwoImZb72HzujdAYeWyty-Xsl3-TWUNYXNBgqtp5mgtjeJnwShPjAyYVrTHkFd5dfbCmSpsl9vIEWya3ZRJh_FVALbklDgw7BEexrthPYTzU4gsrfvtRBS1zX9-BKoXvfuGAvFhLfcAv3AFkKkqEksom-29Iw4vNF_FMuPh3i-R1JEBH_Eb-JNaJ4ofYMY3lc-PBnmumXZ8CKmUXO67wlAXDnvUQHhkZzegugVIXHErMLyAQhRoWbNAQfUbmTyof65IQDk-rWC5izAiIcYzX-jD9TANj0XJYAXQLx52UJaF3cB0rxcpLluAfTHTWYB5DF7cBuKRzTtrF57NvI7RZUn9rug3-JFlEIHDUPC-hRdHr4VxcyGO6V_DLbtzzGZFG0jCnXZ2DEOblOPp2BS8mZMy9tyKfegAT26PFHYice81vd006dqJEavQRBqs_-jENd_rvy5DBNCjn77-Ta9pGuwhQDpPjzlL_omBVk0FYmM6fAYhkY4O5qsyCInLeLnqdc8ggS2nI3-h_gbMda6eEwQrdipCfsZigbBDZF9fBcxKqfykfArIcD2XNEhn3JRW4ld2yeKhLqexUrQ_gGrdqTr6uzzmRRzXqmmvJwPjDlz4r8DApJVvahLDyhJmLCjzMFCBfcxGt6WyMMOTxXjXJg-Pnp2dACaXX-kt3flqHUPpJDycCs-SZ6eQySxxMflkogVau8ZQdxBa4jOhKxeSSTDodqCqHgD7aRKlG0L2bsylp7sopyDQQBKzp44IF3gcN6nsk0qZXv3x6TI4LRJI23VMOFeLOI8YX3kBNCnOIfAybdAprCyKMQOAi9UZJ1hFv1_Z3XOjhv3mHQ-cfGKjwqpjXM52IGKlbbcN0KCsLPhI9R5dC2ejBuXA1RPnIHGD7WSsGm6ahp5Sz_NL9ANfmSF8r3cLQQhGQDDTDmKMbS0-MzLCBTkkOdCKpGDXuwfSAcYwJdmpuW3pRxF4RYskTYhFAHst-EpndheSFSVLiq_pNXX4T8Uu5O-N4VZWQC9ybJs-ihID7jEJw8eDR6zyUNryq4EdM5czC4dKyexvn9zVdycj3fzhto_Yf_adwEIPp0rUa8VRW35zDPMQshywaLQse4mUQ5KAgBS559vIaB8VzF6f2HKj4hZQT45Qa0JqzhIpmQvRgTFFAR1gNdZI3niYPPfZ6gtRmTUhAcvz4pnt6hnxg2vkYwfo9oKPA2Ires5Q8JQ1jfXPRohfHdqItnRSYM8CRIiav3DLKINUy2UUW7JEaaTwtJoVGhFsHKlpTn3jIG4KaQiVnfTPQq-h_uHNMy_awbmVtCNUi1vulWG_ZUVpwiSrcezct1C3aqKt_J-7byXfRFbslKUrhcBSdWXAroCXw4qoxFLcb8KgRHy7KclsuLM-iTci8Yf4wb6EogM2YkcafNClJ5bnQLI_DrLTQBSwpTyMLKmPdwQXgl-LM-dFnpzYw_c2kXgfLB3HTqxVZ1ggSlTDPuYSVRQGQskyZDM8iiIz0aAzNX3NFPzAt9nqw3tEoCO-FN8TUyfrO4vm3nj_jKx7JJcInQENaX39mEQyrYwxd6ahTBpIYuF89Y8JoTos5KjowIw6cqBONvBIqZJVXj5rPuefuDqaSIdse5V02gFTEnXMBddbuA1Tpj56azS4cb5vxb8bOaKwXMdhFcgV11EsnTTEEi9DeEOncItQsnNGT8D1mkBDUHFMkyPdRI-ugCjqj3fDOD5NgqEDt3Vqwr50_6Td-Ye_YNpIUX7C7ptgCh3qnPdv6pnvIVz8ExvJvRBVI8rhUtEpT6Aopw7uHDlgQ-mEnyC9jMPQcbyH2jgRDGsuEGMi1j1qkcK7Oo0ylIhedvFqFn7Uah9TJcyyx3nULnmYswd5Jj2mZBwLsxhwRgW-9pzQ-09se0fRIVrcSV6ygP5asHk3gUaT9cUqwFkR6VDtnryptRcV98NWFku6IuxiO1uLmxJASQgroCNke5XrwqnZSpSQQERRivLVz6vv4IUSqtBqHQcf_OVvxdAQowDdiCzU8cE7iXToKJDy-CAs3sKkffnkRAhuUtCIsEAfXEodogVHmVmPyBlnr4DFCRquLLCi0Dj5-_swiueXLH52wBfbA791ydfT5zGZgr6zJHEfWvrGgwjiYr_Q9M4v9SHPlAIdYWgGmPqycxyzmxxaWGkO7cEVaVdDBSbja4jlLj0qNN_pfpv-VdtzJIVbCIFbCIjdikFZGmjRJnSUI-D1uHd-3E2sBFFf_ \ No newline at end of file diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml index 5102cd4e59..be2bb17a5b 100644 --- a/docs/logical_data_model.puml +++ b/docs/logical_data_model.puml @@ -435,23 +435,26 @@ class GrantNumberLinks{ deletedAt : timestamp with time zone } -class GrantReplacementTypes{ - * id : integer : -!issue='column reference missing' mapsTo : integer : REFERENCES "GrantReplacementTypes".id +!issue='model missing for table' +class GrantReplacement #pink;line:red;line.bold;text:red { +!issue='column should not allow null' * id : integer : +!issue='column reference missing' grantReplacementTypeId : integer : REFERENCES "GrantReplacementTypes".id +!issue='column should not allow null' +!issue='column reference missing' * replacedGrantId : integer : REFERENCES "Grants".id +!issue='column should not allow null' +!issue='column reference missing' * replacingGrantId : integer : REFERENCES "Grants".id !issue='column should not allow null' * createdAt : timestamp with time zone : now() -!issue='column should not allow null' * name : text !issue='column should not allow null' * updatedAt : timestamp with time zone : now() - deletedAt : timestamp with time zone + replacementDate : date } -class GrantReplacements{ +class GrantReplacementTypes{ * id : integer : - grantReplacementTypeId : integer : REFERENCES "GrantReplacementTypes".id -!issue='column should not allow null' * replacedGrantId : integer : REFERENCES "Grants".id -!issue='column should not allow null' * replacingGrantId : integer : REFERENCES "Grants".id +!issue='column reference missing' mapsTo : integer : REFERENCES "GrantReplacementTypes".id !issue='column should not allow null' * createdAt : timestamp with time zone : now() +!issue='column should not allow null' * name : text !issue='column should not allow null' * updatedAt : timestamp with time zone : now() -!issue='column type does not match model: date != timestamp with time zone' replacementDate : date + deletedAt : timestamp with time zone } class Grants{ @@ -1630,21 +1633,22 @@ class ZALGrantNumberLinks{ session_sig : text } -class ZALGrantReplacementTypes{ - * id : bigint : - * data_id : bigint - * dml_as : bigint - * dml_by : bigint - * dml_timestamp : timestamp with time zone - * dml_txid : uuid - * dml_type : enum +!issue='model missing for table' +class ZALGrantReplacement #pink;line:red;line.bold;text:red { +!issue='column should not allow null' * id : bigint : +!issue='column should not allow null' * data_id : bigint +!issue='column should not allow null' * dml_as : bigint +!issue='column should not allow null' * dml_by : bigint +!issue='column should not allow null' * dml_timestamp : timestamp with time zone +!issue='column should not allow null' * dml_txid : uuid +!issue='column should not allow null' * dml_type : enum descriptor_id : integer new_row_data : jsonb old_row_data : jsonb session_sig : text } -class ZALGrantReplacements{ +class ZALGrantReplacementTypes{ * id : bigint : * data_id : bigint * dml_as : bigint @@ -2476,9 +2480,6 @@ class ZALZAFilter{ Files "1" --[#black,plain,thickness=2]-- "1" ImportFiles : importFile, file Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNumberLink -!issue='associations need to be defined both directions' -!issue='associations need to be camel case' -Grants "1" --[#d54309,plain,thickness=2]-- "1" GrantReplacements : Grant ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses @@ -2528,16 +2529,16 @@ Goals "1" --[#black,dashed,thickness=2]--{ "n" Objectives : objectives, goal Goals "1" --[#black,dashed,thickness=2]--{ "n" SimScoreGoalCaches : scoreOne, scoreTwo, goalOne, goalTwo GrantNumberLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringClassSummaries : monitoringClassSummaries, grantNumberLink GrantNumberLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewGrantees, grantNumberLink +!issue='associations need to be defined both directions' !issue='associations need to be camel case' -!issue='associations need to be camel case' -GrantReplacementTypes "1" --[#black,dashed,thickness=2]--{ "n" GrantReplacements : GrantReplacements, GrantReplacementType +GrantReplacementTypes "1" --[#d54309,dashed,thickness=2]--{ "n" undefined : GrantReplacements Grants "1" --[#black,dashed,thickness=2]--{ "n" ActivityRecipients : grant, activityRecipients Grants "1" --[#black,dashed,thickness=2]--{ "n" Goals : grant, goals Grants "1" --[#black,dashed,thickness=2]--{ "n" Grants : oldGrants, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" GroupGrants : groupGrants, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" ProgramPersonnel : programPersonnel, grant Grants "1" --[#black,dashed,thickness=2]--{ "n" Programs : programs, grant -Grants "1" --[#black,dashed,thickness=2]--{ "n" undefined : grantRelationships, activeGrantRelationships +Grants "1" --[#black,dashed,thickness=2]--{ "n" undefined : grantRelationships, activeGrantRelationships, replacedGrantReplacements, replacingGrantReplacements Groups "1" --[#black,dashed,thickness=2]--{ "n" GroupCollaborators : group, groupCollaborators Groups "1" --[#black,dashed,thickness=2]--{ "n" GroupGrants : group, groupGrants ImportFiles "1" --[#black,dashed,thickness=2]--{ "n" ImportDataFiles : importFile, importDataFiles @@ -2652,6 +2653,10 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes !issue='association missing from models'!issue='associations need to be defined both directions' +GrantReplacementTypes o--[#yellow,bold,thickness=2]--o GrantReplacement : missing-from-model +!issue='associations need to be defined both directions' GrantReplacementTypes o--[#yellow,bold,thickness=2]--o GrantReplacementTypes : missing-from-model +!issue='associations need to be defined both directions' +Grants o--[#yellow,bold,thickness=2]--o GrantReplacement : missing-from-model @enduml diff --git a/src/migrations/20240830172706-populate-grant-replacements.js b/src/migrations/20240830172706-populate-grant-replacements.js index f6b08b14ef..427e865b83 100644 --- a/src/migrations/20240830172706-populate-grant-replacements.js +++ b/src/migrations/20240830172706-populate-grant-replacements.js @@ -43,8 +43,8 @@ module.exports = { }, }, { transaction }); - // Create GrantReplacements table - await queryInterface.createTable('GrantReplacements', { + // Create GrantReplacement table + await queryInterface.createTable('GrantReplacement', { id: { type: Sequelize.INTEGER, autoIncrement: true, @@ -91,7 +91,7 @@ module.exports = { }, { transaction }); await queryInterface.sequelize.query(/* sql */` - INSERT INTO "GrantReplacements" ( + INSERT INTO "GrantReplacement" ( "replacedGrantId", "replacingGrantId", "replacementDate", @@ -134,7 +134,7 @@ module.exports = { allowNull: true, }, { transaction }); - await queryInterface.dropTable('GrantReplacements', { transaction }); + await queryInterface.dropTable('GrantReplacement', { transaction }); await queryInterface.dropTable('GrantReplacementTypes', { transaction }); }, ), diff --git a/src/migrations/20240830173101-create-grant-relationship-to-active.js b/src/migrations/20240830173101-create-grant-relationship-to-active.js index baf77731e0..835b6b73db 100644 --- a/src/migrations/20240830173101-create-grant-relationship-to-active.js +++ b/src/migrations/20240830173101-create-grant-relationship-to-active.js @@ -24,9 +24,9 @@ module.exports = { NULL::int AS "activeGrantId", ARRAY[g."id"] AS "visited_grantIds" -- Initialize the array with the first grantId FROM "Grants" g - JOIN "GrantReplacements" gr1 + JOIN "GrantReplacement" gr1 ON g.id = gr1."replacingGrantId" - LEFT JOIN "GrantReplacements" gr2 + LEFT JOIN "GrantReplacement" gr2 ON g.id = gr2."replacedGrantId" WHERE g.status != 'Active' AND gr2.id IS NULL @@ -39,7 +39,7 @@ module.exports = { NULL::int AS "activeGrantId", ARRAY[g."id"] AS "visited_grantIds" -- Initialize the array with the first grantId FROM "Grants" g - JOIN "GrantReplacements" gr + JOIN "GrantReplacement" gr ON g.id = gr."replacingGrantId" OR g.id = gr."replacedGrantId" WHERE g.status != 'Active' @@ -53,7 +53,7 @@ module.exports = { rcte."activeGrantId", "visited_grantIds" || g."id" -- Append the current grantId to the array FROM recursive_cte rcte - JOIN "GrantReplacements" gr + JOIN "GrantReplacement" gr ON rcte."grantId" = gr."replacingGrantId" JOIN "Grants" g ON g."id" = gr."replacedGrantId" diff --git a/src/models/grantReplacement.js b/src/models/grantReplacement.js index 939e646f7f..4fee886eb4 100644 --- a/src/models/grantReplacement.js +++ b/src/models/grantReplacement.js @@ -1,18 +1,18 @@ const { Model } = require('sequelize'); export default (sequelize, DataTypes) => { - class GrantReplacements extends Model { + class GrantReplacement extends Model { static associate(models) { - GrantReplacements.belongsTo(models.GrantReplacementTypes, { foreignKey: 'grantReplacementTypeId' }); - GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacedGrantId' }); - GrantReplacements.belongsTo(models.Grant, { foreignKey: 'replacingGrantId' }); + GrantReplacement.belongsTo(models.GrantReplacementTypes, { foreignKey: 'grantReplacementTypeId' }); + GrantReplacement.belongsTo(models.Grant, { foreignKey: 'replacedGrantId' }); + GrantReplacement.belongsTo(models.Grant, { foreignKey: 'replacingGrantId' }); - models.Grant.hasMany(GrantReplacements, { foreignKey: 'replacedGrantId', as: 'replacedGrantReplacements' }); - models.Grant.hasMany(GrantReplacements, { foreignKey: 'replacingGrantId', as: 'replacingGrantReplacements' }); + models.Grant.hasMany(GrantReplacement, { foreignKey: 'replacedGrantId', as: 'replacedGrantReplacements' }); + models.Grant.hasMany(GrantReplacement, { foreignKey: 'replacingGrantId', as: 'replacingGrantReplacements' }); } } - GrantReplacements.init({ + GrantReplacement.init({ replacedGrantId: DataTypes.INTEGER, replacingGrantId: DataTypes.INTEGER, grantReplacementTypeId: DataTypes.INTEGER, @@ -21,8 +21,8 @@ export default (sequelize, DataTypes) => { updatedAt: DataTypes.DATE, }, { sequelize, - modelName: 'GrantReplacements', + modelName: 'GrantReplacement', }); - return GrantReplacements; + return GrantReplacement; }; diff --git a/src/models/grantReplacementType.js b/src/models/grantReplacementType.js index 35a7d00341..739a6626d8 100644 --- a/src/models/grantReplacementType.js +++ b/src/models/grantReplacementType.js @@ -3,7 +3,7 @@ const { Model } = require('sequelize'); export default (sequelize, DataTypes) => { class GrantReplacementTypes extends Model { static associate(models) { - GrantReplacementTypes.hasMany(models.GrantReplacements, { foreignKey: 'grantReplacementTypeId' }); + GrantReplacementTypes.hasMany(models.GrantReplacement, { foreignKey: 'grantReplacementTypeId' }); } }